Ship the three C4 examples and model:scaffold --example
- examples/: c4-standard-web-app, c4-e-shop (was online-shop), c4-event-based-integration; the same models the extension offers under Archify | New Model… - model-scaffold --example <name> copies one and renames id, name, description and the overview title; never overwrites - tests cover every example and the copy; docs updated
This commit is contained in:
1 parent
b6b5e57922
commit
55894fbade
27 files changed
+538
-17
No files matched your search
@@ -1,17 +1,31 @@
|
||||
// Creates a model: main.archify with the model block, and with --c4 an
|
||||
// overview diagram, empty registries and the C4 folders with starter
|
||||
// diagrams that validate. Never overwrites existing files.
|
||||
// diagrams that validate; or, with --example, a copy of one of the shipped
|
||||
// example models (the same three the VS Code extension offers under
|
||||
// "Archify | New Model…"). Never overwrites existing files.
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { ID, MAIN } from '../../services/model.mjs';
|
||||
|
||||
export const help = `model-scaffold — create main.archify (and with --c4 the C4 folders)
|
||||
const EXAMPLES_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..', 'examples');
|
||||
|
||||
/** The shipped examples: folder under examples/ → what it shows. */
|
||||
export const EXAMPLES = {
|
||||
'c4-standard-web-app': 'C4 - Standard Web App: single-page app, API, database, cache, file storage, identity provider',
|
||||
'c4-e-shop': 'C4 - E-Shop: web shop, API, worker, queue, payment and email providers',
|
||||
'c4-event-based-integration': 'C4 - Event-Based Integration: event bus, router, dead-letter queue, event store, consumers',
|
||||
};
|
||||
|
||||
export const help = `model-scaffold — create main.archify (and with --c4 the C4 folders), or copy an example
|
||||
--dir <path> folder of the model; created when missing (required)
|
||||
--id <id> model id, letters/digits/-/_ (required)
|
||||
--name <text> model name (required)
|
||||
--id <id> model id, letters/digits/-/_ (required unless --example)
|
||||
--name <text> model name (required unless --example)
|
||||
--description <text> model description (optional)
|
||||
--c4 also create registry/, 0100-context, 0200-containers, 0300-components`;
|
||||
--c4 also create registry/, 0100-context, 0200-containers, 0300-components
|
||||
--example <name> copy a complete example instead: ${Object.keys(EXAMPLES).join(' | ')}
|
||||
(id, name and description default to the example's own)`;
|
||||
|
||||
function block(text) {
|
||||
// YAML folded scalar: safe for colons and long sentences.
|
||||
@@ -168,9 +182,65 @@ connections:
|
||||
- { from: orders, to: db, label: SQL }
|
||||
`;
|
||||
|
||||
function listFiles(dir, rel = '') {
|
||||
return fs.readdirSync(path.join(dir, rel), { withFileTypes: true }).flatMap((entry) => {
|
||||
const next = rel ? `${rel}/${entry.name}` : entry.name;
|
||||
return entry.isDirectory() ? listFiles(dir, next) : [next];
|
||||
});
|
||||
}
|
||||
|
||||
/** The example's main.archify with id, name, description and overview title replaced when given. */
|
||||
function mainFromExample(text, { id, name, description }) {
|
||||
let out = text;
|
||||
if (id) out = out.replace(/^( id: ).*$/m, `$1${id}`);
|
||||
if (name) {
|
||||
out = out.replace(/^( name: ).*$/m, `$1${JSON.stringify(name)}`);
|
||||
out = out.replace(/^( title: ).*$/m, `$1${JSON.stringify(`${name} — overview`)}`);
|
||||
}
|
||||
if (description) out = out.replace(/^ description: >\n(?: .*\n)+/m, ` description: ${block(description)}\n`);
|
||||
return out;
|
||||
}
|
||||
|
||||
async function copyExample(input) {
|
||||
const example = String(input.example);
|
||||
if (!EXAMPLES[example]) return { success: false, error: `Unknown example "${example}". Choose one of: ${Object.keys(EXAMPLES).join(', ')}.`, examples: EXAMPLES };
|
||||
const source = path.join(EXAMPLES_DIR, example);
|
||||
const dir = path.resolve(String(input.dir));
|
||||
const id = input.id && input.id !== true ? String(input.id) : undefined;
|
||||
if (id && !ID.test(id)) return { success: false, error: `--id "${id}" must match ${ID} (letters, digits, - and _; starts with a letter).` };
|
||||
const name = input.name && input.name !== true ? String(input.name) : undefined;
|
||||
const description = input.description && input.description !== true ? String(input.description) : undefined;
|
||||
|
||||
const files = listFiles(source);
|
||||
const existing = files.filter((file) => fs.existsSync(path.join(dir, file)));
|
||||
if (existing.length) return { success: false, error: `Refusing to overwrite existing files in ${dir}.`, existing };
|
||||
if (fs.existsSync(dir) && !fs.statSync(dir).isDirectory()) return { success: false, error: `${dir} is not a folder.` };
|
||||
for (const file of files) {
|
||||
const full = path.join(dir, file);
|
||||
fs.mkdirSync(path.dirname(full), { recursive: true });
|
||||
const text = fs.readFileSync(path.join(source, file), 'utf8');
|
||||
fs.writeFileSync(full, file === MAIN ? mainFromExample(text, { id, name, description }) : text);
|
||||
}
|
||||
const main = fs.readFileSync(path.join(dir, MAIN), 'utf8');
|
||||
return {
|
||||
success: true,
|
||||
root: dir,
|
||||
example,
|
||||
model: { id: /^ id: (.*)$/m.exec(main)?.[1], name: JSON.parse(/^ name: (".*"|.*)$/m.exec(main)?.[1].replace(/^([^"].*)$/, '"$1"') ?? '""') },
|
||||
created: files,
|
||||
nextSteps: [
|
||||
`task model:validate -- --dir="${dir}"`,
|
||||
'Rename, add or remove elements in registry/*.archify-registry and the diagrams; keep "ref:" pointing at registry ids.',
|
||||
`Open ${path.join(dir, MAIN)} in VS Code with the Archify Diagram Viewer extension to see the model page.`,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export async function run(input) {
|
||||
const missing = ['dir', 'id', 'name'].filter((key) => !input[key] || input[key] === true);
|
||||
if (missing.length) return { success: false, error: `Missing ${missing.map((key) => `--${key}`).join(', ')}.`, help };
|
||||
if (!input.dir || input.dir === true) return { success: false, error: 'Missing --dir.', help };
|
||||
if (input.example) return copyExample(input);
|
||||
const missing = ['id', 'name'].filter((key) => !input[key] || input[key] === true);
|
||||
if (missing.length) return { success: false, error: `Missing ${missing.map((key) => `--${key}`).join(', ')} (or pass --example).`, help };
|
||||
const id = String(input.id);
|
||||
if (!ID.test(id)) return { success: false, error: `--id "${id}" must match ${ID} (letters, digits, - and _; starts with a letter).` };
|
||||
const name = String(input.name);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
// The skill's own checks. Runs every action against examples/online-shop and
|
||||
// The skill's own checks. Runs every action against examples/c4-e-shop and
|
||||
// a scaffolded temp model. Prints YAML (success first); exit 1 on any failure.
|
||||
|
||||
import fs from 'node:fs';
|
||||
@@ -11,7 +11,7 @@ import { toYaml } from '../services/yaml.mjs';
|
||||
|
||||
const scripts = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
const skill = path.resolve(scripts, '..');
|
||||
const example = path.join(skill, 'examples', 'online-shop');
|
||||
const example = path.join(skill, 'examples', 'c4-e-shop');
|
||||
const cli = path.join(scripts, 'src', 'cli', 'commands.mjs');
|
||||
const checks = [];
|
||||
|
||||
@@ -168,6 +168,36 @@ try {
|
||||
check('plain model validates (model block only)', ['--action', 'model-validate', '--dir', plain], (r) => {
|
||||
assert(r.status === 0, `exit ${r.status}`);
|
||||
});
|
||||
|
||||
// The shipped examples: the same three the extension offers under "Archify | New Model…".
|
||||
const examplesDir = path.join(skill, 'examples');
|
||||
const exampleNames = fs.readdirSync(examplesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
||||
check('examples/ holds exactly the three C4 examples', [], () => {
|
||||
assert(exampleNames.join(',') === 'c4-e-shop,c4-event-based-integration,c4-standard-web-app', `unexpected examples: ${exampleNames.join(',')}`);
|
||||
});
|
||||
for (const name of exampleNames) {
|
||||
check(`example ${name} validates with 0 problems`, ['--action', 'model-validate', '--dir', path.join(examplesDir, name)], (r) => {
|
||||
assert(r.status === 0, `exit ${r.status}`);
|
||||
assert(yamlValue(r.stdout, ' errors') === '0' && yamlValue(r.stdout, ' warnings') === '0', 'example has problems');
|
||||
assert(yamlValue(r.stdout, ' diagrams') === '4', 'expected 4 diagrams');
|
||||
});
|
||||
}
|
||||
const fromExample = path.join(tmp, 'from-example');
|
||||
check('model-scaffold --example copies and renames an example', ['--action', 'model-scaffold', '--dir', fromExample, '--example', 'c4-event-based-integration', '--id', 'hub', '--name', 'Integration Hub', '--description', 'Our hub: events between systems'], (r) => {
|
||||
assert(r.status === 0, `exit ${r.status}`);
|
||||
const main = fs.readFileSync(path.join(fromExample, 'main.archify'), 'utf8');
|
||||
assert(/^ id: hub$/m.test(main) && /^ name: "Integration Hub"$/m.test(main) && /Our hub: events between systems/.test(main) && /Integration Hub — overview/.test(main), `main.archify not renamed:\n${main}`);
|
||||
assert(fs.existsSync(path.join(fromExample, 'registry', 'containers.archify-registry')) && fs.existsSync(path.join(fromExample, '0300-components', 'router.archify')), 'example files missing');
|
||||
});
|
||||
check('copied example validates', ['--action', 'model-validate', '--dir', fromExample], (r) => {
|
||||
assert(r.status === 0, `exit ${r.status}`);
|
||||
assert(yamlValue(r.stdout, ' errors') === '0' && yamlValue(r.stdout, ' warnings') === '0', 'copied example has problems');
|
||||
});
|
||||
check('model-scaffold --example refuses to overwrite and rejects unknown names', ['--action', 'model-scaffold', '--dir', fromExample, '--example', 'c4-e-shop'], (r) => {
|
||||
assert(r.status === 1 && /Refusing to overwrite/.test(r.stdout), 'no refusal');
|
||||
const unknown = cliRun(['--action', 'model-scaffold', '--dir', path.join(tmp, 'x'), '--example', 'nope']);
|
||||
assert(unknown.status === 1 && /Unknown example/.test(unknown.stdout), 'unknown example accepted');
|
||||
});
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user