diff --git a/packages/@n8n/instance-ai/src/workspace/sandbox-setup.ts b/packages/@n8n/instance-ai/src/workspace/sandbox-setup.ts index f5f59bae519..956fd95e19e 100644 --- a/packages/@n8n/instance-ai/src/workspace/sandbox-setup.ts +++ b/packages/@n8n/instance-ai/src/workspace/sandbox-setup.ts @@ -22,7 +22,7 @@ */ import type { Workspace } from '@mastra/core/workspace'; -import { getExampleFiles } from '@n8n/workflow-sdk/examples-loader'; +import { getExampleFiles, type ExampleFile } from '@n8n/workflow-sdk/examples-loader'; import { createRequire } from 'node:module'; import type { Logger } from '../logger'; @@ -329,7 +329,17 @@ export async function getWorkspaceRoot(workspace: Workspace): Promise { */ export async function writeCuratedExamples(workspace: Workspace, logger?: Logger): Promise { const start = Date.now(); - const { files: exampleFiles, indexTxt } = getExampleFiles(); + // Examples are nice-to-have — never block the build when loading them fails. + let exampleFiles: ExampleFile[]; + let indexTxt: string; + try { + ({ files: exampleFiles, indexTxt } = getExampleFiles()); + } catch (error) { + logger?.warn('[sandbox-setup] curated examples unavailable, continuing without', { + error: error instanceof Error ? error.message : String(error), + }); + return; + } if (exampleFiles.length === 0) return; const root = await getWorkspaceRoot(workspace); diff --git a/packages/@n8n/workflow-sdk/src/examples-loader.ts b/packages/@n8n/workflow-sdk/src/examples-loader.ts index 1f1e4cad29b..7abd1bd8d22 100644 --- a/packages/@n8n/workflow-sdk/src/examples-loader.ts +++ b/packages/@n8n/workflow-sdk/src/examples-loader.ts @@ -15,14 +15,12 @@ import * as fs from 'fs'; import * as path from 'path'; import { emitInstanceAi } from './codegen/emit-instance-ai'; -import { ensureExtracted } from './examples-zip'; +import { ensureExtracted, WORKFLOWS_CACHE_DIR } from './examples-zip'; import type { WorkflowJSON } from './types/base'; -// Resolve relative to this file. At runtime this lives at /dist/examples-loader.js, -// so `../examples` reaches /examples/. +// Manifest ships read-only in the package; workflows live in WORKFLOWS_CACHE_DIR. const EXAMPLES_DIR = path.resolve(__dirname, '..', 'examples'); const MANIFEST_PATH = path.join(EXAMPLES_DIR, 'manifest.json'); -const WORKFLOWS_DIR = path.join(EXAMPLES_DIR, 'workflows'); const NODES_INLINE_LIMIT = 5; const INDEX_NODE_SEPARATOR = ','; @@ -95,7 +93,7 @@ function loadFromDisk(): ExampleFilesBundle { const indexLines: string[] = []; for (const entry of entries) { - const wfPath = path.join(WORKFLOWS_DIR, `${entry.slug}.json`); + const wfPath = path.join(WORKFLOWS_CACHE_DIR, `${entry.slug}.json`); if (!fs.existsSync(wfPath)) continue; // eslint-disable-next-line n8n-local-rules/no-uncaught-json-parse -- Internal workflow fixture const wf = JSON.parse(fs.readFileSync(wfPath, 'utf-8')) as WorkflowJSON; diff --git a/packages/@n8n/workflow-sdk/src/examples-zip.ts b/packages/@n8n/workflow-sdk/src/examples-zip.ts index ae633725bf2..c9ed841046b 100644 --- a/packages/@n8n/workflow-sdk/src/examples-zip.ts +++ b/packages/@n8n/workflow-sdk/src/examples-zip.ts @@ -8,12 +8,35 @@ */ import AdmZip from 'adm-zip'; import * as fs from 'fs'; +import * as os from 'os'; import * as path from 'path'; const EXAMPLES_DIR = path.resolve(__dirname, '..', 'examples'); const ZIP_PATH = path.join(EXAMPLES_DIR, 'templates.zip'); const MANIFEST_PATH = path.join(EXAMPLES_DIR, 'manifest.json'); -const WORKFLOWS_DIR = path.join(EXAMPLES_DIR, 'workflows'); + +function sdkVersion(): string { + try { + const pkgPath = path.resolve(__dirname, '..', 'package.json'); + // eslint-disable-next-line n8n-local-rules/no-uncaught-json-parse -- Own package.json + return ( + (JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as { version?: string }).version ?? + 'unversioned' + ); + } catch { + return 'unversioned'; + } +} + +// Tmp cache for unzipped workflows — keyed by SDK version so upgrades extract +// fresh. We can't unzip back into the package because node_modules is +// read-only inside n8n's Docker image. +export const WORKFLOWS_CACHE_DIR = path.join( + os.tmpdir(), + 'n8n-workflow-sdk', + sdkVersion(), + 'workflows', +); interface ManifestEntry { slug: string; @@ -41,7 +64,7 @@ export function needsExtraction(): boolean { const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf-8')) as ManifestFile; for (const entry of manifest.workflows ?? []) { if (!entry.success || entry.skip) continue; - const filePath = path.join(WORKFLOWS_DIR, `${entry.slug}.json`); + const filePath = path.join(WORKFLOWS_CACHE_DIR, `${entry.slug}.json`); if (!fs.existsSync(filePath)) return true; } return false; @@ -55,14 +78,14 @@ export function extractFromZip(): void { if (!fs.existsSync(ZIP_PATH)) { throw new Error(`Examples zip not found: ${ZIP_PATH}`); } - if (!fs.existsSync(WORKFLOWS_DIR)) { - fs.mkdirSync(WORKFLOWS_DIR, { recursive: true }); + if (!fs.existsSync(WORKFLOWS_CACHE_DIR)) { + fs.mkdirSync(WORKFLOWS_CACHE_DIR, { recursive: true }); } const zip = new AdmZip(ZIP_PATH); for (const entry of zip.getEntries()) { if (entry.isDirectory) continue; - zip.extractEntryTo(entry, WORKFLOWS_DIR, false, true); + zip.extractEntryTo(entry, WORKFLOWS_CACHE_DIR, false, true); } }