mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-11 22:50:06 +00:00
feat(core): Add backend module scaffolding (no-changelog) (#40482)
This commit is contained in:
@@ -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/<name>/<frontend|backend>`.
|
||||
@@ -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/<name>/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/<name>/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
|
||||
|
||||
|
||||
@@ -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/<name>/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/<name>`. All 37 real backend
|
||||
modules are there. For this reason `packages/modules/<name>/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
|
||||
|
||||
|
||||
@@ -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/<name>/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/<name>`. 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 };
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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/<name>. Use `pnpm setup-backend-module`\n' +
|
||||
'for a backend module that runs.',
|
||||
);
|
||||
}
|
||||
|
||||
// `pnpm --filter <pkg> 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'),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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/<name>`, 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/<name>`; 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.
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
// Export the public API here. Do not export {{PascalName}}Module from this file.
|
||||
export {};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { ModuleInterface } from '@n8n/decorators';
|
||||
import { BackendModule } from '@n8n/decorators';
|
||||
|
||||
@BackendModule({ name: '{{name}}' })
|
||||
export class {{PascalName}}Module implements ModuleInterface {}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createVitestConfigWithDecorators } from '@n8n/vitest-config/node-decorators';
|
||||
|
||||
export default createVitestConfigWithDecorators({}, { pinCjs: ['@n8n/decorators'] });
|
||||
@@ -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-<name>` 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:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user