@loomweaver/cli 0.9.1 → 0.9.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 +730 -189
- package/package.json +1 -1
package/dist/main.mjs
CHANGED
|
@@ -98,34 +98,15 @@ function ensureBuildTarget(target, amendment, projectRoot) {
|
|
|
98
98
|
added.push(...assets.added.map((entry) => `assets: ${entry}`));
|
|
99
99
|
}
|
|
100
100
|
next["options"] = options;
|
|
101
|
-
if (amendment.inlineCritical !== void 0 || amendment.serviceWorker) {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
if (amendment.inlineCritical !== void 0) {
|
|
112
|
-
const critical = ensureInlineCritical(
|
|
113
|
-
production["optimization"],
|
|
114
|
-
amendment.inlineCritical
|
|
115
|
-
);
|
|
116
|
-
if (critical.declined) {
|
|
117
|
-
declined.push(
|
|
118
|
-
"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"
|
|
119
|
-
);
|
|
120
|
-
} else if (critical.changed) {
|
|
121
|
-
production["optimization"] = critical.value;
|
|
122
|
-
added.push(
|
|
123
|
-
`production optimization.styles.inlineCritical: ${amendment.inlineCritical}`
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
configurations["production"] = production;
|
|
128
|
-
next["configurations"] = configurations;
|
|
101
|
+
if (amendment.inlineCritical !== void 0 || amendment.serviceWorker || amendment.initialBudget) {
|
|
102
|
+
const production = ensureProductionConfiguration(
|
|
103
|
+
next["configurations"],
|
|
104
|
+
amendment,
|
|
105
|
+
projectRoot
|
|
106
|
+
);
|
|
107
|
+
next["configurations"] = production.value;
|
|
108
|
+
added.push(...production.added);
|
|
109
|
+
declined.push(...production.declined);
|
|
129
110
|
}
|
|
130
111
|
return { value: next, added, declined };
|
|
131
112
|
}
|
|
@@ -139,6 +120,99 @@ function ensureStylesheetSource(css, source) {
|
|
|
139
120
|
@source '${source}';
|
|
140
121
|
`;
|
|
141
122
|
}
|
|
123
|
+
function ensureProductionConfiguration(value, amendment, projectRoot) {
|
|
124
|
+
const configurations = { ...asObject(value) };
|
|
125
|
+
const production = { ...asObject(configurations["production"]) };
|
|
126
|
+
const added = [];
|
|
127
|
+
const declined = [];
|
|
128
|
+
if (amendment.serviceWorker && production["serviceWorker"] === void 0) {
|
|
129
|
+
production["serviceWorker"] = joinProjectPath(
|
|
130
|
+
projectRoot,
|
|
131
|
+
amendment.serviceWorker
|
|
132
|
+
);
|
|
133
|
+
added.push(`production serviceWorker: ${production["serviceWorker"]}`);
|
|
134
|
+
}
|
|
135
|
+
if (amendment.inlineCritical !== void 0) {
|
|
136
|
+
const critical = ensureInlineCritical(
|
|
137
|
+
production["optimization"],
|
|
138
|
+
amendment.inlineCritical
|
|
139
|
+
);
|
|
140
|
+
if (critical.declined) {
|
|
141
|
+
declined.push(
|
|
142
|
+
"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"
|
|
143
|
+
);
|
|
144
|
+
} else if (critical.changed) {
|
|
145
|
+
production["optimization"] = critical.value;
|
|
146
|
+
added.push(
|
|
147
|
+
`production optimization.styles.inlineCritical: ${amendment.inlineCritical}`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (amendment.initialBudget) {
|
|
152
|
+
const budgets = ensureInitialBudget(
|
|
153
|
+
production["budgets"],
|
|
154
|
+
amendment.initialBudget
|
|
155
|
+
);
|
|
156
|
+
if (budgets.changed) {
|
|
157
|
+
production["budgets"] = budgets.value;
|
|
158
|
+
added.push(
|
|
159
|
+
`production budget initial: ${amendment.initialBudget.warning} warning, ${amendment.initialBudget.error} error`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
configurations["production"] = production;
|
|
164
|
+
return { value: configurations, added, declined };
|
|
165
|
+
}
|
|
166
|
+
function ensureInitialBudget(budgets, wanted) {
|
|
167
|
+
const entries = Array.isArray(budgets) ? [...budgets] : [];
|
|
168
|
+
const index = entries.findIndex(
|
|
169
|
+
(entry) => asObject(entry)?.["type"] === "initial"
|
|
170
|
+
);
|
|
171
|
+
if (index === -1) {
|
|
172
|
+
return {
|
|
173
|
+
value: [
|
|
174
|
+
...entries,
|
|
175
|
+
{
|
|
176
|
+
type: "initial",
|
|
177
|
+
maximumWarning: wanted.warning,
|
|
178
|
+
maximumError: wanted.error
|
|
179
|
+
}
|
|
180
|
+
],
|
|
181
|
+
changed: true
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const current = asObject(entries[index]) ?? {};
|
|
185
|
+
if (!isBelow(current["maximumError"], wanted.error)) {
|
|
186
|
+
return { value: entries, changed: false };
|
|
187
|
+
}
|
|
188
|
+
entries[index] = {
|
|
189
|
+
...current,
|
|
190
|
+
maximumWarning: wanted.warning,
|
|
191
|
+
maximumError: wanted.error
|
|
192
|
+
};
|
|
193
|
+
return { value: entries, changed: true };
|
|
194
|
+
}
|
|
195
|
+
function isBelow(recorded, wanted) {
|
|
196
|
+
const left = toBytes(recorded);
|
|
197
|
+
const right = toBytes(wanted);
|
|
198
|
+
return left !== void 0 && right !== void 0 && left < right;
|
|
199
|
+
}
|
|
200
|
+
function toBytes(size) {
|
|
201
|
+
if (typeof size !== "string") {
|
|
202
|
+
return void 0;
|
|
203
|
+
}
|
|
204
|
+
const match = /^([\d.]+)\s*(b|kb|mb|gb)?$/i.exec(size.trim());
|
|
205
|
+
if (!match) {
|
|
206
|
+
return void 0;
|
|
207
|
+
}
|
|
208
|
+
const units = {
|
|
209
|
+
b: 1,
|
|
210
|
+
kb: 1e3,
|
|
211
|
+
mb: 1e3 * 1e3,
|
|
212
|
+
gb: 1e3 * 1e3 * 1e3
|
|
213
|
+
};
|
|
214
|
+
return Number(match[1]) * units[(match[2] ?? "b").toLowerCase()];
|
|
215
|
+
}
|
|
142
216
|
function ensureInlineCritical(optimization, inlineCritical) {
|
|
143
217
|
if (typeof optimization === "boolean") {
|
|
144
218
|
return { value: optimization, changed: false, declined: true };
|
|
@@ -276,10 +350,11 @@ function joinNamespaces(source, id) {
|
|
|
276
350
|
if (names.includes(`'${id}'`)) {
|
|
277
351
|
return { source, joined: true };
|
|
278
352
|
}
|
|
353
|
+
const namespaces = [...names, `'${id}'`].join(", ");
|
|
279
354
|
return {
|
|
280
355
|
source: source.replace(
|
|
281
356
|
NAMESPACES,
|
|
282
|
-
() => `provideTranslationNamespaces(${
|
|
357
|
+
() => `provideTranslationNamespaces(${namespaces})`
|
|
283
358
|
),
|
|
284
359
|
joined: true
|
|
285
360
|
};
|
|
@@ -346,15 +421,19 @@ function describeAmendment(amendment) {
|
|
|
346
421
|
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.`;
|
|
347
422
|
}
|
|
348
423
|
if (amendment.kind === "compose-plugin") {
|
|
349
|
-
const providers = (amendment.providers ?? []).map((provider) => provider.line.replace(/,$/, ""));
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
424
|
+
const providers = (amendment.providers ?? []).map((provider) => `${provider.line.replace(/,$/, "")}, `).join("");
|
|
425
|
+
const capabilities = amendment.capabilities.map((capability) => `'${capability}'`).join(", ");
|
|
426
|
+
const imports = (amendment.providers ?? []).flatMap((provider) => provider.from ?? []).map(
|
|
427
|
+
(imported) => ` import { ${imported.symbols.join(", ")} } from '${imported.path}'.`
|
|
428
|
+
).join("");
|
|
429
|
+
return `Register ${amendment.id} in the composition root: import { ${amendment.symbol} }, ${providers}provideTranslationNamespaces('${amendment.id}'), provideCapabilityGrants({ ${amendment.id}: [${capabilities}] }) and ...providePlugins(${amendment.symbol}).${imports} Without it none of its contributions appear.`;
|
|
353
430
|
}
|
|
354
431
|
return [
|
|
355
432
|
...amendment.styles.length > 0 ? [`name ${amendment.styles.join(", ")} in styles`] : [],
|
|
356
433
|
...amendment.assets.length > 0 ? [
|
|
357
|
-
`add assets for ${amendment.assets.map(
|
|
434
|
+
`add assets for ${amendment.assets.map(
|
|
435
|
+
(asset) => asset.output ? `${asset.input} served under ${asset.output}` : asset.input
|
|
436
|
+
).join(
|
|
358
437
|
", "
|
|
359
438
|
)} (the shell fetches its own strings at runtime, so without that glob every label in the chrome renders as its raw translation key)`
|
|
360
439
|
] : [],
|
|
@@ -363,7 +442,10 @@ function describeAmendment(amendment) {
|
|
|
363
442
|
] : [],
|
|
364
443
|
...amendment.inlineCritical === void 0 ? [] : [
|
|
365
444
|
`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)`
|
|
366
|
-
]
|
|
445
|
+
],
|
|
446
|
+
...amendment.initialBudget ? [
|
|
447
|
+
`raise the initial bundle budget to ${amendment.initialBudget.warning} warning and ${amendment.initialBudget.error} error in the production configuration (a fresh workspace carries one sized for an empty application, and the workbench alone spends almost all of it, so a release build fails)`
|
|
448
|
+
] : []
|
|
367
449
|
].map((step, index) => `${index + 1}. ${step}`).join(" ");
|
|
368
450
|
}
|
|
369
451
|
|
|
@@ -755,7 +837,7 @@ function pause(): Promise<void> {
|
|
|
755
837
|
}
|
|
756
838
|
|
|
757
839
|
// ../devkit/src/recipes/angular-weaver/agent-files.ts
|
|
758
|
-
var AG_UI_ADAPTER_VERSION = "0.9.
|
|
840
|
+
var AG_UI_ADAPTER_VERSION = "0.9.3";
|
|
759
841
|
var AG_UI_PROTOCOL_VERSION = "0.0.x";
|
|
760
842
|
function connectionFile(w) {
|
|
761
843
|
return `import { signal } from '@angular/core';
|
|
@@ -1122,109 +1204,8 @@ function toneHelper() {
|
|
|
1122
1204
|
].join("\n");
|
|
1123
1205
|
}
|
|
1124
1206
|
|
|
1125
|
-
// ../devkit/src/recipes/angular-weaver/
|
|
1126
|
-
var DEFAULT_MENU_SLOT = "content/tab/context";
|
|
1207
|
+
// ../devkit/src/recipes/angular-weaver/weaver-plugin.ts
|
|
1127
1208
|
var SURFACE_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><rect x="4" y="4" width="16" height="16" rx="3"/><path d="M8 9h8M8 13h8M8 17h5"/></svg>';
|
|
1128
|
-
function accessLiteral(spec) {
|
|
1129
|
-
if (spec === "authenticated") return "{ authenticated: true }";
|
|
1130
|
-
if (spec === "anonymous") return "{ authenticated: false }";
|
|
1131
|
-
if (spec.startsWith("role:")) {
|
|
1132
|
-
const role = spec.slice("role:".length);
|
|
1133
|
-
if (!role)
|
|
1134
|
-
throw new Error('Access "role:" needs a role name, e.g. "role:admin".');
|
|
1135
|
-
if (/['\\]/.test(role))
|
|
1136
|
-
throw new Error(
|
|
1137
|
-
`Access role "${role}" must not contain quotes or backslashes.`
|
|
1138
|
-
);
|
|
1139
|
-
return `{ anyRole: ['${role}'] }`;
|
|
1140
|
-
}
|
|
1141
|
-
throw new Error(
|
|
1142
|
-
`Unknown access "${spec}". Use "authenticated", "anonymous" or "role:<name>".`
|
|
1143
|
-
);
|
|
1144
|
-
}
|
|
1145
|
-
function resolveMenuSlot(menu) {
|
|
1146
|
-
if (menu === true) {
|
|
1147
|
-
return DEFAULT_MENU_SLOT;
|
|
1148
|
-
}
|
|
1149
|
-
return typeof menu === "string" && menu.length ? menu : void 0;
|
|
1150
|
-
}
|
|
1151
|
-
var PLATFORM_BOUND_CHORD_TOKENS = /* @__PURE__ */ new Set([
|
|
1152
|
-
"cmd",
|
|
1153
|
-
"command",
|
|
1154
|
-
"ctrl",
|
|
1155
|
-
"control",
|
|
1156
|
-
"meta"
|
|
1157
|
-
]);
|
|
1158
|
-
function assertPlatformNeutralChord(shortcut) {
|
|
1159
|
-
const tokens = shortcut.toLowerCase().split("+").map((token) => token.trim());
|
|
1160
|
-
const bound = tokens.find((token) => PLATFORM_BOUND_CHORD_TOKENS.has(token));
|
|
1161
|
-
if (bound) {
|
|
1162
|
-
throw new Error(
|
|
1163
|
-
`Shortcut "${shortcut}" binds the platform-specific "${bound}" key. Use the neutral 'mod' token (e.g. 'mod+shift+k') \u2014 the host renders it as \u2318 on macOS and Ctrl elsewhere.`
|
|
1164
|
-
);
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
function resolveFeatures(id, input) {
|
|
1168
|
-
const menuSlot = resolveMenuSlot(input?.menu);
|
|
1169
|
-
const barItem = Boolean(input?.barItem);
|
|
1170
|
-
const hasShortcut = Boolean(input?.shortcut?.trim());
|
|
1171
|
-
if (hasShortcut) {
|
|
1172
|
-
assertPlatformNeutralChord(input?.shortcut?.trim() ?? "");
|
|
1173
|
-
}
|
|
1174
|
-
const instanceable = Boolean(input?.instanceable);
|
|
1175
|
-
const container = Boolean(input?.container);
|
|
1176
|
-
if (instanceable && container) {
|
|
1177
|
-
throw new Error(
|
|
1178
|
-
'A surface cannot be both a container and instanceable: a container tab holds its own ":id" and is therefore routable, while named instances exist only for a docked, non-routable surface. Pick one.'
|
|
1179
|
-
);
|
|
1180
|
-
}
|
|
1181
|
-
const agent = Boolean(input?.agent);
|
|
1182
|
-
return {
|
|
1183
|
-
command: Boolean(input?.command) || menuSlot !== void 0 || barItem || hasShortcut || agent,
|
|
1184
|
-
menuSlot,
|
|
1185
|
-
settings: Boolean(input?.settings),
|
|
1186
|
-
access: input?.access ? accessLiteral(input.access) : void 0,
|
|
1187
|
-
shortcut: input?.shortcut?.trim() || `mod+shift+${id.charAt(0)}`,
|
|
1188
|
-
barItem,
|
|
1189
|
-
about: Boolean(input?.about),
|
|
1190
|
-
instanceable,
|
|
1191
|
-
container,
|
|
1192
|
-
agent,
|
|
1193
|
-
spec: input?.spec !== false
|
|
1194
|
-
};
|
|
1195
|
-
}
|
|
1196
|
-
function deriveCapabilities(features) {
|
|
1197
|
-
const set = /* @__PURE__ */ new Set(["contributions", "navigation"]);
|
|
1198
|
-
if (features.command) set.add("ui");
|
|
1199
|
-
if (features.about) {
|
|
1200
|
-
set.add("ui");
|
|
1201
|
-
set.add("host");
|
|
1202
|
-
}
|
|
1203
|
-
if (features.agent) {
|
|
1204
|
-
set.add("ui");
|
|
1205
|
-
set.add("automation");
|
|
1206
|
-
}
|
|
1207
|
-
return KNOWN_CAPABILITIES.filter((capability) => set.has(capability));
|
|
1208
|
-
}
|
|
1209
|
-
function resolveWeaverInput(input) {
|
|
1210
|
-
if (!isKebabId(input.id)) {
|
|
1211
|
-
throw new Error(
|
|
1212
|
-
`Weaver id must be kebab-case (e.g. "notes"); got "${input.id}".`
|
|
1213
|
-
);
|
|
1214
|
-
}
|
|
1215
|
-
const features = resolveFeatures(input.id, input.features);
|
|
1216
|
-
const capabilities = input.capabilities?.length ? [...input.capabilities] : deriveCapabilities(features);
|
|
1217
|
-
return {
|
|
1218
|
-
id: input.id,
|
|
1219
|
-
name: input.name?.trim() || toTitleCase(input.id),
|
|
1220
|
-
className: toPascalCase(input.id),
|
|
1221
|
-
propertyName: toCamelCase(input.id),
|
|
1222
|
-
capabilities,
|
|
1223
|
-
features,
|
|
1224
|
-
prefix: input.prefix?.trim() || "lw",
|
|
1225
|
-
importPath: input.importPath?.trim() || `@loomweaver/${input.id}-weaver`
|
|
1226
|
-
};
|
|
1227
|
-
}
|
|
1228
1209
|
function containerChildIds(w) {
|
|
1229
1210
|
return [`${w.id}.canvas`, `${w.id}.details`];
|
|
1230
1211
|
}
|
|
@@ -1361,7 +1342,7 @@ function settingsBlock(w) {
|
|
|
1361
1342
|
" });"
|
|
1362
1343
|
].join("\n");
|
|
1363
1344
|
}
|
|
1364
|
-
function
|
|
1345
|
+
function pluginImports(w) {
|
|
1365
1346
|
const imports = ["import { Plugin } from '@loomweaver/plugin-sdk';"];
|
|
1366
1347
|
if (w.features.container) {
|
|
1367
1348
|
imports.push(
|
|
@@ -1385,6 +1366,9 @@ function pluginFile(w) {
|
|
|
1385
1366
|
if (w.features.settings) {
|
|
1386
1367
|
imports.unshift("import { signal } from '@angular/core';");
|
|
1387
1368
|
}
|
|
1369
|
+
return imports;
|
|
1370
|
+
}
|
|
1371
|
+
function pluginConsts(w) {
|
|
1388
1372
|
const consts = [`const icon =
|
|
1389
1373
|
'${SURFACE_ICON}';`];
|
|
1390
1374
|
if (w.features.settings) {
|
|
@@ -1393,6 +1377,10 @@ function pluginFile(w) {
|
|
|
1393
1377
|
`const ${w.propertyName}Note = signal('');`
|
|
1394
1378
|
);
|
|
1395
1379
|
}
|
|
1380
|
+
if (w.features.command) consts.push(toneHelper());
|
|
1381
|
+
return consts;
|
|
1382
|
+
}
|
|
1383
|
+
function pluginBody(w) {
|
|
1396
1384
|
const body = [` ctx.contributeIcons({ '${w.id}': icon });`];
|
|
1397
1385
|
if (w.features.agent) {
|
|
1398
1386
|
body.push(
|
|
@@ -1407,14 +1395,17 @@ function pluginFile(w) {
|
|
|
1407
1395
|
if (w.features.menuSlot) body.push(menuBlock(w));
|
|
1408
1396
|
if (w.features.settings) body.push(settingsBlock(w));
|
|
1409
1397
|
if (w.features.agent) body.push(agentSurfaceBlock(w));
|
|
1398
|
+
return body;
|
|
1399
|
+
}
|
|
1400
|
+
function pluginFile(w) {
|
|
1401
|
+
const body = pluginBody(w);
|
|
1410
1402
|
const deactivate = w.features.agent ? `
|
|
1411
1403
|
deactivate() {
|
|
1412
1404
|
${w.propertyName}Agent.set(null);
|
|
1413
1405
|
},` : "";
|
|
1414
|
-
|
|
1415
|
-
return `${imports.join("\n")}
|
|
1406
|
+
return `${pluginImports(w).join("\n")}
|
|
1416
1407
|
|
|
1417
|
-
${
|
|
1408
|
+
${pluginConsts(w).join("\n\n")}
|
|
1418
1409
|
|
|
1419
1410
|
export const ${w.propertyName}Plugin: Plugin = {
|
|
1420
1411
|
manifest: {
|
|
@@ -1428,6 +1419,109 @@ ${body.join("\n")}
|
|
|
1428
1419
|
};
|
|
1429
1420
|
`;
|
|
1430
1421
|
}
|
|
1422
|
+
|
|
1423
|
+
// ../devkit/src/recipes/angular-weaver/recipe.ts
|
|
1424
|
+
var DEFAULT_MENU_SLOT = "content/tab/context";
|
|
1425
|
+
function accessLiteral(spec) {
|
|
1426
|
+
if (spec === "authenticated") return "{ authenticated: true }";
|
|
1427
|
+
if (spec === "anonymous") return "{ authenticated: false }";
|
|
1428
|
+
if (spec.startsWith("role:")) {
|
|
1429
|
+
const role = spec.slice("role:".length);
|
|
1430
|
+
if (!role)
|
|
1431
|
+
throw new Error('Access "role:" needs a role name, e.g. "role:admin".');
|
|
1432
|
+
if (/['\\]/.test(role))
|
|
1433
|
+
throw new Error(
|
|
1434
|
+
`Access role "${role}" must not contain quotes or backslashes.`
|
|
1435
|
+
);
|
|
1436
|
+
return `{ anyRole: ['${role}'] }`;
|
|
1437
|
+
}
|
|
1438
|
+
throw new Error(
|
|
1439
|
+
`Unknown access "${spec}". Use "authenticated", "anonymous" or "role:<name>".`
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
function resolveMenuSlot(menu) {
|
|
1443
|
+
if (menu === true) {
|
|
1444
|
+
return DEFAULT_MENU_SLOT;
|
|
1445
|
+
}
|
|
1446
|
+
return typeof menu === "string" && menu.length ? menu : void 0;
|
|
1447
|
+
}
|
|
1448
|
+
var PLATFORM_BOUND_CHORD_TOKENS = /* @__PURE__ */ new Set([
|
|
1449
|
+
"cmd",
|
|
1450
|
+
"command",
|
|
1451
|
+
"ctrl",
|
|
1452
|
+
"control",
|
|
1453
|
+
"meta"
|
|
1454
|
+
]);
|
|
1455
|
+
function assertPlatformNeutralChord(shortcut) {
|
|
1456
|
+
const tokens = shortcut.toLowerCase().split("+").map((token) => token.trim());
|
|
1457
|
+
const bound = tokens.find((token) => PLATFORM_BOUND_CHORD_TOKENS.has(token));
|
|
1458
|
+
if (bound) {
|
|
1459
|
+
throw new Error(
|
|
1460
|
+
`Shortcut "${shortcut}" binds the platform-specific "${bound}" key. Use the neutral 'mod' token (e.g. 'mod+shift+k') \u2014 the host renders it as \u2318 on macOS and Ctrl elsewhere.`
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
function resolveFeatures(id, input) {
|
|
1465
|
+
const menuSlot = resolveMenuSlot(input?.menu);
|
|
1466
|
+
const barItem = Boolean(input?.barItem);
|
|
1467
|
+
const hasShortcut = Boolean(input?.shortcut?.trim());
|
|
1468
|
+
if (hasShortcut) {
|
|
1469
|
+
assertPlatformNeutralChord(input?.shortcut?.trim() ?? "");
|
|
1470
|
+
}
|
|
1471
|
+
const instanceable = Boolean(input?.instanceable);
|
|
1472
|
+
const container = Boolean(input?.container);
|
|
1473
|
+
if (instanceable && container) {
|
|
1474
|
+
throw new Error(
|
|
1475
|
+
'A surface cannot be both a container and instanceable: a container tab holds its own ":id" and is therefore routable, while named instances exist only for a docked, non-routable surface. Pick one.'
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
const agent = Boolean(input?.agent);
|
|
1479
|
+
return {
|
|
1480
|
+
command: Boolean(input?.command) || menuSlot !== void 0 || barItem || hasShortcut || agent,
|
|
1481
|
+
menuSlot,
|
|
1482
|
+
settings: Boolean(input?.settings),
|
|
1483
|
+
access: input?.access ? accessLiteral(input.access) : void 0,
|
|
1484
|
+
shortcut: input?.shortcut?.trim() || `mod+shift+${id.charAt(0)}`,
|
|
1485
|
+
barItem,
|
|
1486
|
+
about: Boolean(input?.about),
|
|
1487
|
+
instanceable,
|
|
1488
|
+
container,
|
|
1489
|
+
agent,
|
|
1490
|
+
spec: input?.spec !== false
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
function deriveCapabilities(features) {
|
|
1494
|
+
const set = /* @__PURE__ */ new Set(["contributions", "navigation"]);
|
|
1495
|
+
if (features.command) set.add("ui");
|
|
1496
|
+
if (features.about) {
|
|
1497
|
+
set.add("ui");
|
|
1498
|
+
set.add("host");
|
|
1499
|
+
}
|
|
1500
|
+
if (features.agent) {
|
|
1501
|
+
set.add("ui");
|
|
1502
|
+
set.add("automation");
|
|
1503
|
+
}
|
|
1504
|
+
return KNOWN_CAPABILITIES.filter((capability) => set.has(capability));
|
|
1505
|
+
}
|
|
1506
|
+
function resolveWeaverInput(input) {
|
|
1507
|
+
if (!isKebabId(input.id)) {
|
|
1508
|
+
throw new Error(
|
|
1509
|
+
`Weaver id must be kebab-case (e.g. "notes"); got "${input.id}".`
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
const features = resolveFeatures(input.id, input.features);
|
|
1513
|
+
const capabilities = input.capabilities?.length ? [...input.capabilities] : deriveCapabilities(features);
|
|
1514
|
+
return {
|
|
1515
|
+
id: input.id,
|
|
1516
|
+
name: input.name?.trim() || toTitleCase(input.id),
|
|
1517
|
+
className: toPascalCase(input.id),
|
|
1518
|
+
propertyName: toCamelCase(input.id),
|
|
1519
|
+
capabilities,
|
|
1520
|
+
features,
|
|
1521
|
+
prefix: input.prefix?.trim() || "lw",
|
|
1522
|
+
importPath: input.importPath?.trim() || `@loomweaver/${input.id}-weaver`
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1431
1525
|
function aboutDialogFile(w) {
|
|
1432
1526
|
return `import { Component, inject } from '@angular/core';
|
|
1433
1527
|
import { DialogRef, PluginHost } from '@loomweaver/plugin-sdk';
|
|
@@ -1738,9 +1832,13 @@ var framePlugin = {
|
|
|
1738
1832
|
};
|
|
1739
1833
|
|
|
1740
1834
|
// ../devkit/src/recipes/platform-version.ts
|
|
1741
|
-
var PLATFORM_VERSION = "0.9.
|
|
1835
|
+
var PLATFORM_VERSION = "0.9.3";
|
|
1742
1836
|
|
|
1743
1837
|
// ../devkit/src/recipes/angular-distribution/amendments.ts
|
|
1838
|
+
var DISTRIBUTION_INITIAL_BUDGET = {
|
|
1839
|
+
warning: "1.5MB",
|
|
1840
|
+
error: "2MB"
|
|
1841
|
+
};
|
|
1744
1842
|
function distributionAmendments(d) {
|
|
1745
1843
|
return [
|
|
1746
1844
|
{
|
|
@@ -1774,7 +1872,8 @@ function distributionAmendments(d) {
|
|
|
1774
1872
|
}
|
|
1775
1873
|
],
|
|
1776
1874
|
serviceWorker: "ngsw-config.json",
|
|
1777
|
-
inlineCritical: false
|
|
1875
|
+
inlineCritical: false,
|
|
1876
|
+
initialBudget: DISTRIBUTION_INITIAL_BUDGET
|
|
1778
1877
|
}
|
|
1779
1878
|
];
|
|
1780
1879
|
}
|
|
@@ -1969,9 +2068,10 @@ function readme2(d) {
|
|
|
1969
2068
|
"the stylesheet with an **inline** `onload` handler that the policy blocks \u2014 the app then renders",
|
|
1970
2069
|
"completely unstyled, and only in production builds.",
|
|
1971
2070
|
"",
|
|
1972
|
-
"
|
|
1973
|
-
"
|
|
1974
|
-
"
|
|
2071
|
+
"The initial-bundle budget in your build target was raised for the same reason: the shell is a",
|
|
2072
|
+
"whole application chrome and spends most of Angular's default before your own code arrives, so",
|
|
2073
|
+
"a production build would have failed on a number meant for an empty app. It is still a budget,",
|
|
2074
|
+
"and it is yours to move.",
|
|
1975
2075
|
"",
|
|
1976
2076
|
"## Ship less than the whole workbench",
|
|
1977
2077
|
"",
|
|
@@ -2024,7 +2124,7 @@ function resolveDistributionInput(input) {
|
|
|
2024
2124
|
);
|
|
2025
2125
|
}
|
|
2026
2126
|
const directory = input.directory === void 0 ? `apps/${input.name}` : input.directory.trim();
|
|
2027
|
-
const depth = directory.split("/").filter(Boolean).length;
|
|
2127
|
+
const depth = normalizeProjectRoot(directory).split("/").filter(Boolean).length;
|
|
2028
2128
|
return {
|
|
2029
2129
|
name: input.name,
|
|
2030
2130
|
title: input.title?.trim() || toTitleCase(input.name),
|
|
@@ -2744,10 +2844,10 @@ function weaverAmendments(input, where) {
|
|
|
2744
2844
|
version: AG_UI_PROTOCOL_VERSION
|
|
2745
2845
|
}
|
|
2746
2846
|
] : [];
|
|
2747
|
-
|
|
2748
|
-
if (!directory) {
|
|
2847
|
+
if (where === void 0 || where === "") {
|
|
2749
2848
|
return packages;
|
|
2750
2849
|
}
|
|
2850
|
+
const directory = normalizeProjectRoot(where);
|
|
2751
2851
|
return [
|
|
2752
2852
|
...packages,
|
|
2753
2853
|
{
|
|
@@ -2756,19 +2856,19 @@ function weaverAmendments(input, where) {
|
|
|
2756
2856
|
assets: [
|
|
2757
2857
|
{
|
|
2758
2858
|
glob: "**/*.json",
|
|
2759
|
-
input:
|
|
2859
|
+
input: joinProjectPath(directory, "src/lib/i18n"),
|
|
2760
2860
|
from: "workspace",
|
|
2761
2861
|
output: `i18n/${w.id}`
|
|
2762
2862
|
}
|
|
2763
2863
|
]
|
|
2764
2864
|
},
|
|
2765
|
-
{ kind: "stylesheet-source", sourceRoot:
|
|
2865
|
+
{ kind: "stylesheet-source", sourceRoot: joinProjectPath(directory, "src") },
|
|
2766
2866
|
{
|
|
2767
2867
|
kind: "compose-plugin",
|
|
2768
2868
|
id: w.id,
|
|
2769
2869
|
symbol: `${w.propertyName}Plugin`,
|
|
2770
2870
|
capabilities: w.capabilities,
|
|
2771
|
-
sourceRoot:
|
|
2871
|
+
sourceRoot: joinProjectPath(directory, "src")
|
|
2772
2872
|
}
|
|
2773
2873
|
];
|
|
2774
2874
|
}
|
|
@@ -2776,10 +2876,10 @@ function weaverAmendments(input, where) {
|
|
|
2776
2876
|
// ../devkit/src/recipes/auth-source/recipe-amendments.ts
|
|
2777
2877
|
function authSourceAmendments(input, where) {
|
|
2778
2878
|
const a = resolveAuthSourceInput(input);
|
|
2779
|
-
|
|
2780
|
-
if (a.bare || !directory) {
|
|
2879
|
+
if (a.bare || where === void 0 || where === "") {
|
|
2781
2880
|
return [];
|
|
2782
2881
|
}
|
|
2882
|
+
const directory = normalizeProjectRoot(where);
|
|
2783
2883
|
return [
|
|
2784
2884
|
{
|
|
2785
2885
|
kind: "build-target",
|
|
@@ -2787,7 +2887,7 @@ function authSourceAmendments(input, where) {
|
|
|
2787
2887
|
assets: [
|
|
2788
2888
|
{
|
|
2789
2889
|
glob: "**/*.json",
|
|
2790
|
-
input:
|
|
2890
|
+
input: joinProjectPath(directory, "i18n"),
|
|
2791
2891
|
from: "workspace",
|
|
2792
2892
|
output: `i18n/${SESSION_PLUGIN_ID}`
|
|
2793
2893
|
}
|
|
@@ -3488,7 +3588,7 @@ function read(ts, file, call) {
|
|
|
3488
3588
|
return {
|
|
3489
3589
|
at: at2,
|
|
3490
3590
|
id: id.text,
|
|
3491
|
-
callable: callable
|
|
3591
|
+
callable: callable?.kind === ts.SyntaxKind.TrueKeyword,
|
|
3492
3592
|
described: properties.has("description"),
|
|
3493
3593
|
answers: properties.has("answers"),
|
|
3494
3594
|
returnsValue: returnsValue(ts, properties.get("run")),
|
|
@@ -3616,8 +3716,9 @@ function loadTypeScript(fromDir) {
|
|
|
3616
3716
|
}
|
|
3617
3717
|
|
|
3618
3718
|
// src/lib/run.ts
|
|
3619
|
-
import {
|
|
3620
|
-
import {
|
|
3719
|
+
import { spawnSync } from "node:child_process";
|
|
3720
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync5 } from "node:fs";
|
|
3721
|
+
import { join as join4 } from "node:path";
|
|
3621
3722
|
|
|
3622
3723
|
// src/lib/args.ts
|
|
3623
3724
|
var ArgError = class extends Error {
|
|
@@ -3963,10 +4064,7 @@ var Amender = class {
|
|
|
3963
4064
|
if (!/@import\s+['"]tailwindcss['"]/.test(css)) {
|
|
3964
4065
|
return;
|
|
3965
4066
|
}
|
|
3966
|
-
const source = posix.relative(
|
|
3967
|
-
this.displayName(dirname2(entry)),
|
|
3968
|
-
amendment.sourceRoot
|
|
3969
|
-
);
|
|
4067
|
+
const source = posix.relative(this.displayName(dirname2(entry)), amendment.sourceRoot) || ".";
|
|
3970
4068
|
const next = ensureStylesheetSource(css, source);
|
|
3971
4069
|
if (next === css) {
|
|
3972
4070
|
return;
|
|
@@ -4093,8 +4191,409 @@ function relativeImport(fromDir, sourceRoot) {
|
|
|
4093
4191
|
return path.startsWith(".") ? path : `./${path}`;
|
|
4094
4192
|
}
|
|
4095
4193
|
|
|
4194
|
+
// src/lib/init.ts
|
|
4195
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
|
|
4196
|
+
import { join as join3 } from "node:path";
|
|
4197
|
+
|
|
4198
|
+
// src/lib/init-plan.ts
|
|
4199
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
|
|
4200
|
+
import { basename as basename2, join as join2, resolve as resolve5 } from "node:path";
|
|
4201
|
+
|
|
4202
|
+
// src/lib/nx-applications.ts
|
|
4203
|
+
import { existsSync as existsSync3, readdirSync } from "node:fs";
|
|
4204
|
+
import { basename, join, relative as relative3, sep as sep3 } from "node:path";
|
|
4205
|
+
var SKIPPED = /* @__PURE__ */ new Set(["node_modules", "dist", "tmp", "coverage"]);
|
|
4206
|
+
function nxApplications(workspaceRoot) {
|
|
4207
|
+
const found = [];
|
|
4208
|
+
const walk = (folder) => {
|
|
4209
|
+
const project = join(folder, "project.json");
|
|
4210
|
+
if (existsSync3(project)) {
|
|
4211
|
+
const config = readJsonFile(project);
|
|
4212
|
+
const name = typeof config["name"] === "string" ? config["name"] : basename(folder);
|
|
4213
|
+
if (config["projectType"] === "application" && !name.endsWith("-e2e")) {
|
|
4214
|
+
found.push({
|
|
4215
|
+
name,
|
|
4216
|
+
root: relative3(workspaceRoot, folder).split(sep3).join("/")
|
|
4217
|
+
});
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
for (const entry of readdirSync(folder, { withFileTypes: true })) {
|
|
4221
|
+
if (!entry.isDirectory() || SKIPPED.has(entry.name) || entry.name.startsWith(".")) {
|
|
4222
|
+
continue;
|
|
4223
|
+
}
|
|
4224
|
+
walk(join(folder, entry.name));
|
|
4225
|
+
}
|
|
4226
|
+
};
|
|
4227
|
+
walk(workspaceRoot);
|
|
4228
|
+
return found.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
4229
|
+
}
|
|
4230
|
+
|
|
4231
|
+
// src/lib/package-manager.ts
|
|
4232
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
4233
|
+
import { resolve as resolve4 } from "node:path";
|
|
4234
|
+
var PACKAGE_MANAGERS = [
|
|
4235
|
+
"npm",
|
|
4236
|
+
"pnpm",
|
|
4237
|
+
"yarn",
|
|
4238
|
+
"bun"
|
|
4239
|
+
];
|
|
4240
|
+
var LOCKFILES = [
|
|
4241
|
+
["package-lock.json", "npm"],
|
|
4242
|
+
["npm-shrinkwrap.json", "npm"],
|
|
4243
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
4244
|
+
["yarn.lock", "yarn"],
|
|
4245
|
+
["bun.lock", "bun"],
|
|
4246
|
+
["bun.lockb", "bun"]
|
|
4247
|
+
];
|
|
4248
|
+
function detectPackageManager(root) {
|
|
4249
|
+
for (const [lockfile, manager] of LOCKFILES) {
|
|
4250
|
+
if (existsSync4(resolve4(root, lockfile))) {
|
|
4251
|
+
return { manager, lockfile };
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
return { manager: "npm" };
|
|
4255
|
+
}
|
|
4256
|
+
function packageManagerFrom(value) {
|
|
4257
|
+
if (value === void 0) {
|
|
4258
|
+
return void 0;
|
|
4259
|
+
}
|
|
4260
|
+
if (!PACKAGE_MANAGERS.includes(value)) {
|
|
4261
|
+
throw new ArgError(
|
|
4262
|
+
`Option --package-manager must be one of: ${PACKAGE_MANAGERS.join(", ")}.`
|
|
4263
|
+
);
|
|
4264
|
+
}
|
|
4265
|
+
return value;
|
|
4266
|
+
}
|
|
4267
|
+
function installCommand(manager, packages, dev) {
|
|
4268
|
+
const devFlag = manager === "bun" ? "-d" : "-D";
|
|
4269
|
+
const verb = manager === "npm" ? "install" : "add";
|
|
4270
|
+
return [manager, verb, ...dev ? [devFlag] : [], ...packages];
|
|
4271
|
+
}
|
|
4272
|
+
function execCommand(manager, argv) {
|
|
4273
|
+
if (manager === "npm") {
|
|
4274
|
+
return ["npx", ...argv];
|
|
4275
|
+
}
|
|
4276
|
+
if (manager === "pnpm") {
|
|
4277
|
+
return ["pnpm", "exec", ...argv];
|
|
4278
|
+
}
|
|
4279
|
+
if (manager === "bun") {
|
|
4280
|
+
return ["bunx", ...argv];
|
|
4281
|
+
}
|
|
4282
|
+
return ["yarn", ...argv];
|
|
4283
|
+
}
|
|
4284
|
+
function runScriptCommand(manager, script) {
|
|
4285
|
+
return `${manager} ${script}`;
|
|
4286
|
+
}
|
|
4287
|
+
|
|
4288
|
+
// src/lib/init-plan.ts
|
|
4289
|
+
var RUNTIME_PACKAGES = [
|
|
4290
|
+
"@loomweaver/shell",
|
|
4291
|
+
"@loomweaver/plugin-sdk",
|
|
4292
|
+
"@loomweaver/frame-kit",
|
|
4293
|
+
"@angular/cdk",
|
|
4294
|
+
"@jsverse/transloco",
|
|
4295
|
+
"@ng-icons/heroicons"
|
|
4296
|
+
];
|
|
4297
|
+
var STYLE_PACKAGES = [
|
|
4298
|
+
"tailwindcss",
|
|
4299
|
+
"@tailwindcss/postcss",
|
|
4300
|
+
"@tailwindcss/typography"
|
|
4301
|
+
];
|
|
4302
|
+
var SERVICE_WORKER = "@angular/service-worker";
|
|
4303
|
+
var NX_COLLECTION = "@loomweaver/devkit";
|
|
4304
|
+
function planInit(args, deps) {
|
|
4305
|
+
const found = findWorkspace(deps.cwd);
|
|
4306
|
+
if (!found?.kind) {
|
|
4307
|
+
throw new WorkspaceError(
|
|
4308
|
+
`No Angular application or Nx workspace here: expected an angular.json or an nx.json at or above ${resolve5(deps.cwd)}. Create one first ("ng new my-studio --style=css --ssr=false", or "npx create-nx-workspace") and run init inside it.`
|
|
4309
|
+
);
|
|
4310
|
+
}
|
|
4311
|
+
const workspace = found;
|
|
4312
|
+
const manifest2 = readManifest(workspace.root);
|
|
4313
|
+
const override = packageManagerFrom(stringFlag(args, "package-manager"));
|
|
4314
|
+
const detected = detectPackageManager(workspace.root);
|
|
4315
|
+
const app = workspace.kind === "nx" ? chooseApplication(workspace.root, stringFlag(args, "app")) : void 0;
|
|
4316
|
+
const name = app?.name ?? kebab(manifest2.name ?? basename2(workspace.root));
|
|
4317
|
+
const styles = stringFlag(args, "styles") ?? "tailwind";
|
|
4318
|
+
const declared = { ...manifest2.dependencies, ...manifest2.devDependencies };
|
|
4319
|
+
const missing = (names) => names.filter((candidate) => !Object.hasOwn(declared, candidate));
|
|
4320
|
+
return {
|
|
4321
|
+
workspace,
|
|
4322
|
+
manager: override ?? detected.manager,
|
|
4323
|
+
lockfile: override ? void 0 : detected.lockfile,
|
|
4324
|
+
name,
|
|
4325
|
+
title: stringFlag(args, "title") ?? titleCase(name),
|
|
4326
|
+
styles,
|
|
4327
|
+
weaver: weaverId(args),
|
|
4328
|
+
runtime: [
|
|
4329
|
+
...missing(RUNTIME_PACKAGES),
|
|
4330
|
+
...Object.hasOwn(declared, SERVICE_WORKER) ? [] : [serviceWorkerSpec(workspace.root, manifest2)]
|
|
4331
|
+
],
|
|
4332
|
+
dev: [
|
|
4333
|
+
...styles === "tailwind" ? missing(STYLE_PACKAGES) : [],
|
|
4334
|
+
...workspace.kind === "nx" && !Object.hasOwn(declared, NX_COLLECTION) ? [collectionSpec(deps.version)] : []
|
|
4335
|
+
],
|
|
4336
|
+
app,
|
|
4337
|
+
dryRun: boolFlag(args, "dry-run") === true
|
|
4338
|
+
};
|
|
4339
|
+
}
|
|
4340
|
+
function chooseApplication(root, wanted) {
|
|
4341
|
+
const apps = nxApplications(root);
|
|
4342
|
+
const names = apps.map((candidate) => candidate.name).join(", ");
|
|
4343
|
+
if (wanted) {
|
|
4344
|
+
const app = apps.find((candidate) => candidate.name === wanted);
|
|
4345
|
+
if (!app) {
|
|
4346
|
+
throw new WorkspaceError(
|
|
4347
|
+
`No application named "${wanted}" in this workspace${apps.length > 0 ? `; there are: ${names}` : ""}.`
|
|
4348
|
+
);
|
|
4349
|
+
}
|
|
4350
|
+
return app;
|
|
4351
|
+
}
|
|
4352
|
+
if (apps.length === 1) {
|
|
4353
|
+
return apps[0];
|
|
4354
|
+
}
|
|
4355
|
+
if (apps.length === 0) {
|
|
4356
|
+
throw new WorkspaceError(
|
|
4357
|
+
'This Nx workspace has no application (no project.json with "projectType": "application"). Generate one first, then run init with --app <name>.'
|
|
4358
|
+
);
|
|
4359
|
+
}
|
|
4360
|
+
throw new WorkspaceError(
|
|
4361
|
+
`This Nx workspace has ${apps.length} applications, so none was chosen: ${names}. Name one with --app.`
|
|
4362
|
+
);
|
|
4363
|
+
}
|
|
4364
|
+
function weaverId(args) {
|
|
4365
|
+
const value = args.flags["weaver"];
|
|
4366
|
+
if (value === false) {
|
|
4367
|
+
return void 0;
|
|
4368
|
+
}
|
|
4369
|
+
if (value === true) {
|
|
4370
|
+
throw new ArgError(
|
|
4371
|
+
"Option --weaver needs a plugin id, or pass --no-weaver."
|
|
4372
|
+
);
|
|
4373
|
+
}
|
|
4374
|
+
return value ?? "notes";
|
|
4375
|
+
}
|
|
4376
|
+
function readManifest(root) {
|
|
4377
|
+
const file = join2(root, "package.json");
|
|
4378
|
+
return existsSync5(file) ? readJsonFile(file) : {};
|
|
4379
|
+
}
|
|
4380
|
+
function serviceWorkerSpec(root, manifest2) {
|
|
4381
|
+
const installed = join2(root, "node_modules/@angular/core/package.json");
|
|
4382
|
+
if (existsSync5(installed)) {
|
|
4383
|
+
const { version } = JSON.parse(readFileSync3(installed, "utf8"));
|
|
4384
|
+
return `${SERVICE_WORKER}@${version}`;
|
|
4385
|
+
}
|
|
4386
|
+
const declared = manifest2.dependencies?.["@angular/core"];
|
|
4387
|
+
return declared ? `${SERVICE_WORKER}@${declared}` : SERVICE_WORKER;
|
|
4388
|
+
}
|
|
4389
|
+
function collectionSpec(version) {
|
|
4390
|
+
return version === "0.0.0" ? `${NX_COLLECTION}@latest` : `${NX_COLLECTION}@${version}`;
|
|
4391
|
+
}
|
|
4392
|
+
function kebab(value) {
|
|
4393
|
+
const unscoped = value.includes("/") ? value.slice(value.lastIndexOf("/") + 1) : value;
|
|
4394
|
+
return unscoped.replaceAll(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).join("-");
|
|
4395
|
+
}
|
|
4396
|
+
function titleCase(name) {
|
|
4397
|
+
return name.split("-").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
4398
|
+
}
|
|
4399
|
+
|
|
4400
|
+
// src/lib/init.ts
|
|
4401
|
+
var INIT_FLAGS = [
|
|
4402
|
+
"title",
|
|
4403
|
+
"styles",
|
|
4404
|
+
"weaver",
|
|
4405
|
+
"app",
|
|
4406
|
+
"package-manager",
|
|
4407
|
+
"dry-run"
|
|
4408
|
+
];
|
|
4409
|
+
function init(args, io, deps) {
|
|
4410
|
+
rejectUnknownFlags(args, INIT_FLAGS);
|
|
4411
|
+
const plan = planInit(args, deps);
|
|
4412
|
+
report(io, plan);
|
|
4413
|
+
let didSomething = false;
|
|
4414
|
+
for (const step of stepsFor(plan)) {
|
|
4415
|
+
if (step.done) {
|
|
4416
|
+
io.out(`${step.label}: already there.`);
|
|
4417
|
+
continue;
|
|
4418
|
+
}
|
|
4419
|
+
didSomething = true;
|
|
4420
|
+
const code = perform(step, plan, io, deps);
|
|
4421
|
+
if (code !== 0) {
|
|
4422
|
+
io.err(`${step.label} failed; nothing after it was run.`);
|
|
4423
|
+
return code;
|
|
4424
|
+
}
|
|
4425
|
+
}
|
|
4426
|
+
if (!plan.weaver) {
|
|
4427
|
+
io.out(
|
|
4428
|
+
"First weaver: skipped as asked; the rail stays empty until a plugin is composed in."
|
|
4429
|
+
);
|
|
4430
|
+
}
|
|
4431
|
+
if (!didSomething) {
|
|
4432
|
+
io.out(
|
|
4433
|
+
"Nothing left to do: the packages, the distribution and the weaver are all in place."
|
|
4434
|
+
);
|
|
4435
|
+
}
|
|
4436
|
+
io.out(
|
|
4437
|
+
`${plan.dryRun ? "Then serve" : "Serve"} it with: ${serveCommand(plan)}`
|
|
4438
|
+
);
|
|
4439
|
+
return 0;
|
|
4440
|
+
}
|
|
4441
|
+
function perform(step, plan, io, deps) {
|
|
4442
|
+
if (step.command) {
|
|
4443
|
+
if (plan.dryRun) {
|
|
4444
|
+
io.out(`Would run: ${step.command.join(" ")}`);
|
|
4445
|
+
return 0;
|
|
4446
|
+
}
|
|
4447
|
+
io.out(`${step.label}: ${step.describe}`);
|
|
4448
|
+
deps.exec(step.command, plan.workspace.root);
|
|
4449
|
+
return 0;
|
|
4450
|
+
}
|
|
4451
|
+
if (step.argv) {
|
|
4452
|
+
io.out(
|
|
4453
|
+
plan.dryRun ? `Would ${step.label.toLowerCase()}: ${step.describe}` : `${step.label}: ${step.describe}`
|
|
4454
|
+
);
|
|
4455
|
+
if (plan.dryRun && step.after) {
|
|
4456
|
+
io.out(` (${step.after})`);
|
|
4457
|
+
}
|
|
4458
|
+
return deps.run(plan.dryRun ? [...step.argv, "--dry-run"] : step.argv);
|
|
4459
|
+
}
|
|
4460
|
+
return 0;
|
|
4461
|
+
}
|
|
4462
|
+
function report(io, plan) {
|
|
4463
|
+
const where = plan.workspace.kind === "nx" ? `Nx workspace, application ${plan.app?.name}` : "Angular CLI application";
|
|
4464
|
+
io.out(
|
|
4465
|
+
`${plan.dryRun ? "Would take" : "Taking"} ${plan.workspace.root} to a product: ${where}, packages with ${managerLabel(plan)}.`
|
|
4466
|
+
);
|
|
4467
|
+
}
|
|
4468
|
+
function managerLabel(plan) {
|
|
4469
|
+
if (plan.lockfile) {
|
|
4470
|
+
return `${plan.manager} (from ${plan.lockfile})`;
|
|
4471
|
+
}
|
|
4472
|
+
if (plan.manager === "npm") {
|
|
4473
|
+
return "npm (no lockfile found, so npm)";
|
|
4474
|
+
}
|
|
4475
|
+
return plan.manager;
|
|
4476
|
+
}
|
|
4477
|
+
function stepsFor(plan) {
|
|
4478
|
+
return plan.workspace.kind === "nx" ? nxSteps(plan) : angularSteps(plan);
|
|
4479
|
+
}
|
|
4480
|
+
function angularSteps(plan) {
|
|
4481
|
+
const root = plan.workspace.root;
|
|
4482
|
+
const steps = [
|
|
4483
|
+
...installSteps(plan),
|
|
4484
|
+
{
|
|
4485
|
+
label: "Distribution",
|
|
4486
|
+
describe: `scaffold the composition root for "${plan.title}" (${plan.styles}) and wire the build`,
|
|
4487
|
+
done: composesShell(join3(root, "src/app/app.config.ts")),
|
|
4488
|
+
argv: [
|
|
4489
|
+
"distribution",
|
|
4490
|
+
"--name",
|
|
4491
|
+
plan.name,
|
|
4492
|
+
"--title",
|
|
4493
|
+
plan.title,
|
|
4494
|
+
"--styles",
|
|
4495
|
+
plan.styles,
|
|
4496
|
+
"--out",
|
|
4497
|
+
root,
|
|
4498
|
+
"--force"
|
|
4499
|
+
]
|
|
4500
|
+
}
|
|
4501
|
+
];
|
|
4502
|
+
if (plan.weaver) {
|
|
4503
|
+
steps.push({
|
|
4504
|
+
label: "First weaver",
|
|
4505
|
+
describe: weaverDescription(plan.weaver),
|
|
4506
|
+
done: existsSync6(join3(root, "src", plan.weaver, "src/index.ts")),
|
|
4507
|
+
after: "planned against the composition root as it is now; once the distribution step has run, the weaver composes into it",
|
|
4508
|
+
argv: [
|
|
4509
|
+
"weaver",
|
|
4510
|
+
"--id",
|
|
4511
|
+
plan.weaver,
|
|
4512
|
+
"--command",
|
|
4513
|
+
"--out",
|
|
4514
|
+
join3(root, "src", plan.weaver)
|
|
4515
|
+
]
|
|
4516
|
+
});
|
|
4517
|
+
}
|
|
4518
|
+
return steps;
|
|
4519
|
+
}
|
|
4520
|
+
function nxSteps(plan) {
|
|
4521
|
+
const root = plan.workspace.root;
|
|
4522
|
+
const app = plan.app;
|
|
4523
|
+
if (!app) {
|
|
4524
|
+
return [];
|
|
4525
|
+
}
|
|
4526
|
+
const generator = (name, argv) => execCommand(plan.manager, ["nx", "g", `${NX_COLLECTION}:${name}`, ...argv]);
|
|
4527
|
+
const steps = [
|
|
4528
|
+
...installSteps(plan),
|
|
4529
|
+
{
|
|
4530
|
+
label: "Distribution",
|
|
4531
|
+
describe: `nx g ${NX_COLLECTION}:distribution over ${app.name} for "${plan.title}" (${plan.styles})`,
|
|
4532
|
+
done: composesShell(join3(root, app.root, "src/app/app.config.ts")),
|
|
4533
|
+
command: generator("distribution", [
|
|
4534
|
+
"--name",
|
|
4535
|
+
app.name,
|
|
4536
|
+
"--directory",
|
|
4537
|
+
app.root,
|
|
4538
|
+
"--title",
|
|
4539
|
+
plan.title,
|
|
4540
|
+
"--styles",
|
|
4541
|
+
plan.styles,
|
|
4542
|
+
"--force"
|
|
4543
|
+
])
|
|
4544
|
+
}
|
|
4545
|
+
];
|
|
4546
|
+
if (plan.weaver) {
|
|
4547
|
+
steps.push({
|
|
4548
|
+
label: "First weaver",
|
|
4549
|
+
describe: weaverDescription(plan.weaver),
|
|
4550
|
+
done: existsSync6(join3(root, "libs", `${plan.weaver}-weaver`)),
|
|
4551
|
+
command: generator("weaver", [
|
|
4552
|
+
"--id",
|
|
4553
|
+
plan.weaver,
|
|
4554
|
+
"--command",
|
|
4555
|
+
"--app",
|
|
4556
|
+
app.name
|
|
4557
|
+
])
|
|
4558
|
+
});
|
|
4559
|
+
}
|
|
4560
|
+
return steps;
|
|
4561
|
+
}
|
|
4562
|
+
function installSteps(plan) {
|
|
4563
|
+
return [
|
|
4564
|
+
{
|
|
4565
|
+
label: "Install",
|
|
4566
|
+
describe: plan.runtime.join(" "),
|
|
4567
|
+
done: plan.runtime.length === 0,
|
|
4568
|
+
command: installCommand(plan.manager, plan.runtime, false)
|
|
4569
|
+
},
|
|
4570
|
+
{
|
|
4571
|
+
label: "Install for development",
|
|
4572
|
+
describe: plan.dev.join(" "),
|
|
4573
|
+
done: plan.dev.length === 0,
|
|
4574
|
+
command: installCommand(plan.manager, plan.dev, true)
|
|
4575
|
+
}
|
|
4576
|
+
];
|
|
4577
|
+
}
|
|
4578
|
+
function weaverDescription(id) {
|
|
4579
|
+
return `"${id}" with a command on mod+shift+${id.charAt(0)}`;
|
|
4580
|
+
}
|
|
4581
|
+
function serveCommand(plan) {
|
|
4582
|
+
if (plan.workspace.kind === "nx") {
|
|
4583
|
+
return execCommand(plan.manager, [
|
|
4584
|
+
"nx",
|
|
4585
|
+
"serve",
|
|
4586
|
+
plan.app?.name ?? ""
|
|
4587
|
+
]).join(" ");
|
|
4588
|
+
}
|
|
4589
|
+
return runScriptCommand(plan.manager, "start");
|
|
4590
|
+
}
|
|
4591
|
+
function composesShell(file) {
|
|
4592
|
+
return existsSync6(file) && readFileSync4(file, "utf8").includes("provideShell(");
|
|
4593
|
+
}
|
|
4594
|
+
|
|
4096
4595
|
// src/lib/scaffold.ts
|
|
4097
|
-
import { relative as
|
|
4596
|
+
import { relative as relative4, resolve as resolve6 } from "node:path";
|
|
4098
4597
|
function findScaffold2(name) {
|
|
4099
4598
|
const scaffold2 = findScaffold(name);
|
|
4100
4599
|
if (!scaffold2) {
|
|
@@ -4111,16 +4610,16 @@ function allowedFlagsFor(scaffold2) {
|
|
|
4111
4610
|
]);
|
|
4112
4611
|
}
|
|
4113
4612
|
function readFlag(args, option) {
|
|
4114
|
-
const
|
|
4115
|
-
const value = args.flags[
|
|
4613
|
+
const kebab2 = kebabCase(option.name);
|
|
4614
|
+
const value = args.flags[kebab2] ?? args.flags[option.name];
|
|
4116
4615
|
if (value === void 0) {
|
|
4117
4616
|
return void 0;
|
|
4118
4617
|
}
|
|
4119
4618
|
if (option.type === "string" && typeof value !== "string") {
|
|
4120
|
-
throw new ArgError(`Option --${
|
|
4619
|
+
throw new ArgError(`Option --${kebab2} needs a value.`);
|
|
4121
4620
|
}
|
|
4122
4621
|
if (option.type === "boolean" && typeof value !== "boolean") {
|
|
4123
|
-
throw new ArgError(`Option --${
|
|
4622
|
+
throw new ArgError(`Option --${kebab2} does not take a value.`);
|
|
4124
4623
|
}
|
|
4125
4624
|
return value;
|
|
4126
4625
|
}
|
|
@@ -4130,9 +4629,7 @@ function valuesFor(scaffold2, args) {
|
|
|
4130
4629
|
const value = readFlag(args, option);
|
|
4131
4630
|
if (value === void 0) {
|
|
4132
4631
|
if (option.required) {
|
|
4133
|
-
throw new ArgError(
|
|
4134
|
-
`Option --${kebabCase(option.name)} is required.`
|
|
4135
|
-
);
|
|
4632
|
+
throw new ArgError(`Option --${kebabCase(option.name)} is required.`);
|
|
4136
4633
|
}
|
|
4137
4634
|
continue;
|
|
4138
4635
|
}
|
|
@@ -4146,8 +4643,13 @@ function valuesFor(scaffold2, args) {
|
|
|
4146
4643
|
return values;
|
|
4147
4644
|
}
|
|
4148
4645
|
function directoryFromOut(out) {
|
|
4149
|
-
const
|
|
4150
|
-
|
|
4646
|
+
const target = resolve6(out ?? ".");
|
|
4647
|
+
const base = findWorkspace(target)?.root ?? process.cwd();
|
|
4648
|
+
const below = relative4(base, target);
|
|
4649
|
+
if (below.startsWith("..")) {
|
|
4650
|
+
return "";
|
|
4651
|
+
}
|
|
4652
|
+
return below || ".";
|
|
4151
4653
|
}
|
|
4152
4654
|
function scaffoldValues(scaffold2, args) {
|
|
4153
4655
|
const values = valuesFor(scaffold2, args);
|
|
@@ -4178,16 +4680,16 @@ import {
|
|
|
4178
4680
|
rmSync,
|
|
4179
4681
|
writeFileSync as writeFileSync2
|
|
4180
4682
|
} from "node:fs";
|
|
4181
|
-
import { dirname as dirname3, isAbsolute, relative as
|
|
4683
|
+
import { dirname as dirname3, isAbsolute, relative as relative5, resolve as resolve7 } from "node:path";
|
|
4182
4684
|
var WriteError = class extends Error {
|
|
4183
4685
|
};
|
|
4184
4686
|
function planWrite(files, root) {
|
|
4185
|
-
const absoluteRoot =
|
|
4687
|
+
const absoluteRoot = resolve7(root);
|
|
4186
4688
|
const planned = [];
|
|
4187
4689
|
const conflicts = [];
|
|
4188
4690
|
for (const path of Object.keys(files).toSorted((a, b) => a.localeCompare(b))) {
|
|
4189
|
-
const absolute =
|
|
4190
|
-
const inside =
|
|
4691
|
+
const absolute = resolve7(absoluteRoot, path);
|
|
4692
|
+
const inside = relative5(absoluteRoot, absolute);
|
|
4191
4693
|
if (inside.startsWith("..") || isAbsolute(inside)) {
|
|
4192
4694
|
throw new WriteError(`Refusing to write outside the target directory: ${path}`);
|
|
4193
4695
|
}
|
|
@@ -4215,7 +4717,7 @@ function entryExists(absolute) {
|
|
|
4215
4717
|
}
|
|
4216
4718
|
}
|
|
4217
4719
|
function assertResolvesInsideRoot(root, path, absolute) {
|
|
4218
|
-
const inside =
|
|
4720
|
+
const inside = relative5(realpathSync(root), realpathSync(dirname3(absolute)));
|
|
4219
4721
|
if (inside.startsWith("..") || isAbsolute(inside)) {
|
|
4220
4722
|
throw new WriteError(
|
|
4221
4723
|
`Refusing to write through a link that leaves the target directory: ${path}`
|
|
@@ -4229,7 +4731,7 @@ function replaceSymlinkEntry(absolute) {
|
|
|
4229
4731
|
}
|
|
4230
4732
|
|
|
4231
4733
|
// src/lib/run.ts
|
|
4232
|
-
var VERSION = "0.9.
|
|
4734
|
+
var VERSION = "0.9.3";
|
|
4233
4735
|
function help() {
|
|
4234
4736
|
const commands = SCAFFOLDS.map((s) => ` ${s.name.padEnd(16)}${s.summary}`);
|
|
4235
4737
|
return [
|
|
@@ -4241,6 +4743,10 @@ function help() {
|
|
|
4241
4743
|
...commands,
|
|
4242
4744
|
"",
|
|
4243
4745
|
"Other commands:",
|
|
4746
|
+
" init take the Angular application or Nx workspace you are in to a running product:",
|
|
4747
|
+
" install the platform, scaffold the distribution and a first weaver, say what to serve",
|
|
4748
|
+
" [--title <t>] [--styles tailwind|precompiled] [--weaver <id>|--no-weaver] [--app <nx app>]",
|
|
4749
|
+
" [--package-manager npm|pnpm|yarn|bun] [--dry-run]",
|
|
4244
4750
|
" list print every scaffold with its options",
|
|
4245
4751
|
" validate-manifest --id <id> [--name <name>] [--capabilities <a,b>]",
|
|
4246
4752
|
" validate-i18n --dir <dir> check <lang>.json bundles for key parity",
|
|
@@ -4299,15 +4805,17 @@ function validateManifestCommand(args, io) {
|
|
|
4299
4805
|
}
|
|
4300
4806
|
function readBundles(dir) {
|
|
4301
4807
|
const bundles = {};
|
|
4302
|
-
for (const entry of
|
|
4808
|
+
for (const entry of readdirSync2(dir)) {
|
|
4303
4809
|
if (!entry.endsWith(".json")) {
|
|
4304
4810
|
continue;
|
|
4305
4811
|
}
|
|
4306
4812
|
const language = entry.slice(0, -".json".length);
|
|
4307
4813
|
try {
|
|
4308
|
-
bundles[language] = JSON.parse(
|
|
4814
|
+
bundles[language] = JSON.parse(readFileSync5(join4(dir, entry), "utf8"));
|
|
4309
4815
|
} catch (error) {
|
|
4310
|
-
throw new ArgError(
|
|
4816
|
+
throw new ArgError(
|
|
4817
|
+
`${entry} is not valid JSON: ${error.message}`
|
|
4818
|
+
);
|
|
4311
4819
|
}
|
|
4312
4820
|
}
|
|
4313
4821
|
if (Object.keys(bundles).length === 0) {
|
|
@@ -4326,15 +4834,15 @@ function validateI18nCommand(args, io) {
|
|
|
4326
4834
|
function readSources(dir) {
|
|
4327
4835
|
const sources = [];
|
|
4328
4836
|
const walk = (folder) => {
|
|
4329
|
-
for (const entry of
|
|
4837
|
+
for (const entry of readdirSync2(folder, { withFileTypes: true })) {
|
|
4330
4838
|
if (entry.name === "node_modules" || entry.name === "dist" || entry.name.startsWith(".")) {
|
|
4331
4839
|
continue;
|
|
4332
4840
|
}
|
|
4333
|
-
const path =
|
|
4841
|
+
const path = join4(folder, entry.name);
|
|
4334
4842
|
if (entry.isDirectory()) {
|
|
4335
4843
|
walk(path);
|
|
4336
4844
|
} else if (entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts") && !entry.name.endsWith(".spec.ts")) {
|
|
4337
|
-
sources.push({ path, text:
|
|
4845
|
+
sources.push({ path, text: readFileSync5(path, "utf8") });
|
|
4338
4846
|
}
|
|
4339
4847
|
}
|
|
4340
4848
|
};
|
|
@@ -4357,19 +4865,25 @@ function validateCommandsCommand(args, io) {
|
|
|
4357
4865
|
`typescript is not installed where ${dir} can reach it; the check reads sources with the TypeScript compiler API, so run it inside the project.`
|
|
4358
4866
|
);
|
|
4359
4867
|
}
|
|
4360
|
-
return reportFindings(
|
|
4868
|
+
return reportFindings(
|
|
4869
|
+
io,
|
|
4870
|
+
validateCommands(readSources(dir), ts),
|
|
4871
|
+
boolFlag(args, "strict") === true
|
|
4872
|
+
);
|
|
4361
4873
|
}
|
|
4362
4874
|
function readCatalog(file) {
|
|
4363
4875
|
let raw;
|
|
4364
4876
|
try {
|
|
4365
|
-
raw =
|
|
4877
|
+
raw = readFileSync5(file, "utf8");
|
|
4366
4878
|
} catch (error) {
|
|
4367
4879
|
throw new ArgError(`Cannot read ${file}: ${error.message}`);
|
|
4368
4880
|
}
|
|
4369
4881
|
try {
|
|
4370
4882
|
return JSON.parse(raw);
|
|
4371
4883
|
} catch (error) {
|
|
4372
|
-
throw new ArgError(
|
|
4884
|
+
throw new ArgError(
|
|
4885
|
+
`${file} is not valid JSON: ${error.message}`
|
|
4886
|
+
);
|
|
4373
4887
|
}
|
|
4374
4888
|
}
|
|
4375
4889
|
function validateCatalogCommand(args, io) {
|
|
@@ -4446,7 +4960,26 @@ function scaffold(args, io) {
|
|
|
4446
4960
|
reportAmendments(io, amend, true);
|
|
4447
4961
|
return 0;
|
|
4448
4962
|
}
|
|
4449
|
-
|
|
4963
|
+
var ExecError = class extends Error {
|
|
4964
|
+
};
|
|
4965
|
+
function execInherit(command, cwd) {
|
|
4966
|
+
const result = spawnSync(command[0], command.slice(1), {
|
|
4967
|
+
cwd,
|
|
4968
|
+
stdio: "inherit",
|
|
4969
|
+
shell: process.platform === "win32"
|
|
4970
|
+
});
|
|
4971
|
+
if (result.error) {
|
|
4972
|
+
throw new ExecError(
|
|
4973
|
+
`Could not run "${command.join(" ")}": ${result.error.message}`
|
|
4974
|
+
);
|
|
4975
|
+
}
|
|
4976
|
+
if (result.status !== 0) {
|
|
4977
|
+
throw new ExecError(
|
|
4978
|
+
`"${command.join(" ")}" exited with ${result.status ?? "a signal"}.`
|
|
4979
|
+
);
|
|
4980
|
+
}
|
|
4981
|
+
}
|
|
4982
|
+
function run(argv, io, deps) {
|
|
4450
4983
|
let args;
|
|
4451
4984
|
try {
|
|
4452
4985
|
args = parseArgs(argv);
|
|
@@ -4467,6 +5000,14 @@ function run(argv, io) {
|
|
|
4467
5000
|
return 1;
|
|
4468
5001
|
}
|
|
4469
5002
|
try {
|
|
5003
|
+
if (args.command === "init") {
|
|
5004
|
+
return init(args, io, {
|
|
5005
|
+
cwd: deps?.cwd ?? process.cwd(),
|
|
5006
|
+
exec: deps?.exec ?? execInherit,
|
|
5007
|
+
run: deps?.run ?? ((inner) => run(inner, io, deps)),
|
|
5008
|
+
version: deps?.version ?? VERSION
|
|
5009
|
+
});
|
|
5010
|
+
}
|
|
4470
5011
|
if (args.command === "list") {
|
|
4471
5012
|
return list(args, io);
|
|
4472
5013
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loomweaver/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"description": "LoomWeaver: open-source plugin platform for Angular workbenches. The scaffolding CLI: generates weavers, distributions and integrations into any project, with no Nx workspace and no checkout required.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"loomweaver",
|