@atelier-ui/create-workspace 0.2.41 → 0.2.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -4
- package/package.json +1 -1
- package/src/generators/preset/files/contracts/README.md +29 -0
- package/src/generators/preset/files/contracts/button.contract.ts.template +20 -0
- package/src/generators/preset/files/contracts/types.ts.template +55 -0
- package/src/generators/preset/files/figma/snapshot.json +164 -0
- package/src/generators/preset/files/storybook/angular/atl-button.stories.ts.template +38 -0
- package/src/generators/preset/files/storybook/angular/main.ts.template +39 -0
- package/src/generators/preset/files/storybook/angular/preview.ts.template +30 -0
- package/src/generators/preset/files/storybook/angular/tsconfig.json +16 -0
- package/src/generators/preset/files/storybook/angular/vitest.config.ts.template +40 -0
- package/src/generators/preset/files/storybook/angular/vitest.setup.ts.template +14 -0
- package/src/generators/preset/files/storybook/react/atl-button.stories.tsx +31 -0
- package/src/generators/preset/files/storybook/react/main.ts.template +37 -0
- package/src/generators/preset/files/storybook/react/preview.tsx +29 -0
- package/src/generators/preset/files/storybook/react/vitest.config.ts.template +32 -0
- package/src/generators/preset/files/storybook/react/vitest.setup.ts.template +8 -0
- package/src/generators/preset/files/storybook/vue/atl-button.stories.ts.template +34 -0
- package/src/generators/preset/files/storybook/vue/main.ts.template +38 -0
- package/src/generators/preset/files/storybook/vue/preview.ts.template +29 -0
- package/src/generators/preset/files/storybook/vue/vitest.config.ts.template +32 -0
- package/src/generators/preset/files/storybook/vue/vitest.setup.ts.template +9 -0
- package/src/generators/preset/files/styles/tokens.css +59 -43
- package/src/generators/preset/files/tools/scripts/check-contracts.mjs +1644 -0
- package/src/generators/preset/files/tools/scripts/figma-snapshot-contracts.mjs +370 -0
- package/src/generators/preset/files/tools/scripts/lib/docgen.mjs +573 -0
- package/src/generators/preset/files/tools/scripts/lib/ts-eval.js +126 -0
- package/src/generators/preset/files/tools/scripts/preflight.mjs +220 -30
- package/src/generators/preset/files/tools/stylelint-rules/index.js +21 -0
- package/src/generators/preset/files/tools/stylelint-rules/no-primitive-token.js +362 -0
- package/src/generators/preset/files/tools/stylelint-rules/no-raw-color-literal.js +154 -0
- package/src/generators/preset/files/tools/stylelint-rules/no-token-bypass.js +497 -0
- package/src/generators/preset/files/tools/stylelint-rules/no-undeclared-token.js +122 -0
- package/src/generators/preset/files/tools/stylelint-rules/utils.js +71 -0
- package/src/generators/preset/preset.d.ts +1 -0
- package/src/generators/preset/preset.js +955 -5
- package/src/generators/preset/preset.js.map +1 -1
- package/src/generators/preset/schema.d.ts +1 -0
- package/src/generators/preset/schema.json +5 -0
|
@@ -1,22 +1,450 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.installSkills = installSkills;
|
|
3
4
|
exports.presetGenerator = presetGenerator;
|
|
4
5
|
const tslib_1 = require("tslib");
|
|
5
6
|
const devkit_1 = require("@nx/devkit");
|
|
7
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
8
|
const node_fs_1 = require("node:fs");
|
|
7
9
|
const node_path_1 = require("node:path");
|
|
8
10
|
function readTemplate(relativePath) {
|
|
9
11
|
return (0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, 'files', relativePath), 'utf-8');
|
|
10
12
|
}
|
|
11
13
|
const SITE_URL = 'https://atelier.pieper.io';
|
|
14
|
+
// Pinned to the exact version this monorepo runs (root package.json
|
|
15
|
+
// devDependencies) — the workshop needs the same Storybook every attendee's
|
|
16
|
+
// neighbour has, not whatever `latest` resolves to on the day of the cohort.
|
|
17
|
+
const STORYBOOK_VERSION = '10.6.0';
|
|
18
|
+
// Storybook's `framework.name` package per scaffolded app, and the exact
|
|
19
|
+
// devDependency set each one needs beyond the common addons below.
|
|
20
|
+
const STORYBOOK_FRAMEWORK_PACKAGE = {
|
|
21
|
+
angular: '@storybook/angular-vite',
|
|
22
|
+
react: '@storybook/react-vite',
|
|
23
|
+
vue: '@storybook/vue3-vite',
|
|
24
|
+
};
|
|
25
|
+
// Templates that contain literal TypeScript source (not JSX) are suffixed
|
|
26
|
+
// `.template` in files/storybook/<fw>/ so that neither tsc nor the asset-copy
|
|
27
|
+
// glob touches them before this generator ships: tsconfig.lib.json's
|
|
28
|
+
// `include: ["src/**/*.ts"]` would otherwise compile them as part of this
|
|
29
|
+
// package's own build, and project.json's build `assets` glob
|
|
30
|
+
// (`**/!(*.ts)`) explicitly excludes `.ts` files from the verbatim asset
|
|
31
|
+
// copy — between the two, a template literally named `main.ts` would be
|
|
32
|
+
// transpiled away and never reach dist/ under a name this generator's
|
|
33
|
+
// `readTemplate()` can find at runtime. `.tsx` files (no literal `.ts`
|
|
34
|
+
// suffix) hit neither rule and are stored under their real name. Verified the
|
|
35
|
+
// hard way once already: `contracts/types.ts` and `contracts/button.contract.ts`
|
|
36
|
+
// (no `.template` suffix) threw `ENOENT` from inside a packed npm tarball —
|
|
37
|
+
// see sync-preflight.mjs's comment on those two entries.
|
|
38
|
+
function storybookTemplateName(framework, base) {
|
|
39
|
+
if (base === 'main.ts')
|
|
40
|
+
return `storybook/${framework}/main.ts.template`;
|
|
41
|
+
// vitest.config.ts and .storybook/vitest.setup.ts are always written out
|
|
42
|
+
// as plain `.ts` regardless of framework — unlike main.ts/preview/the
|
|
43
|
+
// example story, react does NOT get a `.tsx` variant for these (neither
|
|
44
|
+
// file contains JSX), so both need the `.template` dodge on every framework.
|
|
45
|
+
if (base === 'vitest.config' || base === 'vitest.setup')
|
|
46
|
+
return `storybook/${framework}/${base}.ts.template`;
|
|
47
|
+
const ext = framework === 'react' ? 'tsx' : 'ts.template';
|
|
48
|
+
return `storybook/${framework}/${base}.${ext}`;
|
|
49
|
+
}
|
|
50
|
+
// The destination filename written into the scaffolded workspace — a plain,
|
|
51
|
+
// correct `.ts`/`.tsx` extension regardless of the source template's name.
|
|
52
|
+
function storybookOutputExt(framework) {
|
|
53
|
+
return framework === 'react' ? 'tsx' : 'ts';
|
|
54
|
+
}
|
|
55
|
+
// Splices `block` (one or more object literals, as raw source text, starting
|
|
56
|
+
// with a leading comma) into the flat ESLint config a framework's own
|
|
57
|
+
// application generator (@nx/angular, @nx/react, @nx/vue — all invoked with
|
|
58
|
+
// `linter: 'eslint'` below) already wrote at `path`, immediately before the
|
|
59
|
+
// file's closing `];`, and prepends any `imports` the block needs.
|
|
60
|
+
//
|
|
61
|
+
// This is text surgery, not an AST rewrite, and that is deliberate: the
|
|
62
|
+
// generated file is Nx's, not ours, and a future Nx version reshaping it
|
|
63
|
+
// should not silently swallow this addition the way editing its AST in place
|
|
64
|
+
// might. `@nx/eslint`'s own flat-config codegen (confirmed by actually
|
|
65
|
+
// running the three application generators against an in-memory Tree —
|
|
66
|
+
// verified for this Nx version, not assumed) always emits a single
|
|
67
|
+
// `export default [ ... ];` as the file's last statement, so anchoring on the
|
|
68
|
+
// final `];` is stable across the Nx versions this generator supports.
|
|
69
|
+
// `formatFiles(tree)` at the end of presetGenerator reformats whatever this
|
|
70
|
+
// produces, so exact indentation here doesn't matter.
|
|
71
|
+
//
|
|
72
|
+
// Throws loudly if the file is missing or doesn't have that shape, rather
|
|
73
|
+
// than silently skipping the addition — same reasoning as the storybook
|
|
74
|
+
// targets' `updateJson` calls below: a workshop app whose ESLint config
|
|
75
|
+
// silently didn't get the framework's accessibility rule is worse than a
|
|
76
|
+
// generator that stops.
|
|
77
|
+
function appendToFlatEslintConfig(tree, path, block, imports = []) {
|
|
78
|
+
const content = tree.read(path, 'utf-8');
|
|
79
|
+
if (content === null) {
|
|
80
|
+
throw new Error(`Cannot find ${path} — expected the framework application generator to have already written a flat ESLint config there (this generator always passes linter: 'eslint').`);
|
|
81
|
+
}
|
|
82
|
+
const closeIndex = content.lastIndexOf('];');
|
|
83
|
+
if (closeIndex === -1) {
|
|
84
|
+
throw new Error(`${path} does not end with a flat-config array ("];") — the framework application generator's ESLint output may have changed shape; appendToFlatEslintConfig needs updating.`);
|
|
85
|
+
}
|
|
86
|
+
const importPrefix = imports.length ? `${imports.join('\n')}\n` : '';
|
|
87
|
+
tree.write(path, importPrefix +
|
|
88
|
+
content.slice(0, closeIndex) +
|
|
89
|
+
block +
|
|
90
|
+
content.slice(closeIndex));
|
|
91
|
+
}
|
|
92
|
+
// Ported CSS-discipline stylelint rules (ADR-0130): assembles the scaffold's
|
|
93
|
+
// own stylelint.config.mjs — one override block per selected framework's own
|
|
94
|
+
// `workshop-<fw>/src` tree, mirroring this repo's own stylelint.config.mjs
|
|
95
|
+
// (which does the same per libs/{angular,react,vue}). Built as a template
|
|
96
|
+
// string here rather than a static files/ template, because both the block
|
|
97
|
+
// count and the app-relative paths it names depend on which frameworks were
|
|
98
|
+
// selected — the same reason CLAUDE.md/README.md below are assembled from
|
|
99
|
+
// `frameworks`, not copied verbatim.
|
|
100
|
+
//
|
|
101
|
+
// Only three of the four shipped rules are wired: `atelier/no-primitive-token`
|
|
102
|
+
// polices reaching past the semantic token tier into a primitive ramp (e.g.
|
|
103
|
+
// `--ui-color-teal-500`), which presupposes knowing that ramp exists — this
|
|
104
|
+
// workspace's own tokens.css has no such tiering to police. The file still
|
|
105
|
+
// ships (see the loop that writes tools/stylelint-rules/* below) because
|
|
106
|
+
// index.js requires all four rule files unconditionally, and a
|
|
107
|
+
// byte-identical index.js (kept in sync with the canonical copy by
|
|
108
|
+
// sync-preflight.mjs) is worth more than a scaffold-specific fork that drops
|
|
109
|
+
// one require().
|
|
110
|
+
//
|
|
111
|
+
// Neither wired rule is given `componentRoot` or `allowlistsFile`: this
|
|
112
|
+
// workspace ships no allowlists.js, so every exemption map defaults to empty
|
|
113
|
+
// and the staleness scan those two options drive never runs (documented in
|
|
114
|
+
// each rule's own header in tools/stylelint-rules/) — passing them here would
|
|
115
|
+
// configure a scan that can never find anything.
|
|
116
|
+
function buildStylelintConfig(frameworks) {
|
|
117
|
+
const overrides = frameworks
|
|
118
|
+
.map((framework) => {
|
|
119
|
+
const appName = `workshop-${framework}`;
|
|
120
|
+
const tokensCss = `${appName}/src/styles/tokens.css`;
|
|
121
|
+
return ` {
|
|
122
|
+
files: ['${appName}/src/**/*.css'],
|
|
123
|
+
rules: {
|
|
124
|
+
'atelier/no-raw-color-literal': true,
|
|
125
|
+
'atelier/no-undeclared-token': [true, { tokenFiles: ['${tokensCss}'] }],
|
|
126
|
+
'atelier/no-token-bypass': [true, { tokenFile: '${tokensCss}' }],
|
|
127
|
+
},
|
|
128
|
+
},`;
|
|
129
|
+
})
|
|
130
|
+
.join('\n');
|
|
131
|
+
return `// This workspace's own CSS-discipline rules only ('tools/stylelint-rules/')
|
|
132
|
+
// — ported from the parent Atelier monorepo (ADR-0130). No
|
|
133
|
+
// stylelint-config-standard or any other base config: stylelint 16+ ships no
|
|
134
|
+
// built-in formatting/stylistic rules at all (split out to the separate,
|
|
135
|
+
// opt-in @stylistic plugin, which this workspace does not install), so there
|
|
136
|
+
// is nothing here that could fight Prettier.
|
|
137
|
+
//
|
|
138
|
+
// Three of the four shipped rules are wired below. atelier/no-primitive-token
|
|
139
|
+
// is NOT — it polices reaching past the semantic token tier into a primitive
|
|
140
|
+
// ramp, which presupposes knowing that ramp exists, and this workspace's own
|
|
141
|
+
// tokens.css has no such tiering. The three below catch what an attendee does
|
|
142
|
+
// by writing ordinary component CSS on day one: a raw color literal, a
|
|
143
|
+
// typo'd or undeclared --ui-* token, and a literal that duplicates a token's
|
|
144
|
+
// value instead of binding to it.
|
|
145
|
+
//
|
|
146
|
+
// tokens.css itself is EXCLUDED from every framework's stylelint target (via
|
|
147
|
+
// --ignore-pattern in the app's project.json, not an exemption here): it is
|
|
148
|
+
// the one file that legitimately spells out raw color/dimension literals as
|
|
149
|
+
// token DEFINITIONS — the opposite of what these rules police in a file that
|
|
150
|
+
// CONSUMES tokens.
|
|
151
|
+
import atelier from './tools/stylelint-rules/index.js';
|
|
152
|
+
|
|
153
|
+
export default {
|
|
154
|
+
plugins: [atelier],
|
|
155
|
+
rules: {},
|
|
156
|
+
overrides: [
|
|
157
|
+
${overrides}
|
|
158
|
+
],
|
|
159
|
+
};
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
// The four storybookjs/mcp skills this generator installs post-scaffold (S2).
|
|
163
|
+
// Pinned the same way ADR-0110 pins figma-console-mcp: a skill install is a
|
|
164
|
+
// remote pull of skill *text*, and an un-pinned CLI could silently change what
|
|
165
|
+
// gets written into an attendee's .claude/skills/ between one workshop and the
|
|
166
|
+
// next.
|
|
167
|
+
const SKILLS_CLI_VERSION = '1.5.25';
|
|
168
|
+
const SKILLS_ADD_ARGV = [
|
|
169
|
+
'-y',
|
|
170
|
+
`skills@${SKILLS_CLI_VERSION}`,
|
|
171
|
+
'add',
|
|
172
|
+
'storybookjs/mcp',
|
|
173
|
+
'--skill',
|
|
174
|
+
'*',
|
|
175
|
+
'--agent',
|
|
176
|
+
'claude-code',
|
|
177
|
+
'--yes',
|
|
178
|
+
'--copy',
|
|
179
|
+
];
|
|
180
|
+
// Kept in sync with SKILLS_ADD_ARGV by hand — this is the string shown to a
|
|
181
|
+
// human (the failure warning, CLAUDE.md, README.md), quoted the way a real
|
|
182
|
+
// shell needs it. It is unrelated to how SKILLS_ADD_ARGV itself reaches the
|
|
183
|
+
// child process below: on POSIX it goes straight to `spawn` as an argv array
|
|
184
|
+
// with no shell involved (so nothing there needs quoting at all), and on
|
|
185
|
+
// Windows it is re-quoted for cmd.exe by `quoteForCmdExe` — this constant
|
|
186
|
+
// exists only for the text a person reads and re-types by hand.
|
|
187
|
+
const SKILLS_ADD_COMMAND_FOR_HUMANS = `npx -y skills@${SKILLS_CLI_VERSION} add storybookjs/mcp --skill "*" --agent claude-code --yes --copy`;
|
|
188
|
+
// Bounds the CLI's own git clone (it reads this env var itself) so a bad
|
|
189
|
+
// conference network fails fast with a clear message instead of hanging.
|
|
190
|
+
const SKILLS_CLONE_TIMEOUT_MS = 60000;
|
|
191
|
+
// The contract loop (ADR-0121 S4): figma-snapshot-contracts.mjs imports the
|
|
192
|
+
// MCP SDK directly (the same client figma-snapshot.mjs uses), pinned to the
|
|
193
|
+
// exact version this monorepo runs (root package.json devDependencies) so
|
|
194
|
+
// the scaffold's copy behaves the same as the canonical script.
|
|
195
|
+
const MCP_SDK_VERSION = '^1.29.0';
|
|
196
|
+
// ts-eval.js (shared by check-contracts.mjs and figma-snapshot-contracts.mjs)
|
|
197
|
+
// needs `typescript` at runtime. Every framework's Nx application generator
|
|
198
|
+
// already adds it, so this is a safety net, not the primary source — see the
|
|
199
|
+
// conditional add near the end of presetGenerator, which only includes this
|
|
200
|
+
// constant in the devDependencies write when `typescript` isn't already
|
|
201
|
+
// present.
|
|
202
|
+
const TYPESCRIPT_VERSION = '6.0.3';
|
|
203
|
+
// Ported CSS-discipline stylelint rules (ADR-0130): pinned the same way
|
|
204
|
+
// STORYBOOK_VERSION above is — the exact version this monorepo actually
|
|
205
|
+
// runs, not the caret range root package.json declares (`^17.15.0`); the
|
|
206
|
+
// resolved, installed version (package-lock.json) is 17.15.0, and that's
|
|
207
|
+
// the one every attendee's neighbour should get too. No `postcss-html`
|
|
208
|
+
// devDependency: that's only needed to lint `.astro` files, and the
|
|
209
|
+
// scaffold has none.
|
|
210
|
+
const STYLELINT_VERSION = '17.15.0';
|
|
211
|
+
// Formatting enforcement (measured 2026-09-12 against a real workspace
|
|
212
|
+
// generated through this preset via a local verdaccio, per
|
|
213
|
+
// tasks/todo.md — not assumed from Nx's docs): a freshly `create-nx-workspace`'d
|
|
214
|
+
// tree declares no `prettier` devDependency and writes no `.prettierrc` — this
|
|
215
|
+
// monorepo's own ADR-0127 baseline ("configured and never enforced") does not
|
|
216
|
+
// even get as far as "configured" here, it has to be added from scratch. Root
|
|
217
|
+
// package.json's own declaration (`~3.9.6`) is already a tight tilde, not the
|
|
218
|
+
// wide caret range some of this file's other *_VERSION constants have to
|
|
219
|
+
// narrow — package-lock.json resolves it to the same 3.9.6, so there's no
|
|
220
|
+
// "declared vs. actually resolved" gap to reconcile.
|
|
221
|
+
const PRETTIER_VERSION = '~3.9.6';
|
|
222
|
+
// Browser-mode Storybook tests (owner correction, 2026-09-10, to ADR-0123's
|
|
223
|
+
// "no test runner" decision — see the dated correction on that record).
|
|
224
|
+
// Every scaffolded app gets its own vitest.config.ts + storybook-test target,
|
|
225
|
+
// mirroring libs/{angular,react,vue}/vitest.storybook.config.ts. Versions are
|
|
226
|
+
// copied verbatim from the monorepo's own root package.json — most are exact
|
|
227
|
+
// pins there; `vitest` and `@vitest/browser-playwright` are themselves caret
|
|
228
|
+
// ranges in root package.json, so they stay caret ranges here too, rather
|
|
229
|
+
// than inventing an exact pin root itself doesn't have.
|
|
230
|
+
const VITEST_VERSION = '^4.0.8';
|
|
231
|
+
const VITEST_BROWSER_PLAYWRIGHT_VERSION = '^4.1.0';
|
|
232
|
+
// @vitest/browser-playwright declares a PEER (not transitive) dependency on
|
|
233
|
+
// `playwright` itself (peerDependencies: { playwright: "*" }). Root
|
|
234
|
+
// package.json has no bare `playwright` entry — only `@playwright/test`
|
|
235
|
+
// (^1.36.0), whose own dependency on `playwright` satisfies the peer there
|
|
236
|
+
// via monorepo-wide hoisting. A standalone scaffold gets no such hoist, so
|
|
237
|
+
// the bare peer is pinned here directly, at the same range @playwright/test
|
|
238
|
+
// itself uses — there is no "exact version root package.json has" for the
|
|
239
|
+
// bare package to copy, since root never names it.
|
|
240
|
+
const PLAYWRIGHT_VERSION = '^1.36.0';
|
|
241
|
+
const VITE_PLUGIN_REACT_VERSION = '6.1.1';
|
|
242
|
+
const VITE_PLUGIN_VUE_VERSION = '^6.0.5';
|
|
243
|
+
const ANALOGJS_VITE_PLUGIN_ANGULAR_VERSION = '2.7.1';
|
|
244
|
+
// Only Vue's vitest.setup.ts.template imports this (custom jest-dom
|
|
245
|
+
// matchers), mirroring libs/vue/.storybook/vitest.setup.ts exactly — React
|
|
246
|
+
// and Angular's setup files don't use it.
|
|
247
|
+
const TESTING_LIBRARY_JEST_DOM_VERSION = '^6.9.1';
|
|
248
|
+
// The Vue eslint.config.mjs addition above (see the appendToFlatEslintConfig
|
|
249
|
+
// call in the vue branch) imports this directly. @nx/eslint's own root config
|
|
250
|
+
// setup already adds it unconditionally regardless of framework or of
|
|
251
|
+
// whether `prettier` itself is installed (confirmed by running all three
|
|
252
|
+
// application generators against an empty Tree), so the conditional add near
|
|
253
|
+
// the end of presetGenerator below is a safety net, not the primary source —
|
|
254
|
+
// same reasoning as TYPESCRIPT_VERSION above. Version matches what that run
|
|
255
|
+
// installed.
|
|
256
|
+
const ESLINT_CONFIG_PRETTIER_VERSION = '^10.0.0';
|
|
257
|
+
// Hard kill for the whole child process — a backstop above the CLI's own
|
|
258
|
+
// clone timeout, covering the install/copy phase after the clone too.
|
|
259
|
+
const SKILLS_INSTALL_TIMEOUT_MS = 120000;
|
|
260
|
+
// `execFile` (and its promisified form) never actually spawns a shell — it
|
|
261
|
+
// forwards only a fixed allowlist of options to the underlying `spawn` call,
|
|
262
|
+
// and `stdio` is not on that list. Measured: `promisify(execFile)(...,
|
|
263
|
+
// { stdio: 'inherit' })` still resolves with the child's stdout captured in
|
|
264
|
+
// `result.stdout`, and nothing is written to this process's own stdout in
|
|
265
|
+
// the meantime — the attendee would see a silent multi-minute pause (the
|
|
266
|
+
// CLI's own clone can take that long over a bad conference network) and then
|
|
267
|
+
// either a success or failure with no progress in between. `spawn` (used
|
|
268
|
+
// below) genuinely honours `stdio: 'inherit'`, wiring the child's streams to
|
|
269
|
+
// this process's own so the CLI's progress reaches the terminal live.
|
|
270
|
+
//
|
|
271
|
+
// `process.platform` is read live inside `runSkillsAddCommand` (rather than
|
|
272
|
+
// cached in a module-level constant) so a test can stub it per-case without
|
|
273
|
+
// having to reset and re-import the module.
|
|
274
|
+
// `spawn`'s `shell: true` mode does not escape an `args` array for you — it
|
|
275
|
+
// just space-joins `[command, ...args]` before handing the result to the
|
|
276
|
+
// shell (this is exactly what Node's DEP0190 deprecation warns about:
|
|
277
|
+
// "Passing args to a child process with shell option true ... arguments are
|
|
278
|
+
// not escaped, only concatenated"). So this is not a general-purpose Windows
|
|
279
|
+
// command-line escaper — every token in SKILLS_ADD_ARGV is a fixed literal
|
|
280
|
+
// (a package specifier, a flag name, or the bare `*`) with no embedded quotes
|
|
281
|
+
// of its own, and wrapping a token in quotes whenever it contains whitespace
|
|
282
|
+
// or a metacharacter is sufficient for that fixed, known argv. `*`/`?` are
|
|
283
|
+
// included even though cmd.exe itself does not glob-expand them (unlike a
|
|
284
|
+
// POSIX shell, wildcard expansion on Windows is the invoked program's own
|
|
285
|
+
// job, not the shell's) — quoting them anyway is the belt-and-suspenders
|
|
286
|
+
// match for the POSIX side's guarantee, so `--skill "*"` reaches npx as a
|
|
287
|
+
// literal one-character string on both platforms by construction, not by
|
|
288
|
+
// relying on cmd.exe's particular behaviour.
|
|
289
|
+
function quoteForCmdExe(token) {
|
|
290
|
+
return /[\s"^&|<>()*?]/.test(token)
|
|
291
|
+
? `"${token.replace(/"/g, '\\"')}"`
|
|
292
|
+
: token;
|
|
293
|
+
}
|
|
294
|
+
// Runs `npx <SKILLS_ADD_ARGV...>` with the child's stdio wired to this
|
|
295
|
+
// process's own, resolving on a clean exit and rejecting with a descriptive
|
|
296
|
+
// error otherwise (non-zero exit, the install timeout killing the child, or
|
|
297
|
+
// the child never starting at all) — installSkills' catch block below turns
|
|
298
|
+
// any of those into the same non-fatal warning.
|
|
299
|
+
function runSkillsAddCommand(cwd, env) {
|
|
300
|
+
return new Promise((resolve, reject) => {
|
|
301
|
+
const isWindows = process.platform === 'win32';
|
|
302
|
+
let settled = false;
|
|
303
|
+
// Set only by the Windows timeout backstop below — the `exit` handler
|
|
304
|
+
// needs it because a `taskkill /F` termination is not guaranteed to show
|
|
305
|
+
// up as a `signal` on the `exit` event the way POSIX's SIGTERM does.
|
|
306
|
+
let timedOut = false;
|
|
307
|
+
const settle = (fn) => {
|
|
308
|
+
if (settled)
|
|
309
|
+
return;
|
|
310
|
+
settled = true;
|
|
311
|
+
fn();
|
|
312
|
+
};
|
|
313
|
+
const spawnOptions = Object.assign({ cwd,
|
|
314
|
+
env, stdio: 'inherit' }, (isWindows
|
|
315
|
+
? {}
|
|
316
|
+
: { timeout: SKILLS_INSTALL_TIMEOUT_MS, killSignal: 'SIGTERM' }));
|
|
317
|
+
const child = isWindows
|
|
318
|
+
? // Windows can only execute a `.cmd` file (npx resolves to npx.cmd
|
|
319
|
+
// there) through cmd.exe, and Node no longer shells out to run one
|
|
320
|
+
// implicitly — a hardening in the Node 18.20 / 20.12 lines now
|
|
321
|
+
// requires the caller to opt in with `shell: true`, or the spawn
|
|
322
|
+
// never starts at all. That is exactly the platform `--copy` above
|
|
323
|
+
// was chosen for, so this path has to work. `shell: true` (rather
|
|
324
|
+
// than hardcoding the `npx.cmd` binary name) is enough on its own:
|
|
325
|
+
// once cmd.exe is in the loop it resolves the bare `npx` via its own
|
|
326
|
+
// PATHEXT lookup, the same as typing `npx` at a Windows prompt.
|
|
327
|
+
// Because `shell: true` does not escape an args array (see
|
|
328
|
+
// quoteForCmdExe above), the whole command line is built and quoted
|
|
329
|
+
// by hand instead of passing SKILLS_ADD_ARGV as a separate array.
|
|
330
|
+
(0, node_child_process_1.spawn)(['npx', ...SKILLS_ADD_ARGV].map(quoteForCmdExe).join(' '), Object.assign(Object.assign({}, spawnOptions), { shell: true }))
|
|
331
|
+
: // POSIX: no shell is spawned at all, so nothing is in a position to
|
|
332
|
+
// glob-expand the literal `*` in `--skill *` — `npx` is exec'd
|
|
333
|
+
// directly with SKILLS_ADD_ARGV as its argv.
|
|
334
|
+
(0, node_child_process_1.spawn)('npx', SKILLS_ADD_ARGV, spawnOptions);
|
|
335
|
+
// Windows-only backstop for SKILLS_INSTALL_TIMEOUT_MS. `spawn`'s own
|
|
336
|
+
// `timeout` + `killSignal` (used above for POSIX) only terminates the
|
|
337
|
+
// process `child` actually refers to — under `shell: true` that is
|
|
338
|
+
// cmd.exe, not `npx`. cmd.exe does not propagate termination to the
|
|
339
|
+
// `npx`/npm/git descendants it launched, so relying on the same option
|
|
340
|
+
// here would kill the shell while the clone it kicked off kept running
|
|
341
|
+
// in the background — orphaned, and still writing into the scaffolded
|
|
342
|
+
// workspace after this function had already rejected. `taskkill /T`
|
|
343
|
+
// kills the whole process tree rooted at cmd.exe's pid instead of just
|
|
344
|
+
// cmd.exe itself.
|
|
345
|
+
//
|
|
346
|
+
// UNVERIFIED ON WINDOWS: reasoned through from documented cmd.exe/
|
|
347
|
+
// taskkill behaviour, but there is no Windows machine or CI runner
|
|
348
|
+
// available in this environment to actually exercise it. Verify on a
|
|
349
|
+
// real Windows box before relying on it.
|
|
350
|
+
let windowsTimeoutHandle;
|
|
351
|
+
if (isWindows) {
|
|
352
|
+
windowsTimeoutHandle = setTimeout(() => {
|
|
353
|
+
if (settled)
|
|
354
|
+
return;
|
|
355
|
+
timedOut = true;
|
|
356
|
+
if (typeof child.pid === 'number') {
|
|
357
|
+
// `taskkill` is a stock Windows binary, so this should never itself
|
|
358
|
+
// fail to spawn — but an unhandled `error` event on a ChildProcess
|
|
359
|
+
// throws, and there is nothing more useful to do here than swallow
|
|
360
|
+
// it: the `exit` handler above has already been told (`timedOut`)
|
|
361
|
+
// and will reject with the real, readable timeout message either way.
|
|
362
|
+
(0, node_child_process_1.spawn)('taskkill', ['/pid', String(child.pid), '/T', '/F'], {
|
|
363
|
+
stdio: 'ignore',
|
|
364
|
+
}).on('error', () => undefined);
|
|
365
|
+
}
|
|
366
|
+
}, SKILLS_INSTALL_TIMEOUT_MS);
|
|
367
|
+
}
|
|
368
|
+
child.on('error', (error) => {
|
|
369
|
+
// The child never started (e.g. `npx` not found on PATH).
|
|
370
|
+
clearTimeout(windowsTimeoutHandle);
|
|
371
|
+
settle(() => reject(error));
|
|
372
|
+
});
|
|
373
|
+
child.on('exit', (code, signal) => {
|
|
374
|
+
clearTimeout(windowsTimeoutHandle);
|
|
375
|
+
if (signal || timedOut) {
|
|
376
|
+
settle(() => reject(new Error(`npx was killed${signal ? ` by ${signal}` : ''} before finishing — most likely the ` +
|
|
377
|
+
`${SKILLS_INSTALL_TIMEOUT_MS}ms install timeout on a slow or unreachable network`)));
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (code !== 0) {
|
|
381
|
+
settle(() => reject(new Error(`npx exited with code ${code}`)));
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
settle(resolve);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
// Exported (rather than inlined into the returned post-generator task) so it
|
|
389
|
+
// can be exercised directly in tests without also invoking the real
|
|
390
|
+
// `npm install` that `installTask`/`removePresetTask` run when called.
|
|
391
|
+
function installSkills(tree, skillsEnabled) {
|
|
392
|
+
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
393
|
+
if (!skillsEnabled)
|
|
394
|
+
return;
|
|
395
|
+
console.log(`\n◇ Installing storybookjs/mcp skills (stories, storybook-init, storybook-setup, storybook-upgrade)…`);
|
|
396
|
+
try {
|
|
397
|
+
// Runs in the scaffolded workspace's real root: by the time the
|
|
398
|
+
// post-generator task calls this, Nx has already flushed the virtual Tree
|
|
399
|
+
// to disk (that's what lets installTask run a real `npm install`).
|
|
400
|
+
// `--copy` (not the CLI's default symlink farm) because an attendee on
|
|
401
|
+
// Windows without developer mode cannot create symlinks. `--agent
|
|
402
|
+
// claude-code` (not `--all`) so the scaffold doesn't also grow
|
|
403
|
+
// `.cursor/`, `.codex/`, etc.
|
|
404
|
+
const env = Object.assign(Object.assign({}, process.env), { DO_NOT_TRACK: '1', SKILLS_CLONE_TIMEOUT_MS: String(SKILLS_CLONE_TIMEOUT_MS) });
|
|
405
|
+
yield runSkillsAddCommand(tree.root, env);
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
// Non-fatal by design: a scaffold that dies on this last step because a
|
|
409
|
+
// conference network can't reach GitHub is a ruined workshop morning; a
|
|
410
|
+
// workspace merely missing its skills is a nuisance the attendee (or
|
|
411
|
+
// facilitator) can fix with the command below.
|
|
412
|
+
console.warn(`\n⚠ Could not install the storybookjs/mcp skills automatically (${error instanceof Error ? error.message : String(error)}).`);
|
|
413
|
+
console.warn(` Run this by hand once you have network access:\n`);
|
|
414
|
+
console.warn(` ${SKILLS_ADD_COMMAND_FOR_HUMANS}\n`);
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
}
|
|
12
418
|
function presetGenerator(tree, options) {
|
|
13
419
|
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
14
|
-
var _a, _b;
|
|
420
|
+
var _a, _b, _c, _d, _e;
|
|
15
421
|
const frameworks = ((_a = options.frameworks) !== null && _a !== void 0 ? _a : 'angular')
|
|
16
422
|
.split(',')
|
|
17
423
|
.map((f) => f.trim())
|
|
18
424
|
.filter(Boolean);
|
|
425
|
+
const skillsEnabled = (_b = options.skills) !== null && _b !== void 0 ? _b : true;
|
|
19
426
|
const deps = {};
|
|
427
|
+
// Every scaffolded workspace gets a local Storybook — not behind a flag
|
|
428
|
+
// (2026-09-10 owner decision, tasks/todo.md "Storybook + the storybookjs/mcp
|
|
429
|
+
// skills in the scaffolded workspace"). It exists so the storybookjs/mcp
|
|
430
|
+
// skills installed below have a Storybook ≥ 10.5 with `@storybook/addon-mcp`
|
|
431
|
+
// to talk to; without it, the `stories` skill's own first move on any UI task
|
|
432
|
+
// would be to propose installing one.
|
|
433
|
+
const storybookDevDeps = {
|
|
434
|
+
storybook: STORYBOOK_VERSION,
|
|
435
|
+
'@storybook/addon-mcp': STORYBOOK_VERSION,
|
|
436
|
+
'@storybook/addon-a11y': STORYBOOK_VERSION,
|
|
437
|
+
'@storybook/addon-docs': STORYBOOK_VERSION,
|
|
438
|
+
// Browser-mode Storybook tests (owner correction 2026-09-10 to ADR-0123) —
|
|
439
|
+
// shared regardless of how many frameworks are selected; the
|
|
440
|
+
// framework-specific Vite plugin is added conditionally, after the
|
|
441
|
+
// frameworks loop below, once every app generator has had a chance to
|
|
442
|
+
// bring its own in.
|
|
443
|
+
'@storybook/addon-vitest': STORYBOOK_VERSION,
|
|
444
|
+
vitest: VITEST_VERSION,
|
|
445
|
+
'@vitest/browser-playwright': VITEST_BROWSER_PLAYWRIGHT_VERSION,
|
|
446
|
+
playwright: PLAYWRIGHT_VERSION,
|
|
447
|
+
};
|
|
20
448
|
for (const framework of frameworks) {
|
|
21
449
|
const appName = `workshop-${framework}`;
|
|
22
450
|
if (framework === 'angular') {
|
|
@@ -36,7 +464,7 @@ function presetGenerator(tree, options) {
|
|
|
36
464
|
// ever ran. Optional peers are not auto-installed, so the first thing that
|
|
37
465
|
// installs it is the line below, at the version that matches.
|
|
38
466
|
yield (0, devkit_1.ensurePackage)('@nx/angular', devkit_1.NX_VERSION);
|
|
39
|
-
const { applicationGenerator: angularAppGenerator } = require('@nx/angular/generators');
|
|
467
|
+
const { applicationGenerator: angularAppGenerator, } = require('@nx/angular/generators');
|
|
40
468
|
yield angularAppGenerator(tree, {
|
|
41
469
|
name: appName,
|
|
42
470
|
directory: appName,
|
|
@@ -47,8 +475,39 @@ function presetGenerator(tree, options) {
|
|
|
47
475
|
skipTests: true,
|
|
48
476
|
e2eTestRunner: 'none',
|
|
49
477
|
skipFormat: true,
|
|
478
|
+
// Without this, the generator's own linter choice defaults to
|
|
479
|
+
// `normalizeLinterOption`'s non-interactive fallback: follow an
|
|
480
|
+
// eslint setup already detected in the tree, or `'none'` if there is
|
|
481
|
+
// none yet. A single-framework Angular scaffold (the common,
|
|
482
|
+
// documented case — ADR-0084) starts from a genuinely empty tree, so
|
|
483
|
+
// that fallback silently produced ZERO eslint.config.mjs and no
|
|
484
|
+
// eslint devDependency at all — verified by running the real
|
|
485
|
+
// generator against an empty workspace with this line omitted.
|
|
486
|
+
// React and Vue below already pass this explicitly; Angular didn't,
|
|
487
|
+
// which was the actual defect, not merely a posture gap.
|
|
488
|
+
linter: 'eslint',
|
|
50
489
|
});
|
|
51
490
|
deps['@atelier-ui/angular'] = 'latest';
|
|
491
|
+
// `flat/angular-template` above (written into workshop-angular's own
|
|
492
|
+
// eslint.config.mjs by the application generator, confirmed by a real
|
|
493
|
+
// run) already carries angular-eslint's `templateAccessibility` preset
|
|
494
|
+
// — see libs/angular/eslint.config.mjs's comment for the 11 rules that
|
|
495
|
+
// covers. This one isn't part of that preset (not `:accessibility:`
|
|
496
|
+
// tagged in angular-eslint's README): WCAG 2.4.3, positive tabindex
|
|
497
|
+
// fights natural DOM tab order. Same addition libs/angular's own config
|
|
498
|
+
// makes on top of the identical preset.
|
|
499
|
+
appendToFlatEslintConfig(tree, `${appName}/eslint.config.mjs`, `,
|
|
500
|
+
{
|
|
501
|
+
// Not covered by \`flat/angular-template\` above (angular-eslint's own
|
|
502
|
+
// \`templateAccessibility\` preset only) — WCAG 2.4.3, positive tabindex
|
|
503
|
+
// fights natural DOM tab order. Mirrors libs/angular/eslint.config.mjs's
|
|
504
|
+
// identical addition on top of the same preset.
|
|
505
|
+
files: ['**/*.html'],
|
|
506
|
+
rules: {
|
|
507
|
+
'@angular-eslint/template/no-positive-tabindex': 'error',
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
`);
|
|
52
511
|
}
|
|
53
512
|
if (framework === 'react') {
|
|
54
513
|
console.log(`\n◇ Generating React workshop app…`);
|
|
@@ -66,6 +525,11 @@ function presetGenerator(tree, options) {
|
|
|
66
525
|
skipFormat: true,
|
|
67
526
|
});
|
|
68
527
|
deps['@atelier-ui/react'] = 'latest';
|
|
528
|
+
// Deliberately nothing added here. `flat/react` above (written into
|
|
529
|
+
// workshop-react's own eslint.config.mjs by the application generator,
|
|
530
|
+
// confirmed by a real run) already carries jsx-a11y's full 18-rule
|
|
531
|
+
// active set — the same rule set libs/react/eslint.config.mjs relies on
|
|
532
|
+
// without adding anything of its own either.
|
|
69
533
|
}
|
|
70
534
|
if (framework === 'vue') {
|
|
71
535
|
console.log(`\n◇ Generating Vue workshop app…`);
|
|
@@ -82,6 +546,59 @@ function presetGenerator(tree, options) {
|
|
|
82
546
|
skipFormat: true,
|
|
83
547
|
});
|
|
84
548
|
deps['@atelier-ui/vue'] = 'latest';
|
|
549
|
+
// @nx/vue's own application generator (confirmed by a real run against
|
|
550
|
+
// an in-memory Tree, packed from the exact pinned version — @nx/vue is
|
|
551
|
+
// an optional peer, not installed in this workspace) DOES match `.vue`
|
|
552
|
+
// files and wire vue-eslint-parser + typescript-eslint's parser for
|
|
553
|
+
// them via `parserOptions.parser` — the gap this generator's own
|
|
554
|
+
// libs/vue/eslint.config.mjs once had (`.vue` files matched by no
|
|
555
|
+
// config at all) does not exist in the scaffold. What IS missing,
|
|
556
|
+
// mirrored below from libs/vue/eslint.config.mjs:
|
|
557
|
+
//
|
|
558
|
+
// 1. `eslint-config-prettier` after eslint-plugin-vue's rules, scoped
|
|
559
|
+
// to `.vue` — without it, eslint-plugin-vue's layout/whitespace
|
|
560
|
+
// rules (max-attributes-per-line, html-self-closing, …) fight
|
|
561
|
+
// Prettier. eslint-config-prettier is already an unconditional
|
|
562
|
+
// devDependency here (added by @nx/eslint's own root config setup,
|
|
563
|
+
// confirmed by the same run), independent of whether `prettier`
|
|
564
|
+
// itself is installed — the defensive add near the end of
|
|
565
|
+
// presetGenerator below is a safety net for that, not the primary
|
|
566
|
+
// source.
|
|
567
|
+
// 2. `vue/no-unused-properties` — not part of any eslint-plugin-vue
|
|
568
|
+
// preset, opted in explicitly.
|
|
569
|
+
//
|
|
570
|
+
// Deliberately NOT mirrored: libs/vue/eslint.config.mjs also re-scopes
|
|
571
|
+
// eslint-plugin-vue's `flat/recommended` essential/strongly-recommended
|
|
572
|
+
// /recommended blocks to `**/*.vue` (verified here too: those blocks
|
|
573
|
+
// carry no `files` restriction in the installed eslint-plugin-vue, so
|
|
574
|
+
// @nx/vue's own `...vue.configs['flat/recommended']` spread — which
|
|
575
|
+
// this generator does not rewrite, only append to — applies Vue-only
|
|
576
|
+
// rules to every .ts/.js file in the app as well). Doing the same here
|
|
577
|
+
// would mean rewriting a block the application generator wrote, not
|
|
578
|
+
// appending to it, which is the one thing this helper is built to
|
|
579
|
+
// avoid; the practical exposure in a fresh single-app scaffold is low
|
|
580
|
+
// (no .spec.ts/.stories.ts test-double files exist yet at this point in
|
|
581
|
+
// the generator, and @nx/vue already turns off the rule most likely to
|
|
582
|
+
// misfire on plain .ts — vue/multi-word-component-names — project-wide
|
|
583
|
+
// on its own). Recorded here rather than silently left unmentioned.
|
|
584
|
+
appendToFlatEslintConfig(tree, `${appName}/eslint.config.mjs`, `,
|
|
585
|
+
{
|
|
586
|
+
// eslint-plugin-vue's flat/recommended (spread above by @nx/vue's own
|
|
587
|
+
// application generator) ships layout/whitespace rules that fight
|
|
588
|
+
// Prettier — same gap libs/vue/eslint.config.mjs closes.
|
|
589
|
+
files: ['**/*.vue'],
|
|
590
|
+
rules: eslintConfigPrettier.rules,
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
// Not part of any eslint-plugin-vue preset — opted in explicitly, same as
|
|
594
|
+
// libs/vue/eslint.config.mjs: catches a declared prop nothing in the
|
|
595
|
+
// component reads.
|
|
596
|
+
files: ['**/*.vue'],
|
|
597
|
+
rules: {
|
|
598
|
+
'vue/no-unused-properties': ['error', { groups: ['props'] }],
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
`, [`import eslintConfigPrettier from 'eslint-config-prettier';`]);
|
|
85
602
|
}
|
|
86
603
|
// Copy design tokens into the scaffolded app so attendees can edit them
|
|
87
604
|
// directly. They're not imported from the @atelier-ui/<fw> npm package
|
|
@@ -90,9 +607,192 @@ function presetGenerator(tree, options) {
|
|
|
90
607
|
tree.write(`${appName}/src/styles/tokens.css`, readTemplate('styles/tokens.css'));
|
|
91
608
|
// Prepend the tokens import to the app's global stylesheet
|
|
92
609
|
const stylesPath = `${appName}/src/styles.css`;
|
|
93
|
-
const existing = tree.exists(stylesPath)
|
|
610
|
+
const existing = tree.exists(stylesPath)
|
|
611
|
+
? ((_c = tree.read(stylesPath, 'utf-8')) !== null && _c !== void 0 ? _c : '')
|
|
612
|
+
: '';
|
|
94
613
|
tree.write(stylesPath, `@import './styles/tokens.css';\n\n${existing}`);
|
|
614
|
+
// Storybook config: mirrors libs/{angular,react,vue}/.storybook/main.ts
|
|
615
|
+
// minus staticDirs, the BUILD_STORYBOOK viteFinal block (hosted-path-only),
|
|
616
|
+
// and @storybook/addon-designs (no Figma handoff doc to link from here).
|
|
617
|
+
// @storybook/addon-vitest DOES ship (owner correction 2026-09-10 to
|
|
618
|
+
// ADR-0123's original "no test runner" call).
|
|
619
|
+
tree.write(`${appName}/.storybook/main.ts`, readTemplate(storybookTemplateName(framework, 'main.ts')));
|
|
620
|
+
tree.write(`${appName}/.storybook/preview.${storybookOutputExt(framework)}`, readTemplate(storybookTemplateName(framework, 'preview')));
|
|
621
|
+
if (framework === 'angular') {
|
|
622
|
+
// The only framework that needs its own Storybook-scoped tsconfig — see
|
|
623
|
+
// libs/angular/.storybook/tsconfig.json, which this mirrors against the
|
|
624
|
+
// tsconfig.json the Angular application generator actually writes (both
|
|
625
|
+
// shapes verified: `files: []`, `include: []`, `references` to app/spec).
|
|
626
|
+
tree.write(`${appName}/.storybook/tsconfig.json`, readTemplate('storybook/angular/tsconfig.json'));
|
|
627
|
+
}
|
|
628
|
+
// One example story per app: small on purpose — it exists so Storybook
|
|
629
|
+
// isn't empty and the attendee has a working pattern to copy, not as a
|
|
630
|
+
// second component showcase.
|
|
631
|
+
tree.write(`${appName}/src/atl-button.stories.${storybookOutputExt(framework)}`, readTemplate(storybookTemplateName(framework, 'atl-button.stories')));
|
|
632
|
+
// Browser-mode Storybook tests (owner correction 2026-09-10 to ADR-0123 —
|
|
633
|
+
// storybook-test / check:stories ship with the scaffold after all).
|
|
634
|
+
// `vitest.config.ts` is always plain `.ts`, never `.tsx` — see
|
|
635
|
+
// storybookTemplateName()'s comment. The literal filename matters:
|
|
636
|
+
// @storybook/addon-vitest's `test-run` tool walks up from a story's
|
|
637
|
+
// .storybook directory looking for the nearest vitest/vite config by
|
|
638
|
+
// this standard name.
|
|
639
|
+
tree.write(`${appName}/vitest.config.ts`, readTemplate(storybookTemplateName(framework, 'vitest.config')));
|
|
640
|
+
tree.write(`${appName}/.storybook/vitest.setup.ts`, readTemplate(storybookTemplateName(framework, 'vitest.setup')));
|
|
641
|
+
// The application generator (@nx/{angular,react,vue}:application, above)
|
|
642
|
+
// guarantees `${appName}/project.json` exists at this point — updateJson
|
|
643
|
+
// reads it first and throws `Cannot find ${path}` if it doesn't, which is
|
|
644
|
+
// exactly the loud failure we want: a workshop app with a Storybook config
|
|
645
|
+
// directory but no way to start it is worse than a generator that stops.
|
|
646
|
+
const storybookPort = 6006 + frameworks.indexOf(framework);
|
|
647
|
+
(0, devkit_1.updateJson)(tree, `${appName}/project.json`, (config) => {
|
|
648
|
+
var _a;
|
|
649
|
+
(_a = config.targets) !== null && _a !== void 0 ? _a : (config.targets = {});
|
|
650
|
+
config.targets['storybook'] = {
|
|
651
|
+
executor: 'nx:run-commands',
|
|
652
|
+
options: {
|
|
653
|
+
command: `npx storybook dev --config-dir ${appName}/.storybook --port ${storybookPort}`,
|
|
654
|
+
},
|
|
655
|
+
};
|
|
656
|
+
config.targets['build-storybook'] = {
|
|
657
|
+
executor: 'nx:run-commands',
|
|
658
|
+
outputs: [`{workspaceRoot}/dist/storybook/${appName}`],
|
|
659
|
+
options: {
|
|
660
|
+
command: `npx storybook build --config-dir ${appName}/.storybook --output-dir dist/storybook/${appName}`,
|
|
661
|
+
},
|
|
662
|
+
};
|
|
663
|
+
// Mirrors libs/{angular,react,vue}/project.json's own "storybook-test"
|
|
664
|
+
// target exactly, modulo the config filename (vitest.config.ts here,
|
|
665
|
+
// vitest.storybook.config.ts there — see storybookTemplateName()'s
|
|
666
|
+
// comment on why the scaffold uses the standard name instead).
|
|
667
|
+
config.targets['storybook-test'] = {
|
|
668
|
+
executor: 'nx:run-commands',
|
|
669
|
+
options: {
|
|
670
|
+
command: 'npx vitest run --config vitest.config.ts',
|
|
671
|
+
cwd: appName,
|
|
672
|
+
},
|
|
673
|
+
};
|
|
674
|
+
// Ported CSS-discipline rules (ADR-0130): a sibling target, not folded
|
|
675
|
+
// into `lint` — same reasoning as this repo's own libs/{fw}/project.json
|
|
676
|
+
// (lint is inferred by @nx/eslint/plugin per project; overriding it to
|
|
677
|
+
// also shell out would re-implement what inference gives for free).
|
|
678
|
+
// --ignore-pattern excludes this app's own tokens.css from the run: it
|
|
679
|
+
// is a token DEFINITION file, the one place these rules' raw literals
|
|
680
|
+
// are supposed to live, not a file that reads/consumes tokens.
|
|
681
|
+
config.targets['stylelint'] = {
|
|
682
|
+
executor: 'nx:run-commands',
|
|
683
|
+
options: {
|
|
684
|
+
command: `stylelint '${appName}/src/**/*.css' --ignore-pattern '${appName}/src/styles/tokens.css' --config stylelint.config.mjs`,
|
|
685
|
+
},
|
|
686
|
+
};
|
|
687
|
+
return config;
|
|
688
|
+
});
|
|
689
|
+
storybookDevDeps[STORYBOOK_FRAMEWORK_PACKAGE[framework]] =
|
|
690
|
+
STORYBOOK_VERSION;
|
|
691
|
+
// @storybook/react-vite and @storybook/vue3-vite each carry their
|
|
692
|
+
// non-vite renderer counterpart as a plain (non-peer) `dependency`, not
|
|
693
|
+
// something npm/pnpm is told the app needs directly — but the story and
|
|
694
|
+
// preview templates import straight from '@storybook/react' /
|
|
695
|
+
// '@storybook/vue3' for the `Meta`/`StoryObj`/`Preview` types. That
|
|
696
|
+
// resolves today only because npm hoists it; pnpm's stricter, non-hoisted
|
|
697
|
+
// layout would leave the import unresolved. Angular has no matching case:
|
|
698
|
+
// '@storybook/angular-vite' has no such counterpart to hoist, and the
|
|
699
|
+
// angular templates import their types from '@storybook/angular-vite'
|
|
700
|
+
// itself (see files/storybook/angular/*.template) — adding
|
|
701
|
+
// '@storybook/angular' back here would reintroduce exactly the ERESOLVE
|
|
702
|
+
// this generator now avoids (its peer on @angular-devkit/build-angular is
|
|
703
|
+
// not satisfiable by a freshly scaffolded Angular 22 app).
|
|
704
|
+
if (framework === 'react') {
|
|
705
|
+
storybookDevDeps['@storybook/react'] = STORYBOOK_VERSION;
|
|
706
|
+
}
|
|
707
|
+
if (framework === 'vue') {
|
|
708
|
+
storybookDevDeps['@storybook/vue3'] = STORYBOOK_VERSION;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
// ─── Prettier (formatting enforcement) ───────────────────────────────────
|
|
712
|
+
// Measured 2026-09-12 (tasks/todo.md, "A. Formatting enforcement"): a
|
|
713
|
+
// fresh `create-nx-workspace` tree ships neither a `prettier`
|
|
714
|
+
// devDependency nor a `.prettierrc` — this is the ADR-0127 problem before
|
|
715
|
+
// even the "configured" half exists, so both are written here rather than
|
|
716
|
+
// assumed present. Content is byte-identical to this monorepo's own
|
|
717
|
+
// `.prettierrc` — no reason for a workshop attendee to start from a
|
|
718
|
+
// different formatting convention than the repo whose components they're
|
|
719
|
+
// consuming.
|
|
720
|
+
console.log(`\n◇ Wiring Prettier…`);
|
|
721
|
+
(0, devkit_1.writeJson)(tree, '.prettierrc', { singleQuote: true });
|
|
722
|
+
// ─── Stylelint (ported CSS-discipline rules, ADR-0130) ───────────────────
|
|
723
|
+
console.log(`\n◇ Wiring stylelint (ported CSS-discipline rules)…`);
|
|
724
|
+
tree.write('stylelint.config.mjs', buildStylelintConfig(frameworks));
|
|
725
|
+
// Six files, byte-identical to the canonical copies in tools/stylelint-rules/
|
|
726
|
+
// (kept that way by this repo's own sync-preflight.mjs) — see
|
|
727
|
+
// buildStylelintConfig's comment above for why no-primitive-token.js ships
|
|
728
|
+
// even though it's never wired into the config just written.
|
|
729
|
+
for (const ruleFile of [
|
|
730
|
+
'index.js',
|
|
731
|
+
'utils.js',
|
|
732
|
+
'no-raw-color-literal.js',
|
|
733
|
+
'no-undeclared-token.js',
|
|
734
|
+
'no-primitive-token.js',
|
|
735
|
+
'no-token-bypass.js',
|
|
736
|
+
]) {
|
|
737
|
+
tree.write(`tools/stylelint-rules/${ruleFile}`, readTemplate(`tools/stylelint-rules/${ruleFile}`));
|
|
95
738
|
}
|
|
739
|
+
// The config and the rule files above both live outside every project that
|
|
740
|
+
// reads them (the per-app `stylelint` targets added in the loop above) —
|
|
741
|
+
// exactly the cache trap ADR-0130 proved twice in this repo's own
|
|
742
|
+
// nx.json. Mirrors this repo's own targetDefaults.stylelint, minus
|
|
743
|
+
// `tools/scripts/lib/allowlists.js` (the scaffold ships no such file — see
|
|
744
|
+
// buildStylelintConfig's comment on why neither wired rule is given
|
|
745
|
+
// `allowlistsFile`). The application generators above are guaranteed to
|
|
746
|
+
// have already written nx.json (create-nx-workspace writes it before any
|
|
747
|
+
// preset runs), so `updateJson` here is safe the same way the package.json
|
|
748
|
+
// update below is.
|
|
749
|
+
(0, devkit_1.updateJson)(tree, 'nx.json', (config) => {
|
|
750
|
+
var _a;
|
|
751
|
+
(_a = config.targetDefaults) !== null && _a !== void 0 ? _a : (config.targetDefaults = {});
|
|
752
|
+
config.targetDefaults['stylelint'] = {
|
|
753
|
+
cache: true,
|
|
754
|
+
inputs: [
|
|
755
|
+
'default',
|
|
756
|
+
'^default',
|
|
757
|
+
'{workspaceRoot}/stylelint.config.mjs',
|
|
758
|
+
'{workspaceRoot}/tools/stylelint-rules/**/*',
|
|
759
|
+
...frameworks.map((framework) => `{workspaceRoot}/workshop-${framework}/src/styles/tokens.css`),
|
|
760
|
+
{ externalDependencies: ['stylelint'] },
|
|
761
|
+
],
|
|
762
|
+
};
|
|
763
|
+
return config;
|
|
764
|
+
});
|
|
765
|
+
// ─── The contract loop (ADR-0121 S4) ─────────────────────────────────────
|
|
766
|
+
// Ships once, scoped to the FIRST selected framework — a contracts.config.json
|
|
767
|
+
// names exactly one framework, the same "the workshop uses one framework"
|
|
768
|
+
// precedent frameworks[0] already sets elsewhere in this generator (the
|
|
769
|
+
// README's "Getting started" nx serve command).
|
|
770
|
+
const primaryFramework = frameworks[0];
|
|
771
|
+
const primaryApp = `workshop-${primaryFramework}`;
|
|
772
|
+
console.log(`\n◇ Writing the contract loop (check:contracts, the example AtlButton contract, the Figma snapshot projection)…`);
|
|
773
|
+
// .ts.template, not .ts — see the comment on storybookTemplateName() above:
|
|
774
|
+
// a literal `.ts` file under files/ is compiled away by this package's own
|
|
775
|
+
// tsconfig.lib.json (`include: ["src/**/*.ts"]`) and never reaches dist/
|
|
776
|
+
// under a name readTemplate() can find at runtime. The OUTPUT filenames
|
|
777
|
+
// below stay plain `.ts` — only the template source needs the suffix.
|
|
778
|
+
tree.write(`${primaryApp}/src/contracts/types.ts`, readTemplate('contracts/types.ts.template'));
|
|
779
|
+
tree.write(`${primaryApp}/src/contracts/README.md`, readTemplate('contracts/README.md'));
|
|
780
|
+
tree.write(`${primaryApp}/src/contracts/button.contract.ts`, readTemplate('contracts/button.contract.ts.template'));
|
|
781
|
+
tree.write('tools/scripts/check-contracts.mjs', readTemplate('tools/scripts/check-contracts.mjs'));
|
|
782
|
+
tree.write('tools/scripts/lib/ts-eval.js', readTemplate('tools/scripts/lib/ts-eval.js'));
|
|
783
|
+
tree.write('tools/scripts/lib/docgen.mjs', readTemplate('tools/scripts/lib/docgen.mjs'));
|
|
784
|
+
tree.write('tools/scripts/figma-snapshot-contracts.mjs', readTemplate('tools/scripts/figma-snapshot-contracts.mjs'));
|
|
785
|
+
// The AtlButton-only projection of this repo's own tools/figma/snapshot.json
|
|
786
|
+
// (gen-scaffold-snapshot.mjs) — gives check:contracts a Figma side for the
|
|
787
|
+
// example contract + story on day one, before the attendee ever runs
|
|
788
|
+
// figma:snapshot themselves.
|
|
789
|
+
tree.write('tools/figma/snapshot.json', readTemplate('figma/snapshot.json'));
|
|
790
|
+
(0, devkit_1.writeJson)(tree, 'contracts.config.json', {
|
|
791
|
+
framework: primaryFramework,
|
|
792
|
+
contracts: `${primaryApp}/src/contracts`,
|
|
793
|
+
stories: [`${primaryApp}/src`],
|
|
794
|
+
snapshot: 'tools/figma/snapshot.json',
|
|
795
|
+
});
|
|
96
796
|
console.log(`\n◇ Writing project files (CLAUDE.md, README, .mcp.json)…`);
|
|
97
797
|
// Write CLAUDE.md with framework-specific guidance
|
|
98
798
|
const frameworkSections = frameworks
|
|
@@ -169,10 +869,126 @@ Key tokens:
|
|
|
169
869
|
- Do not install other UI component libraries alongside Atelier
|
|
170
870
|
- Do not add inline hex colors or hardcoded spacing values
|
|
171
871
|
|
|
872
|
+
## Formatting
|
|
873
|
+
|
|
874
|
+
Prettier is configured (\`.prettierrc\`, \`{ singleQuote: true }\`): \`npm run format\`
|
|
875
|
+
(\`prettier --write .\`) and \`npm run check:format\` (\`prettier --check .\`). The
|
|
876
|
+
scaffold itself is already Prettier-clean — \`check:format\` passes right after
|
|
877
|
+
\`npm install\`, before you've written a line of your own.
|
|
878
|
+
|
|
172
879
|
## Apps
|
|
173
880
|
|
|
174
881
|
${frameworks.map((f) => `- \`workshop-${f}\` — run with \`npx nx serve workshop-${f}\``).join('\n')}
|
|
175
882
|
|
|
883
|
+
## Storybook
|
|
884
|
+
|
|
885
|
+
Every app has its own local Storybook, started per framework:
|
|
886
|
+
|
|
887
|
+
${frameworks.map((f, i) => `- \`workshop-${f}\` — \`npx nx storybook workshop-${f}\` — http://localhost:${6006 + i}`).join('\n')}
|
|
888
|
+
|
|
889
|
+
Build a static Storybook (CI, hosting) with \`npx nx build-storybook workshop-<fw>\`.
|
|
890
|
+
|
|
891
|
+
Every story is also a browser-mode test (\`@storybook/addon-vitest\`). One-time setup
|
|
892
|
+
after \`npm install\`:
|
|
893
|
+
|
|
894
|
+
\`\`\`bash
|
|
895
|
+
npx playwright install chromium
|
|
896
|
+
\`\`\`
|
|
897
|
+
|
|
898
|
+
Then run every app's stories headless in Chromium with axe checks:
|
|
899
|
+
|
|
900
|
+
\`\`\`bash
|
|
901
|
+
npm run check:stories
|
|
902
|
+
\`\`\`
|
|
903
|
+
|
|
904
|
+
## Agent Skills
|
|
905
|
+
|
|
906
|
+
${skillsEnabled
|
|
907
|
+
? `The scaffold attempted to install four \`storybookjs/mcp\` skills for Claude
|
|
908
|
+
Code (\`.claude/skills/\`) during setup — a failed clone (offline, unreachable
|
|
909
|
+
registry) does not stop the workspace from being created, so this file can't
|
|
910
|
+
promise they actually landed. Check \`.claude/skills/\` for the four directories
|
|
911
|
+
below; re-run the command further down if any are missing:
|
|
912
|
+
|
|
913
|
+
- **stories** — invoke first, before creating, editing, or deleting components, stories, styles, CSS, themes, colors, or design tokens
|
|
914
|
+
- **storybook-init** — adding Storybook to a project that does not have it configured yet
|
|
915
|
+
- **storybook-setup** — Storybook is installed and you want a working \`preview\` file and stories for real components
|
|
916
|
+
- **storybook-upgrade** — Storybook exists but needs an upgrade
|
|
917
|
+
|
|
918
|
+
Re-run or update the install at any time:
|
|
919
|
+
|
|
920
|
+
\`\`\`bash
|
|
921
|
+
${SKILLS_ADD_COMMAND_FOR_HUMANS}
|
|
922
|
+
\`\`\`
|
|
923
|
+
|
|
924
|
+
**Which MCP tools these skills can reach depends on \`.mcp.json\`, not just on the
|
|
925
|
+
skills being present.** \`.mcp.json\` wires only the *hosted* Storybook MCP servers
|
|
926
|
+
(\`storybook-<fw>\` → ${SITE_URL}/storybook-<fw>/mcp), which serve the \`docs\`
|
|
927
|
+
toolset — \`docs-list\`, \`docs-show\`, \`docs-show-story\` all work out of the box.
|
|
928
|
+
The skills' \`dev\`-toolset tools (\`stories-preview\`, \`get-storybook-story-instructions\`)
|
|
929
|
+
instead come from \`@storybook/addon-mcp\` inside a *running local* Storybook
|
|
930
|
+
(already configured in every app's \`.storybook/main.ts\`) — that local endpoint is
|
|
931
|
+
deliberately not wired into \`.mcp.json\` by default, the same as this repo's own
|
|
932
|
+
root \`.mcp.json\`, because a fixed \`localhost\` entry would fail to connect on
|
|
933
|
+
every Claude Code session started without that Storybook already running. To use
|
|
934
|
+
the \`dev\` toolset: start \`npx nx storybook workshop-<fw>\` (see Storybook below),
|
|
935
|
+
then add this to \`.mcp.json\`'s \`mcpServers\` (only while that Storybook is up):
|
|
936
|
+
|
|
937
|
+
\`\`\`json
|
|
938
|
+
"storybook-<fw>-local": {
|
|
939
|
+
"type": "http",
|
|
940
|
+
"url": "http://localhost:<port>/mcp"
|
|
941
|
+
}
|
|
942
|
+
\`\`\`
|
|
943
|
+
|
|
944
|
+
(\`<port>\` is the one listed for that app under Storybook below.) \`test-run\`
|
|
945
|
+
works too, once that local Storybook is running — every story here IS a
|
|
946
|
+
render + accessibility test (\`@storybook/addon-vitest\`, run offline via
|
|
947
|
+
\`npm run check:stories\`; see "The Contract Loop" below).`
|
|
948
|
+
: `Skipped for this workspace (\`skills: false\`). Install the four \`storybookjs/mcp\`
|
|
949
|
+
skills for Claude Code by hand:
|
|
950
|
+
|
|
951
|
+
\`\`\`bash
|
|
952
|
+
${SKILLS_ADD_COMMAND_FOR_HUMANS}
|
|
953
|
+
\`\`\`
|
|
954
|
+
`}
|
|
955
|
+
|
|
956
|
+
## The Contract Loop
|
|
957
|
+
|
|
958
|
+
A contract (\`${primaryApp}/src/contracts/<name>.contract.ts\`) is the one hand-authored
|
|
959
|
+
spec file per component: the Figma master's node id, plus intentional Figma ↔ code
|
|
960
|
+
mismatches (\`figmaOnly\`, \`codeOnly\`, \`axisMap\`) — never props, defaults, or
|
|
961
|
+
descriptions; those live in the component's own types/JSDoc and its stories.
|
|
962
|
+
|
|
963
|
+
Order: read the Figma handoff → write the contract → write one story per variant and
|
|
964
|
+
interaction state → \`npm run check:contracts\` → \`figma_check_design_parity\` in Storybook.
|
|
965
|
+
|
|
966
|
+
\`check:contracts\` joins the contract, the component's docgen, and \`tools/figma/snapshot.json\`
|
|
967
|
+
offline — no browser, no Storybook build — and reports one line per finding:
|
|
968
|
+
- \`[CONTRACT-MISSING]\` / \`[CONTRACT-NODE]\` — no contract file, or its node id disagrees
|
|
969
|
+
- \`[AXIS]\` / \`[BOOLEAN]\` / \`[ENUM-UNDRAWN]\` — a Figma property has no matching code prop
|
|
970
|
+
- \`[COVERAGE]\` / \`[COVERAGE-BOOL]\` — a variant value or boolean is never rendered by a story
|
|
971
|
+
- \`[FIGMA-ONLY]\` / \`[STALE-EXEMPTION]\` / \`[UNMIRRORED]\` — an exemption is missing, stale, or unexplained
|
|
972
|
+
- \`[NO-STORY-META]\` / \`[NO-MASTER]\` — a contract with nothing yet to check it against
|
|
973
|
+
- \`[CONTRACT-IMPORT]\` — a story meta doesn't import its component's contract and set
|
|
974
|
+
\`contract\` in \`parameters\` (a warning here, until this workspace ships a docs block
|
|
975
|
+
that renders it)
|
|
976
|
+
|
|
977
|
+
Refresh \`tools/figma/snapshot.json\` from the real master with the Figma Desktop Bridge
|
|
978
|
+
connected: edit the \`--file\` placeholder in \`package.json\`'s \`figma:snapshot\` script to
|
|
979
|
+
your own Figma file key, then run \`npm run figma:snapshot\`.
|
|
980
|
+
|
|
981
|
+
\`check:contracts\` proves shape and story coverage; \`npm run check:stories\` (every story,
|
|
982
|
+
rendered headless in Chromium via \`@storybook/addon-vitest\`, with axe) proves rendering
|
|
983
|
+
and accessibility. It does so for components whose source lives in this workspace. For
|
|
984
|
+
components imported from \`@atelier-ui/${primaryFramework}\` — the example \`AtlButton\`
|
|
985
|
+
included — the check recognises that the import resolves into \`node_modules\` and skips
|
|
986
|
+
docgen for it — the same rule in every framework — so it has nothing to compare and
|
|
987
|
+
reports only \`[NO-STORY-META]\`; their prop tables come from the hosted
|
|
988
|
+
Storybook MCP (\`docs-show\`) instead. A green \`check:contracts\` on the example story
|
|
989
|
+
therefore proves the wiring, not the example. Run \`npx playwright install chromium\`
|
|
990
|
+
once after \`npm install\` — see Storybook below.
|
|
991
|
+
|
|
176
992
|
## Troubleshooting
|
|
177
993
|
|
|
178
994
|
Run the preflight self-check to verify your environment:
|
|
@@ -202,8 +1018,62 @@ file exports). The Desktop Bridge covers creation and inspection without a token
|
|
|
202
1018
|
- MCP Playground (inspect tool responses): ${SITE_URL}/mcp
|
|
203
1019
|
- CLAUDE.md template: ${SITE_URL}/claude-md
|
|
204
1020
|
`);
|
|
205
|
-
//
|
|
206
|
-
|
|
1021
|
+
// The contract loop's own devDependencies (ADR-0121 S4): the MCP SDK
|
|
1022
|
+
// figma-snapshot-contracts.mjs imports directly, always; `typescript` only
|
|
1023
|
+
// when the framework application generator (above) didn't already add it —
|
|
1024
|
+
// read from the tree's package.json as it stands right now, after every
|
|
1025
|
+
// framework's generator has run and before this generator's own writes
|
|
1026
|
+
// below it.
|
|
1027
|
+
const pkgSoFar = (0, devkit_1.readJson)(tree, 'package.json');
|
|
1028
|
+
const existingDeps = Object.assign(Object.assign({}, ((_d = pkgSoFar.dependencies) !== null && _d !== void 0 ? _d : {})), ((_e = pkgSoFar.devDependencies) !== null && _e !== void 0 ? _e : {}));
|
|
1029
|
+
const hasTypescript = Boolean(existingDeps.typescript);
|
|
1030
|
+
const contractLoopDevDeps = {
|
|
1031
|
+
'@modelcontextprotocol/sdk': MCP_SDK_VERSION,
|
|
1032
|
+
};
|
|
1033
|
+
if (!hasTypescript) {
|
|
1034
|
+
contractLoopDevDeps.typescript = TYPESCRIPT_VERSION;
|
|
1035
|
+
}
|
|
1036
|
+
// Browser-mode Storybook tests (owner correction 2026-09-10 to ADR-0123):
|
|
1037
|
+
// each selected framework needs its own Vite plugin for the vitest browser
|
|
1038
|
+
// pipeline, added only when the framework's own application generator
|
|
1039
|
+
// (above) didn't already bring it in. React and Vue's vite-based app
|
|
1040
|
+
// generators typically already do; Angular's esbuild-based one never does.
|
|
1041
|
+
const viteFrameworkDevDeps = {};
|
|
1042
|
+
if (frameworks.includes('react') && !existingDeps['@vitejs/plugin-react']) {
|
|
1043
|
+
viteFrameworkDevDeps['@vitejs/plugin-react'] = VITE_PLUGIN_REACT_VERSION;
|
|
1044
|
+
}
|
|
1045
|
+
if (frameworks.includes('vue') && !existingDeps['@vitejs/plugin-vue']) {
|
|
1046
|
+
viteFrameworkDevDeps['@vitejs/plugin-vue'] = VITE_PLUGIN_VUE_VERSION;
|
|
1047
|
+
}
|
|
1048
|
+
if (frameworks.includes('angular') &&
|
|
1049
|
+
!existingDeps['@analogjs/vite-plugin-angular']) {
|
|
1050
|
+
viteFrameworkDevDeps['@analogjs/vite-plugin-angular'] =
|
|
1051
|
+
ANALOGJS_VITE_PLUGIN_ANGULAR_VERSION;
|
|
1052
|
+
}
|
|
1053
|
+
if (frameworks.includes('vue') &&
|
|
1054
|
+
!existingDeps['@testing-library/jest-dom']) {
|
|
1055
|
+
viteFrameworkDevDeps['@testing-library/jest-dom'] =
|
|
1056
|
+
TESTING_LIBRARY_JEST_DOM_VERSION;
|
|
1057
|
+
}
|
|
1058
|
+
if (frameworks.includes('vue') && !existingDeps['eslint-config-prettier']) {
|
|
1059
|
+
viteFrameworkDevDeps['eslint-config-prettier'] =
|
|
1060
|
+
ESLINT_CONFIG_PRETTIER_VERSION;
|
|
1061
|
+
}
|
|
1062
|
+
// Ported CSS-discipline rules (ADR-0130). No postcss-html: that's only
|
|
1063
|
+
// needed to lint .astro files, and the scaffold has none.
|
|
1064
|
+
const stylelintDevDeps = {
|
|
1065
|
+
stylelint: STYLELINT_VERSION,
|
|
1066
|
+
};
|
|
1067
|
+
// Formatting enforcement — see the "Prettier" section above for why this
|
|
1068
|
+
// is written at all.
|
|
1069
|
+
const prettierDevDeps = {
|
|
1070
|
+
prettier: PRETTIER_VERSION,
|
|
1071
|
+
};
|
|
1072
|
+
// Install selected @atelier-ui/* packages (dependencies) and Storybook +
|
|
1073
|
+
// the contract loop's own tools + the vitest browser-mode tooling +
|
|
1074
|
+
// stylelint + prettier (devDependencies, exact pins — see the *_VERSION
|
|
1075
|
+
// constants above)
|
|
1076
|
+
const installTask = (0, devkit_1.addDependenciesToPackageJson)(tree, deps, Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, storybookDevDeps), contractLoopDevDeps), viteFrameworkDevDeps), stylelintDevDeps), prettierDevDeps));
|
|
207
1077
|
// Remove the preset package itself — create-nx-workspace adds it automatically
|
|
208
1078
|
// but it's a build-time tool and should not be in the workspace's dependencies
|
|
209
1079
|
const removePresetTask = (0, devkit_1.removeDependenciesFromPackageJson)(tree, ['@atelier-ui/create-workspace'], []);
|
|
@@ -217,6 +1087,50 @@ file exports). The Desktop Bridge covers creation and inspection without a token
|
|
|
217
1087
|
var _a;
|
|
218
1088
|
pkg.scripts = (_a = pkg.scripts) !== null && _a !== void 0 ? _a : {};
|
|
219
1089
|
pkg.scripts.preflight = 'node tools/scripts/preflight.mjs';
|
|
1090
|
+
// The contract loop (ADR-0121 S4). `figma:snapshot` ships with a literal
|
|
1091
|
+
// placeholder rather than the Atelier file key — this preset has no
|
|
1092
|
+
// "which Figma file is yours" schema option (only the boolean
|
|
1093
|
+
// `figmaMcp`), and QMnDD8uZQPldPrlCwZZ58T is THIS repo's own file, not
|
|
1094
|
+
// the attendee's. CLAUDE.md's "The contract loop" section spells out how
|
|
1095
|
+
// to fill it in.
|
|
1096
|
+
pkg.scripts['check:contracts'] = 'node tools/scripts/check-contracts.mjs';
|
|
1097
|
+
pkg.scripts['figma:snapshot'] =
|
|
1098
|
+
'node tools/scripts/figma-snapshot-contracts.mjs --file <YOUR_FIGMA_FILE_KEY>';
|
|
1099
|
+
// Browser-mode Storybook tests (owner correction 2026-09-10 to ADR-0123).
|
|
1100
|
+
// Identical to the monorepo's own root package.json script.
|
|
1101
|
+
pkg.scripts['check:stories'] = 'nx run-many -t storybook-test --parallel=1';
|
|
1102
|
+
// Ported CSS-discipline rules (ADR-0130). Identical to the monorepo's own
|
|
1103
|
+
// root package.json script.
|
|
1104
|
+
pkg.scripts['check:stylelint'] = 'nx run-many -t stylelint';
|
|
1105
|
+
// Formatting enforcement. Deliberately this monorepo's own
|
|
1106
|
+
// `prettier --write .` / `prettier --check .` shape, not `nx format:*` —
|
|
1107
|
+
// measured 2026-09-12 against a real generated workspace, two distinct
|
|
1108
|
+
// failure modes:
|
|
1109
|
+
// 1. Unconditional, on the pristine, untouched scaffold: with no
|
|
1110
|
+
// formatter resolvable in node_modules, `nx format:check` exits 0
|
|
1111
|
+
// printing "No formatter configured" — a silent pass before any git
|
|
1112
|
+
// logic even runs. Worth keeping in mind even though this preset
|
|
1113
|
+
// now installs prettier: the tool's own failure mode is "nothing is
|
|
1114
|
+
// configured, so nothing is wrong", not a loud error.
|
|
1115
|
+
// 2. Conditional: once prettier is installed, `nx format:check`
|
|
1116
|
+
// un-flagged does NOT silently pass immediately after scaffolding —
|
|
1117
|
+
// `git init` stages files without committing, so the `main` ref
|
|
1118
|
+
// doesn't resolve yet, Nx catches that specific git failure and
|
|
1119
|
+
// falls back to an all-files scan, correctly reporting every
|
|
1120
|
+
// unformatted file with exit 1 (after an alarming
|
|
1121
|
+
// `Command failed: ... fatal: ambiguous argument 'main'` on
|
|
1122
|
+
// stderr). The silent zero-file pass only appears once the
|
|
1123
|
+
// attendee makes their own first commit, putting `main` and `HEAD`
|
|
1124
|
+
// at the same revision — the steady state a workspace reaches
|
|
1125
|
+
// within minutes of scaffolding, and the one `nx format:check`
|
|
1126
|
+
// un-flagged reads as "nothing changed, nothing to check".
|
|
1127
|
+
// `prettier --check .` has neither failure mode: it always scans the
|
|
1128
|
+
// whole tree (honouring .gitignore's node_modules/dist/.nx exclusions
|
|
1129
|
+
// already, so no .prettierignore is needed), and if prettier were ever
|
|
1130
|
+
// missing it fails loudly rather than reporting a silent pass. A script
|
|
1131
|
+
// an attendee runs unmodified should not depend on remembering `--all`.
|
|
1132
|
+
pkg.scripts.format = 'prettier --write .';
|
|
1133
|
+
pkg.scripts['check:format'] = 'prettier --check .';
|
|
220
1134
|
return pkg;
|
|
221
1135
|
});
|
|
222
1136
|
// Write .mcp.json with MCP servers for selected frameworks
|
|
@@ -257,9 +1171,44 @@ ${frameworks.map((f) => `- \`workshop-${f}\` — \`@atelier-ui/${f}\``).join('\n
|
|
|
257
1171
|
|
|
258
1172
|
\`\`\`bash
|
|
259
1173
|
npm install
|
|
1174
|
+
npx playwright install chromium # one-time — needed by npm run check:stories
|
|
260
1175
|
npx nx serve workshop-${frameworks[0]}
|
|
261
1176
|
\`\`\`
|
|
262
1177
|
|
|
1178
|
+
## Formatting
|
|
1179
|
+
|
|
1180
|
+
Prettier is configured (\`.prettierrc\`): \`npm run format\` (\`prettier --write .\`) and
|
|
1181
|
+
\`npm run check:format\` (\`prettier --check .\`). The scaffold is already
|
|
1182
|
+
Prettier-clean out of the box — \`check:format\` passes right after \`npm install\`.
|
|
1183
|
+
|
|
1184
|
+
## Storybook
|
|
1185
|
+
|
|
1186
|
+
${frameworks.map((f, i) => `- \`workshop-${f}\` — \`npx nx storybook workshop-${f}\` — http://localhost:${6006 + i}`).join('\n')}
|
|
1187
|
+
|
|
1188
|
+
Every story is also a browser-mode test — run them all headless in Chromium with
|
|
1189
|
+
\`npm run check:stories\`.
|
|
1190
|
+
|
|
1191
|
+
## Agent Skills
|
|
1192
|
+
|
|
1193
|
+
${skillsEnabled
|
|
1194
|
+
? `The scaffold attempted to install the four \`storybookjs/mcp\` skills (\`stories\`,
|
|
1195
|
+
\`storybook-init\`, \`storybook-setup\`, \`storybook-upgrade\`) for Claude Code under
|
|
1196
|
+
\`.claude/skills/\` — check that directory, since a failed clone doesn't stop the
|
|
1197
|
+
workspace from being created. Re-run or update them with:
|
|
1198
|
+
|
|
1199
|
+
\`\`\`bash
|
|
1200
|
+
${SKILLS_ADD_COMMAND_FOR_HUMANS}
|
|
1201
|
+
\`\`\`
|
|
1202
|
+
|
|
1203
|
+
Note: \`test-run\` works once a local Storybook is running — every story here is a
|
|
1204
|
+
render + accessibility test (\`@storybook/addon-vitest\`, also runnable offline via
|
|
1205
|
+
\`npm run check:stories\`). See CLAUDE.md for details.`
|
|
1206
|
+
: `Skipped (\`skills: false\`). Install by hand:
|
|
1207
|
+
|
|
1208
|
+
\`\`\`bash
|
|
1209
|
+
${SKILLS_ADD_COMMAND_FOR_HUMANS}
|
|
1210
|
+
\`\`\``}
|
|
1211
|
+
|
|
263
1212
|
## MCP
|
|
264
1213
|
|
|
265
1214
|
Claude Code MCP servers are pre-configured in \`.mcp.json\`.
|
|
@@ -275,6 +1224,7 @@ Browse components at ${SITE_URL}
|
|
|
275
1224
|
yield installTask();
|
|
276
1225
|
console.log(`\n◇ Cleaning up preset package…`);
|
|
277
1226
|
yield removePresetTask();
|
|
1227
|
+
yield installSkills(tree, skillsEnabled);
|
|
278
1228
|
});
|
|
279
1229
|
});
|
|
280
1230
|
}
|