@loomweaver/cli 0.7.2 → 0.7.4

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 +1007 -305
  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' }",
@@ -851,223 +1122,7 @@ function renderRegions(indent) {
851
1122
  var PLACEHOLDER_LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="512" height="512" viewBox="87.5 -53 1129.9 1129.9"><g paint-order="stroke"><path d="m0 0 12.6-10.6-9.2-8-14.5 12.1q-.7.8 1.2 1.5A17 17 0 0 1-2.4-.5Q-1 .7 0 0" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#2e96c9;fill-rule:nonzero;opacity:1" transform="matrix(15.6303 0 0 -15.6303 265.2 368.8)"/><path d="m0 0 19-16c.6-.5.5-1.6-.2-2.3q-2.8-3-7.5-4.5c-1.2-.4-2.8-.1-3.4.4l-17 14.3z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#2e96c9;fill-rule:nonzero;opacity:1" transform="matrix(15.6303 0 0 -15.6303 593.2 644.5)"/><path d="m0 0 5.8-4.9-9.2-8-2.8 2.3q-.9 1 0 2.5l4 7.4Q-1 .7 0 0" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#c59a2f;fill-rule:nonzero;opacity:1" transform="matrix(15.6303 0 0 -15.6303 386.3 170)"/><path d="m0 0 12.4-10.4-9.2-8.1L-9.2-8.1Z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#c59a2f;fill-rule:nonzero;opacity:1" transform="matrix(15.6303 0 0 -15.6303 608.2 356.6)"/><path d="m0 0 5-4.2c.6-.5.6-1.6 0-2.5l-4-7.4q-1-1.4-2.1-.7l-8 6.7z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#c59a2f;fill-rule:nonzero;opacity:1" transform="matrix(15.6303 0 0 -15.6303 933.4 630)"/><path d="m0 0 14.6-12.3-9.2-8L-11-6.6c-.6.5-.6 1.6.2 2.4Q-8-1.2-3.3.3C-2.2.7-.6.5 0 0" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#2e96c9;fill-rule:nonzero;opacity:1" transform="matrix(15.6303 0 0 -15.6303 589.5 28.9)"/><path d="m0 0 17-14.3q.8-.8-1.3-1.4a17 17 0 0 1-7.5-4.5c-.7-.8-1.8-1-2.4-.5l-15 12.6Z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#2e96c9;fill-rule:nonzero;opacity:1" transform="matrix(15.6303 0 0 -15.6303 948.5 330.8)"/><path d="m0 0 8.4-7-13.6-12-8.4 7z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#2e96c9;fill-rule:nonzero;opacity:1" transform="matrix(15.6303 0 0 -15.6303 514.5 213.2)"/><path d="m0 0 5.7-4.8q1.3-1 .4-2L3-9.5l-8.4 7 3 2.7A2 2 0 0 0 0 0" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#2e96c9;fill-rule:nonzero;opacity:1" transform="matrix(15.6303 0 0 -15.6303 743 48)"/><path d="m0 0 8.4-7-2.2-2q-1-.7-2.3.2L-2-4q-1 1.2-.2 2z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#2e96c9;fill-rule:nonzero;opacity:1" transform="matrix(15.6303 0 0 -15.6303 158.6 526.5)"/><path d="m0 0 8.4-7L-5-18.9c-.5-.5-1.6-.4-2.3.2l-5.8 4.8q-1 1-.3 2z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#2e96c9;fill-rule:nonzero;opacity:1" transform="matrix(15.6303 0 0 -15.6303 492.8 507)"/><path d="m0 0 5.7-4.8q1.3-1 .4-2l-14.7-13-8.4 7.1L-2.3.3C-1.8.6-.7.5 0 0" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#2e96c9;fill-rule:nonzero;opacity:1" transform="matrix(15.6303 0 0 -15.6303 902.6 182)"/><path d="m0 0 5.9-5Q7-6 6.2-7l-3-2.7-8.7 7.2L-2.4.3Q-1.4.9 0 0" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#2e96c9;fill-rule:nonzero;opacity:1" transform="matrix(15.6303 0 0 -15.6303 1065.8 319.3)"/><path d="m0 0 8.6-7.2-13.6-12-8.6 7.2z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#2e96c9;fill-rule:nonzero;opacity:1" transform="matrix(15.6303 0 0 -15.6303 836.9 484.2)"/><path d="m0 0 8.6-7.2-2.2-2q-1-.6-2.4.3l-5.9 5Q-3-3-2.2-2z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:#2e96c9;fill-rule:nonzero;opacity:1" transform="matrix(15.6303 0 0 -15.6303 481 797.5)"/><g transform="matrix(15.6303 0 0 -15.6303 877.6 706.6)"><linearGradient id="a" x1="0" x2="1" y1="0" y2="0" gradientTransform="rotate(135.7 64.4 103.4)scale(2.18666 -2.18666)" gradientUnits="userSpaceOnUse"><stop offset="0%" style="stop-color:#c59a2f;stop-opacity:1"/><stop offset="100%" style="stop-color:#614d06;stop-opacity:1"/></linearGradient><path d="m176.7 129.6 2-1.7 9.2 8-2 1.8z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:url(#a);fill-rule:nonzero;opacity:1" transform="translate(-182.3 -132.8)"/></g><g transform="matrix(15.6303 0 0 -15.6303 711.4 567.2)"><linearGradient id="b" x1="0" x2="1" y1="0" y2="0" gradientTransform="rotate(135.7 56.7 106)scale(-2.18726 2.18726)" gradientUnits="userSpaceOnUse"><stop offset="0%" style="stop-color:#c59a2f;stop-opacity:1"/><stop offset="100%" style="stop-color:#614d06;stop-opacity:1"/></linearGradient><path d="m166 138.5 2.1-1.7 9.2 8.1-2 1.7z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:url(#b);fill-rule:nonzero;opacity:1" transform="translate(-171.7 -141.7)"/></g><g transform="matrix(15.6303 0 0 -15.6303 532.5 867.7)"><linearGradient id="c" x1="0" x2="1" y1="0" y2="0" gradientTransform="rotate(45.7 -65.1 251)scale(2.11643 -2.11643)" gradientUnits="userSpaceOnUse"><stop offset="0%" style="stop-color:#2e96c9;stop-opacity:1"/><stop offset="100%" style="stop-color:#2479a3;stop-opacity:1"/></linearGradient><path d="m155 125.2 8.5-7.2 2 1.8-8.6 7.2z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:url(#c);fill-rule:nonzero;opacity:1" transform="translate(-160.2 -122.5)"/></g><g transform="matrix(15.6303 0 0 -15.6303 707.2 713.6)"><linearGradient id="d" x1="0" x2="1" y1="0" y2="0" gradientTransform="rotate(-134.3 114 30.2)scale(2.11524 -2.11524)" gradientUnits="userSpaceOnUse"><stop offset="0%" style="stop-color:#2e96c9;stop-opacity:1"/><stop offset="100%" style="stop-color:#2479a3;stop-opacity:1"/></linearGradient><path d="m166.1 135 8.6-7.1 2 1.7-8.6 7.2z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:url(#d);fill-rule:nonzero;opacity:1" transform="translate(-171.4 -132.3)"/></g><g transform="matrix(15.6303 0 0 -15.6303 888.2 554.5)"><linearGradient id="e" x1="0" x2="1" y1="0" y2="0" gradientTransform="rotate(45.7 -77.5 288)scale(2.11554 -2.11554)" gradientUnits="userSpaceOnUse"><stop offset="0%" style="stop-color:#2e96c9;stop-opacity:1"/><stop offset="100%" style="stop-color:#2479a3;stop-opacity:1"/></linearGradient><path d="m177.7 145.3 8.6-7.3 2 1.8-8.6 7.2z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:url(#e);fill-rule:nonzero;opacity:1" transform="translate(-183 -142.5)"/></g><g transform="matrix(15.6303 0 0 -15.6303 1062 401.2)"><linearGradient id="f" x1="0" x2="1" y1="0" y2="0" gradientTransform="rotate(45.7 -84.1 307.3)scale(-2.11583 2.11583)" gradientUnits="userSpaceOnUse"><stop offset="0%" style="stop-color:#2e96c9;stop-opacity:1"/><stop offset="100%" style="stop-color:#2479a3;stop-opacity:1"/></linearGradient><path d="m188.8 155 8.6-7.2 2 1.8-8.6 7.2z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:url(#f);fill-rule:nonzero;opacity:1" transform="translate(-194.1 -152.3)"/></g><g transform="matrix(15.6303 0 0 -15.6303 206.5 595.5)"><linearGradient id="g" x1="0" x2="1" y1="0" y2="0" gradientTransform="rotate(225.7 98.9 40.5)scale(-2.0962 2.0962)" gradientUnits="userSpaceOnUse"><stop offset="0%" style="stop-color:#2e96c9;stop-opacity:1"/><stop offset="100%" style="stop-color:#2479a3;stop-opacity:1"/></linearGradient><path d="m134.3 142.5 8.2-7 2 1.8-8.3 7z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:url(#g);fill-rule:nonzero;opacity:1" transform="translate(-139.4 -139.9)"/></g><g transform="matrix(15.6303 0 0 -15.6303 381 441.4)"><linearGradient id="h" x1="0" x2="1" y1="0" y2="0" gradientTransform="rotate(-134.3 107 43.3)scale(2.0962 -2.0962)" gradientUnits="userSpaceOnUse"><stop offset="0%" style="stop-color:#2e96c9;stop-opacity:1"/><stop offset="100%" style="stop-color:#2479a3;stop-opacity:1"/></linearGradient><path d="m145.4 152.3 8.3-6.9 2 1.8-8.3 6.9z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:url(#h);fill-rule:nonzero;opacity:1" transform="translate(-150.5 -149.8)"/></g><g transform="matrix(15.6303 0 0 -15.6303 562 282.3)"><linearGradient id="i" x1="0" x2="1" y1="0" y2="0" gradientTransform="rotate(225.7 114.5 45.7)scale(-2.0953 2.0953)" gradientUnits="userSpaceOnUse"><stop offset="0%" style="stop-color:#2e96c9;stop-opacity:1"/><stop offset="100%" style="stop-color:#2479a3;stop-opacity:1"/></linearGradient><path d="m157 162.5 8.2-7 2 1.8-8.2 7z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:url(#i);fill-rule:nonzero;opacity:1" transform="translate(-162.1 -160)"/></g><g transform="matrix(15.6303 0 0 -15.6303 736 129)"><linearGradient id="j" x1="0" x2="1" y1="0" y2="0" gradientTransform="rotate(45.7 -115.1 291)scale(-2.0956 2.0956)" gradientUnits="userSpaceOnUse"><stop offset="0%" style="stop-color:#2e96c9;stop-opacity:1"/><stop offset="100%" style="stop-color:#2479a3;stop-opacity:1"/></linearGradient><path d="m168.1 172.3 8.3-6.9 2 1.7-8.3 7z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:url(#j);fill-rule:nonzero;opacity:1" transform="translate(-173.2 -169.7)"/></g><g transform="matrix(15.6303 0 0 -15.6303 544.8 441.3)"><linearGradient id="k" x1="0" x2="1" y1="0" y2="0" gradientTransform="rotate(135.7 50.4 108)scale(2.18666 -2.18666)" gradientUnits="userSpaceOnUse"><stop offset="0%" style="stop-color:#c59a2f;stop-opacity:1"/><stop offset="100%" style="stop-color:#614d06;stop-opacity:1"/></linearGradient><path d="m155.9 147 2-1.6 9.2 8-2 1.8z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:url(#k);fill-rule:nonzero;opacity:1" transform="translate(-161.5 -150.3)"/></g><g transform="matrix(15.6303 0 0 -15.6303 390.7 294.8)"><linearGradient id="l" x1="0" x2="1" y1="0" y2="0" gradientTransform="rotate(135.7 42.9 110.5)scale(-2.17357 2.17357)" gradientUnits="userSpaceOnUse"><stop offset="0%" style="stop-color:#c59a2f;stop-opacity:1"/><stop offset="100%" style="stop-color:#614d06;stop-opacity:1"/></linearGradient><path d="m145.6 156 2-1.7 9 8-2 1.6z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:url(#l);fill-rule:nonzero;opacity:1" transform="translate(-151.2 -159.1)"/></g><g transform="matrix(15.6303 0 0 -15.6303 375.9 585.7)"><linearGradient id="m" x1="0" x2="1" y1="0" y2="0" gradientTransform="rotate(135.7 46.2 101)scale(-1.9968 1.9968)" gradientUnits="userSpaceOnUse"><stop offset="0%" style="stop-color:#2e96c9;stop-opacity:1"/><stop offset="100%" style="stop-color:#2479a3;stop-opacity:1"/></linearGradient><path d="m144.7 137.2 1.8-1.5 9.2 8.1-1.8 1.5z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:url(#m);fill-rule:nonzero;opacity:1" transform="translate(-150.2 -140.5)"/></g><g transform="matrix(15.6303 0 0 -15.6303 733.7 274)"><linearGradient id="n" x1="0" x2="1" y1="0" y2="0" gradientTransform="rotate(-44.3 284 -131.7)scale(1.9968 -1.9968)" gradientUnits="userSpaceOnUse"><stop offset="0%" style="stop-color:#2e96c9;stop-opacity:1"/><stop offset="100%" style="stop-color:#2479a3;stop-opacity:1"/></linearGradient><path d="m167.6 157.2 1.8-1.5 9.2 8-1.8 1.6z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:url(#n);fill-rule:nonzero;opacity:1" transform="translate(-173.1 -160.5)"/></g><g transform="matrix(15.6303 0 0 -15.6303 893.6 408.2)"><linearGradient id="o" x1="0" x2="1" y1="0" y2="0" gradientTransform="rotate(135.7 61 113.2)scale(1.9962 -1.9962)" gradientUnits="userSpaceOnUse"><stop offset="0%" style="stop-color:#2e96c9;stop-opacity:1"/><stop offset="100%" style="stop-color:#2479a3;stop-opacity:1"/></linearGradient><path d="m177.8 148.6 1.8-1.5 9.2 8-1.8 1.6z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:url(#o);fill-rule:nonzero;opacity:1" transform="translate(-183.3 -151.9)"/></g><g transform="matrix(15.6303 0 0 -15.6303 535.9 720)"><linearGradient id="p" x1="0" x2="1" y1="0" y2="0" gradientTransform="rotate(135.7 53.6 98.6)scale(1.9962 -1.9962)" gradientUnits="userSpaceOnUse"><stop offset="0%" style="stop-color:#2e96c9;stop-opacity:1"/><stop offset="100%" style="stop-color:#2479a3;stop-opacity:1"/></linearGradient><path d="m155 128.6 1.7-1.5 9.2 8.1-1.8 1.5z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:url(#p);fill-rule:nonzero;opacity:1" transform="translate(-160.4 -132)"/></g><g transform="matrix(15.6303 0 0 -15.6303 717.3 421.6)"><linearGradient id="q" x1="0" x2="1" y1="0" y2="0" gradientTransform="rotate(45.7 -93.5 280)scale(-2.095 2.095)" gradientUnits="userSpaceOnUse"><stop offset="0%" style="stop-color:#2e96c9;stop-opacity:1"/><stop offset="100%" style="stop-color:#2479a3;stop-opacity:1"/></linearGradient><path d="m167 153.6 8.2-7 2 1.8-8.3 7z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:url(#q);fill-rule:nonzero;opacity:1" transform="translate(-172 -151)"/></g><g transform="matrix(15.6303 0 0 -15.6303 542.1 575.9)"><linearGradient id="r" x1="0" x2="1" y1="0" y2="0" gradientTransform="rotate(45.7 -87 261)scale(2.0956 -2.0956)" gradientUnits="userSpaceOnUse"><stop offset="0%" style="stop-color:#2e96c9;stop-opacity:1"/><stop offset="100%" style="stop-color:#2479a3;stop-opacity:1"/></linearGradient><path d="m155.7 143.8 8.3-7 2 1.8-8.3 6.9z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-dashoffset:0;stroke-linejoin:miter;stroke-miterlimit:4;fill:url(#r);fill-rule:nonzero;opacity:1" transform="translate(-160.8 -141.2)"/></g></g></svg>
852
1123
  `;
853
1124
 
854
- // ../devkit/src/recipes/angular-distribution/recipe.ts
855
- var STYLES = ["tailwind", "precompiled"];
856
- function resolveDistributionInput(input) {
857
- if (!isKebabId(input.name)) {
858
- throw new Error(
859
- `Distribution name must be kebab-case (e.g. "acme-studio"); got "${input.name}".`
860
- );
861
- }
862
- const styles = input.styles ?? "tailwind";
863
- if (!STYLES.includes(styles)) {
864
- throw new Error(
865
- `Unknown styles option "${styles}"; expected one of ${STYLES.join(", ")}.`
866
- );
867
- }
868
- const directory = input.directory === void 0 ? `apps/${input.name}` : input.directory.trim();
869
- const depth = directory.split("/").filter(Boolean).length;
870
- return {
871
- name: input.name,
872
- title: input.title?.trim() || toTitleCase(input.name),
873
- nodeModulesFromSrc: `${"../".repeat(depth + 1)}node_modules`,
874
- withTests: input.withTests !== false,
875
- styles
876
- };
877
- }
878
- function mainTs() {
879
- return `import { bootstrapApplication } from '@angular/platform-browser';
880
- import { appConfig } from './app/app.config';
881
- import { App } from './app/app';
882
-
883
- bootstrapApplication(App, appConfig).catch((err) => console.error(err));
884
- `;
885
- }
886
- function appConfigTs(d) {
887
- return `import { ApplicationConfig } from '@angular/core';
888
- import {
889
- provideLayout,
890
- provideShell,
891
- provideShellRouter,
892
- type ShellLayout,
893
- } from '@loomweaver/shell';
894
- import { provideProductIdentity } from '@loomweaver/plugin-sdk';
895
-
896
- /* Which regions exist and where they dock. Contributions target these ids, so a region a
897
- weaver names but this layout omits renders nothing \u2014 silently. 'primary' (rail) and
898
- 'status-bar' (bar) are what the scaffolded weaver targets. */
899
- export const layout: ShellLayout = {
900
- regions: [
901
- ${renderRegions(" ")}
902
- ],
903
- };
904
-
905
- /* Everything this product is made of goes in this array: your weavers, their capability
906
- grants and your branding. The shell arrives with every capability on; switch gestures
907
- off with provideShellFeatures and drop contributions with provideShell({ omit }).
908
- See LOOMWEAVER.md. */
909
- export const appConfig: ApplicationConfig = {
910
- providers: [
911
- provideShellRouter(),
912
- provideShell(),
913
- provideLayout(layout),
914
- provideProductIdentity({
915
- name: '${d.title}',
916
- tagline: 'Built on LoomWeaver',
917
- logoUrl: 'logo.svg',
918
- }),
919
- ],
920
- };
921
- `;
922
- }
923
- function appTs() {
924
- return `import { Component } from '@angular/core';
925
- import { Shell } from '@loomweaver/shell';
926
-
927
- @Component({
928
- selector: 'app-root',
929
- imports: [Shell],
930
- templateUrl: './app.html',
931
- })
932
- export class App {}
933
- `;
934
- }
935
- function appHtml() {
936
- return `<lw-shell />
937
- `;
938
- }
939
- function appConfigSpec() {
940
- return `import { layout } from './app.config';
941
-
942
- /* A green starting point that pins the one trap the compiler cannot catch: a contribution aimed at
943
- a region id this layout omits renders nothing, and says nothing. List the ids your weavers target
944
- here, and this fails the day someone edits the layout instead of failing in the browser. */
945
- describe('layout', () => {
946
- it('declares the regions contributions target', () => {
947
- const ids = layout.regions.map((region) => region.id);
948
- for (const id of ['primary', 'status-bar', 'main']) {
949
- expect(ids).toContain(id);
950
- }
951
- });
952
- });
953
- `;
954
- }
955
- function indexHtml(d) {
956
- return `<!DOCTYPE html>
957
- <html lang="en">
958
- <head>
959
- <meta charset="utf-8" />
960
- <title>${d.title}</title>
961
- <base href="/" />
962
- <meta
963
- http-equiv="Content-Security-Policy"
964
- content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-src 'self'; object-src 'none'; base-uri 'self'"
965
- />
966
- <meta name="viewport" content="width=device-width, initial-scale=1" />
967
- <meta name="theme-color" content="#2E96C9" />
968
- <link rel="icon" type="image/svg+xml" href="logo.svg" />
969
- <link rel="manifest" href="manifest.webmanifest" />
970
- </head>
971
- <body>
972
- <app-root></app-root>
973
- </body>
974
- </html>
975
- `;
976
- }
977
- function precompiledCss() {
978
- return `/* The stylesheet we compiled: the design tokens, the .lw-* class contracts and every utility
979
- the shell's own templates use. This application needs no Tailwind to build.
980
-
981
- BRINGING YOUR OWN CSS FRAMEWORK? Import it INTO A CASCADE LAYER. Every rule we ship is layered,
982
- and unlayered CSS outranks layered CSS whatever its specificity \u2014 so an unlayered Bootstrap
983
- Reboot (button { border-radius: 0 }) strips the chrome's radii and borders without a fight.
984
- A @layer statement is one of the few things allowed before @import:
985
-
986
- @layer vendor;
987
- @import 'bootstrap/dist/css/bootstrap.css' layer(vendor);
988
-
989
- Then re-theme by pointing the --lw-* tokens at your framework's variables. For Bootstrap 5.3 the
990
- whole 29-token mapping is a scaffold:
991
-
992
- loomweaver theme --name acme --preset bootstrap
993
- @import './themes/acme.css'; (after the import below) */
994
-
995
- @import '@loomweaver/shell/styles/shell.css';
996
- `;
997
- }
998
- function tailwindCss(d) {
999
- return `@import 'tailwindcss';
1000
-
1001
- /* LoomWeaver design tokens + theme (light/dark, brand colors). Every @import has to precede the
1002
- other at-rules below: that is what plain CSS requires, and it is what keeps editors from
1003
- flagging a misplaced @import in a file you did not write. */
1004
- @import '@loomweaver/shell/styles/theme.css';
1005
-
1006
- @plugin '@tailwindcss/typography';
1007
-
1008
- /* Generate the utility classes the shell (and your own components) use. The @source path must
1009
- reach your workspace's node_modules FROM THIS FILE \u2014 adjust the ../ hops if this project does
1010
- not sit at that depth, or Tailwind silently emits none of the shell's classes. */
1011
- @source '${d.nodeModulesFromSrc}/@loomweaver/shell';
1012
- @source './';
1013
- `;
1014
- }
1015
- function stylesCss(d) {
1016
- return d.styles === "precompiled" ? precompiledCss() : tailwindCss(d);
1017
- }
1018
- function ngswConfig() {
1019
- return JSON.stringify(
1020
- {
1021
- $schema: "./node_modules/@angular/service-worker/config/schema.json",
1022
- index: "/index.html",
1023
- assetGroups: [
1024
- {
1025
- name: "app",
1026
- installMode: "prefetch",
1027
- resources: {
1028
- files: ["/index.html", "/manifest.webmanifest", "/*.css", "/*.js"]
1029
- }
1030
- },
1031
- {
1032
- name: "i18n",
1033
- installMode: "prefetch",
1034
- updateMode: "prefetch",
1035
- resources: {
1036
- files: ["/i18n/**/*.json"]
1037
- }
1038
- },
1039
- {
1040
- name: "assets",
1041
- installMode: "lazy",
1042
- updateMode: "prefetch",
1043
- resources: {
1044
- files: [
1045
- "/**/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2|ico)"
1046
- ]
1047
- }
1048
- }
1049
- ]
1050
- },
1051
- null,
1052
- 2
1053
- );
1054
- }
1055
- function manifest(d) {
1056
- return JSON.stringify(
1057
- {
1058
- name: d.title,
1059
- short_name: d.title,
1060
- description: `${d.title} \u2014 a LoomWeaver distribution.`,
1061
- start_url: "/",
1062
- display: "standalone",
1063
- background_color: "#ffffff",
1064
- theme_color: "#2E96C9",
1065
- icons: [{ src: "logo.svg", type: "image/svg+xml", sizes: "any" }]
1066
- },
1067
- null,
1068
- 2
1069
- );
1070
- }
1125
+ // ../devkit/src/recipes/angular-distribution/readme.ts
1071
1126
  function stylesNotes(d) {
1072
1127
  if (d.styles === "precompiled") {
1073
1128
  return [
@@ -1086,20 +1141,15 @@ function stylesNotes(d) {
1086
1141
  }
1087
1142
  return [
1088
1143
  "`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:",
1144
+ "write Tailwind utilities in your own templates. The scaffold wrote `.postcssrc.json` beside your",
1145
+ "`package.json` for you, because without it the stylesheet is read as plain CSS: no utility class",
1146
+ "is emitted, the workbench renders unstyled, and the build still reports success. The packages are",
1147
+ "the one thing left, because a scaffold does not install:",
1091
1148
  "",
1092
1149
  "```sh",
1093
1150
  "npm install -D tailwindcss @tailwindcss/postcss @tailwindcss/typography",
1094
1151
  "```",
1095
1152
  "",
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
1153
  "Use **semantic tokens only** in your own templates (`bg-surface`, `text-content`, `text-brand`,",
1104
1154
  "`border-border`), never raw palette colours.",
1105
1155
  "",
@@ -1179,45 +1229,75 @@ function readme2(d) {
1179
1229
  "manifest's `icons` and add an `apple-touch-icon` link to `index.html`, and the app becomes",
1180
1230
  "installable. Until then it runs and caches offline, it simply is not offered for installation.",
1181
1231
  "",
1182
- "## Styles",
1232
+ "## The two searches, and the badges that say so",
1183
1233
  "",
1184
- ...stylesNotes(d),
1234
+ "The shell seeds two searches and binds them itself: `mod+k` opens the command search, `mod+p`",
1235
+ "the search over open work (`mod` is \u2318 on macOS, Ctrl elsewhere). Both work whether or not you",
1236
+ "do anything. What the generated `app.config.ts` adds is only the way to *see* them: a badge in",
1237
+ "the top bar for the command search, one at the leading edge of the status bar for open work,",
1238
+ "each printing its own chord in the spelling of the platform it runs on.",
1185
1239
  "",
1186
- "## Build wiring",
1240
+ "They are placed apart on purpose. Two identical-looking search badges side by side in the top",
1241
+ "bar read as a duplicate rather than as two different things.",
1187
1242
  "",
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:",
1192
- "",
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"',
1243
+ "```ts",
1244
+ "provideCommandPaletteEntry(); // top bar, end slot, order 5",
1245
+ "provideQuickOpenEntry({ bar: 'top-bar', order: 4 }); // \u2026or put it wherever you want",
1201
1246
  "```",
1202
1247
  "",
1203
- "And in the **production** configuration of that same target:",
1248
+ "A badge never outlives what it opens. Drop the search and its badge goes with it; the same",
1249
+ "happens for a session that may not run it. You will not be left with a control that does",
1250
+ "nothing. Switching shortcuts off is the one exception: the badge stays and still opens the",
1251
+ "search, it simply prints no chord, because nothing here advertises a key that does nothing.",
1252
+ "",
1253
+ "Four things you may want, and the line for each:",
1254
+ "",
1255
+ "```ts",
1256
+ "// 1. Keep the search, drop only the badge \u2014 it then opens by shortcut alone.",
1257
+ "provideShell({ omit: ['shell.commandPaletteEntry', 'shell.quickOpenEntry'] }),",
1258
+ "",
1259
+ "// 2. Drop the search itself. This takes mod+p with it: the chord is derived from the",
1260
+ "// registered command, so removing the command unbinds the key. The badge goes too.",
1261
+ "provideShell({ omit: ['shell.quickOpen'] }),",
1262
+ "",
1263
+ "// 3. Make mod+k run something of yours, keeping the shell's id. Last registration wins,",
1264
+ "// so your command replaces the built-in one and inherits its place everywhere.",
1265
+ "// Register it from your own plugin with the id 'shell.commandPalette'.",
1204
1266
  "",
1205
- "```jsonc",
1206
- '"optimization": { "styles": { "inlineCritical": false } }',
1267
+ "// 4. Bind mod+k to a command of your own, under your own id.",
1268
+ "provideShell({ omit: ['shell.commandPalette'] }), // \u2026then declare shortcut: 'mod+k' on yours",
1207
1269
  "```",
1208
1270
  "",
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.",
1271
+ "What not to do is the fifth case: declaring `mod+k` on a command of your own while the",
1272
+ "built-in one is still registered. Two commands then hold one chord. The shell warns about it in",
1273
+ "the console and the later registration wins, but which registration is later is not something",
1274
+ "your composition root decides. Omit the built-in, or take its id. Never race it.",
1275
+ "",
1276
+ "## Styles",
1277
+ "",
1278
+ ...stylesNotes(d),
1279
+ "",
1280
+ "## Build wiring",
1281
+ "",
1282
+ "The scaffold did this. Your build target now names the stylesheet, three asset globs, the",
1283
+ "service worker and one production setting, and the run that wrote these files listed each one it",
1284
+ "added. Anything you had already set was left exactly as you set it.",
1285
+ "",
1286
+ "What each is for, so that nobody removes one as clutter. The **`@loomweaver/shell/i18n` glob**",
1287
+ "serves the strings the shell fetches at runtime; without it every label in the chrome renders as",
1288
+ "its raw translation key and nothing errors. The **frame-kit** glob only matters if you host",
1289
+ "sandboxed (iframe) plugins \u2014 until you install that package the glob simply matches nothing.",
1290
+ "**`serviceWorker`** emits the worker that `provideShell()` already registers for you (inert in",
1291
+ "dev) \u2014 never add `provideServiceWorker` yourself, and if you would rather ship no worker at all,",
1292
+ "drop `ngsw-config.json` and pass `provideShell({ serviceWorker: false })`, because otherwise the",
1293
+ "registration 404s in production. **`optimization.styles.inlineCritical: false`** is not optional:",
1294
+ "the `index.html` above ships a strict `script-src 'self'`, and Angular's critical-CSS pass loads",
1295
+ "the stylesheet with an **inline** `onload` handler that the policy blocks \u2014 the app then renders",
1296
+ "completely unstyled, and only in production builds.",
1297
+ "",
1298
+ "One thing is still yours, because the scaffold cannot know your budget: a production build warns",
1299
+ "that the initial bundle exceeds Angular's 500 kB default. The shell is a whole application",
1300
+ "chrome, so raise the budgets in your build target.",
1221
1301
  "",
1222
1302
  "## Ship less than the whole workbench",
1223
1303
  "",
@@ -1254,8 +1334,236 @@ function readme2(d) {
1254
1334
  ""
1255
1335
  ].join("\n");
1256
1336
  }
1337
+
1338
+ // ../devkit/src/recipes/angular-distribution/recipe.ts
1339
+ var STYLES = ["tailwind", "precompiled"];
1340
+ function resolveDistributionInput(input) {
1341
+ if (!isKebabId(input.name)) {
1342
+ throw new Error(
1343
+ `Distribution name must be kebab-case (e.g. "acme-studio"); got "${input.name}".`
1344
+ );
1345
+ }
1346
+ const styles = input.styles ?? "tailwind";
1347
+ if (!STYLES.includes(styles)) {
1348
+ throw new Error(
1349
+ `Unknown styles option "${styles}"; expected one of ${STYLES.join(", ")}.`
1350
+ );
1351
+ }
1352
+ const directory = input.directory === void 0 ? `apps/${input.name}` : input.directory.trim();
1353
+ const depth = directory.split("/").filter(Boolean).length;
1354
+ return {
1355
+ name: input.name,
1356
+ title: input.title?.trim() || toTitleCase(input.name),
1357
+ nodeModulesFromSrc: `${"../".repeat(depth + 1)}node_modules`,
1358
+ withTests: input.withTests !== false,
1359
+ styles
1360
+ };
1361
+ }
1362
+ function mainTs() {
1363
+ return `import { bootstrapApplication } from '@angular/platform-browser';
1364
+ import { appConfig } from './app/app.config';
1365
+ import { App } from './app/app';
1366
+
1367
+ bootstrapApplication(App, appConfig).catch((err) => console.error(err));
1368
+ `;
1369
+ }
1370
+ function appConfigTs(d) {
1371
+ return `import { ApplicationConfig } from '@angular/core';
1372
+ import {
1373
+ provideCommandPaletteEntry,
1374
+ provideLayout,
1375
+ provideQuickOpenEntry,
1376
+ provideShell,
1377
+ provideShellRouter,
1378
+ type ShellLayout,
1379
+ } from '@loomweaver/shell';
1380
+ import { provideProductIdentity } from '@loomweaver/plugin-sdk';
1381
+
1382
+ /* Which regions exist and where they dock. Contributions target these ids, so a region a
1383
+ weaver names but this layout omits renders nothing \u2014 silently. 'primary' (rail) and
1384
+ 'status-bar' (bar) are what the scaffolded weaver targets. */
1385
+ export const layout: ShellLayout = {
1386
+ regions: [
1387
+ ${renderRegions(" ")}
1388
+ ],
1389
+ };
1390
+
1391
+ /* Everything this product is made of goes in this array: your weavers, their capability
1392
+ grants and your branding. The shell arrives with every capability on; switch gestures
1393
+ off with provideShellFeatures and drop contributions with provideShell({ omit }).
1394
+ See LOOMWEAVER.md. */
1395
+ export const appConfig: ApplicationConfig = {
1396
+ providers: [
1397
+ provideShellRouter(),
1398
+ provideShell(),
1399
+ provideLayout(layout),
1400
+ /* The two searches the shell seeds: mod+k over commands, mod+p over open work. Both work
1401
+ without these two lines; these put the shortcut on screen for a user who does not know
1402
+ it. Delete either one, or pass { bar, slot, order } to place it elsewhere. */
1403
+ provideCommandPaletteEntry(),
1404
+ provideQuickOpenEntry(),
1405
+ provideProductIdentity({
1406
+ name: '${d.title}',
1407
+ tagline: 'Built on LoomWeaver',
1408
+ logoUrl: 'logo.svg',
1409
+ }),
1410
+ ],
1411
+ };
1412
+ `;
1413
+ }
1414
+ function appTs() {
1415
+ return `import { Component } from '@angular/core';
1416
+ import { Shell } from '@loomweaver/shell';
1417
+
1418
+ @Component({
1419
+ selector: 'app-root',
1420
+ imports: [Shell],
1421
+ templateUrl: './app.html',
1422
+ })
1423
+ export class App {}
1424
+ `;
1425
+ }
1426
+ function appHtml() {
1427
+ return `<lw-shell />
1428
+ `;
1429
+ }
1430
+ function appConfigSpec() {
1431
+ return `import { layout } from './app.config';
1432
+
1433
+ /* A green starting point that pins the one trap the compiler cannot catch: a contribution aimed at
1434
+ a region id this layout omits renders nothing, and says nothing. List the ids your weavers target
1435
+ here, and this fails the day someone edits the layout instead of failing in the browser. */
1436
+ describe('layout', () => {
1437
+ it('declares the regions contributions target', () => {
1438
+ const ids = layout.regions.map((region) => region.id);
1439
+ for (const id of ['primary', 'status-bar', 'main']) {
1440
+ expect(ids).toContain(id);
1441
+ }
1442
+ });
1443
+ });
1444
+ `;
1445
+ }
1446
+ function indexHtml(d) {
1447
+ return `<!DOCTYPE html>
1448
+ <html lang="en">
1449
+ <head>
1450
+ <meta charset="utf-8" />
1451
+ <title>${d.title}</title>
1452
+ <base href="/" />
1453
+ <meta
1454
+ http-equiv="Content-Security-Policy"
1455
+ content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-src 'self'; object-src 'none'; base-uri 'self'"
1456
+ />
1457
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1458
+ <meta name="theme-color" content="#2E96C9" />
1459
+ <link rel="icon" type="image/svg+xml" href="logo.svg" />
1460
+ <link rel="manifest" href="manifest.webmanifest" />
1461
+ </head>
1462
+ <body>
1463
+ <app-root></app-root>
1464
+ </body>
1465
+ </html>
1466
+ `;
1467
+ }
1468
+ function precompiledCss() {
1469
+ return `/* The stylesheet we compiled: the design tokens, the .lw-* class contracts and every utility
1470
+ the shell's own templates use. This application needs no Tailwind to build.
1471
+
1472
+ BRINGING YOUR OWN CSS FRAMEWORK? Import it INTO A CASCADE LAYER. Every rule we ship is layered,
1473
+ and unlayered CSS outranks layered CSS whatever its specificity \u2014 so an unlayered Bootstrap
1474
+ Reboot (button { border-radius: 0 }) strips the chrome's radii and borders without a fight.
1475
+ A @layer statement is one of the few things allowed before @import:
1476
+
1477
+ @layer vendor;
1478
+ @import 'bootstrap/dist/css/bootstrap.css' layer(vendor);
1479
+
1480
+ Then re-theme by pointing the --lw-* tokens at your framework's variables. For Bootstrap 5.3 the
1481
+ whole 29-token mapping is a scaffold:
1482
+
1483
+ loomweaver theme --name acme --preset bootstrap
1484
+ @import './themes/acme.css'; (after the import below) */
1485
+
1486
+ @import '@loomweaver/shell/styles/shell.css';
1487
+ `;
1488
+ }
1489
+ function tailwindCss(d) {
1490
+ return `@import 'tailwindcss';
1491
+
1492
+ /* LoomWeaver design tokens + theme (light/dark, brand colors). Every @import has to precede the
1493
+ other at-rules below: that is what plain CSS requires, and it is what keeps editors from
1494
+ flagging a misplaced @import in a file you did not write. */
1495
+ @import '@loomweaver/shell/styles/theme.css';
1496
+
1497
+ @plugin '@tailwindcss/typography';
1498
+
1499
+ /* Generate the utility classes the shell (and your own components) use. The @source path must
1500
+ reach your workspace's node_modules FROM THIS FILE \u2014 adjust the ../ hops if this project does
1501
+ not sit at that depth, or Tailwind silently emits none of the shell's classes. */
1502
+ @source '${d.nodeModulesFromSrc}/@loomweaver/shell';
1503
+ @source './';
1504
+ `;
1505
+ }
1506
+ function stylesCss(d) {
1507
+ return d.styles === "precompiled" ? precompiledCss() : tailwindCss(d);
1508
+ }
1509
+ function ngswConfig() {
1510
+ return JSON.stringify(
1511
+ {
1512
+ $schema: "./node_modules/@angular/service-worker/config/schema.json",
1513
+ index: "/index.html",
1514
+ assetGroups: [
1515
+ {
1516
+ name: "app",
1517
+ installMode: "prefetch",
1518
+ resources: {
1519
+ files: ["/index.html", "/manifest.webmanifest", "/*.css", "/*.js"]
1520
+ }
1521
+ },
1522
+ {
1523
+ name: "i18n",
1524
+ installMode: "prefetch",
1525
+ updateMode: "prefetch",
1526
+ resources: {
1527
+ files: ["/i18n/**/*.json"]
1528
+ }
1529
+ },
1530
+ {
1531
+ name: "assets",
1532
+ installMode: "lazy",
1533
+ updateMode: "prefetch",
1534
+ resources: {
1535
+ files: [
1536
+ "/**/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2|ico)"
1537
+ ]
1538
+ }
1539
+ }
1540
+ ]
1541
+ },
1542
+ null,
1543
+ 2
1544
+ );
1545
+ }
1546
+ function manifest(d) {
1547
+ return JSON.stringify(
1548
+ {
1549
+ name: d.title,
1550
+ short_name: d.title,
1551
+ description: `${d.title} \u2014 a LoomWeaver distribution.`,
1552
+ start_url: "/",
1553
+ display: "standalone",
1554
+ background_color: "#ffffff",
1555
+ theme_color: "#2E96C9",
1556
+ icons: [{ src: "logo.svg", type: "image/svg+xml", sizes: "any" }]
1557
+ },
1558
+ null,
1559
+ 2
1560
+ );
1561
+ }
1257
1562
  var angularDistribution = {
1258
1563
  id: "angular-distribution",
1564
+ amend(input) {
1565
+ return distributionAmendments(resolveDistributionInput(input));
1566
+ },
1259
1567
  build(input) {
1260
1568
  const d = resolveDistributionInput(input);
1261
1569
  return {
@@ -1546,6 +1854,67 @@ var layout = {
1546
1854
  }
1547
1855
  };
1548
1856
 
1857
+ // ../devkit/src/recipes/angular-weaver/amendments.ts
1858
+ function weaverAmendments(input, where) {
1859
+ const w = resolveWeaverInput(input);
1860
+ const directory = (where ?? "").replace(/^\.?\/*/, "").replace(/\/+$/, "");
1861
+ if (!directory) {
1862
+ return [];
1863
+ }
1864
+ return [
1865
+ {
1866
+ kind: "build-target",
1867
+ styles: [],
1868
+ assets: [
1869
+ {
1870
+ glob: "**/*.json",
1871
+ input: `${directory}/src/lib/i18n`,
1872
+ from: "workspace",
1873
+ output: `i18n/${w.id}`
1874
+ }
1875
+ ]
1876
+ },
1877
+ { kind: "stylesheet-source", sourceRoot: `${directory}/src` },
1878
+ {
1879
+ kind: "compose-plugin",
1880
+ id: w.id,
1881
+ symbol: `${w.propertyName}Plugin`,
1882
+ capabilities: w.capabilities,
1883
+ sourceRoot: `${directory}/src`
1884
+ }
1885
+ ];
1886
+ }
1887
+
1888
+ // ../devkit/src/lib/scaffolds/inputs.ts
1889
+ function weaverInput(values) {
1890
+ return {
1891
+ id: str(values, "id") ?? "",
1892
+ name: str(values, "name"),
1893
+ prefix: str(values, "prefix"),
1894
+ importPath: str(values, "importPath"),
1895
+ features: {
1896
+ command: bool(values, "command"),
1897
+ shortcut: str(values, "shortcut"),
1898
+ menu: str(values, "menu"),
1899
+ barItem: bool(values, "barItem"),
1900
+ settings: bool(values, "settings"),
1901
+ about: bool(values, "about"),
1902
+ instanceable: bool(values, "instanceable"),
1903
+ container: bool(values, "container"),
1904
+ access: str(values, "access"),
1905
+ spec: bool(values, "spec")
1906
+ }
1907
+ };
1908
+ }
1909
+ function distributionInput(values) {
1910
+ return {
1911
+ name: str(values, "name") ?? "",
1912
+ title: str(values, "title"),
1913
+ directory: str(values, "directory"),
1914
+ styles: str(values, "styles") ?? "tailwind"
1915
+ };
1916
+ }
1917
+
1549
1918
  // ../devkit/src/lib/scaffolds/scaffolds.ts
1550
1919
  function str(values, name) {
1551
1920
  const value = values[name];
@@ -1684,24 +2053,8 @@ var SCAFFOLDS = [
1684
2053
  APP_OPTION,
1685
2054
  ...PLACEMENT_OPTIONS
1686
2055
  ],
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
- })
2056
+ build: (values) => generate(angularWeaver, weaverInput(values)),
2057
+ amend: (values) => weaverAmendments(weaverInput(values), str(values, "directory"))
1705
2058
  },
1706
2059
  {
1707
2060
  name: "frame-plugin",
@@ -1757,12 +2110,8 @@ var SCAFFOLDS = [
1757
2110
  },
1758
2111
  ...PLACEMENT_OPTIONS
1759
2112
  ],
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
- })
2113
+ build: (values) => generate(angularDistribution, distributionInput(values)),
2114
+ amend: (values) => amendments(angularDistribution, distributionInput(values))
1766
2115
  },
1767
2116
  {
1768
2117
  name: "auth-source",
@@ -2113,7 +2462,7 @@ function validateCatalog(catalog, known = KNOWN_CAPABILITIES) {
2113
2462
  }
2114
2463
 
2115
2464
  // src/lib/run.ts
2116
- import { readdirSync, readFileSync } from "node:fs";
2465
+ import { readdirSync, readFileSync as readFileSync3 } from "node:fs";
2117
2466
  import { join } from "node:path";
2118
2467
 
2119
2468
  // src/lib/args.ts
@@ -2203,8 +2552,336 @@ function boolFlag(args, name) {
2203
2552
  return value;
2204
2553
  }
2205
2554
 
2555
+ // src/lib/amend.ts
2556
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
2557
+ import { dirname as dirname2, posix, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
2558
+
2559
+ // src/lib/workspace.ts
2560
+ import { existsSync, readFileSync } from "node:fs";
2561
+ import { dirname, relative, resolve, sep } from "node:path";
2562
+ var WorkspaceError = class extends Error {
2563
+ };
2564
+ function findWorkspace(from) {
2565
+ let dir = resolve(from);
2566
+ for (; ; ) {
2567
+ if (existsSync(resolve(dir, "package.json"))) {
2568
+ return { root: dir, ...buildConfigIn(dir) };
2569
+ }
2570
+ const parent = dirname(dir);
2571
+ if (parent === dir) {
2572
+ return void 0;
2573
+ }
2574
+ dir = parent;
2575
+ }
2576
+ }
2577
+ function resolveBuildProject(workspace, target) {
2578
+ const projects = readProjects(workspace);
2579
+ if (projects.length === 0) {
2580
+ throw new WorkspaceError(
2581
+ `No project with a build target found in ${workspace.configFile ?? workspace.root}.`
2582
+ );
2583
+ }
2584
+ const inside = projects.filter((project) => contains(project.root, relativeTo(workspace.root, target))).sort((a, b) => b.root.length - a.root.length);
2585
+ if (inside.length > 0) {
2586
+ return inside[0];
2587
+ }
2588
+ if (projects.length === 1) {
2589
+ return projects[0];
2590
+ }
2591
+ throw new WorkspaceError(
2592
+ `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.`
2593
+ );
2594
+ }
2595
+ function readJsonFile(file) {
2596
+ try {
2597
+ return JSON.parse(readFileSync(file, "utf8"));
2598
+ } catch (error) {
2599
+ throw new WorkspaceError(
2600
+ `${file} is not valid JSON: ${error.message}`
2601
+ );
2602
+ }
2603
+ }
2604
+ function buildConfigIn(dir) {
2605
+ const angular = resolve(dir, "angular.json");
2606
+ if (existsSync(angular)) {
2607
+ return { configFile: angular, kind: "angular" };
2608
+ }
2609
+ const nx = resolve(dir, "nx.json");
2610
+ if (existsSync(nx)) {
2611
+ return { configFile: nx, kind: "nx" };
2612
+ }
2613
+ return {};
2614
+ }
2615
+ function readProjects(workspace) {
2616
+ if (workspace.kind !== "angular" || !workspace.configFile) {
2617
+ return [];
2618
+ }
2619
+ const config = readJsonFile(workspace.configFile);
2620
+ const projects = asObject2(asObject2(config)?.["projects"]) ?? {};
2621
+ return Object.entries(projects).filter(([, project]) => hasBuildTarget(project)).map(([name, project]) => ({
2622
+ name,
2623
+ root: normalise(asObject2(project)?.["root"])
2624
+ }));
2625
+ }
2626
+ function hasBuildTarget(project) {
2627
+ const architect = asObject2(project)?.["architect"] ?? asObject2(project)?.["targets"];
2628
+ return asObject2(architect)?.["build"] !== void 0;
2629
+ }
2630
+ function contains(projectRoot, target) {
2631
+ return projectRoot === "" || target === projectRoot || target.startsWith(`${projectRoot}/`);
2632
+ }
2633
+ function relativeTo(root, target) {
2634
+ return relative(root, resolve(target)).split(sep).join("/");
2635
+ }
2636
+ function normalise(value) {
2637
+ return typeof value === "string" ? value.replace(/^\.?\/*/, "").replace(/\/+$/, "") : "";
2638
+ }
2639
+ function asObject2(value) {
2640
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
2641
+ }
2642
+
2643
+ // src/lib/amend.ts
2644
+ var JS_POSTCSS_CONFIGS = [
2645
+ "postcss.config.js",
2646
+ "postcss.config.mjs",
2647
+ "postcss.config.cjs",
2648
+ ".postcssrc.js"
2649
+ ];
2650
+ function planAmend(amendments2, target) {
2651
+ if (amendments2.length === 0) {
2652
+ return { amendments: [], remaining: [] };
2653
+ }
2654
+ const workspace = findWorkspace(target);
2655
+ if (!workspace) {
2656
+ return {
2657
+ amendments: [],
2658
+ remaining: [
2659
+ `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 ")}`
2660
+ ]
2661
+ };
2662
+ }
2663
+ return new Amender(workspace, target).plan(amendments2);
2664
+ }
2665
+ function applyAmend(plan) {
2666
+ for (const amendment of plan.amendments) {
2667
+ writeFileSync(amendment.file, amendment.content, "utf8");
2668
+ }
2669
+ }
2670
+ var Amender = class {
2671
+ constructor(workspace, target) {
2672
+ this.workspace = workspace;
2673
+ this.target = target;
2674
+ this.planned = [];
2675
+ this.remaining = [];
2676
+ this.configAdded = [];
2677
+ }
2678
+ plan(amendments2) {
2679
+ for (const amendment of amendments2) {
2680
+ this.planOne(amendment);
2681
+ }
2682
+ this.flushConfig();
2683
+ return { amendments: this.planned, remaining: this.remaining };
2684
+ }
2685
+ planOne(amendment) {
2686
+ if (amendment.kind === "postcss") {
2687
+ this.planPostcss(amendment);
2688
+ return;
2689
+ }
2690
+ if (this.workspace.kind !== "angular") {
2691
+ this.remaining.push(this.nonAngularNote(amendment));
2692
+ return;
2693
+ }
2694
+ const project = this.resolveProject();
2695
+ if (!project) {
2696
+ return;
2697
+ }
2698
+ if (amendment.kind === "build-target") {
2699
+ this.planBuildTarget(amendment, project);
2700
+ } else if (amendment.kind === "stylesheet-source") {
2701
+ this.planStylesheetSource(amendment, project);
2702
+ } else {
2703
+ this.planComposePlugin(amendment, project);
2704
+ }
2705
+ }
2706
+ planPostcss(amendment) {
2707
+ const inTheWay = JS_POSTCSS_CONFIGS.find(
2708
+ (name) => existsSync2(resolve2(this.workspace.root, name))
2709
+ );
2710
+ if (inTheWay) {
2711
+ this.remaining.push(
2712
+ `${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.`
2713
+ );
2714
+ return;
2715
+ }
2716
+ const file = resolve2(this.workspace.root, amendment.file);
2717
+ const result = ensurePostcssPlugin(
2718
+ existsSync2(file) ? readJsonFile(file) : void 0,
2719
+ amendment
2720
+ );
2721
+ this.remaining.push(...result.declined);
2722
+ if (result.added.length === 0) {
2723
+ return;
2724
+ }
2725
+ this.planned.push({
2726
+ file,
2727
+ display: this.displayName(file),
2728
+ added: result.added,
2729
+ content: `${JSON.stringify(result.value, null, 2)}
2730
+ `
2731
+ });
2732
+ }
2733
+ planBuildTarget(amendment, project) {
2734
+ const target = this.buildTarget(project.name);
2735
+ if (!target) {
2736
+ this.remaining.push(
2737
+ `${project.name} has no build target to wire, so add it by hand: ${describeAmendment(amendment)}.`
2738
+ );
2739
+ return;
2740
+ }
2741
+ const result = ensureBuildTarget(target.value, amendment, project.root);
2742
+ this.remaining.push(...result.declined);
2743
+ if (result.added.length === 0) {
2744
+ return;
2745
+ }
2746
+ target.set(result.value);
2747
+ this.configAdded.push(...result.added);
2748
+ }
2749
+ planStylesheetSource(amendment, project) {
2750
+ const entry = this.entryStylesheet(project);
2751
+ if (!entry || !existsSync2(entry)) {
2752
+ this.remaining.push(
2753
+ `No entry stylesheet is wired for ${project.name}, so add it yourself: ${describeAmendment(amendment)}.`
2754
+ );
2755
+ return;
2756
+ }
2757
+ const css = readFileSync2(entry, "utf8");
2758
+ if (!/@import\s+['"]tailwindcss['"]/.test(css)) {
2759
+ return;
2760
+ }
2761
+ const source = posix.relative(
2762
+ this.displayName(dirname2(entry)),
2763
+ amendment.sourceRoot
2764
+ );
2765
+ const next = ensureStylesheetSource(css, source);
2766
+ if (next === css) {
2767
+ return;
2768
+ }
2769
+ this.planned.push({
2770
+ file: entry,
2771
+ display: this.displayName(entry),
2772
+ added: [`@source '${source}'`],
2773
+ content: next
2774
+ });
2775
+ }
2776
+ planComposePlugin(amendment, project) {
2777
+ const root = resolve2(
2778
+ this.workspace.root,
2779
+ project.root,
2780
+ "src/app/app.config.ts"
2781
+ );
2782
+ const importPath = relativeImport(
2783
+ this.displayName(dirname2(root)),
2784
+ amendment.sourceRoot
2785
+ );
2786
+ if (!existsSync2(root)) {
2787
+ this.remaining.push(this.composeNote(amendment, importPath));
2788
+ return;
2789
+ }
2790
+ const source = readFileSync2(root, "utf8");
2791
+ const result = composePlugin(source, amendment, importPath);
2792
+ if (!result.composed) {
2793
+ this.remaining.push(this.composeNote(amendment, importPath));
2794
+ return;
2795
+ }
2796
+ if (result.source === source) {
2797
+ return;
2798
+ }
2799
+ this.planned.push({
2800
+ file: root,
2801
+ display: this.displayName(root),
2802
+ added: [`${amendment.symbol}, its translations and its capability grants`],
2803
+ content: result.source
2804
+ });
2805
+ }
2806
+ composeNote(amendment, importPath) {
2807
+ 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(" ");
2808
+ }
2809
+ flushConfig() {
2810
+ if (this.configAdded.length === 0 || !this.config) {
2811
+ return;
2812
+ }
2813
+ const file = this.workspace.configFile;
2814
+ this.planned.push({
2815
+ file,
2816
+ display: this.displayName(file),
2817
+ added: this.configAdded,
2818
+ content: `${JSON.stringify(this.config, null, 2)}
2819
+ `
2820
+ });
2821
+ }
2822
+ resolveProject() {
2823
+ if (this.project) {
2824
+ return this.project;
2825
+ }
2826
+ try {
2827
+ this.project = resolveBuildProject(this.workspace, this.target);
2828
+ return this.project;
2829
+ } catch (error) {
2830
+ this.remaining.push(error.message);
2831
+ return void 0;
2832
+ }
2833
+ }
2834
+ readConfig() {
2835
+ this.config ??= readJsonFile(this.workspace.configFile);
2836
+ return this.config;
2837
+ }
2838
+ buildTarget(name) {
2839
+ const project = asObject3(asObject3(this.readConfig()["projects"])?.[name]);
2840
+ if (!project) {
2841
+ return void 0;
2842
+ }
2843
+ for (const key of ["architect", "targets"]) {
2844
+ const targets = asObject3(project[key]);
2845
+ if (targets?.["build"] !== void 0) {
2846
+ return {
2847
+ value: targets["build"],
2848
+ set: (next) => {
2849
+ targets["build"] = next;
2850
+ }
2851
+ };
2852
+ }
2853
+ }
2854
+ return void 0;
2855
+ }
2856
+ entryStylesheet(project) {
2857
+ const styles = asObject3(asObject3(this.buildTarget(project.name)?.value)?.["options"])?.["styles"];
2858
+ if (!Array.isArray(styles)) {
2859
+ return void 0;
2860
+ }
2861
+ const entry = styles.find(
2862
+ (style) => typeof style === "string" && style.endsWith(".css")
2863
+ );
2864
+ return entry === void 0 ? void 0 : resolve2(this.workspace.root, entry);
2865
+ }
2866
+ nonAngularNote(amendment) {
2867
+ const where = this.workspace.kind === "nx" ? "the project's own project.json" : "your build configuration";
2868
+ return `This route wires an Angular CLI workspace only, so add ${describeAmendment(amendment)} to ${where} yourself. The Nx generator does it for you.`;
2869
+ }
2870
+ displayName(file) {
2871
+ const inside = relative2(this.workspace.root, file).split(sep2).join("/");
2872
+ return inside.startsWith("..") ? file : inside;
2873
+ }
2874
+ };
2875
+ function asObject3(value) {
2876
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
2877
+ }
2878
+ function relativeImport(fromDir, sourceRoot) {
2879
+ const path = posix.relative(fromDir, sourceRoot);
2880
+ return path.startsWith(".") ? path : `./${path}`;
2881
+ }
2882
+
2206
2883
  // src/lib/scaffold.ts
2207
- import { relative, resolve } from "node:path";
2884
+ import { relative as relative3, resolve as resolve3 } from "node:path";
2208
2885
  function findScaffold2(name) {
2209
2886
  const scaffold2 = findScaffold(name);
2210
2887
  if (!scaffold2) {
@@ -2256,23 +2933,28 @@ function valuesFor(scaffold2, args) {
2256
2933
  return values;
2257
2934
  }
2258
2935
  function directoryFromOut(out) {
2259
- const below = relative(process.cwd(), resolve(out ?? "."));
2936
+ const below = relative3(process.cwd(), resolve3(out ?? "."));
2260
2937
  return below.startsWith("..") ? "" : below;
2261
2938
  }
2262
- function buildScaffold(scaffold2, args) {
2939
+ function scaffoldValues(scaffold2, args) {
2263
2940
  const values = valuesFor(scaffold2, args);
2264
2941
  const takesDirectory = scaffold2.options.some(
2265
2942
  (option) => option.name === "directory"
2266
2943
  );
2944
+ if (!takesDirectory) {
2945
+ return values;
2946
+ }
2267
2947
  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
- );
2948
+ return {
2949
+ ...values,
2950
+ directory: directoryFromOut(typeof out === "string" ? out : void 0)
2951
+ };
2952
+ }
2953
+ function buildScaffold(scaffold2, args) {
2954
+ return scaffold2.build(scaffoldValues(scaffold2, args));
2955
+ }
2956
+ function amendmentsFor(scaffold2, args) {
2957
+ return scaffold2.amend?.(scaffoldValues(scaffold2, args)) ?? [];
2276
2958
  }
2277
2959
 
2278
2960
  // src/lib/write.ts
@@ -2281,18 +2963,18 @@ import {
2281
2963
  mkdirSync,
2282
2964
  realpathSync,
2283
2965
  rmSync,
2284
- writeFileSync
2966
+ writeFileSync as writeFileSync2
2285
2967
  } from "node:fs";
2286
- import { dirname, isAbsolute, relative as relative2, resolve as resolve2 } from "node:path";
2968
+ import { dirname as dirname3, isAbsolute, relative as relative4, resolve as resolve4 } from "node:path";
2287
2969
  var WriteError = class extends Error {
2288
2970
  };
2289
2971
  function planWrite(files, root) {
2290
- const absoluteRoot = resolve2(root);
2972
+ const absoluteRoot = resolve4(root);
2291
2973
  const planned = [];
2292
2974
  const conflicts = [];
2293
2975
  for (const path of Object.keys(files).sort()) {
2294
- const absolute = resolve2(absoluteRoot, path);
2295
- const inside = relative2(absoluteRoot, absolute);
2976
+ const absolute = resolve4(absoluteRoot, path);
2977
+ const inside = relative4(absoluteRoot, absolute);
2296
2978
  if (inside.startsWith("..") || isAbsolute(inside)) {
2297
2979
  throw new WriteError(`Refusing to write outside the target directory: ${path}`);
2298
2980
  }
@@ -2305,10 +2987,10 @@ function planWrite(files, root) {
2305
2987
  }
2306
2988
  function applyWrite(files, plan) {
2307
2989
  for (const file of plan.files) {
2308
- mkdirSync(dirname(file.absolute), { recursive: true });
2990
+ mkdirSync(dirname3(file.absolute), { recursive: true });
2309
2991
  assertResolvesInsideRoot(plan.root, file.path, file.absolute);
2310
2992
  replaceSymlinkEntry(file.absolute);
2311
- writeFileSync(file.absolute, files[file.path], "utf8");
2993
+ writeFileSync2(file.absolute, files[file.path], "utf8");
2312
2994
  }
2313
2995
  }
2314
2996
  function entryExists(absolute) {
@@ -2320,7 +3002,7 @@ function entryExists(absolute) {
2320
3002
  }
2321
3003
  }
2322
3004
  function assertResolvesInsideRoot(root, path, absolute) {
2323
- const inside = relative2(realpathSync(root), realpathSync(dirname(absolute)));
3005
+ const inside = relative4(realpathSync(root), realpathSync(dirname3(absolute)));
2324
3006
  if (inside.startsWith("..") || isAbsolute(inside)) {
2325
3007
  throw new WriteError(
2326
3008
  `Refusing to write through a link that leaves the target directory: ${path}`
@@ -2334,7 +3016,7 @@ function replaceSymlinkEntry(absolute) {
2334
3016
  }
2335
3017
 
2336
3018
  // src/lib/run.ts
2337
- var VERSION = "0.7.2";
3019
+ var VERSION = "0.7.4";
2338
3020
  function help() {
2339
3021
  const commands = SCAFFOLDS.map((s) => ` ${s.name.padEnd(16)}${s.summary}`);
2340
3022
  return [
@@ -2406,7 +3088,7 @@ function readBundles(dir) {
2406
3088
  }
2407
3089
  const language = entry.slice(0, -".json".length);
2408
3090
  try {
2409
- bundles[language] = JSON.parse(readFileSync(join(dir, entry), "utf8"));
3091
+ bundles[language] = JSON.parse(readFileSync3(join(dir, entry), "utf8"));
2410
3092
  } catch (error) {
2411
3093
  throw new ArgError(`${entry} is not valid JSON: ${error.message}`);
2412
3094
  }
@@ -2427,7 +3109,7 @@ function validateI18nCommand(args, io) {
2427
3109
  function readCatalog(file) {
2428
3110
  let raw;
2429
3111
  try {
2430
- raw = readFileSync(file, "utf8");
3112
+ raw = readFileSync3(file, "utf8");
2431
3113
  } catch (error) {
2432
3114
  throw new ArgError(`Cannot read ${file}: ${error.message}`);
2433
3115
  }
@@ -2445,6 +3127,21 @@ function validateCatalogCommand(args, io) {
2445
3127
  boolFlag(args, "strict") === true
2446
3128
  );
2447
3129
  }
3130
+ function reportAmendments(io, amend, done) {
3131
+ if (amend.amendments.length > 0) {
3132
+ io.out(
3133
+ done ? `Wired ${amend.amendments.length} workspace file(s):` : `Would wire ${amend.amendments.length} workspace file(s):`
3134
+ );
3135
+ for (const amendment of amend.amendments) {
3136
+ io.out(` ${amendment.display}`);
3137
+ amendment.added.forEach((entry) => io.out(` + ${entry}`));
3138
+ }
3139
+ }
3140
+ if (amend.remaining.length > 0) {
3141
+ io.out("Still to do by hand:");
3142
+ amend.remaining.forEach((entry) => io.out(` - ${entry}`));
3143
+ }
3144
+ }
2448
3145
  function scaffold(args, io) {
2449
3146
  const descriptor = findScaffold2(args.command);
2450
3147
  rejectUnknownFlags(args, [
@@ -2454,8 +3151,10 @@ function scaffold(args, io) {
2454
3151
  "force"
2455
3152
  ]);
2456
3153
  const files = buildScaffold(descriptor, args);
2457
- const plan = planWrite(files, stringFlag(args, "out") ?? ".");
3154
+ const out = stringFlag(args, "out") ?? ".";
3155
+ const plan = planWrite(files, out);
2458
3156
  const paths = plan.files.map((file) => file.path);
3157
+ const amend = planAmend(amendmentsFor(descriptor, args), out);
2459
3158
  if (boolFlag(args, "dry-run")) {
2460
3159
  io.out(`Would write ${paths.length} file(s) into ${plan.root}:`);
2461
3160
  paths.forEach((path) => io.out(` ${path}`));
@@ -2465,6 +3164,7 @@ function scaffold(args, io) {
2465
3164
  );
2466
3165
  plan.conflicts.forEach((path) => io.out(` ${path}`));
2467
3166
  }
3167
+ reportAmendments(io, amend, false);
2468
3168
  return 0;
2469
3169
  }
2470
3170
  if (plan.conflicts.length > 0 && !boolFlag(args, "force")) {
@@ -2475,8 +3175,10 @@ function scaffold(args, io) {
2475
3175
  return 1;
2476
3176
  }
2477
3177
  applyWrite(files, plan);
3178
+ applyAmend(amend);
2478
3179
  io.out(`Wrote ${paths.length} file(s) into ${plan.root}:`);
2479
3180
  paths.forEach((path) => io.out(` ${path}`));
3181
+ reportAmendments(io, amend, true);
2480
3182
  return 0;
2481
3183
  }
2482
3184
  function run(argv, io) {