@loomweaver/cli 0.7.2 → 0.7.3

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.
Files changed (2) hide show
  1. package/dist/main.mjs +737 -88
  2. package/package.json +1 -1
package/dist/main.mjs CHANGED
@@ -12,6 +12,243 @@ function generate(recipe, input) {
12
12
  }
13
13
  return files;
14
14
  }
15
+ function amendments(recipe, input) {
16
+ return recipe.amend?.(input) ?? [];
17
+ }
18
+
19
+ // ../devkit/src/lib/amend/merge.ts
20
+ function joinProjectPath(projectRoot, path) {
21
+ const root = projectRoot.replace(/^\.?\/*/, "").replace(/\/+$/, "");
22
+ return root ? `${root}/${path}` : path;
23
+ }
24
+ function resolveAssetInput(glob, projectRoot) {
25
+ return glob.from === "project" ? joinProjectPath(projectRoot, glob.input) : glob.input;
26
+ }
27
+ function ensurePostcssPlugin(existing, amendment) {
28
+ const root = asObject(existing) ?? {};
29
+ const plugins = asObject(root["plugins"]);
30
+ if (plugins === void 0 && root["plugins"] !== void 0) {
31
+ return {
32
+ value: root,
33
+ added: [],
34
+ declined: [`${amendment.file}: "plugins" is not an object`]
35
+ };
36
+ }
37
+ const next = { ...plugins ?? {} };
38
+ if (amendment.plugin in next) {
39
+ return { value: root, added: [], declined: [] };
40
+ }
41
+ next[amendment.plugin] = {};
42
+ return {
43
+ value: { ...root, plugins: next },
44
+ added: [`${amendment.file}: ${amendment.plugin}`],
45
+ declined: []
46
+ };
47
+ }
48
+ function ensureBuildTarget(target, amendment, projectRoot) {
49
+ const next = { ...asObject(target) ?? {} };
50
+ const added = [];
51
+ const declined = [];
52
+ const options = { ...asObject(next["options"]) ?? {} };
53
+ const styles = ensureStrings(
54
+ options["styles"],
55
+ amendment.styles.map((style) => joinProjectPath(projectRoot, style))
56
+ );
57
+ if (styles.added.length > 0) {
58
+ options["styles"] = styles.value;
59
+ added.push(...styles.added.map((entry) => `styles: ${entry}`));
60
+ }
61
+ const assets = ensureAssets(options["assets"], amendment.assets, projectRoot);
62
+ if (assets.added.length > 0) {
63
+ options["assets"] = assets.value;
64
+ added.push(...assets.added.map((entry) => `assets: ${entry}`));
65
+ }
66
+ next["options"] = options;
67
+ if (amendment.inlineCritical !== void 0 || amendment.serviceWorker) {
68
+ const configurations = { ...asObject(next["configurations"]) ?? {} };
69
+ const production = { ...asObject(configurations["production"]) ?? {} };
70
+ if (amendment.serviceWorker && production["serviceWorker"] === void 0) {
71
+ production["serviceWorker"] = joinProjectPath(
72
+ projectRoot,
73
+ amendment.serviceWorker
74
+ );
75
+ added.push(`production serviceWorker: ${production["serviceWorker"]}`);
76
+ }
77
+ if (amendment.inlineCritical !== void 0) {
78
+ const critical = ensureInlineCritical(
79
+ production["optimization"],
80
+ amendment.inlineCritical
81
+ );
82
+ if (critical.declined) {
83
+ declined.push(
84
+ "production optimization is a boolean, so inlineCritical cannot be set beside it \u2014 a release build then loads the stylesheet with an inline handler the generated content-security policy blocks, and renders unstyled"
85
+ );
86
+ } else if (critical.changed) {
87
+ production["optimization"] = critical.value;
88
+ added.push(
89
+ `production optimization.styles.inlineCritical: ${amendment.inlineCritical}`
90
+ );
91
+ }
92
+ }
93
+ configurations["production"] = production;
94
+ next["configurations"] = configurations;
95
+ }
96
+ return { value: next, added, declined };
97
+ }
98
+ function ensureStylesheetSource(css, source) {
99
+ const quoted = source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
100
+ if (new RegExp(`@source\\s+['"]${quoted}/?['"]`).test(css)) {
101
+ return css;
102
+ }
103
+ return `${css.trimEnd()}
104
+
105
+ @source '${source}';
106
+ `;
107
+ }
108
+ function ensureInlineCritical(optimization, inlineCritical) {
109
+ if (typeof optimization === "boolean") {
110
+ return { value: optimization, changed: false, declined: true };
111
+ }
112
+ const root = { ...asObject(optimization) ?? {} };
113
+ const styles = asObject(root["styles"]);
114
+ if (styles === void 0 && root["styles"] !== void 0) {
115
+ return { value: optimization, changed: false, declined: true };
116
+ }
117
+ if (styles?.["inlineCritical"] !== void 0) {
118
+ return { value: optimization, changed: false, declined: false };
119
+ }
120
+ return {
121
+ value: { ...root, styles: { ...styles ?? {}, inlineCritical } },
122
+ changed: true,
123
+ declined: false
124
+ };
125
+ }
126
+ function ensureStrings(existing, wanted) {
127
+ const list2 = Array.isArray(existing) ? [...existing] : [];
128
+ const added = [];
129
+ for (const entry of wanted) {
130
+ if (!list2.includes(entry)) {
131
+ list2.push(entry);
132
+ added.push(entry);
133
+ }
134
+ }
135
+ return { value: list2, added };
136
+ }
137
+ function ensureAssets(existing, wanted, projectRoot) {
138
+ const list2 = Array.isArray(existing) ? [...existing] : [];
139
+ const added = [];
140
+ for (const glob of wanted) {
141
+ const input = resolveAssetInput(glob, projectRoot);
142
+ if (list2.some((entry) => inputOf(entry) === input)) {
143
+ continue;
144
+ }
145
+ list2.push({
146
+ glob: glob.glob,
147
+ input,
148
+ ...glob.output === void 0 ? {} : { output: glob.output }
149
+ });
150
+ added.push(input);
151
+ }
152
+ return { value: list2, added };
153
+ }
154
+ function inputOf(entry) {
155
+ if (typeof entry === "string") {
156
+ return entry;
157
+ }
158
+ const asset = asObject(entry);
159
+ return typeof asset?.["input"] === "string" ? asset["input"] : void 0;
160
+ }
161
+ function asObject(value) {
162
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
163
+ }
164
+
165
+ // ../devkit/src/lib/amend/compose.ts
166
+ var PROVIDERS = /(export\s+const\s+appConfig\s*:[^=]*=\s*\{[\s\S]*?providers\s*:\s*\[)([\s\S]*?)(\n(\s*)\],)/;
167
+ var SHELL_IMPORT = /import\s*\{([^}]*)\}\s*from\s*'@loomweaver\/shell';/;
168
+ function composePlugin(source, amendment, importPath) {
169
+ if (source.includes(amendment.symbol)) {
170
+ return { source, composed: true };
171
+ }
172
+ const providers = PROVIDERS.exec(source);
173
+ const shellImport = SHELL_IMPORT.exec(source);
174
+ if (!providers || !shellImport) {
175
+ return { source, composed: false };
176
+ }
177
+ const withImports = source.replace(
178
+ SHELL_IMPORT,
179
+ `import {${withShellSymbols(shellImport[1])}} from '@loomweaver/shell';
180
+ import { ${amendment.symbol} } from '${importPath}';`
181
+ );
182
+ const indent = `${providers[4]} `;
183
+ const lines = [
184
+ `${indent}provideTranslationNamespaces('${amendment.id}'),`,
185
+ `${indent}provideCapabilityGrants({ ${amendment.id}: [${amendment.capabilities.map((capability) => `'${capability}'`).join(", ")}] }),`,
186
+ `${indent}...providePlugins(${amendment.symbol}),`
187
+ ].join("\n");
188
+ return {
189
+ source: withImports.replace(
190
+ PROVIDERS,
191
+ (_all, head, body, tail) => `${head}${body}
192
+ ${lines}${tail}`
193
+ ),
194
+ composed: true
195
+ };
196
+ }
197
+ function composeLines(amendment, importPath) {
198
+ return [
199
+ `import { ${amendment.symbol} } from '${importPath}';`,
200
+ "import { providePlugins, provideCapabilityGrants, provideTranslationNamespaces } from '@loomweaver/shell';",
201
+ `provideTranslationNamespaces('${amendment.id}'),`,
202
+ `provideCapabilityGrants({ ${amendment.id}: [${amendment.capabilities.map((capability) => `'${capability}'`).join(", ")}] }),`,
203
+ `...providePlugins(${amendment.symbol}),`
204
+ ];
205
+ }
206
+ function withShellSymbols(existing) {
207
+ const wanted = [
208
+ "provideCapabilityGrants",
209
+ "providePlugins",
210
+ "provideTranslationNamespaces"
211
+ ];
212
+ const present = existing.split(",").map((symbol) => symbol.trim()).filter(Boolean);
213
+ const missing = wanted.filter(
214
+ (symbol) => !present.some((entry) => entry.replace(/^type\s+/, "") === symbol)
215
+ );
216
+ if (missing.length === 0) {
217
+ return existing;
218
+ }
219
+ const multiline = existing.includes("\n");
220
+ const all = [...present, ...missing].sort((a, b) => a.localeCompare(b));
221
+ return multiline ? `
222
+ ${all.join(",\n ")},
223
+ ` : ` ${all.join(", ")} `;
224
+ }
225
+
226
+ // ../devkit/src/lib/amend/describe.ts
227
+ function describeAmendment(amendment) {
228
+ if (amendment.kind === "postcss") {
229
+ return `Write ${amendment.file} beside your package.json, naming ${amendment.plugin}. Without it the stylesheet is read as plain CSS: no utility class is emitted, the workbench renders unstyled, and the build still reports success.`;
230
+ }
231
+ if (amendment.kind === "stylesheet-source") {
232
+ return `Add an @source entry for '${amendment.sourceRoot}' to the application's entry stylesheet, resolved from that stylesheet. Without it none of that code's utilities are emitted.`;
233
+ }
234
+ if (amendment.kind === "compose-plugin") {
235
+ return `Register ${amendment.id} in the composition root: import { ${amendment.symbol} }, provideTranslationNamespaces('${amendment.id}'), provideCapabilityGrants({ ${amendment.id}: [${amendment.capabilities.map((capability) => `'${capability}'`).join(
236
+ ", "
237
+ )}] }) and ...providePlugins(${amendment.symbol}). Without it none of its contributions appear.`;
238
+ }
239
+ return [
240
+ ...amendment.styles.length > 0 ? [`name ${amendment.styles.join(", ")} in styles`] : [],
241
+ ...amendment.assets.length > 0 ? [
242
+ `add assets for ${amendment.assets.map((asset) => asset.input).join(", ")} (the shell fetches its own strings at runtime, so without that glob every label in the chrome renders as its raw translation key)`
243
+ ] : [],
244
+ ...amendment.serviceWorker ? [
245
+ `set serviceWorker to ${amendment.serviceWorker} in the production configuration (provideShell registers a worker that 404s otherwise)`
246
+ ] : [],
247
+ ...amendment.inlineCritical === void 0 ? [] : [
248
+ `set optimization.styles.inlineCritical to ${amendment.inlineCritical} in the production configuration (the generated content-security policy blocks the inline handler Angular's critical-CSS pass attaches, so a release build renders unstyled)`
249
+ ]
250
+ ].map((step, index) => `${index + 1}. ${step}`).join(" ");
251
+ }
15
252
 
16
253
  // ../devkit/src/lib/generate/casing.ts
17
254
  function isKebabId(value) {
@@ -834,6 +1071,40 @@ var framePlugin = {
834
1071
  }
835
1072
  };
836
1073
 
1074
+ // ../devkit/src/recipes/angular-distribution/amendments.ts
1075
+ function distributionAmendments(d) {
1076
+ return [
1077
+ ...d.styles === "tailwind" ? [
1078
+ {
1079
+ kind: "postcss",
1080
+ file: ".postcssrc.json",
1081
+ plugin: "@tailwindcss/postcss"
1082
+ }
1083
+ ] : [],
1084
+ {
1085
+ kind: "build-target",
1086
+ styles: ["src/styles.css"],
1087
+ assets: [
1088
+ { glob: "**/*", input: "public", from: "project" },
1089
+ {
1090
+ glob: "**/*",
1091
+ input: "node_modules/@loomweaver/shell/i18n",
1092
+ from: "workspace",
1093
+ output: "i18n"
1094
+ },
1095
+ {
1096
+ glob: "**/*",
1097
+ input: "node_modules/@loomweaver/frame-kit/dist",
1098
+ from: "workspace",
1099
+ output: "frame-kit"
1100
+ }
1101
+ ],
1102
+ serviceWorker: "ngsw-config.json",
1103
+ inlineCritical: false
1104
+ }
1105
+ ];
1106
+ }
1107
+
837
1108
  // ../devkit/src/recipes/shell-regions.ts
838
1109
  var SHELL_REGIONS = [
839
1110
  "{ id: 'top-bar', type: 'bar', dock: 'top' }",
@@ -1086,20 +1357,15 @@ function stylesNotes(d) {
1086
1357
  }
1087
1358
  return [
1088
1359
  "`src/styles.css` compiles the shell's source theme with Tailwind 4, which is also what lets you",
1089
- "write Tailwind utilities in your own templates. It needs two things the scaffold cannot add for",
1090
- "you \u2014 the packages:",
1360
+ "write Tailwind utilities in your own templates. The scaffold wrote `.postcssrc.json` beside your",
1361
+ "`package.json` for you, because without it the stylesheet is read as plain CSS: no utility class",
1362
+ "is emitted, the workbench renders unstyled, and the build still reports success. The packages are",
1363
+ "the one thing left, because a scaffold does not install:",
1091
1364
  "",
1092
1365
  "```sh",
1093
1366
  "npm install -D tailwindcss @tailwindcss/postcss @tailwindcss/typography",
1094
1367
  "```",
1095
1368
  "",
1096
- "and the PostCSS plugin, in a file next to your `package.json`:",
1097
- "",
1098
- "```jsonc",
1099
- "// .postcssrc.json",
1100
- '{ "plugins": { "@tailwindcss/postcss": {} } }',
1101
- "```",
1102
- "",
1103
1369
  "Use **semantic tokens only** in your own templates (`bg-surface`, `text-content`, `text-brand`,",
1104
1370
  "`border-border`), never raw palette colours.",
1105
1371
  "",
@@ -1185,39 +1451,25 @@ function readme2(d) {
1185
1451
  "",
1186
1452
  "## Build wiring",
1187
1453
  "",
1188
- "The Nx generator put all of this in `project.json`. Scaffolded over the CLI or MCP, add it to",
1189
- "your build target yourself \u2014 `angular.json` under `projects.<name>.architect.build.options` with",
1190
- "the Angular CLI, `project.json` under `targets.build.options` in Nx. Paths inside `assets` are",
1191
- "resolved from the workspace root, so they read the same either way:",
1454
+ "The scaffold did this. Your build target now names the stylesheet, three asset globs, the",
1455
+ "service worker and one production setting, and the run that wrote these files listed each one it",
1456
+ "added. Anything you had already set was left exactly as you set it.",
1192
1457
  "",
1193
- "```jsonc",
1194
- '"styles": ["src/styles.css"],',
1195
- '"assets": [',
1196
- ' { "glob": "**/*", "input": "public" },',
1197
- ' { "glob": "**/*", "input": "node_modules/@loomweaver/shell/i18n", "output": "i18n" },',
1198
- ' { "glob": "**/*", "input": "node_modules/@loomweaver/frame-kit/dist", "output": "frame-kit" }',
1199
- "],",
1200
- '"serviceWorker": "ngsw-config.json"',
1201
- "```",
1202
- "",
1203
- "And in the **production** configuration of that same target:",
1458
+ "What each is for, so that nobody removes one as clutter. The **`@loomweaver/shell/i18n` glob**",
1459
+ "serves the strings the shell fetches at runtime; without it every label in the chrome renders as",
1460
+ "its raw translation key and nothing errors. The **frame-kit** glob only matters if you host",
1461
+ "sandboxed (iframe) plugins \u2014 until you install that package the glob simply matches nothing.",
1462
+ "**`serviceWorker`** emits the worker that `provideShell()` already registers for you (inert in",
1463
+ "dev) \u2014 never add `provideServiceWorker` yourself, and if you would rather ship no worker at all,",
1464
+ "drop `ngsw-config.json` and pass `provideShell({ serviceWorker: false })`, because otherwise the",
1465
+ "registration 404s in production. **`optimization.styles.inlineCritical: false`** is not optional:",
1466
+ "the `index.html` above ships a strict `script-src 'self'`, and Angular's critical-CSS pass loads",
1467
+ "the stylesheet with an **inline** `onload` handler that the policy blocks \u2014 the app then renders",
1468
+ "completely unstyled, and only in production builds.",
1204
1469
  "",
1205
- "```jsonc",
1206
- '"optimization": { "styles": { "inlineCritical": false } }',
1207
- "```",
1208
- "",
1209
- "Each of those earns its place. The **`@loomweaver/shell/i18n` glob** is the one whose absence is easy",
1210
- "to misread: the shell fetches its own UI strings at runtime, so without it every label in the",
1211
- "chrome renders as its raw translation key and nothing errors. The **frame-kit** glob only",
1212
- "matters if you host sandboxed (iframe) plugins \u2014 install that package then; until you do, the",
1213
- "glob simply matches nothing. **`serviceWorker`** emits the worker that `provideShell()` already",
1214
- "registers for you (inert in dev) \u2014 never add `provideServiceWorker` yourself, and if you would",
1215
- "rather ship no worker at all, drop `ngsw-config.json` and pass",
1216
- "`provideShell({ serviceWorker: false })`, because otherwise the registration 404s in production.",
1217
- "**`inlineCritical: false`** is not optional here: the `index.html` above ships a strict",
1218
- "`script-src 'self'`, and Angular's critical-CSS pass loads the stylesheet with an **inline**",
1219
- "`onload` handler that the policy blocks \u2014 the app then renders completely unstyled, and only in",
1220
- "production builds.",
1470
+ "One thing is still yours, because the scaffold cannot know your budget: a production build warns",
1471
+ "that the initial bundle exceeds Angular's 500 kB default. The shell is a whole application",
1472
+ "chrome, so raise the budgets in your build target.",
1221
1473
  "",
1222
1474
  "## Ship less than the whole workbench",
1223
1475
  "",
@@ -1256,6 +1508,9 @@ function readme2(d) {
1256
1508
  }
1257
1509
  var angularDistribution = {
1258
1510
  id: "angular-distribution",
1511
+ amend(input) {
1512
+ return distributionAmendments(resolveDistributionInput(input));
1513
+ },
1259
1514
  build(input) {
1260
1515
  const d = resolveDistributionInput(input);
1261
1516
  return {
@@ -1546,6 +1801,67 @@ var layout = {
1546
1801
  }
1547
1802
  };
1548
1803
 
1804
+ // ../devkit/src/recipes/angular-weaver/amendments.ts
1805
+ function weaverAmendments(input, where) {
1806
+ const w = resolveWeaverInput(input);
1807
+ const directory = (where ?? "").replace(/^\.?\/*/, "").replace(/\/+$/, "");
1808
+ if (!directory) {
1809
+ return [];
1810
+ }
1811
+ return [
1812
+ {
1813
+ kind: "build-target",
1814
+ styles: [],
1815
+ assets: [
1816
+ {
1817
+ glob: "**/*.json",
1818
+ input: `${directory}/src/lib/i18n`,
1819
+ from: "workspace",
1820
+ output: `i18n/${w.id}`
1821
+ }
1822
+ ]
1823
+ },
1824
+ { kind: "stylesheet-source", sourceRoot: `${directory}/src` },
1825
+ {
1826
+ kind: "compose-plugin",
1827
+ id: w.id,
1828
+ symbol: `${w.propertyName}Plugin`,
1829
+ capabilities: w.capabilities,
1830
+ sourceRoot: `${directory}/src`
1831
+ }
1832
+ ];
1833
+ }
1834
+
1835
+ // ../devkit/src/lib/scaffolds/inputs.ts
1836
+ function weaverInput(values) {
1837
+ return {
1838
+ id: str(values, "id") ?? "",
1839
+ name: str(values, "name"),
1840
+ prefix: str(values, "prefix"),
1841
+ importPath: str(values, "importPath"),
1842
+ features: {
1843
+ command: bool(values, "command"),
1844
+ shortcut: str(values, "shortcut"),
1845
+ menu: str(values, "menu"),
1846
+ barItem: bool(values, "barItem"),
1847
+ settings: bool(values, "settings"),
1848
+ about: bool(values, "about"),
1849
+ instanceable: bool(values, "instanceable"),
1850
+ container: bool(values, "container"),
1851
+ access: str(values, "access"),
1852
+ spec: bool(values, "spec")
1853
+ }
1854
+ };
1855
+ }
1856
+ function distributionInput(values) {
1857
+ return {
1858
+ name: str(values, "name") ?? "",
1859
+ title: str(values, "title"),
1860
+ directory: str(values, "directory"),
1861
+ styles: str(values, "styles") ?? "tailwind"
1862
+ };
1863
+ }
1864
+
1549
1865
  // ../devkit/src/lib/scaffolds/scaffolds.ts
1550
1866
  function str(values, name) {
1551
1867
  const value = values[name];
@@ -1684,24 +2000,8 @@ var SCAFFOLDS = [
1684
2000
  APP_OPTION,
1685
2001
  ...PLACEMENT_OPTIONS
1686
2002
  ],
1687
- build: (values) => generate(angularWeaver, {
1688
- id: str(values, "id") ?? "",
1689
- name: str(values, "name"),
1690
- prefix: str(values, "prefix"),
1691
- importPath: str(values, "importPath"),
1692
- features: {
1693
- command: bool(values, "command"),
1694
- shortcut: str(values, "shortcut"),
1695
- menu: str(values, "menu"),
1696
- barItem: bool(values, "barItem"),
1697
- settings: bool(values, "settings"),
1698
- about: bool(values, "about"),
1699
- instanceable: bool(values, "instanceable"),
1700
- container: bool(values, "container"),
1701
- access: str(values, "access"),
1702
- spec: bool(values, "spec")
1703
- }
1704
- })
2003
+ build: (values) => generate(angularWeaver, weaverInput(values)),
2004
+ amend: (values) => weaverAmendments(weaverInput(values), str(values, "directory"))
1705
2005
  },
1706
2006
  {
1707
2007
  name: "frame-plugin",
@@ -1757,12 +2057,8 @@ var SCAFFOLDS = [
1757
2057
  },
1758
2058
  ...PLACEMENT_OPTIONS
1759
2059
  ],
1760
- build: (values) => generate(angularDistribution, {
1761
- name: str(values, "name") ?? "",
1762
- title: str(values, "title"),
1763
- directory: str(values, "directory"),
1764
- styles: str(values, "styles") ?? "tailwind"
1765
- })
2060
+ build: (values) => generate(angularDistribution, distributionInput(values)),
2061
+ amend: (values) => amendments(angularDistribution, distributionInput(values))
1766
2062
  },
1767
2063
  {
1768
2064
  name: "auth-source",
@@ -2113,7 +2409,7 @@ function validateCatalog(catalog, known = KNOWN_CAPABILITIES) {
2113
2409
  }
2114
2410
 
2115
2411
  // src/lib/run.ts
2116
- import { readdirSync, readFileSync } from "node:fs";
2412
+ import { readdirSync, readFileSync as readFileSync3 } from "node:fs";
2117
2413
  import { join } from "node:path";
2118
2414
 
2119
2415
  // src/lib/args.ts
@@ -2203,8 +2499,336 @@ function boolFlag(args, name) {
2203
2499
  return value;
2204
2500
  }
2205
2501
 
2502
+ // src/lib/amend.ts
2503
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
2504
+ import { dirname as dirname2, posix, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
2505
+
2506
+ // src/lib/workspace.ts
2507
+ import { existsSync, readFileSync } from "node:fs";
2508
+ import { dirname, relative, resolve, sep } from "node:path";
2509
+ var WorkspaceError = class extends Error {
2510
+ };
2511
+ function findWorkspace(from) {
2512
+ let dir = resolve(from);
2513
+ for (; ; ) {
2514
+ if (existsSync(resolve(dir, "package.json"))) {
2515
+ return { root: dir, ...buildConfigIn(dir) };
2516
+ }
2517
+ const parent = dirname(dir);
2518
+ if (parent === dir) {
2519
+ return void 0;
2520
+ }
2521
+ dir = parent;
2522
+ }
2523
+ }
2524
+ function resolveBuildProject(workspace, target) {
2525
+ const projects = readProjects(workspace);
2526
+ if (projects.length === 0) {
2527
+ throw new WorkspaceError(
2528
+ `No project with a build target found in ${workspace.configFile ?? workspace.root}.`
2529
+ );
2530
+ }
2531
+ const inside = projects.filter((project) => contains(project.root, relativeTo(workspace.root, target))).sort((a, b) => b.root.length - a.root.length);
2532
+ if (inside.length > 0) {
2533
+ return inside[0];
2534
+ }
2535
+ if (projects.length === 1) {
2536
+ return projects[0];
2537
+ }
2538
+ throw new WorkspaceError(
2539
+ `More than one project could be the target, so none was chosen: ${projects.map((project) => project.name).join(", ")}. Generate into the project's own directory.`
2540
+ );
2541
+ }
2542
+ function readJsonFile(file) {
2543
+ try {
2544
+ return JSON.parse(readFileSync(file, "utf8"));
2545
+ } catch (error) {
2546
+ throw new WorkspaceError(
2547
+ `${file} is not valid JSON: ${error.message}`
2548
+ );
2549
+ }
2550
+ }
2551
+ function buildConfigIn(dir) {
2552
+ const angular = resolve(dir, "angular.json");
2553
+ if (existsSync(angular)) {
2554
+ return { configFile: angular, kind: "angular" };
2555
+ }
2556
+ const nx = resolve(dir, "nx.json");
2557
+ if (existsSync(nx)) {
2558
+ return { configFile: nx, kind: "nx" };
2559
+ }
2560
+ return {};
2561
+ }
2562
+ function readProjects(workspace) {
2563
+ if (workspace.kind !== "angular" || !workspace.configFile) {
2564
+ return [];
2565
+ }
2566
+ const config = readJsonFile(workspace.configFile);
2567
+ const projects = asObject2(asObject2(config)?.["projects"]) ?? {};
2568
+ return Object.entries(projects).filter(([, project]) => hasBuildTarget(project)).map(([name, project]) => ({
2569
+ name,
2570
+ root: normalise(asObject2(project)?.["root"])
2571
+ }));
2572
+ }
2573
+ function hasBuildTarget(project) {
2574
+ const architect = asObject2(project)?.["architect"] ?? asObject2(project)?.["targets"];
2575
+ return asObject2(architect)?.["build"] !== void 0;
2576
+ }
2577
+ function contains(projectRoot, target) {
2578
+ return projectRoot === "" || target === projectRoot || target.startsWith(`${projectRoot}/`);
2579
+ }
2580
+ function relativeTo(root, target) {
2581
+ return relative(root, resolve(target)).split(sep).join("/");
2582
+ }
2583
+ function normalise(value) {
2584
+ return typeof value === "string" ? value.replace(/^\.?\/*/, "").replace(/\/+$/, "") : "";
2585
+ }
2586
+ function asObject2(value) {
2587
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
2588
+ }
2589
+
2590
+ // src/lib/amend.ts
2591
+ var JS_POSTCSS_CONFIGS = [
2592
+ "postcss.config.js",
2593
+ "postcss.config.mjs",
2594
+ "postcss.config.cjs",
2595
+ ".postcssrc.js"
2596
+ ];
2597
+ function planAmend(amendments2, target) {
2598
+ if (amendments2.length === 0) {
2599
+ return { amendments: [], remaining: [] };
2600
+ }
2601
+ const workspace = findWorkspace(target);
2602
+ if (!workspace) {
2603
+ return {
2604
+ amendments: [],
2605
+ remaining: [
2606
+ `No workspace was found above the target directory, so nothing could be wired here. Add it by hand, or generate inside the workspace: ${amendments2.map(describeAmendment).join(" \xB7 ")}`
2607
+ ]
2608
+ };
2609
+ }
2610
+ return new Amender(workspace, target).plan(amendments2);
2611
+ }
2612
+ function applyAmend(plan) {
2613
+ for (const amendment of plan.amendments) {
2614
+ writeFileSync(amendment.file, amendment.content, "utf8");
2615
+ }
2616
+ }
2617
+ var Amender = class {
2618
+ constructor(workspace, target) {
2619
+ this.workspace = workspace;
2620
+ this.target = target;
2621
+ this.planned = [];
2622
+ this.remaining = [];
2623
+ this.configAdded = [];
2624
+ }
2625
+ plan(amendments2) {
2626
+ for (const amendment of amendments2) {
2627
+ this.planOne(amendment);
2628
+ }
2629
+ this.flushConfig();
2630
+ return { amendments: this.planned, remaining: this.remaining };
2631
+ }
2632
+ planOne(amendment) {
2633
+ if (amendment.kind === "postcss") {
2634
+ this.planPostcss(amendment);
2635
+ return;
2636
+ }
2637
+ if (this.workspace.kind !== "angular") {
2638
+ this.remaining.push(this.nonAngularNote(amendment));
2639
+ return;
2640
+ }
2641
+ const project = this.resolveProject();
2642
+ if (!project) {
2643
+ return;
2644
+ }
2645
+ if (amendment.kind === "build-target") {
2646
+ this.planBuildTarget(amendment, project);
2647
+ } else if (amendment.kind === "stylesheet-source") {
2648
+ this.planStylesheetSource(amendment, project);
2649
+ } else {
2650
+ this.planComposePlugin(amendment, project);
2651
+ }
2652
+ }
2653
+ planPostcss(amendment) {
2654
+ const inTheWay = JS_POSTCSS_CONFIGS.find(
2655
+ (name) => existsSync2(resolve2(this.workspace.root, name))
2656
+ );
2657
+ if (inTheWay) {
2658
+ this.remaining.push(
2659
+ `${inTheWay} is written as code and cannot be merged into, so add ${amendment.plugin} to it yourself; until then the stylesheet emits no utility class and the workbench renders unstyled.`
2660
+ );
2661
+ return;
2662
+ }
2663
+ const file = resolve2(this.workspace.root, amendment.file);
2664
+ const result = ensurePostcssPlugin(
2665
+ existsSync2(file) ? readJsonFile(file) : void 0,
2666
+ amendment
2667
+ );
2668
+ this.remaining.push(...result.declined);
2669
+ if (result.added.length === 0) {
2670
+ return;
2671
+ }
2672
+ this.planned.push({
2673
+ file,
2674
+ display: this.displayName(file),
2675
+ added: result.added,
2676
+ content: `${JSON.stringify(result.value, null, 2)}
2677
+ `
2678
+ });
2679
+ }
2680
+ planBuildTarget(amendment, project) {
2681
+ const target = this.buildTarget(project.name);
2682
+ if (!target) {
2683
+ this.remaining.push(
2684
+ `${project.name} has no build target to wire, so add it by hand: ${describeAmendment(amendment)}.`
2685
+ );
2686
+ return;
2687
+ }
2688
+ const result = ensureBuildTarget(target.value, amendment, project.root);
2689
+ this.remaining.push(...result.declined);
2690
+ if (result.added.length === 0) {
2691
+ return;
2692
+ }
2693
+ target.set(result.value);
2694
+ this.configAdded.push(...result.added);
2695
+ }
2696
+ planStylesheetSource(amendment, project) {
2697
+ const entry = this.entryStylesheet(project);
2698
+ if (!entry || !existsSync2(entry)) {
2699
+ this.remaining.push(
2700
+ `No entry stylesheet is wired for ${project.name}, so add it yourself: ${describeAmendment(amendment)}.`
2701
+ );
2702
+ return;
2703
+ }
2704
+ const css = readFileSync2(entry, "utf8");
2705
+ if (!/@import\s+['"]tailwindcss['"]/.test(css)) {
2706
+ return;
2707
+ }
2708
+ const source = posix.relative(
2709
+ this.displayName(dirname2(entry)),
2710
+ amendment.sourceRoot
2711
+ );
2712
+ const next = ensureStylesheetSource(css, source);
2713
+ if (next === css) {
2714
+ return;
2715
+ }
2716
+ this.planned.push({
2717
+ file: entry,
2718
+ display: this.displayName(entry),
2719
+ added: [`@source '${source}'`],
2720
+ content: next
2721
+ });
2722
+ }
2723
+ planComposePlugin(amendment, project) {
2724
+ const root = resolve2(
2725
+ this.workspace.root,
2726
+ project.root,
2727
+ "src/app/app.config.ts"
2728
+ );
2729
+ const importPath = relativeImport(
2730
+ this.displayName(dirname2(root)),
2731
+ amendment.sourceRoot
2732
+ );
2733
+ if (!existsSync2(root)) {
2734
+ this.remaining.push(this.composeNote(amendment, importPath));
2735
+ return;
2736
+ }
2737
+ const source = readFileSync2(root, "utf8");
2738
+ const result = composePlugin(source, amendment, importPath);
2739
+ if (!result.composed) {
2740
+ this.remaining.push(this.composeNote(amendment, importPath));
2741
+ return;
2742
+ }
2743
+ if (result.source === source) {
2744
+ return;
2745
+ }
2746
+ this.planned.push({
2747
+ file: root,
2748
+ display: this.displayName(root),
2749
+ added: [`${amendment.symbol}, its translations and its capability grants`],
2750
+ content: result.source
2751
+ });
2752
+ }
2753
+ composeNote(amendment, importPath) {
2754
+ return `The composition root no longer presents the shape this scaffold generated, so ${amendment.id} was NOT registered and none of its contributions will appear. Add these to it yourself: ` + composeLines(amendment, importPath).join(" ");
2755
+ }
2756
+ flushConfig() {
2757
+ if (this.configAdded.length === 0 || !this.config) {
2758
+ return;
2759
+ }
2760
+ const file = this.workspace.configFile;
2761
+ this.planned.push({
2762
+ file,
2763
+ display: this.displayName(file),
2764
+ added: this.configAdded,
2765
+ content: `${JSON.stringify(this.config, null, 2)}
2766
+ `
2767
+ });
2768
+ }
2769
+ resolveProject() {
2770
+ if (this.project) {
2771
+ return this.project;
2772
+ }
2773
+ try {
2774
+ this.project = resolveBuildProject(this.workspace, this.target);
2775
+ return this.project;
2776
+ } catch (error) {
2777
+ this.remaining.push(error.message);
2778
+ return void 0;
2779
+ }
2780
+ }
2781
+ readConfig() {
2782
+ this.config ??= readJsonFile(this.workspace.configFile);
2783
+ return this.config;
2784
+ }
2785
+ buildTarget(name) {
2786
+ const project = asObject3(asObject3(this.readConfig()["projects"])?.[name]);
2787
+ if (!project) {
2788
+ return void 0;
2789
+ }
2790
+ for (const key of ["architect", "targets"]) {
2791
+ const targets = asObject3(project[key]);
2792
+ if (targets?.["build"] !== void 0) {
2793
+ return {
2794
+ value: targets["build"],
2795
+ set: (next) => {
2796
+ targets["build"] = next;
2797
+ }
2798
+ };
2799
+ }
2800
+ }
2801
+ return void 0;
2802
+ }
2803
+ entryStylesheet(project) {
2804
+ const styles = asObject3(asObject3(this.buildTarget(project.name)?.value)?.["options"])?.["styles"];
2805
+ if (!Array.isArray(styles)) {
2806
+ return void 0;
2807
+ }
2808
+ const entry = styles.find(
2809
+ (style) => typeof style === "string" && style.endsWith(".css")
2810
+ );
2811
+ return entry === void 0 ? void 0 : resolve2(this.workspace.root, entry);
2812
+ }
2813
+ nonAngularNote(amendment) {
2814
+ const where = this.workspace.kind === "nx" ? "the project's own project.json" : "your build configuration";
2815
+ return `This route wires an Angular CLI workspace only, so add ${describeAmendment(amendment)} to ${where} yourself. The Nx generator does it for you.`;
2816
+ }
2817
+ displayName(file) {
2818
+ const inside = relative2(this.workspace.root, file).split(sep2).join("/");
2819
+ return inside.startsWith("..") ? file : inside;
2820
+ }
2821
+ };
2822
+ function asObject3(value) {
2823
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
2824
+ }
2825
+ function relativeImport(fromDir, sourceRoot) {
2826
+ const path = posix.relative(fromDir, sourceRoot);
2827
+ return path.startsWith(".") ? path : `./${path}`;
2828
+ }
2829
+
2206
2830
  // src/lib/scaffold.ts
2207
- import { relative, resolve } from "node:path";
2831
+ import { relative as relative3, resolve as resolve3 } from "node:path";
2208
2832
  function findScaffold2(name) {
2209
2833
  const scaffold2 = findScaffold(name);
2210
2834
  if (!scaffold2) {
@@ -2256,23 +2880,28 @@ function valuesFor(scaffold2, args) {
2256
2880
  return values;
2257
2881
  }
2258
2882
  function directoryFromOut(out) {
2259
- const below = relative(process.cwd(), resolve(out ?? "."));
2883
+ const below = relative3(process.cwd(), resolve3(out ?? "."));
2260
2884
  return below.startsWith("..") ? "" : below;
2261
2885
  }
2262
- function buildScaffold(scaffold2, args) {
2886
+ function scaffoldValues(scaffold2, args) {
2263
2887
  const values = valuesFor(scaffold2, args);
2264
2888
  const takesDirectory = scaffold2.options.some(
2265
2889
  (option) => option.name === "directory"
2266
2890
  );
2891
+ if (!takesDirectory) {
2892
+ return values;
2893
+ }
2267
2894
  const out = args.flags["out"];
2268
- return scaffold2.build(
2269
- takesDirectory ? {
2270
- ...values,
2271
- directory: directoryFromOut(
2272
- typeof out === "string" ? out : void 0
2273
- )
2274
- } : values
2275
- );
2895
+ return {
2896
+ ...values,
2897
+ directory: directoryFromOut(typeof out === "string" ? out : void 0)
2898
+ };
2899
+ }
2900
+ function buildScaffold(scaffold2, args) {
2901
+ return scaffold2.build(scaffoldValues(scaffold2, args));
2902
+ }
2903
+ function amendmentsFor(scaffold2, args) {
2904
+ return scaffold2.amend?.(scaffoldValues(scaffold2, args)) ?? [];
2276
2905
  }
2277
2906
 
2278
2907
  // src/lib/write.ts
@@ -2281,18 +2910,18 @@ import {
2281
2910
  mkdirSync,
2282
2911
  realpathSync,
2283
2912
  rmSync,
2284
- writeFileSync
2913
+ writeFileSync as writeFileSync2
2285
2914
  } from "node:fs";
2286
- import { dirname, isAbsolute, relative as relative2, resolve as resolve2 } from "node:path";
2915
+ import { dirname as dirname3, isAbsolute, relative as relative4, resolve as resolve4 } from "node:path";
2287
2916
  var WriteError = class extends Error {
2288
2917
  };
2289
2918
  function planWrite(files, root) {
2290
- const absoluteRoot = resolve2(root);
2919
+ const absoluteRoot = resolve4(root);
2291
2920
  const planned = [];
2292
2921
  const conflicts = [];
2293
2922
  for (const path of Object.keys(files).sort()) {
2294
- const absolute = resolve2(absoluteRoot, path);
2295
- const inside = relative2(absoluteRoot, absolute);
2923
+ const absolute = resolve4(absoluteRoot, path);
2924
+ const inside = relative4(absoluteRoot, absolute);
2296
2925
  if (inside.startsWith("..") || isAbsolute(inside)) {
2297
2926
  throw new WriteError(`Refusing to write outside the target directory: ${path}`);
2298
2927
  }
@@ -2305,10 +2934,10 @@ function planWrite(files, root) {
2305
2934
  }
2306
2935
  function applyWrite(files, plan) {
2307
2936
  for (const file of plan.files) {
2308
- mkdirSync(dirname(file.absolute), { recursive: true });
2937
+ mkdirSync(dirname3(file.absolute), { recursive: true });
2309
2938
  assertResolvesInsideRoot(plan.root, file.path, file.absolute);
2310
2939
  replaceSymlinkEntry(file.absolute);
2311
- writeFileSync(file.absolute, files[file.path], "utf8");
2940
+ writeFileSync2(file.absolute, files[file.path], "utf8");
2312
2941
  }
2313
2942
  }
2314
2943
  function entryExists(absolute) {
@@ -2320,7 +2949,7 @@ function entryExists(absolute) {
2320
2949
  }
2321
2950
  }
2322
2951
  function assertResolvesInsideRoot(root, path, absolute) {
2323
- const inside = relative2(realpathSync(root), realpathSync(dirname(absolute)));
2952
+ const inside = relative4(realpathSync(root), realpathSync(dirname3(absolute)));
2324
2953
  if (inside.startsWith("..") || isAbsolute(inside)) {
2325
2954
  throw new WriteError(
2326
2955
  `Refusing to write through a link that leaves the target directory: ${path}`
@@ -2334,7 +2963,7 @@ function replaceSymlinkEntry(absolute) {
2334
2963
  }
2335
2964
 
2336
2965
  // src/lib/run.ts
2337
- var VERSION = "0.7.2";
2966
+ var VERSION = "0.7.3";
2338
2967
  function help() {
2339
2968
  const commands = SCAFFOLDS.map((s) => ` ${s.name.padEnd(16)}${s.summary}`);
2340
2969
  return [
@@ -2406,7 +3035,7 @@ function readBundles(dir) {
2406
3035
  }
2407
3036
  const language = entry.slice(0, -".json".length);
2408
3037
  try {
2409
- bundles[language] = JSON.parse(readFileSync(join(dir, entry), "utf8"));
3038
+ bundles[language] = JSON.parse(readFileSync3(join(dir, entry), "utf8"));
2410
3039
  } catch (error) {
2411
3040
  throw new ArgError(`${entry} is not valid JSON: ${error.message}`);
2412
3041
  }
@@ -2427,7 +3056,7 @@ function validateI18nCommand(args, io) {
2427
3056
  function readCatalog(file) {
2428
3057
  let raw;
2429
3058
  try {
2430
- raw = readFileSync(file, "utf8");
3059
+ raw = readFileSync3(file, "utf8");
2431
3060
  } catch (error) {
2432
3061
  throw new ArgError(`Cannot read ${file}: ${error.message}`);
2433
3062
  }
@@ -2445,6 +3074,21 @@ function validateCatalogCommand(args, io) {
2445
3074
  boolFlag(args, "strict") === true
2446
3075
  );
2447
3076
  }
3077
+ function reportAmendments(io, amend, done) {
3078
+ if (amend.amendments.length > 0) {
3079
+ io.out(
3080
+ done ? `Wired ${amend.amendments.length} workspace file(s):` : `Would wire ${amend.amendments.length} workspace file(s):`
3081
+ );
3082
+ for (const amendment of amend.amendments) {
3083
+ io.out(` ${amendment.display}`);
3084
+ amendment.added.forEach((entry) => io.out(` + ${entry}`));
3085
+ }
3086
+ }
3087
+ if (amend.remaining.length > 0) {
3088
+ io.out("Still to do by hand:");
3089
+ amend.remaining.forEach((entry) => io.out(` - ${entry}`));
3090
+ }
3091
+ }
2448
3092
  function scaffold(args, io) {
2449
3093
  const descriptor = findScaffold2(args.command);
2450
3094
  rejectUnknownFlags(args, [
@@ -2454,8 +3098,10 @@ function scaffold(args, io) {
2454
3098
  "force"
2455
3099
  ]);
2456
3100
  const files = buildScaffold(descriptor, args);
2457
- const plan = planWrite(files, stringFlag(args, "out") ?? ".");
3101
+ const out = stringFlag(args, "out") ?? ".";
3102
+ const plan = planWrite(files, out);
2458
3103
  const paths = plan.files.map((file) => file.path);
3104
+ const amend = planAmend(amendmentsFor(descriptor, args), out);
2459
3105
  if (boolFlag(args, "dry-run")) {
2460
3106
  io.out(`Would write ${paths.length} file(s) into ${plan.root}:`);
2461
3107
  paths.forEach((path) => io.out(` ${path}`));
@@ -2465,6 +3111,7 @@ function scaffold(args, io) {
2465
3111
  );
2466
3112
  plan.conflicts.forEach((path) => io.out(` ${path}`));
2467
3113
  }
3114
+ reportAmendments(io, amend, false);
2468
3115
  return 0;
2469
3116
  }
2470
3117
  if (plan.conflicts.length > 0 && !boolFlag(args, "force")) {
@@ -2475,8 +3122,10 @@ function scaffold(args, io) {
2475
3122
  return 1;
2476
3123
  }
2477
3124
  applyWrite(files, plan);
3125
+ applyAmend(amend);
2478
3126
  io.out(`Wrote ${paths.length} file(s) into ${plan.root}:`);
2479
3127
  paths.forEach((path) => io.out(` ${path}`));
3128
+ reportAmendments(io, amend, true);
2480
3129
  return 0;
2481
3130
  }
2482
3131
  function run(argv, io) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loomweaver/cli",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "LoomWeaver scaffolding CLI: generates weavers, distributions and integrations into any project — no Nx workspace, no LoomWeaver checkout and no AI assistant required.",
5
5
  "keywords": [
6
6
  "loomweaver",