mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-19 16:55:02 +02:00
Extends the starter refresh to sync everything n8n-node new generates at the project root (CI/publish workflows, .agents/, AGENTS.md, CLAUDE.md, .gitignore, tooling config) instead of a hand-picked file list, since that list already needed maintenance (ci.yml and publish.yml were templated but excluded) and would keep needing it as node-cli's shared template grows. Syncing directories wholesale would have deleted starter-specific extras living alongside templated files in the same directories (.github/dependabot.yml, .github/workflows/ci-cla-check.yml, .vscode/extensions.json) — caught by re-running the script against a real clone before committing. Switched to a non-destructive merge for anything outside the TEMPLATES' own syncDirs, which only the template-owned nodes/credentials/icons directories still use. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
159 lines
5.8 KiB
JavaScript
159 lines
5.8 KiB
JavaScript
/**
|
|
* Regenerates n8n-nodes-starter's example scaffolding from the `@n8n/node-cli`
|
|
* templates in this repo, built from source rather than installed from npm —
|
|
* so a template change here surfaces as scaffold drift before it ships.
|
|
*
|
|
* n8n-nodes-starter's example content is a composite of two separately
|
|
* generated templates:
|
|
* - nodes/Example/ <- programmatic/example
|
|
* - nodes/GithubIssues/, credentials/, icons/ <- declarative/github-issues
|
|
*
|
|
* Everything else at the project root (tsconfig.json, eslint/prettier config,
|
|
* .vscode/, .gitignore, .github/workflows/{ci,publish}.yml, .agents/,
|
|
* AGENTS.md, CLAUDE.md, ...) is synced wholesale from the first template's
|
|
* generated output, since none of it carries repo-specific customization.
|
|
* `DENYLIST` below is the explicit exception list: files that must diverge
|
|
* per-repo (README, CHANGELOG) or that get narrower, field-level handling
|
|
* (package.json) instead of a blind overwrite.
|
|
*
|
|
* Usage: node refresh-starter-scaffold.mjs --starter-root <path>
|
|
*/
|
|
|
|
import { execFileSync } from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { parseArgs } from 'node:util';
|
|
|
|
const {
|
|
values: { 'starter-root': starterRootArg },
|
|
} = parseArgs({ options: { 'starter-root': { type: 'string' } } });
|
|
|
|
if (!starterRootArg) {
|
|
console.error('Usage: refresh-starter-scaffold.mjs --starter-root <path>');
|
|
process.exit(1);
|
|
}
|
|
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..');
|
|
const nodeCliBin = path.join(repoRoot, 'packages/@n8n/node-cli/bin/n8n-node.mjs');
|
|
const starterRoot = path.resolve(starterRootArg);
|
|
|
|
const TEMPLATES = [
|
|
{
|
|
// Package name matches what's already committed in n8n-nodes-starter's
|
|
// nodes/Example/Example.node.json ("node" field) — using anything else
|
|
// would make that field flip on every refresh for no real reason.
|
|
template: 'programmatic/example',
|
|
pkgName: 'n8n-nodes-example',
|
|
syncDirs: ['nodes/Example'],
|
|
},
|
|
{
|
|
// Same idea, matching nodes/GithubIssues/GithubIssues.node.json.
|
|
template: 'declarative/github-issues',
|
|
pkgName: 'n8n-nodes-github-issues',
|
|
syncDirs: ['nodes/GithubIssues', 'credentials', 'icons'],
|
|
},
|
|
];
|
|
|
|
// Top-level entries in the first template's generated output that are NOT
|
|
// synced wholesale:
|
|
// - nodes/, credentials/, icons/ — handled per-entry above via `syncDirs`
|
|
// - README.md — repo-specific, hand-maintained
|
|
// - CHANGELOG.md — accumulates real release history per-repo
|
|
// - package.json — only its `scripts` block is synced, below
|
|
// - .git — `n8n-node new` inits its own repo per run
|
|
const DENYLIST = new Set([
|
|
'nodes',
|
|
'credentials',
|
|
'icons',
|
|
'README.md',
|
|
'CHANGELOG.md',
|
|
'package.json',
|
|
'.git',
|
|
]);
|
|
|
|
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'n8n-node-scaffold-'));
|
|
|
|
function generate({ template, pkgName }) {
|
|
const cwd = path.join(tmpRoot, pkgName);
|
|
fs.mkdirSync(cwd, { recursive: true });
|
|
console.log(`Running n8n-node new --template ${template}`);
|
|
execFileSync(
|
|
process.execPath,
|
|
[nodeCliBin, 'new', pkgName, '--skip-install', '--force', '--template', template],
|
|
{ cwd, env: { ...process.env, CI: 'true' }, stdio: 'inherit' },
|
|
);
|
|
return path.join(cwd, pkgName);
|
|
}
|
|
|
|
function syncDir(src, dest) {
|
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
fs.cpSync(src, dest, { recursive: true });
|
|
console.log(`Synced ${path.relative(starterRoot, dest)}`);
|
|
}
|
|
|
|
function syncFile(src, dest) {
|
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
fs.copyFileSync(src, dest);
|
|
console.log(`Synced ${path.relative(starterRoot, dest)}`);
|
|
}
|
|
|
|
// Unlike `syncDir` (used for nodes/Example, nodes/GithubIssues, credentials,
|
|
// icons — directories the template owns entirely), these top-level
|
|
// directories can carry starter-specific extras the template doesn't know
|
|
// about (e.g. .github/dependabot.yml, .github/workflows/ci-cla-check.yml,
|
|
// .vscode/extensions.json). A destructive replace would delete those, so
|
|
// this only adds/overwrites the files the template actually contributes.
|
|
function mergeDir(src, dest) {
|
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
const s = path.join(src, entry.name);
|
|
const d = path.join(dest, entry.name);
|
|
if (entry.isDirectory()) {
|
|
fs.mkdirSync(d, { recursive: true });
|
|
mergeDir(s, d);
|
|
} else {
|
|
syncFile(s, d);
|
|
}
|
|
}
|
|
}
|
|
|
|
function syncEverythingElse(generatedRoot) {
|
|
for (const entry of fs.readdirSync(generatedRoot, { withFileTypes: true })) {
|
|
if (DENYLIST.has(entry.name)) continue;
|
|
|
|
const src = path.join(generatedRoot, entry.name);
|
|
const dest = path.join(starterRoot, entry.name);
|
|
if (entry.isDirectory()) {
|
|
mergeDir(src, dest);
|
|
} else {
|
|
syncFile(src, dest);
|
|
}
|
|
}
|
|
}
|
|
|
|
let firstGeneratedRoot;
|
|
|
|
for (const entry of TEMPLATES) {
|
|
const generatedRoot = generate(entry);
|
|
firstGeneratedRoot ??= generatedRoot;
|
|
|
|
for (const relDir of entry.syncDirs) {
|
|
syncDir(path.join(generatedRoot, relDir), path.join(starterRoot, relDir));
|
|
}
|
|
}
|
|
|
|
syncEverythingElse(firstGeneratedRoot);
|
|
|
|
// Only the `scripts` block is refreshed — every other field (name, version,
|
|
// author, repository, the n8n nodes/credentials manifest, devDependency
|
|
// pins, ...) is repo-specific or already kept current by Dependabot.
|
|
const generatedPkg = JSON.parse(fs.readFileSync(path.join(firstGeneratedRoot, 'package.json'), 'utf8'));
|
|
const starterPkgPath = path.join(starterRoot, 'package.json');
|
|
const starterPkg = JSON.parse(fs.readFileSync(starterPkgPath, 'utf8'));
|
|
starterPkg.scripts = generatedPkg.scripts;
|
|
fs.writeFileSync(starterPkgPath, `${JSON.stringify(starterPkg, null, '\t')}\n`);
|
|
console.log('Synced package.json scripts');
|
|
|
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|