From 4ae863717ed545a523d958426747593cf8701245 Mon Sep 17 00:00:00 2001 From: Charlie Kolb Date: Wed, 7 Oct 2026 08:22:23 +0000 Subject: [PATCH] feat(core): Add backend module scaffolding (no-changelog) (#40482) --- packages/@n8n/module-cli/README.md | 11 +- .../@n8n/module-cli/frontend-module-guide.md | 17 +-- packages/@n8n/module-cli/src/backend.mjs | 110 +++++++++++++-- packages/@n8n/module-cli/src/backend.test.mjs | 129 ++++++++++++++++++ packages/@n8n/module-cli/src/main.mjs | 52 +++---- .../src/templates/backend/README.md.template | 32 +++-- .../src/templates/backend/index.ts.template | 2 + .../templates/backend/module.test.ts.template | 10 ++ .../src/templates/backend/module.ts.template | 5 + .../backend/oxlint.config.mts.template | 36 +++++ .../templates/backend/package.json.template | 51 +++++++ .../backend/tsconfig.build.json.template | 10 ++ .../templates/backend/tsconfig.json.template | 17 +++ .../backend/vitest.config.ts.template | 3 + .../backend-module/backend-module-guide.md | 11 +- 15 files changed, 426 insertions(+), 70 deletions(-) create mode 100644 packages/@n8n/module-cli/src/backend.test.mjs create mode 100644 packages/@n8n/module-cli/src/templates/backend/index.ts.template create mode 100644 packages/@n8n/module-cli/src/templates/backend/module.test.ts.template create mode 100644 packages/@n8n/module-cli/src/templates/backend/module.ts.template create mode 100644 packages/@n8n/module-cli/src/templates/backend/oxlint.config.mts.template create mode 100644 packages/@n8n/module-cli/src/templates/backend/package.json.template create mode 100644 packages/@n8n/module-cli/src/templates/backend/tsconfig.build.json.template create mode 100644 packages/@n8n/module-cli/src/templates/backend/tsconfig.json.template create mode 100644 packages/@n8n/module-cli/src/templates/backend/vitest.config.ts.template diff --git a/packages/@n8n/module-cli/README.md b/packages/@n8n/module-cli/README.md index 35e65687c3c..21e08588d1f 100644 --- a/packages/@n8n/module-cli/README.md +++ b/packages/@n8n/module-cli/README.md @@ -5,6 +5,7 @@ Interactive scaffolder for n8n modules. ```bash pnpm n8n-module-sdk create # prompts for name and stack pnpm n8n-module-sdk create my-feature --stack=frontend +pnpm n8n-module-sdk create my-feature --stack=backend ``` `create` writes `packages/modules//`. @@ -28,11 +29,11 @@ the next `format:check` in CI fails on a module nobody touched by hand. ## Backend -Backend modules can run from a built workspace package at -`packages/modules//backend`. Register the package with a lazy import in -`packages/cli/src/modules/modules.manifest.ts`. The scaffolder does not create -this package shape yet. Use `@n8n/backend-module-insights` as the current -example. +The scaffolder creates a built workspace package at `packages/modules//backend`. It also +adds the runtime dependency to `packages/cli/package.json`, adds a lazy import to +`packages/cli/src/modules/modules.manifest.ts`, and adds the module id to the validated list in +`@n8n/backend-common`. The generated entrypoint is empty. Add only the lifecycle methods that the +module needs. Read `scripts/backend-module/backend-module-guide.md` for backend patterns. ## No build step diff --git a/packages/@n8n/module-cli/frontend-module-guide.md b/packages/@n8n/module-cli/frontend-module-guide.md index ca000ce8f2f..882d4208c59 100644 --- a/packages/@n8n/module-cli/frontend-module-guide.md +++ b/packages/@n8n/module-cli/frontend-module-guide.md @@ -62,7 +62,7 @@ The command prints this output: │ pnpm turbo lint --filter=@n8n/frontend-module-my-feature │ │ pnpm turbo test --filter=@n8n/frontend-module-my-feature │ │ │ - │ Guide: packages/@n8n/module-cli/frontend-module-guide.md │ + │ Frontend: packages/@n8n/module-cli/frontend-module-guide.md │ │ │ ╰───────────────────────────────────────────────────────────────────╯ ``` @@ -90,18 +90,13 @@ dependencies first, and the typecheck then passes. `lint` and `test` pass in bot The CLI makes every edit outside the new package idempotent. You can run the command again after a partial failure. -### `--stack=backend` is a placeholder +### `--stack=backend` creates a separate package -The backend half is a reserved path and a README. **Nothing loads it.** +The backend half is a built workspace package at `packages/modules//backend`. The CLI adds +its runtime dependency and lazy registration to `packages/cli`. It also adds the module id to the +validated list in `@n8n/backend-common`. -The backend runtime reads its modules from `packages/cli/src/modules/`. All 37 real backend -modules are there. For this reason `packages/modules//backend` is not a workspace package. - -To create a backend module that runs, use `pnpm setup-backend-module`. Then obey -`scripts/backend-module/backend-module-guide.md`. - -The CLI prints all of this when you ask for the backend half. This guide repeats it, because it is -the one part of `create` that can mislead you. +Read `scripts/backend-module/backend-module-guide.md` before you add backend behavior. ## File structure diff --git a/packages/@n8n/module-cli/src/backend.mjs b/packages/@n8n/module-cli/src/backend.mjs index 7f17c178b1c..c528090c702 100644 --- a/packages/@n8n/module-cli/src/backend.mjs +++ b/packages/@n8n/module-cli/src/backend.mjs @@ -1,19 +1,105 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { writeTemplates } from './scaffold.mjs'; +import { editLines, lineIndex, repoRoot, ScaffoldError, writeTemplates } from './scaffold.mjs'; const TEMPLATE_DIR = fileURLToPath(new URL('templates/backend', import.meta.url)); -/** - * Writes `packages/modules//backend`. The directory holds a README and no more. - * - * This half is not a workspace package on purpose. The backend runtime reads its modules from - * `packages/cli/src/modules/`. A `package.json` here would add the directory to the - * workspace, to the lockfile and to each walk of the turbo tasks, for code that nothing imports. - * `pnpm setup-backend-module` stays the command for a backend module that runs. - */ -export const createBackend = ({ packageDir, substitutions }) => { - writeTemplates(TEMPLATE_DIR, packageDir, [['README.md.template', 'README.md']], substitutions); +/** The three files each backend registration edits. `root` makes them testable in a fixture. */ +export const backendRegistrationFiles = (root) => ({ + cliPackage: join(root, 'packages', 'cli', 'package.json'), + manifest: join(root, 'packages', 'cli', 'src', 'modules', 'modules.manifest.ts'), + moduleNames: join( + root, + 'packages', + '@n8n', + 'backend-common', + 'src', + 'modules', + 'modules.config.ts', + ), +}); - return { edits: [] }; +/** template file, then the path in the new package. */ +const files = (name) => [ + ['package.json.template', 'package.json'], + ['tsconfig.json.template', 'tsconfig.json'], + ['tsconfig.build.json.template', 'tsconfig.build.json'], + ['oxlint.config.mts.template', 'oxlint.config.mts'], + ['vitest.config.ts.template', 'vitest.config.ts'], + ['README.md.template', 'README.md'], + ['index.ts.template', 'src/index.ts'], + ['module.ts.template', `src/${name}.module.ts`], + ['module.test.ts.template', `src/__tests__/${name}.module.test.ts`], +]; + +/** Writes a built backend package and registers its lazy runtime import. */ +export const createBackend = ({ name, packageDir, substitutions, root = repoRoot }) => { + const packageName = `@n8n/backend-module-${name}`; + const target = backendRegistrationFiles(root); + const moduleNames = readFileSync(target.moduleNames, 'utf8'); + const manifest = readFileSync(target.manifest, 'utf8'); + const isKnownModule = moduleNames.split('\n').some((line) => line.trim() === `'${name}',`); + const isRegisteredPackage = manifest.includes(`'${packageName}/module'`); + + if (isKnownModule && !isRegisteredPackage) { + throw new ScaffoldError( + `The backend module id "${name}" is already registered. Use another module id.`, + ); + } + + writeTemplates(TEMPLATE_DIR, packageDir, files(name), substitutions); + + const edits = []; + const record = (path, note) => edits.push({ path, note }); + + // Add the runtime dependency. Keep the list sorted so `pnpm install` does not move the entry. + if ( + editLines(target.cliPackage, (lines) => { + if (lines.some((line) => line.includes(`"${packageName}"`))) return undefined; + + const entry = ` "${packageName}": "workspace:*",`; + const start = lineIndex(lines, /^\s*"dependencies": \{/, 'cli/package.json') + 1; + let at = start; + while (at < lines.length && !/^\s*\},?\s*$/.test(lines[at])) { + const [, dependency] = /^\s*"([^"]+)":/.exec(lines[at]) ?? []; + if (dependency && dependency > packageName) break; + at++; + } + + lines.splice(at, 0, entry); + return lines; + }) + ) { + record(target.cliPackage, 'cli/package.json (runtime dependency)'); + } + + // Register the package import as a thunk. Disabled modules must not load their package. + if ( + editLines(target.manifest, (lines) => { + if (lines.some((line) => line.includes(`'${packageName}/module'`))) return undefined; + + const closing = lineIndex(lines, /^};/, 'modules.manifest.ts'); + lines.splice(closing, 0, `\t'${name}': async () => await import('${packageName}/module'),`); + return lines; + }) + ) { + record(target.manifest, 'cli/src/modules/modules.manifest.ts (lazy registration)'); + } + + // The config validates N8N_ENABLED_MODULES and supplies the ModuleName union used by the manifest. + if ( + editLines(target.moduleNames, (lines) => { + if (lines.some((line) => line.trim() === `'${name}',`)) return undefined; + + const closing = lineIndex(lines, /^] as const;/, 'modules.config.ts'); + lines.splice(closing, 0, `\t'${name}',`); + return lines; + }) + ) { + record(target.moduleNames, '@n8n/backend-common modules.config.ts (module name)'); + } + + return { packageName, edits }; }; diff --git a/packages/@n8n/module-cli/src/backend.test.mjs b/packages/@n8n/module-cli/src/backend.test.mjs new file mode 100644 index 00000000000..64d2a31c0c6 --- /dev/null +++ b/packages/@n8n/module-cli/src/backend.test.mjs @@ -0,0 +1,129 @@ +import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; + +import { backendRegistrationFiles, createBackend } from './backend.mjs'; +import { repoRoot, substitutionsFor } from './scaffold.mjs'; + +const NAME = 'my-feature'; +const PACKAGE = '@n8n/backend-module-my-feature'; + +const makeFixture = () => { + const root = mkdtempSync(join(tmpdir(), 'module-cli-backend-')); + const real = backendRegistrationFiles(repoRoot); + + for (const [key, file] of Object.entries(backendRegistrationFiles(root))) { + mkdirSync(dirname(file), { recursive: true }); + cpSync(real[key], file); + } + + return root; +}; + +const scaffold = (root, name = NAME) => + createBackend({ + name, + packageDir: join(root, 'packages', 'modules', name, 'backend'), + substitutions: substitutionsFor(name), + root, + }); + +const readRegistrations = (root) => + Object.fromEntries( + Object.entries(backendRegistrationFiles(root)).map(([key, file]) => [ + key, + readFileSync(file, 'utf8'), + ]), + ); + +describe('createBackend', () => { + let root; + + beforeEach(() => { + root = makeFixture(); + }); + + afterEach(() => { + rmSync(root, { recursive: true, force: true }); + }); + + it('writes a built backend package from the templates', () => { + const { packageName } = scaffold(root); + const packageDir = join(root, 'packages', 'modules', NAME, 'backend'); + const written = [ + 'package.json', + 'tsconfig.json', + 'tsconfig.build.json', + 'oxlint.config.mts', + 'vitest.config.ts', + 'README.md', + 'src/index.ts', + `src/${NAME}.module.ts`, + `src/__tests__/${NAME}.module.test.ts`, + ]; + + expect(packageName).toBe(PACKAGE); + for (const file of written) { + const body = readFileSync(join(packageDir, file), 'utf8'); + expect({ file, placeholders: body.match(/\{\{\w+\}\}/g) }).toEqual({ + file, + placeholders: null, + }); + } + + const manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8')); + expect(manifest.name).toBe(PACKAGE); + expect(manifest.exports['./module'].import).toBe(`./dist/${NAME}.module.js`); + expect(readFileSync(join(packageDir, `src/${NAME}.module.ts`), 'utf8')).toContain( + "@BackendModule({ name: 'my-feature' })", + ); + }); + + it('registers the package with the backend runtime', () => { + const { edits } = scaffold(root); + const registered = readRegistrations(root); + + expect(edits.map((edit) => edit.note)).toEqual([ + 'cli/package.json (runtime dependency)', + 'cli/src/modules/modules.manifest.ts (lazy registration)', + '@n8n/backend-common modules.config.ts (module name)', + ]); + expect(registered.cliPackage).toContain(`"${PACKAGE}": "workspace:*",`); + expect(registered.manifest).toContain( + `'${NAME}': async () => await import('${PACKAGE}/module'),`, + ); + expect(registered.moduleNames).toContain(`\t'${NAME}',`); + }); + + it('adds nothing to registration files on a second run', () => { + scaffold(root); + const afterFirst = readRegistrations(root); + + const { edits } = scaffold(root); + + expect(edits).toEqual([]); + expect(readRegistrations(root)).toEqual(afterFirst); + }); + + it('puts the CLI dependency between its sorted neighbours', () => { + scaffold(root); + const manifest = JSON.parse(readRegistrations(root).cliPackage); + const dependencies = Object.keys(manifest.dependencies); + const at = dependencies.indexOf(PACKAGE); + + expect(at).toBeGreaterThan(0); + expect(dependencies[at - 1] < PACKAGE).toBe(true); + expect(dependencies[at + 1] > PACKAGE).toBe(true); + }); + + it('rejects an id that belongs to a registered backend module', () => { + const before = readRegistrations(root); + const packageDir = join(root, 'packages', 'modules', 'agents', 'backend'); + + expect(() => scaffold(root, 'agents')).toThrow( + 'The backend module id "agents" is already registered. Use another module id.', + ); + expect(existsSync(packageDir)).toBe(false); + expect(readRegistrations(root)).toEqual(before); + }); +}); diff --git a/packages/@n8n/module-cli/src/main.mjs b/packages/@n8n/module-cli/src/main.mjs index c3b3be8c0c2..a954352e731 100644 --- a/packages/@n8n/module-cli/src/main.mjs +++ b/packages/@n8n/module-cli/src/main.mjs @@ -35,7 +35,7 @@ const scaffold = async (args) => { type: 'select', options: [ { value: 'frontend', label: 'frontend', hint: 'a real, resolvable module package' }, - { value: 'backend', label: 'backend', hint: 'placeholder only, nothing loads it' }, + { value: 'backend', label: 'backend', hint: 'a built backend module package' }, { value: 'both', label: 'both' }, ], cancel: 'reject', @@ -54,7 +54,7 @@ const scaffold = async (args) => { const wants = (side) => stack === side || stack === 'both'; const created = []; let edits = []; - let packageName; + const packageNames = []; if (wants('frontend')) { const result = createFrontend({ @@ -62,14 +62,20 @@ const scaffold = async (args) => { packageDir: join(moduleDir, 'frontend'), substitutions, }); - packageName = result.packageName; - edits = result.edits; + packageNames.push(result.packageName); + edits.push(...result.edits); created.push(`packages/modules/${name}/frontend → ${result.packageName}`); } if (wants('backend')) { - createBackend({ packageDir: join(moduleDir, 'backend'), substitutions }); - created.push(`packages/modules/${name}/backend → placeholder, see its README`); + const result = createBackend({ + name, + packageDir: join(moduleDir, 'backend'), + substitutions, + }); + packageNames.push(result.packageName); + edits.push(...result.edits); + created.push(`packages/modules/${name}/backend → ${result.packageName}`); } const formatted = formatFiles([moduleDir, ...edits.map((edit) => edit.path)]); @@ -82,28 +88,24 @@ const scaffold = async (args) => { consola.warn('Biome did not run. Run `pnpm install`, then `pnpm format`.'); } - if (wants('backend')) { - consola.warn( - 'The backend half is a placeholder. Nothing loads it: the runtime reads backend\n' + - 'modules from packages/cli/src/modules/. Use `pnpm setup-backend-module`\n' + - 'for a backend module that runs.', - ); - } - // `pnpm --filter typecheck` fails on a cold tree. The dependencies of the package are not // built yet, and that command builds none of them. Turbo builds them first. consola.box( - wants('frontend') - ? [ - 'Next:', - ' pnpm install', - ` pnpm turbo typecheck --filter=${packageName}`, - ` pnpm turbo lint --filter=${packageName}`, - ` pnpm turbo test --filter=${packageName}`, - '', - ' Guide: packages/@n8n/module-cli/frontend-module-guide.md', - ].join('\n') - : ['Next:', ` read packages/modules/${name}/backend/README.md`].join('\n'), + [ + 'Next:', + ' pnpm install', + ...packageNames.flatMap((packageName) => [ + ` pnpm turbo typecheck --filter=${packageName}`, + ` pnpm turbo lint --filter=${packageName}`, + ` pnpm turbo test --filter=${packageName}`, + ]), + ...(wants('frontend') + ? ['', ' Frontend: packages/@n8n/module-cli/frontend-module-guide.md'] + : []), + ...(wants('backend') + ? ['', ' Backend guide: scripts/backend-module/backend-module-guide.md'] + : []), + ].join('\n'), ); }; diff --git a/packages/@n8n/module-cli/src/templates/backend/README.md.template b/packages/@n8n/module-cli/src/templates/backend/README.md.template index ba8e0dc1fc6..17db2ef5c76 100644 --- a/packages/@n8n/module-cli/src/templates/backend/README.md.template +++ b/packages/@n8n/module-cli/src/templates/backend/README.md.template @@ -1,19 +1,25 @@ -# {{TitleName}} — backend (placeholder) +# @n8n/backend-module-{{name}} -> **Nothing loads this directory.** The backend runtime discovers modules under -> `packages/cli/src/modules/`, where every real backend module lives. -> This is a reserved path for the day frontend and backend halves of a module -> sit side by side — it is not wired to anything today. - -To create a backend module that actually runs: +Backend feature module. The n8n runtime loads the module through the lazy import in +`packages/cli/src/modules/modules.manifest.ts`. ```bash -pnpm setup-backend-module +pnpm turbo typecheck --filter=@n8n/backend-module-{{name}} +pnpm turbo lint --filter=@n8n/backend-module-{{name}} +pnpm turbo test --filter=@n8n/backend-module-{{name}} ``` -That writes `packages/cli/src/modules/`; see -`scripts/backend-module/backend-module-guide.md`. +Go through Turbo. It builds this package's workspace dependencies first. -Keeping the `id` here identical to the frontend half (`{{name}}`) is what will -make the two sides one module later — both gate off the same -`/rest/module-settings` entry. +Read `scripts/backend-module/backend-module-guide.md` for the module lifecycle, REST controller, +service, repository, entity, configuration, and testing patterns. + +## Import rules + +- Do not import from `packages/cli` or from the `n8n` package. +- Declare each workspace and third-party import in this package. +- Export APIs for other packages from `src/index.ts`. +- Keep the module entrypoint in the `./module` export. Do not export it from `src/index.ts`. + Importing the entrypoint runs the `@BackendModule` decorator. +- Use dynamic imports inside the module entrypoint. This keeps disabled modules unloaded. +- Keep TypeORM imports in entity and repository files. diff --git a/packages/@n8n/module-cli/src/templates/backend/index.ts.template b/packages/@n8n/module-cli/src/templates/backend/index.ts.template new file mode 100644 index 00000000000..7a30cae505d --- /dev/null +++ b/packages/@n8n/module-cli/src/templates/backend/index.ts.template @@ -0,0 +1,2 @@ +// Export the public API here. Do not export {{PascalName}}Module from this file. +export {}; diff --git a/packages/@n8n/module-cli/src/templates/backend/module.test.ts.template b/packages/@n8n/module-cli/src/templates/backend/module.test.ts.template new file mode 100644 index 00000000000..1810cbf21ca --- /dev/null +++ b/packages/@n8n/module-cli/src/templates/backend/module.test.ts.template @@ -0,0 +1,10 @@ +import { ModuleMetadata } from '@n8n/decorators'; +import { Container } from '@n8n/di'; + +import { {{PascalName}}Module } from '../{{name}}.module'; + +describe('{{PascalName}}Module', () => { + it('registers the module entrypoint', () => { + expect(Container.get(ModuleMetadata).get('{{name}}')?.class).toBe({{PascalName}}Module); + }); +}); diff --git a/packages/@n8n/module-cli/src/templates/backend/module.ts.template b/packages/@n8n/module-cli/src/templates/backend/module.ts.template new file mode 100644 index 00000000000..ccd1e90dd09 --- /dev/null +++ b/packages/@n8n/module-cli/src/templates/backend/module.ts.template @@ -0,0 +1,5 @@ +import type { ModuleInterface } from '@n8n/decorators'; +import { BackendModule } from '@n8n/decorators'; + +@BackendModule({ name: '{{name}}' }) +export class {{PascalName}}Module implements ModuleInterface {} diff --git a/packages/@n8n/module-cli/src/templates/backend/oxlint.config.mts.template b/packages/@n8n/module-cli/src/templates/backend/oxlint.config.mts.template new file mode 100644 index 00000000000..23e1fdacee5 --- /dev/null +++ b/packages/@n8n/module-cli/src/templates/backend/oxlint.config.mts.template @@ -0,0 +1,36 @@ +import { backendConfig } from '@n8n/oxlint-config/backend'; +import { defineConfig } from 'oxlint'; + +export default defineConfig({ + extends: [backendConfig], + options: { typeAware: true }, + ignorePatterns: ['coverage/**', 'dist/**'], + rules: { + 'n8n-local-rules/misplaced-n8n-typeorm-import': 'error', + 'n8n-local-rules/no-guardrail-disable': [ + 'error', + { + guarded: [ + { + rule: 'misplaced-n8n-typeorm-import', + message: + 'Keep TypeORM in the persistence layer. Put the query behind a use-case repository method.', + }, + ], + }, + ], + }, + overrides: [ + { + files: [ + './src/**/*.entity.ts', + './src/**/*.repository.ts', + './src/**/__tests__/**/*.ts', + './src/**/*.test.ts', + ], + rules: { + 'n8n-local-rules/misplaced-n8n-typeorm-import': 'off', + }, + }, + ], +}); diff --git a/packages/@n8n/module-cli/src/templates/backend/package.json.template b/packages/@n8n/module-cli/src/templates/backend/package.json.template new file mode 100644 index 00000000000..b96d8ea95af --- /dev/null +++ b/packages/@n8n/module-cli/src/templates/backend/package.json.template @@ -0,0 +1,51 @@ +{ + "name": "@n8n/backend-module-{{name}}", + "version": "0.1.0", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.js" + }, + "./module": { + "types": "./dist/{{name}}.module.d.ts", + "import": "./dist/{{name}}.module.js", + "require": "./dist/{{name}}.module.js" + } + }, + "files": [ + "dist/**/*" + ], + "scripts": { + "clean": "rimraf dist .turbo", + "dev": "pnpm watch", + "typecheck": "tsc --noEmit", + "build": "tsc -p tsconfig.build.json", + "build:unchecked": "tsc -p tsconfig.build.json --noCheck", + "format": "biome format --write .", + "format:check": "biome ci .", + "lint": "oxlint --type-aware . --quiet", + "lint:fix": "oxlint --type-aware . --fix", + "watch": "tsc -p tsconfig.build.json --watch", + "test": "vitest run", + "test:unit": "vitest run", + "test:dev": "vitest --silent=false" + }, + "dependencies": { + "@n8n/decorators": "workspace:*" + }, + "devDependencies": { + "@n8n/di": "workspace:*", + "@n8n/oxlint-config": "workspace:*", + "@n8n/typescript-config": "workspace:*", + "@n8n/vitest-config": "workspace:*", + "oxlint": "catalog:", + "oxlint-tsgolint": "catalog:", + "typescript": "catalog:typescript", + "vitest": "catalog:" + }, + "license": "LicenseRef-n8n-sustainable-use" +} diff --git a/packages/@n8n/module-cli/src/templates/backend/tsconfig.build.json.template b/packages/@n8n/module-cli/src/templates/backend/tsconfig.build.json.template new file mode 100644 index 00000000000..ac376c250d1 --- /dev/null +++ b/packages/@n8n/module-cli/src/templates/backend/tsconfig.build.json.template @@ -0,0 +1,10 @@ +{ + "extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.go.json"], + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/__tests__/**", "src/**/*.test.ts"] +} diff --git a/packages/@n8n/module-cli/src/templates/backend/tsconfig.json.template b/packages/@n8n/module-cli/src/templates/backend/tsconfig.json.template new file mode 100644 index 00000000000..c13182acfb0 --- /dev/null +++ b/packages/@n8n/module-cli/src/templates/backend/tsconfig.json.template @@ -0,0 +1,17 @@ +{ + "extends": [ + "@n8n/typescript-config/tsconfig.common.go.json", + "@n8n/typescript-config/tsconfig.backend.go.json" + ], + "compilerOptions": { + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": ["es2023", "dom"], + "types": ["node", "vitest/globals"], + "strict": true, + "strictFunctionTypes": false, + "strictPropertyInitialization": false, + "useUnknownInCatchVariables": false + }, + "include": ["src/**/*.ts", "vitest.config.ts"] +} diff --git a/packages/@n8n/module-cli/src/templates/backend/vitest.config.ts.template b/packages/@n8n/module-cli/src/templates/backend/vitest.config.ts.template new file mode 100644 index 00000000000..6deb88e8067 --- /dev/null +++ b/packages/@n8n/module-cli/src/templates/backend/vitest.config.ts.template @@ -0,0 +1,3 @@ +import { createVitestConfigWithDecorators } from '@n8n/vitest-config/node-decorators'; + +export default createVitestConfigWithDecorators({}, { pinCjs: ['@n8n/decorators'] }); diff --git a/scripts/backend-module/backend-module-guide.md b/scripts/backend-module/backend-module-guide.md index 855936523e6..6428d66403e 100644 --- a/scripts/backend-module/backend-module-guide.md +++ b/scripts/backend-module/backend-module-guide.md @@ -12,15 +12,18 @@ Benefits of modularity: ## File structure -To set up a backend module, run this command at monorepo root: +To set up a packaged backend module, run this command at the monorepo root: ```sh -pnpm setup-backend-module +pnpm n8n-module-sdk create my-feature --stack=backend ``` -The setup command creates a module at `packages/cli/src/modules/my-feature`. Rename `my-feature` in the directory name and file names. Use kebab-case. +The command creates a built workspace package at `packages/modules/my-feature/backend`. It also +adds the package to the backend runtime manifest. -A decoupled module can instead use a built workspace package at `packages/modules/my-feature/backend`. The package name uses the `@n8n/backend-module-` pattern. Add a lazy package import to `packages/cli/src/modules/modules.manifest.ts`. Add the package as a runtime dependency of `n8n`. +Use `pnpm setup-backend-module` only when the module must stay in +`packages/cli/src/modules/my-feature`. Rename `my-feature` in the generated directory and file +names. Use kebab-case. A module’s file structure is as follows: