@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.
- package/dist/main.mjs +737 -88
- 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.
|
|
1090
|
-
"you
|
|
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
|
|
1189
|
-
"
|
|
1190
|
-
"
|
|
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
|
-
"
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
"
|
|
1200
|
-
|
|
1201
|
-
"
|
|
1202
|
-
"",
|
|
1203
|
-
"
|
|
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
|
-
"
|
|
1206
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
2883
|
+
const below = relative3(process.cwd(), resolve3(out ?? "."));
|
|
2260
2884
|
return below.startsWith("..") ? "" : below;
|
|
2261
2885
|
}
|
|
2262
|
-
function
|
|
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
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
2295
|
-
const inside =
|
|
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(
|
|
2937
|
+
mkdirSync(dirname3(file.absolute), { recursive: true });
|
|
2309
2938
|
assertResolvesInsideRoot(plan.root, file.path, file.absolute);
|
|
2310
2939
|
replaceSymlinkEntry(file.absolute);
|
|
2311
|
-
|
|
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 =
|
|
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.
|
|
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(
|
|
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 =
|
|
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
|
|
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.
|
|
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",
|