@loomweaver/cli 0.16.0 → 0.17.1
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/README.md +3 -3
- package/dist/main.mjs +809 -311
- package/package.json +1 -1
package/dist/main.mjs
CHANGED
|
@@ -126,6 +126,7 @@ function amendments(recipe, input) {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
// ../devkit/src/lib/amend/merge.ts
|
|
129
|
+
import { posix } from "node:path";
|
|
129
130
|
function normalizeProjectRoot(value) {
|
|
130
131
|
const rooted = value.replace(/^\.?\/*/, "");
|
|
131
132
|
let end = rooted.length;
|
|
@@ -141,27 +142,6 @@ function joinProjectPath(projectRoot, path) {
|
|
|
141
142
|
function resolveAssetInput(glob, projectRoot) {
|
|
142
143
|
return glob.from === "project" ? joinProjectPath(projectRoot, glob.input) : glob.input;
|
|
143
144
|
}
|
|
144
|
-
function ensurePostcssPlugin(existing, amendment) {
|
|
145
|
-
const root = asObject(existing) ?? {};
|
|
146
|
-
const plugins = asObject(root["plugins"]);
|
|
147
|
-
if (plugins === void 0 && root["plugins"] !== void 0) {
|
|
148
|
-
return {
|
|
149
|
-
value: root,
|
|
150
|
-
added: [],
|
|
151
|
-
declined: [`${amendment.file}: "plugins" is not an object`]
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
const next = { ...plugins };
|
|
155
|
-
if (Object.hasOwn(next, amendment.plugin)) {
|
|
156
|
-
return { value: root, added: [], declined: [] };
|
|
157
|
-
}
|
|
158
|
-
next[amendment.plugin] = {};
|
|
159
|
-
return {
|
|
160
|
-
value: { ...root, plugins: next },
|
|
161
|
-
added: [`${amendment.file}: ${amendment.plugin}`],
|
|
162
|
-
declined: []
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
145
|
function ensureDependency(manifest2, amendment) {
|
|
166
146
|
const root = asObject(manifest2) ?? {};
|
|
167
147
|
const dependencies = asObject(root["dependencies"]);
|
|
@@ -358,7 +338,7 @@ function ensureAssets(existing, wanted, projectRoot) {
|
|
|
358
338
|
const added = [];
|
|
359
339
|
for (const glob of wanted) {
|
|
360
340
|
const input = resolveAssetInput(glob, projectRoot);
|
|
361
|
-
if (list2.some((entry) => inputOf(entry) === input)) {
|
|
341
|
+
if (list2.some((entry) => inputOf(entry) === input || serves(entry, input, glob.output))) {
|
|
362
342
|
continue;
|
|
363
343
|
}
|
|
364
344
|
list2.push({
|
|
@@ -370,6 +350,16 @@ function ensureAssets(existing, wanted, projectRoot) {
|
|
|
370
350
|
}
|
|
371
351
|
return { value: list2, added };
|
|
372
352
|
}
|
|
353
|
+
function serves(entry, input, output = "") {
|
|
354
|
+
const asset = asObject(entry);
|
|
355
|
+
const from = inputOf(entry);
|
|
356
|
+
if (!asset || from === void 0 || !["**", "**/*"].includes(String(asset["glob"]))) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
const below = posix.relative(from, input);
|
|
360
|
+
const servedAt = posix.join(String(asset["output"] ?? ""), below);
|
|
361
|
+
return !below.startsWith("..") && below !== "" && servedAt === posix.normalize(output);
|
|
362
|
+
}
|
|
373
363
|
function inputOf(entry) {
|
|
374
364
|
if (typeof entry === "string") {
|
|
375
365
|
return entry;
|
|
@@ -380,8 +370,61 @@ function inputOf(entry) {
|
|
|
380
370
|
function asObject(value) {
|
|
381
371
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
382
372
|
}
|
|
373
|
+
function usesTailwind(css) {
|
|
374
|
+
return css.split("\n").some((line) => {
|
|
375
|
+
const directive = line.trimStart();
|
|
376
|
+
return /^@import\s+['"]tailwindcss['"]/.test(directive) || /^@source\s/.test(directive);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
function entryStylesheet(styles) {
|
|
380
|
+
if (!Array.isArray(styles)) {
|
|
381
|
+
return void 0;
|
|
382
|
+
}
|
|
383
|
+
return styles.map((style) => inputOf(style)).find((input) => input?.endsWith(".css") === true);
|
|
384
|
+
}
|
|
385
|
+
function relativeImport(fromDirectory, toDirectory) {
|
|
386
|
+
const path = posix.relative(fromDirectory, toDirectory);
|
|
387
|
+
if (path === "") {
|
|
388
|
+
return ".";
|
|
389
|
+
}
|
|
390
|
+
return path.startsWith("..") ? path : `./${path}`;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ../devkit/src/lib/amend/postcss.ts
|
|
394
|
+
function ensurePostcssPlugin(existing, amendment) {
|
|
395
|
+
const root = asObject(existing) ?? {};
|
|
396
|
+
const plugins = asObject(root["plugins"]);
|
|
397
|
+
if (plugins === void 0 && root["plugins"] !== void 0) {
|
|
398
|
+
return {
|
|
399
|
+
value: root,
|
|
400
|
+
added: [],
|
|
401
|
+
declined: [`${amendment.file}: "plugins" is not an object`]
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
const next = { ...plugins };
|
|
405
|
+
if (Object.hasOwn(next, amendment.plugin)) {
|
|
406
|
+
return { value: root, added: [], declined: [] };
|
|
407
|
+
}
|
|
408
|
+
next[amendment.plugin] = {};
|
|
409
|
+
return {
|
|
410
|
+
value: { ...root, plugins: next },
|
|
411
|
+
added: [`${amendment.file}: ${amendment.plugin}`],
|
|
412
|
+
declined: []
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
var CODE_POSTCSS_CONFIGS = [
|
|
416
|
+
"postcss.config.js",
|
|
417
|
+
"postcss.config.mjs",
|
|
418
|
+
"postcss.config.cjs",
|
|
419
|
+
".postcssrc.js"
|
|
420
|
+
];
|
|
421
|
+
function postcssWrittenAsCode(exists, amendment) {
|
|
422
|
+
const inTheWay = CODE_POSTCSS_CONFIGS.find((name) => exists(name));
|
|
423
|
+
return inTheWay === void 0 ? void 0 : `${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.`;
|
|
424
|
+
}
|
|
383
425
|
|
|
384
426
|
// ../devkit/src/lib/amend/compose.ts
|
|
427
|
+
import { posix as posix2 } from "node:path";
|
|
385
428
|
var APP_CONFIG = /export\s+const\s+appConfig\s*:[^=]*=\s*\{/;
|
|
386
429
|
var NAMESPACES = /provideTranslationNamespaces\(([^)]*)\)/;
|
|
387
430
|
var PROVIDERS_OPEN = /providers\s*:\s*\[/g;
|
|
@@ -426,15 +469,12 @@ function composePlugin(source, amendment, importPath) {
|
|
|
426
469
|
}
|
|
427
470
|
const wanted = providersToAdd(source, amendment);
|
|
428
471
|
const ownSymbols = [amendment.symbol, ...wanted.flatMap((provider) => provider.own ?? [])];
|
|
429
|
-
const foreign = wanted.flatMap((provider) => provider.from ?? []);
|
|
430
472
|
const withImports = source.replace(
|
|
431
473
|
SHELL_IMPORT,
|
|
432
474
|
() => [
|
|
433
|
-
`import {${withShellSymbols(shellImport[1], wanted)}} from '@loomweaver/shell';`,
|
|
434
|
-
...
|
|
435
|
-
|
|
436
|
-
),
|
|
437
|
-
`import { ${ownSymbols.toSorted((a, b) => a.localeCompare(b)).join(", ")} } from '${importPath}';`
|
|
475
|
+
`import {${withShellSymbols(shellImport[1], [...REGISTRATION_SHELL_SYMBOLS, ...shellSymbolsOf(wanted)])}} from '@loomweaver/shell';`,
|
|
476
|
+
...foreignImports(wanted),
|
|
477
|
+
`import { ${sorted(ownSymbols).join(", ")} } from '${importPath}';`
|
|
438
478
|
].join("\n")
|
|
439
479
|
);
|
|
440
480
|
const joined = joinNamespaces(withImports, amendment.id);
|
|
@@ -452,6 +492,71 @@ ${lines}${joined.source.slice(block.insertAt)}`,
|
|
|
452
492
|
kept: keptProviders(source, amendment).map((provider) => provider.line)
|
|
453
493
|
};
|
|
454
494
|
}
|
|
495
|
+
function composeProviders(source, amendment, importPath) {
|
|
496
|
+
const pending = amendment.providers.filter(
|
|
497
|
+
(provider) => !source.includes(provider.line.replace(/,$/, ""))
|
|
498
|
+
);
|
|
499
|
+
const kept = pending.filter(
|
|
500
|
+
(provider) => provider.unless !== void 0 && source.includes(provider.unless)
|
|
501
|
+
);
|
|
502
|
+
const wanted = pending.filter((provider) => !kept.includes(provider));
|
|
503
|
+
const keptLines = kept.map((provider) => provider.line);
|
|
504
|
+
if (wanted.length === 0) {
|
|
505
|
+
return { source, composed: true, kept: keptLines };
|
|
506
|
+
}
|
|
507
|
+
const shellImport = SHELL_IMPORT.exec(source);
|
|
508
|
+
if (!shellImport || !providersBlock(source)) {
|
|
509
|
+
return { source, composed: false, kept: keptLines };
|
|
510
|
+
}
|
|
511
|
+
const own = wanted.flatMap((provider) => provider.own ?? []);
|
|
512
|
+
const withImports = source.replace(
|
|
513
|
+
SHELL_IMPORT,
|
|
514
|
+
() => [
|
|
515
|
+
`import {${withShellSymbols(shellImport[1], shellSymbolsOf(wanted))}} from '@loomweaver/shell';`,
|
|
516
|
+
...foreignImports(wanted),
|
|
517
|
+
...own.length > 0 && importPath !== void 0 ? [`import { ${sorted(own).join(", ")} } from '${importPath}';`] : []
|
|
518
|
+
].join("\n")
|
|
519
|
+
);
|
|
520
|
+
const block = providersBlock(withImports);
|
|
521
|
+
if (!block) {
|
|
522
|
+
return { source, composed: false, kept: keptLines };
|
|
523
|
+
}
|
|
524
|
+
const lines = wanted.map((provider) => `${block.indent} ${provider.line}`).join("\n");
|
|
525
|
+
return {
|
|
526
|
+
source: `${withImports.slice(0, block.insertAt)}
|
|
527
|
+
${lines}${withImports.slice(block.insertAt)}`,
|
|
528
|
+
composed: true,
|
|
529
|
+
kept: keptLines
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
function providerLines(amendment, importPath) {
|
|
533
|
+
const own = amendment.providers.flatMap((provider) => provider.own ?? []);
|
|
534
|
+
return [
|
|
535
|
+
`import { ${shellSymbolsOf(amendment.providers).join(", ")} } from '@loomweaver/shell';`,
|
|
536
|
+
...foreignImports(amendment.providers),
|
|
537
|
+
...own.length > 0 && importPath !== void 0 ? [`import { ${own.join(", ")} } from '${importPath}';`] : [],
|
|
538
|
+
...amendment.providers.map((provider) => provider.line)
|
|
539
|
+
];
|
|
540
|
+
}
|
|
541
|
+
function moduleImport(fromDirectory, module) {
|
|
542
|
+
if (module === void 0) {
|
|
543
|
+
return void 0;
|
|
544
|
+
}
|
|
545
|
+
const directory = relativeImport(fromDirectory, posix2.dirname(module));
|
|
546
|
+
return `${directory}/${posix2.basename(module)}`;
|
|
547
|
+
}
|
|
548
|
+
function keptNote(line) {
|
|
549
|
+
return `kept the ${line.replace(/^\.\.\./, "").split("(", 1)[0]} already there instead of ${line.replace(/,$/, "")}`;
|
|
550
|
+
}
|
|
551
|
+
function shellSymbolsOf(providers) {
|
|
552
|
+
return providers.flatMap((provider) => provider.shell ?? []);
|
|
553
|
+
}
|
|
554
|
+
function foreignImports(providers) {
|
|
555
|
+
return providers.flatMap((provider) => provider.from ?? []).map((entry) => `import { ${sorted(entry.symbols).join(", ")} } from '${entry.path}';`);
|
|
556
|
+
}
|
|
557
|
+
function sorted(symbols) {
|
|
558
|
+
return symbols.toSorted((a, b) => a.localeCompare(b));
|
|
559
|
+
}
|
|
455
560
|
function joinNamespaces(source, id) {
|
|
456
561
|
const existing = NAMESPACES.exec(source);
|
|
457
562
|
if (!existing) {
|
|
@@ -498,21 +603,20 @@ function registrationLines(amendment, providers) {
|
|
|
498
603
|
return [
|
|
499
604
|
...providers.map((provider) => provider.line),
|
|
500
605
|
namespaceLine(amendment.id),
|
|
501
|
-
`provideCapabilityGrants({ ${amendment.id}: [${quotedList(amendment.capabilities)}] }),`,
|
|
606
|
+
`provideCapabilityGrants({ ${grantKey(amendment.id)}: [${quotedList(amendment.capabilities)}] }),`,
|
|
502
607
|
`...providePlugins(${amendment.symbol}),`
|
|
503
608
|
];
|
|
504
609
|
}
|
|
610
|
+
function grantKey(id) {
|
|
611
|
+
return /^[A-Za-z_$][\w$]*$/.test(id) ? id : `'${id}'`;
|
|
612
|
+
}
|
|
505
613
|
function quotedList(values) {
|
|
506
614
|
return values.map((value) => `'${value}'`).join(", ");
|
|
507
615
|
}
|
|
508
616
|
function namespaceLine(id) {
|
|
509
617
|
return `provideTranslationNamespaces('${id}'),`;
|
|
510
618
|
}
|
|
511
|
-
function withShellSymbols(existing,
|
|
512
|
-
const wanted = [
|
|
513
|
-
...REGISTRATION_SHELL_SYMBOLS,
|
|
514
|
-
...providers.flatMap((provider) => provider.shell ?? [])
|
|
515
|
-
];
|
|
619
|
+
function withShellSymbols(existing, wanted) {
|
|
516
620
|
const present = existing.split(",").map((symbol) => symbol.trim()).filter(Boolean);
|
|
517
621
|
const missing = wanted.filter(
|
|
518
622
|
(symbol) => present.every((entry) => entry.replace(/^type\s+/, "") !== symbol)
|
|
@@ -527,6 +631,29 @@ function withShellSymbols(existing, providers) {
|
|
|
527
631
|
` : ` ${all.join(", ")} `;
|
|
528
632
|
}
|
|
529
633
|
|
|
634
|
+
// ../devkit/src/lib/amend/stylesheet-import.ts
|
|
635
|
+
import { posix as posix3 } from "node:path";
|
|
636
|
+
var IMPORT = /^\s*@import\s+(?:url\(\s*)?['"]([^'"]+)['"]/;
|
|
637
|
+
function specifierOf(line) {
|
|
638
|
+
return IMPORT.exec(line)?.[1];
|
|
639
|
+
}
|
|
640
|
+
function importSpecifier(entryStylesheet2, file) {
|
|
641
|
+
const path = posix3.relative(posix3.dirname(entryStylesheet2), file);
|
|
642
|
+
return path.startsWith("..") ? path : `./${path}`;
|
|
643
|
+
}
|
|
644
|
+
function ensureStylesheetImport(css, specifier, after) {
|
|
645
|
+
const lines = css.split("\n");
|
|
646
|
+
if (lines.some((line) => specifierOf(line) === specifier)) {
|
|
647
|
+
return { css, anchored: true };
|
|
648
|
+
}
|
|
649
|
+
const anchor = lines.findLastIndex(
|
|
650
|
+
(line) => specifierOf(line)?.startsWith(after) === true
|
|
651
|
+
);
|
|
652
|
+
const position = anchor === -1 ? lines.findLastIndex((line) => specifierOf(line) !== void 0) : anchor;
|
|
653
|
+
lines.splice(position + 1, 0, `@import '${specifier}';`);
|
|
654
|
+
return { css: lines.join("\n"), anchored: anchor !== -1 };
|
|
655
|
+
}
|
|
656
|
+
|
|
530
657
|
// ../devkit/src/lib/amend/describe.ts
|
|
531
658
|
function describeAmendment(amendment) {
|
|
532
659
|
switch (amendment.kind) {
|
|
@@ -542,6 +669,12 @@ function describeAmendment(amendment) {
|
|
|
542
669
|
case "compose-plugin": {
|
|
543
670
|
return describeRegistration(amendment);
|
|
544
671
|
}
|
|
672
|
+
case "compose-provider": {
|
|
673
|
+
return describeProviders(amendment);
|
|
674
|
+
}
|
|
675
|
+
case "stylesheet-import": {
|
|
676
|
+
return `Import '${amendment.file}' in the application's entry stylesheet, resolved from that stylesheet, after its import of ${amendment.after}. ${amendment.without}`;
|
|
677
|
+
}
|
|
545
678
|
case "build-target": {
|
|
546
679
|
return describeBuildTarget(amendment);
|
|
547
680
|
}
|
|
@@ -560,6 +693,18 @@ function describeRegistration(amendment) {
|
|
|
560
693
|
).join("");
|
|
561
694
|
return `Register ${amendment.id} in the composition root: import { ${amendment.symbol} }, ${calls.slice(0, -1).join(", ")} and ${calls.at(-1)}.${imports} Without it none of its contributions appear.`;
|
|
562
695
|
}
|
|
696
|
+
function describeProviders(amendment) {
|
|
697
|
+
const lines = providerLines(amendment, amendment.module).map((line) => line.replace(/[,;]$/, "")).join("; ");
|
|
698
|
+
const resolved = amendment.module === void 0 ? "" : " Resolve the generated import from the composition root.";
|
|
699
|
+
const keeps = amendment.providers.map((provider) => provider.unless).filter((unless) => unless?.endsWith("(") === true).map((unless) => unless.slice(0, -1));
|
|
700
|
+
const keep = keeps.length > 0 ? ` Where the composition already calls ${keeps.join(" or ")}, that one is the product's choice: keep it, or replace it with this one.` : "";
|
|
701
|
+
return `Add to the composition root, after provideShell(): ${lines}.${resolved}${keep} ${amendment.without}`;
|
|
702
|
+
}
|
|
703
|
+
var STRINGS_COST = "the shell fetches its own strings at runtime, so without that glob every label in the chrome renders as its raw translation key";
|
|
704
|
+
function assetCost(amendment) {
|
|
705
|
+
const own = amendment.assets.map((asset) => asset.without).filter((without) => without !== void 0);
|
|
706
|
+
return own.length > 0 ? own.join("; ") : STRINGS_COST;
|
|
707
|
+
}
|
|
563
708
|
function describeBuildTarget(amendment) {
|
|
564
709
|
const steps = [];
|
|
565
710
|
if (amendment.styles.length > 0) {
|
|
@@ -569,9 +714,7 @@ function describeBuildTarget(amendment) {
|
|
|
569
714
|
const assets = amendment.assets.map(
|
|
570
715
|
(asset) => asset.output ? `${asset.input} served under ${asset.output}` : asset.input
|
|
571
716
|
).join(", ");
|
|
572
|
-
steps.push(
|
|
573
|
-
`add assets for ${assets} (the shell fetches its own strings at runtime, so without that glob every label in the chrome renders as its raw translation key)`
|
|
574
|
-
);
|
|
717
|
+
steps.push(`add assets for ${assets} (${assetCost(amendment)})`);
|
|
575
718
|
}
|
|
576
719
|
if (amendment.serviceWorker) {
|
|
577
720
|
steps.push(
|
|
@@ -922,16 +1065,30 @@ function pause(): Promise<void> {
|
|
|
922
1065
|
`;
|
|
923
1066
|
}
|
|
924
1067
|
|
|
1068
|
+
// ../devkit/src/lib/generate/escape.ts
|
|
1069
|
+
var MARKUP_ENTITIES = {
|
|
1070
|
+
"&": "&",
|
|
1071
|
+
"<": "<",
|
|
1072
|
+
">": ">",
|
|
1073
|
+
'"': """,
|
|
1074
|
+
"'": "'",
|
|
1075
|
+
"{": "{",
|
|
1076
|
+
"}": "}",
|
|
1077
|
+
"@": "@"
|
|
1078
|
+
};
|
|
1079
|
+
function markupText(value) {
|
|
1080
|
+
return value.replaceAll(/[&<>"'{}@]/g, (character) => MARKUP_ENTITIES[character]);
|
|
1081
|
+
}
|
|
1082
|
+
function stringLiteral(value) {
|
|
1083
|
+
const escaped = value.replaceAll("\\", "\\\\").replaceAll("'", String.raw`\'`).replaceAll("\n", String.raw`\n`).replaceAll("\r", String.raw`\r`);
|
|
1084
|
+
return `'${escaped}'`;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
925
1087
|
// ../devkit/src/recipes/angular-weaver/agent-files.ts
|
|
926
1088
|
var AG_UI_PROTOCOL_VERSION = "0.0.x";
|
|
927
1089
|
function connectionFile(weaver) {
|
|
928
1090
|
return `import { signal } from '@angular/core';
|
|
929
|
-
import {
|
|
930
|
-
commandTools,
|
|
931
|
-
type CommandTools,
|
|
932
|
-
type PendingToolCall,
|
|
933
|
-
type ToolDecision,
|
|
934
|
-
} from '@loomweaver/ag-ui';
|
|
1091
|
+
import { commandTools, consentPolicy, type CommandTools } from '@loomweaver/ag-ui';
|
|
935
1092
|
import type { PluginContext } from '@loomweaver/plugin-sdk';
|
|
936
1093
|
|
|
937
1094
|
// The connection is the workbench's half: the commands offered as tools, and each call put to the
|
|
@@ -940,43 +1097,28 @@ import type { PluginContext } from '@loomweaver/plugin-sdk';
|
|
|
940
1097
|
// A factory, not a module-level connection: everything a run needs lives in the closure, so a second
|
|
941
1098
|
// one never shares state with the first.
|
|
942
1099
|
export function connect${weaver.className}(ctx: PluginContext): CommandTools {
|
|
943
|
-
|
|
1100
|
+
// What an agent's word is enough for is each command's own statement, declared where the command
|
|
1101
|
+
// is registered. consentPolicy acts on it: it declines what may never run on an agent's word, runs
|
|
1102
|
+
// what may, and asks through your confirmation for the rest, remembering a yes for a command that
|
|
1103
|
+
// asks first for as long as this connection lives. The workbench states and enforces nothing here,
|
|
1104
|
+
// so replace it with a decision of your own where you need one. A decision can only narrow: the
|
|
1105
|
+
// workbench still refuses whatever it always refused.
|
|
1106
|
+
return commandTools(ctx, {
|
|
1107
|
+
before: consentPolicy(() =>
|
|
1108
|
+
ctx.ui.confirm({
|
|
1109
|
+
title: '${weaver.id}.agent.confirm.title',
|
|
1110
|
+
message: '${weaver.id}.agent.confirm.message',
|
|
1111
|
+
confirmLabel: '${weaver.id}.agent.confirm.yes',
|
|
1112
|
+
cancelLabel: '${weaver.id}.agent.confirm.no',
|
|
1113
|
+
tone: 'warning',
|
|
1114
|
+
}),
|
|
1115
|
+
),
|
|
1116
|
+
});
|
|
944
1117
|
}
|
|
945
1118
|
|
|
946
1119
|
// The one connection this plugin activates, published for its panel. Set in activate(), cleared in
|
|
947
1120
|
// deactivate(), so the panel renders an honest empty state either side of that.
|
|
948
1121
|
export const ${weaver.propertyName}Tools = signal<CommandTools | null>(null);
|
|
949
|
-
|
|
950
|
-
// What an agent's word is enough for is the command's own statement, declared where the command is
|
|
951
|
-
// registered and read off the call here. No list of ids lives beside the commands: a list drifts from
|
|
952
|
-
// what it describes, and it cannot speak for a command another plugin registered. Acting on the
|
|
953
|
-
// statement is this weaver's half: the workbench states it and enforces nothing.
|
|
954
|
-
async function decide(
|
|
955
|
-
ctx: PluginContext,
|
|
956
|
-
call: PendingToolCall,
|
|
957
|
-
): Promise<ToolDecision> {
|
|
958
|
-
if (call.agentConsent === 'never') {
|
|
959
|
-
return {
|
|
960
|
-
decision: 'decline',
|
|
961
|
-
reason: 'an agent may not run this one on its own word.',
|
|
962
|
-
};
|
|
963
|
-
}
|
|
964
|
-
if (call.agentConsent !== 'ask' && call.agentConsent !== 'ask-always') {
|
|
965
|
-
return { decision: 'run' };
|
|
966
|
-
}
|
|
967
|
-
const yes = await ctx.ui.confirm({
|
|
968
|
-
title: '${weaver.id}.agent.confirm.title',
|
|
969
|
-
message: '${weaver.id}.agent.confirm.message',
|
|
970
|
-
confirmLabel: '${weaver.id}.agent.confirm.yes',
|
|
971
|
-
cancelLabel: '${weaver.id}.agent.confirm.no',
|
|
972
|
-
tone: 'warning',
|
|
973
|
-
});
|
|
974
|
-
// A decision can only narrow. Letting a call through does not make it reachable: the workbench
|
|
975
|
-
// still refuses whatever it always refused.
|
|
976
|
-
return yes
|
|
977
|
-
? { decision: 'run' }
|
|
978
|
-
: { decision: 'decline', reason: 'the person at the keyboard said no.' };
|
|
979
|
-
}
|
|
980
1122
|
`;
|
|
981
1123
|
}
|
|
982
1124
|
function specFile(weaver) {
|
|
@@ -990,17 +1132,22 @@ interface Asked {
|
|
|
990
1132
|
readonly args?: CommandArguments;
|
|
991
1133
|
}
|
|
992
1134
|
|
|
993
|
-
function contextThat(confirms: boolean, ran: Asked[]): PluginContext {
|
|
1135
|
+
function contextThat(confirms: boolean, ran: Asked[], asked: string[] = []): PluginContext {
|
|
994
1136
|
return {
|
|
995
1137
|
invocableCommands: () => [
|
|
996
1138
|
// agentConsent travels with the command, which is what the connection reads off the call.
|
|
997
|
-
{ id: '${weaver.id}.hello', title:
|
|
1139
|
+
{ id: '${weaver.id}.hello', title: ${stringLiteral(`${weaver.name} action`)}, description: 'Shows a short message.', agentConsent: 'ask' },
|
|
998
1140
|
],
|
|
999
1141
|
invokeCommand: (id: string, args?: CommandArguments) => {
|
|
1000
1142
|
ran.push({ id, args });
|
|
1001
1143
|
return Promise.resolve({ outcome: 'answered', value: { tone: args?.['tone'] ?? 'info' } });
|
|
1002
1144
|
},
|
|
1003
|
-
ui: {
|
|
1145
|
+
ui: {
|
|
1146
|
+
confirm: (input: { title: string }) => {
|
|
1147
|
+
asked.push(input.title);
|
|
1148
|
+
return Promise.resolve(confirms);
|
|
1149
|
+
},
|
|
1150
|
+
},
|
|
1004
1151
|
} as unknown as PluginContext;
|
|
1005
1152
|
}
|
|
1006
1153
|
|
|
@@ -1040,6 +1187,18 @@ describe('connect${weaver.className}', () => {
|
|
|
1040
1187
|
expect(answer?.error).toBeUndefined();
|
|
1041
1188
|
});
|
|
1042
1189
|
|
|
1190
|
+
it('asks once for a command that asks first, and remembers the yes', async () => {
|
|
1191
|
+
const asked: string[] = [];
|
|
1192
|
+
const ran: Asked[] = [];
|
|
1193
|
+
const tools = connect${weaver.className}(contextThat(true, ran, asked));
|
|
1194
|
+
|
|
1195
|
+
await answersTo(tools, streamedCall('c3', '{}'));
|
|
1196
|
+
await answersTo(tools, streamedCall('c4', '{}'));
|
|
1197
|
+
|
|
1198
|
+
expect(asked).toEqual(['${weaver.id}.agent.confirm.title']);
|
|
1199
|
+
expect(ran).toHaveLength(2);
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1043
1202
|
it('never reaches the workbench when a consequential call is declined', async () => {
|
|
1044
1203
|
const ran: Asked[] = [];
|
|
1045
1204
|
const tools = connect${weaver.className}(contextThat(false, ran));
|
|
@@ -1149,6 +1308,12 @@ var APP_OPTION = {
|
|
|
1149
1308
|
description: "Application to drop into. Inferred when the workspace has exactly one.",
|
|
1150
1309
|
workspaceOnly: true
|
|
1151
1310
|
};
|
|
1311
|
+
var DIRECTORY_OPTION = {
|
|
1312
|
+
name: "directory",
|
|
1313
|
+
type: "string",
|
|
1314
|
+
description: "Where the files land, relative to the workspace root.",
|
|
1315
|
+
workspaceOnly: true
|
|
1316
|
+
};
|
|
1152
1317
|
|
|
1153
1318
|
// ../devkit/src/lib/validate/manifest.ts
|
|
1154
1319
|
var KNOWN_CAPABILITIES = [
|
|
@@ -1162,11 +1327,18 @@ var KNOWN_CAPABILITIES = [
|
|
|
1162
1327
|
];
|
|
1163
1328
|
function validateManifest(manifest2, known = KNOWN_CAPABILITIES) {
|
|
1164
1329
|
const findings = [];
|
|
1165
|
-
if (typeof manifest2.id !== "string" ||
|
|
1330
|
+
if (typeof manifest2.id !== "string" || manifest2.id.trim() === "") {
|
|
1166
1331
|
findings.push({
|
|
1167
1332
|
level: "error",
|
|
1168
1333
|
code: "manifest.id",
|
|
1169
|
-
message: `Plugin id must be a
|
|
1334
|
+
message: `Plugin id must be a non-empty string; got ${JSON.stringify(manifest2.id)}. Grants, settings and every contribution are keyed by it.`,
|
|
1335
|
+
path: "manifest.id"
|
|
1336
|
+
});
|
|
1337
|
+
} else if (!isKebabId(manifest2.id)) {
|
|
1338
|
+
findings.push({
|
|
1339
|
+
level: "warning",
|
|
1340
|
+
code: "manifest.id.convention",
|
|
1341
|
+
message: `Plugin id ${JSON.stringify(manifest2.id)} is not kebab-case. The workbench accepts it; every scaffold and guide writes ids in kebab-case, so it will read differently from the plugins beside it.`,
|
|
1170
1342
|
path: "manifest.id"
|
|
1171
1343
|
});
|
|
1172
1344
|
}
|
|
@@ -1258,6 +1430,13 @@ var PLATFORM_BOUND_CHORD_TOKENS = /* @__PURE__ */ new Set([
|
|
|
1258
1430
|
"control",
|
|
1259
1431
|
"meta"
|
|
1260
1432
|
]);
|
|
1433
|
+
var KEPT_BY_BROWSERS = /* @__PURE__ */ new Set(["n", "t", "w", "q"]);
|
|
1434
|
+
function defaultShortcut(id) {
|
|
1435
|
+
const letter = [...id].find(
|
|
1436
|
+
(character) => /[a-z]/.test(character) && !KEPT_BY_BROWSERS.has(character)
|
|
1437
|
+
);
|
|
1438
|
+
return letter && `mod+shift+${letter}`;
|
|
1439
|
+
}
|
|
1261
1440
|
function assertPlatformNeutralChord(shortcut) {
|
|
1262
1441
|
const tokens = shortcut.toLowerCase().split("+").map((token) => token.trim());
|
|
1263
1442
|
const bound = tokens.find((token) => PLATFORM_BOUND_CHORD_TOKENS.has(token));
|
|
@@ -1287,7 +1466,7 @@ function resolveFeatures(id, input) {
|
|
|
1287
1466
|
menuSlot,
|
|
1288
1467
|
settings: Boolean(input?.settings),
|
|
1289
1468
|
access: input?.access ? accessLiteral(input.access) : void 0,
|
|
1290
|
-
shortcut: input?.shortcut?.trim() ||
|
|
1469
|
+
shortcut: input?.shortcut?.trim() || defaultShortcut(id),
|
|
1291
1470
|
barItem,
|
|
1292
1471
|
about: Boolean(input?.about),
|
|
1293
1472
|
instanceable,
|
|
@@ -1352,7 +1531,7 @@ function commandBlock(weaver) {
|
|
|
1352
1531
|
` { name: 'tone', kind: 'choice', choices: ['info', 'success', 'warning'], description: '${weaver.id}.actionTone' },`,
|
|
1353
1532
|
" ],",
|
|
1354
1533
|
` answers: '${weaver.id}.actionAnswers',`,
|
|
1355
|
-
` shortcut: '${weaver.features.shortcut}'
|
|
1534
|
+
...weaver.features.shortcut ? [` shortcut: '${weaver.features.shortcut}',`] : [],
|
|
1356
1535
|
" callable: true,",
|
|
1357
1536
|
...weaver.features.agent ? [
|
|
1358
1537
|
" // What an agent's word is enough for. The platform states it and enforces nothing:",
|
|
@@ -1584,7 +1763,7 @@ ${pluginConsts(weaver).join("\n\n")}
|
|
|
1584
1763
|
export const ${weaver.propertyName}Plugin: Plugin = {
|
|
1585
1764
|
manifest: {
|
|
1586
1765
|
id: '${weaver.id}',
|
|
1587
|
-
name:
|
|
1766
|
+
name: ${stringLiteral(weaver.name)},
|
|
1588
1767
|
capabilities: [${quotedList(weaver.capabilities)}],
|
|
1589
1768
|
},
|
|
1590
1769
|
activate(ctx) {
|
|
@@ -1598,7 +1777,7 @@ function accessLine(weaver) {
|
|
|
1598
1777
|
}
|
|
1599
1778
|
|
|
1600
1779
|
// ../devkit/src/recipes/platform-version.ts
|
|
1601
|
-
var PLATFORM_VERSION = "0.
|
|
1780
|
+
var PLATFORM_VERSION = "0.17.1";
|
|
1602
1781
|
|
|
1603
1782
|
// ../devkit/src/recipes/readme-notes.ts
|
|
1604
1783
|
var UNTAGGED_NOTE = [
|
|
@@ -1741,7 +1920,7 @@ function readmeFile(weaver) {
|
|
|
1741
1920
|
"## After scaffolding",
|
|
1742
1921
|
"",
|
|
1743
1922
|
"- `src/lib/i18n/de.json` starts as a copy of the English strings \u2014 translate it.",
|
|
1744
|
-
`- A scaffolded command defaults its shortcut to \`mod+shift
|
|
1923
|
+
`- A scaffolded command defaults its shortcut to \`mod+shift+\` and the first letter of the id a browser leaves to the page (it keeps n, t, w and q for itself) \u2014 two weavers that land on the same letter collide; pass \`--shortcut\` or edit the command.`,
|
|
1745
1924
|
...asBullet(UNTAGGED_NOTE),
|
|
1746
1925
|
""
|
|
1747
1926
|
].join("\n");
|
|
@@ -1763,7 +1942,7 @@ export class ${weaver.className}AboutDialog {
|
|
|
1763
1942
|
}
|
|
1764
1943
|
function aboutDialogTemplateFile(weaver) {
|
|
1765
1944
|
return `<div class="flex flex-col items-center gap-2 text-center">
|
|
1766
|
-
<h2 class="text-lg font-semibold text-content">${weaver.name}</h2>
|
|
1945
|
+
<h2 class="text-lg font-semibold text-content">${markupText(weaver.name)}</h2>
|
|
1767
1946
|
<span class="text-xs text-content-faint tabular-nums">v{{ host.version() }}</span>
|
|
1768
1947
|
</div>
|
|
1769
1948
|
`;
|
|
@@ -1820,7 +1999,7 @@ function viewTemplateFile(weaver) {
|
|
|
1820
1999
|
</p>
|
|
1821
2000
|
` : "";
|
|
1822
2001
|
return `<div class="mx-auto flex max-w-2xl flex-col gap-4 p-6">
|
|
1823
|
-
<h2 class="text-lg font-semibold text-content">${weaver.name}</h2>
|
|
2002
|
+
<h2 class="text-lg font-semibold text-content">${markupText(weaver.name)}</h2>
|
|
1824
2003
|
<p class="text-sm text-content-faint">
|
|
1825
2004
|
Your new weaver surface. Register more surfaces, commands, rail items and menus on
|
|
1826
2005
|
<code class="text-content">ctx</code> inside the plugin's
|
|
@@ -1933,7 +2112,7 @@ function pluginHtml(plugin) {
|
|
|
1933
2112
|
<html lang="en">
|
|
1934
2113
|
<head>
|
|
1935
2114
|
<meta charset="utf-8" />
|
|
1936
|
-
<title>${plugin.name} \u2014 frame plugin (logic)</title>
|
|
2115
|
+
<title>${markupText(plugin.name)} \u2014 frame plugin (logic)</title>
|
|
1937
2116
|
</head>
|
|
1938
2117
|
<body>
|
|
1939
2118
|
<script src="/frame-kit/penpal.global.js"></script>
|
|
@@ -1954,10 +2133,10 @@ function pluginJs(plugin) {
|
|
|
1954
2133
|
connection.promise
|
|
1955
2134
|
.then(function (ctx) {
|
|
1956
2135
|
return Promise.all([
|
|
1957
|
-
ctx.toast({ message:
|
|
2136
|
+
ctx.toast({ message: ${stringLiteral(`${plugin.name} ready`)}, kind: 'success', timeoutMs: 4000 }),
|
|
1958
2137
|
ctx.registerSurface({
|
|
1959
2138
|
id: '${plugin.id}.view',
|
|
1960
|
-
title:
|
|
2139
|
+
title: ${stringLiteral(plugin.name)},
|
|
1961
2140
|
iframe: '/${plugin.id}/view.html',
|
|
1962
2141
|
routable: { path: '${plugin.id}', titleIsLiteral: true },
|
|
1963
2142
|
}),
|
|
@@ -1974,23 +2153,18 @@ function viewHtml(plugin) {
|
|
|
1974
2153
|
<html lang="en">
|
|
1975
2154
|
<head>
|
|
1976
2155
|
<meta charset="utf-8" />
|
|
1977
|
-
<title>${plugin.name}</title>
|
|
2156
|
+
<title>${markupText(plugin.name)}</title>
|
|
1978
2157
|
<link rel="stylesheet" href="/frame-kit/lw-frame.css" />
|
|
1979
2158
|
<style>
|
|
1980
|
-
body {
|
|
1981
|
-
margin: 0;
|
|
1982
|
-
font-family: var(--lw-font-sans, system-ui, sans-serif);
|
|
1983
|
-
color: var(--lw-content, #1f2937);
|
|
1984
|
-
background: var(--lw-surface, transparent);
|
|
1985
|
-
}
|
|
2159
|
+
body { margin: 0; }
|
|
1986
2160
|
.wrap { max-width: 42rem; margin: 0 auto; padding: 1.5rem; }
|
|
1987
2161
|
h1 { font-size: 1.125rem; font-weight: 600; }
|
|
1988
|
-
p { color: var(--lw-content-faint
|
|
2162
|
+
p { color: var(--lw-content-faint); }
|
|
1989
2163
|
</style>
|
|
1990
2164
|
</head>
|
|
1991
2165
|
<body>
|
|
1992
2166
|
<div class="wrap">
|
|
1993
|
-
<h1>${plugin.name}</h1>
|
|
2167
|
+
<h1>${markupText(plugin.name)}</h1>
|
|
1994
2168
|
<p>
|
|
1995
2169
|
Your sandboxed surface. It runs isolated in its own iframe, so its body can be built with
|
|
1996
2170
|
any framework (React, Vue, Svelte, vanilla). The frame UI kit (served by the distribution
|
|
@@ -2002,16 +2176,23 @@ function viewHtml(plugin) {
|
|
|
2002
2176
|
<script src="/frame-kit/penpal.global.js"></script>
|
|
2003
2177
|
<script src="/frame-kit/lw-elements.global.js"></script>
|
|
2004
2178
|
<script>
|
|
2005
|
-
globalThis.Penpal.connect({
|
|
2179
|
+
const connection = globalThis.Penpal.connect({
|
|
2006
2180
|
messenger: new globalThis.Penpal.WindowMessenger({
|
|
2007
2181
|
remoteWindow: globalThis.parent,
|
|
2008
2182
|
allowedOrigins: ['*'],
|
|
2009
2183
|
}),
|
|
2010
|
-
methods: {
|
|
2184
|
+
methods: globalThis.LwFrame.surfaceMethods({
|
|
2011
2185
|
render: function (state) {
|
|
2012
2186
|
globalThis.LwFrame.applySurfaceState(state);
|
|
2013
2187
|
},
|
|
2014
|
-
|
|
2188
|
+
stateChanged: function (key, value, loaded) {
|
|
2189
|
+
globalThis.LwFrame.state.apply(key, value, loaded);
|
|
2190
|
+
},
|
|
2191
|
+
}),
|
|
2192
|
+
});
|
|
2193
|
+
connection.promise.then(function (host) {
|
|
2194
|
+
globalThis.LwFrame.connectState(host);
|
|
2195
|
+
globalThis.LwFrame.connectToolbars(host);
|
|
2015
2196
|
});
|
|
2016
2197
|
</script>
|
|
2017
2198
|
</body>
|
|
@@ -2025,24 +2206,25 @@ function readme(plugin) {
|
|
|
2025
2206
|
`A framework-agnostic LoomWeaver plugin: it runs in an isolated \`<iframe sandbox>\` and`,
|
|
2026
2207
|
`receives \`ctx\` over Penpal RPC through the same default-deny broker a trusted plugin uses.`,
|
|
2027
2208
|
"",
|
|
2028
|
-
"##
|
|
2209
|
+
"## How it is served and wired",
|
|
2029
2210
|
"",
|
|
2030
|
-
`
|
|
2031
|
-
|
|
2032
|
-
|
|
2211
|
+
`The scaffold wires it into the distribution: it serves these files under \`/${plugin.id}/\` and the`,
|
|
2212
|
+
"**frame UI kit** (`@loomweaver/frame-kit`) under `/frame-kit/`, and registers and grants the",
|
|
2213
|
+
"plugin in the composition root:",
|
|
2033
2214
|
"",
|
|
2034
|
-
"
|
|
2035
|
-
'
|
|
2036
|
-
|
|
2037
|
-
"",
|
|
2038
|
-
`2. Register + grant it in the composition root:`,
|
|
2215
|
+
"```ts",
|
|
2216
|
+
`...provideFramePlugins({ id: '${plugin.id}', entryUrl: '/${plugin.id}/plugin.html', capabilities: ['contributions', 'ui'] }),`,
|
|
2217
|
+
`provideCapabilityGrants({ '${plugin.id}': ['contributions', 'ui'] }),`,
|
|
2218
|
+
"```",
|
|
2039
2219
|
"",
|
|
2040
|
-
"
|
|
2041
|
-
` provideFramePlugins({ id: '${plugin.id}', entryUrl: '/${plugin.id}/plugin.html', capabilities: ['contributions', 'ui'] });`,
|
|
2042
|
-
` provideCapabilityGrants({ '${plugin.id}': ['contributions', 'ui'] });`,
|
|
2043
|
-
" ```",
|
|
2220
|
+
"Where it was generated without a workspace to wire, those are the steps it named instead.",
|
|
2044
2221
|
"",
|
|
2045
2222
|
`The surface is routable at \`/${plugin.id}\`. Replace \`view.html\` with your own UI in any framework.`,
|
|
2223
|
+
"",
|
|
2224
|
+
"Keep its connection on `LwFrame.surfaceMethods`, `connectState` and `connectToolbars`: they let the",
|
|
2225
|
+
"workbench picture the surface, push the plugin's state and fill any `<lw-toolbar>` you place. Writing",
|
|
2226
|
+
'the surface in TypeScript? Add `"types": ["@loomweaver/frame-kit"]` to your tsconfig and `LwFrame` is',
|
|
2227
|
+
"typed from the kit's `lw-frame.d.ts`.",
|
|
2046
2228
|
""
|
|
2047
2229
|
].join("\n");
|
|
2048
2230
|
}
|
|
@@ -2395,7 +2577,7 @@ export const appConfig: ApplicationConfig = {
|
|
|
2395
2577
|
provideCommandPaletteEntry(),
|
|
2396
2578
|
provideQuickOpenEntry(),
|
|
2397
2579
|
provideProductIdentity({
|
|
2398
|
-
name:
|
|
2580
|
+
name: ${stringLiteral(distribution.title)},
|
|
2399
2581
|
tagline: 'Built on LoomWeaver',
|
|
2400
2582
|
logoUrl: 'logo.svg',
|
|
2401
2583
|
}),
|
|
@@ -2465,7 +2647,7 @@ function indexHtml(distribution) {
|
|
|
2465
2647
|
<html lang="en">
|
|
2466
2648
|
<head>
|
|
2467
2649
|
<meta charset="utf-8" />
|
|
2468
|
-
<title>${distribution.title}</title>
|
|
2650
|
+
<title>${markupText(distribution.title)}</title>
|
|
2469
2651
|
<base href="/" />
|
|
2470
2652
|
<meta
|
|
2471
2653
|
http-equiv="Content-Security-Policy"
|
|
@@ -2835,8 +3017,8 @@ function resolveSettingsStoreInput(input) {
|
|
|
2835
3017
|
function moduleFile2(store) {
|
|
2836
3018
|
return `// A backend-backed settings store for the SETTINGS_STORE port. The platform
|
|
2837
3019
|
// ships local defaults; the product persists settings against its own backend. Only genuine
|
|
2838
|
-
// settings flow through this port \u2014 working state stays on WORKING_STATE_STORE.
|
|
2839
|
-
// provideSettingsStore(new ${store.className}SettingsStore()).
|
|
3020
|
+
// settings flow through this port \u2014 working state stays on WORKING_STATE_STORE. The scaffold
|
|
3021
|
+
// provides it in the composition root: provideSettingsStore(new ${store.className}SettingsStore()).
|
|
2840
3022
|
import { KeyValueStore } from '@loomweaver/shell';
|
|
2841
3023
|
|
|
2842
3024
|
export class ${store.className}SettingsStore implements KeyValueStore {
|
|
@@ -2886,7 +3068,7 @@ function resolveThemeInput(input) {
|
|
|
2886
3068
|
}
|
|
2887
3069
|
function cssFile(theme2) {
|
|
2888
3070
|
return `/* ${theme2.title} theme \u2014 overrides LoomWeaver's --lw-* design tokens.
|
|
2889
|
-
|
|
3071
|
+
The scaffold imports it into your distribution's entry stylesheet, after the shell's styles:
|
|
2890
3072
|
@import '@loomweaver/shell/styles/theme.css';
|
|
2891
3073
|
@import './themes/${theme2.name}.css';
|
|
2892
3074
|
Declared in @layer lw-tenant-theme so it beats any plugin's ctx.contributeTheme
|
|
@@ -2925,8 +3107,8 @@ function cssFile(theme2) {
|
|
|
2925
3107
|
}
|
|
2926
3108
|
function bootstrapFile(theme2) {
|
|
2927
3109
|
return `/* ${theme2.title} theme \u2014 maps LoomWeaver's --lw-* design tokens onto Bootstrap 5.3's --bs-*.
|
|
2928
|
-
|
|
2929
|
-
INTO A LAYER \u2014 unlayered CSS outranks layered CSS whatever its specificity, so Bootstrap's
|
|
3110
|
+
The scaffold imports it into your distribution's entry stylesheet, after the shell's styles.
|
|
3111
|
+
Import Bootstrap itself INTO A LAYER \u2014 unlayered CSS outranks layered CSS whatever its specificity, so Bootstrap's
|
|
2930
3112
|
Reboot (button { border-radius: 0 }) would otherwise beat our .lw-* component classes:
|
|
2931
3113
|
@layer vendor;
|
|
2932
3114
|
@import 'bootstrap/dist/css/bootstrap.css' layer(vendor);
|
|
@@ -3021,8 +3203,8 @@ function resolveLayoutInput(input) {
|
|
|
3021
3203
|
return { name, propertyName: toCamelCase(name) };
|
|
3022
3204
|
}
|
|
3023
3205
|
function moduleFile3(layout2) {
|
|
3024
|
-
return `// A base layout for the distribution.
|
|
3025
|
-
//
|
|
3206
|
+
return `// A base layout for the distribution. The scaffold provides it in src/app/app.config.ts with
|
|
3207
|
+
// provideLayout(${layout2.propertyName}Layout), unless a layout is already provided there. Region ids are what contributions target \u2014 '${RAIL_REGION}' (rail) and '${STATUS_BAR_REGION}' (bar) match
|
|
3026
3208
|
// the devkit weaver defaults, so a scaffolded weaver's rail + bar items land here.
|
|
3027
3209
|
import { ShellLayout } from '@loomweaver/shell';
|
|
3028
3210
|
|
|
@@ -3332,18 +3514,70 @@ var authSourceScaffold = {
|
|
|
3332
3514
|
default: false
|
|
3333
3515
|
},
|
|
3334
3516
|
APP_OPTION,
|
|
3335
|
-
|
|
3336
|
-
name: "directory",
|
|
3337
|
-
type: "string",
|
|
3338
|
-
description: "Where the files land, relative to the workspace root.",
|
|
3339
|
-
workspaceOnly: true
|
|
3340
|
-
}
|
|
3517
|
+
DIRECTORY_OPTION
|
|
3341
3518
|
],
|
|
3342
3519
|
build: (values) => generate(authSource, authSourceInput(values)),
|
|
3343
3520
|
amend: (values) => authSourceAmendments(authSourceInput(values), stringValue(values, "directory"))
|
|
3344
3521
|
};
|
|
3345
3522
|
|
|
3523
|
+
// ../devkit/src/recipes/frame-plugin/amendments.ts
|
|
3524
|
+
var FRAME_PLUGIN_CAPABILITIES = ["contributions", "ui"];
|
|
3525
|
+
function framePluginAmendments(input, where) {
|
|
3526
|
+
if (where === void 0 || where === "") {
|
|
3527
|
+
return [];
|
|
3528
|
+
}
|
|
3529
|
+
const plugin = resolveFramePluginInput(input);
|
|
3530
|
+
const capabilities = quotedList(FRAME_PLUGIN_CAPABILITIES);
|
|
3531
|
+
const entryUrl = `/${plugin.id}/plugin.html`;
|
|
3532
|
+
return [
|
|
3533
|
+
{
|
|
3534
|
+
kind: "package",
|
|
3535
|
+
name: "@loomweaver/frame-kit",
|
|
3536
|
+
version: `^${PLATFORM_VERSION}`
|
|
3537
|
+
},
|
|
3538
|
+
{
|
|
3539
|
+
kind: "build-target",
|
|
3540
|
+
styles: [],
|
|
3541
|
+
assets: [
|
|
3542
|
+
{
|
|
3543
|
+
glob: "**/*",
|
|
3544
|
+
input: normalizeProjectRoot(where),
|
|
3545
|
+
from: "workspace",
|
|
3546
|
+
output: plugin.id,
|
|
3547
|
+
without: `without it nothing is served at /${plugin.id}/ and the plugin never loads`
|
|
3548
|
+
},
|
|
3549
|
+
{
|
|
3550
|
+
glob: "**/*",
|
|
3551
|
+
input: "node_modules/@loomweaver/frame-kit/dist",
|
|
3552
|
+
from: "workspace",
|
|
3553
|
+
output: "frame-kit",
|
|
3554
|
+
without: "without it the frame kit the plugin loads from /frame-kit/ is missing"
|
|
3555
|
+
}
|
|
3556
|
+
]
|
|
3557
|
+
},
|
|
3558
|
+
{
|
|
3559
|
+
kind: "compose-provider",
|
|
3560
|
+
providers: [
|
|
3561
|
+
{
|
|
3562
|
+
line: `...provideFramePlugins({ id: '${plugin.id}', name: ${stringLiteral(plugin.name)}, entryUrl: '${entryUrl}', capabilities: [${capabilities}] }),`,
|
|
3563
|
+
shell: ["provideFramePlugins"],
|
|
3564
|
+
unless: `'${entryUrl}'`
|
|
3565
|
+
},
|
|
3566
|
+
{
|
|
3567
|
+
line: `provideCapabilityGrants({ ${grantKey(plugin.id)}: [${capabilities}] }),`,
|
|
3568
|
+
shell: ["provideCapabilityGrants"],
|
|
3569
|
+
unless: `${grantKey(plugin.id)}: [`
|
|
3570
|
+
}
|
|
3571
|
+
],
|
|
3572
|
+
without: "Without them the plugin is never loaded, and loaded without its grant every call it makes is refused."
|
|
3573
|
+
}
|
|
3574
|
+
];
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3346
3577
|
// ../devkit/src/recipes/frame-plugin/scaffold.ts
|
|
3578
|
+
function framePluginInput(values) {
|
|
3579
|
+
return { id: stringValue(values, "id") ?? "", name: stringValue(values, "name") };
|
|
3580
|
+
}
|
|
3347
3581
|
var framePluginScaffold = {
|
|
3348
3582
|
name: "frame-plugin",
|
|
3349
3583
|
summary: "a framework-agnostic iframe plugin (Penpal + the frame UI kit)",
|
|
@@ -3360,14 +3594,37 @@ var framePluginScaffold = {
|
|
|
3360
3594
|
type: "string",
|
|
3361
3595
|
description: "Human-readable name. Defaults to a title-cased id."
|
|
3362
3596
|
},
|
|
3363
|
-
APP_OPTION
|
|
3597
|
+
APP_OPTION,
|
|
3598
|
+
DIRECTORY_OPTION
|
|
3364
3599
|
],
|
|
3365
|
-
build: (values) => generate(framePlugin,
|
|
3366
|
-
|
|
3367
|
-
name: stringValue(values, "name")
|
|
3368
|
-
})
|
|
3600
|
+
build: (values) => generate(framePlugin, framePluginInput(values)),
|
|
3601
|
+
amend: (values) => framePluginAmendments(framePluginInput(values), stringValue(values, "directory"))
|
|
3369
3602
|
};
|
|
3370
3603
|
|
|
3604
|
+
// ../devkit/src/recipes/layout/amendments.ts
|
|
3605
|
+
function layoutAmendments(input, where) {
|
|
3606
|
+
if (where === void 0 || where === "") {
|
|
3607
|
+
return [];
|
|
3608
|
+
}
|
|
3609
|
+
const layout2 = resolveLayoutInput(input);
|
|
3610
|
+
const symbol = `${layout2.propertyName}Layout`;
|
|
3611
|
+
return [
|
|
3612
|
+
{
|
|
3613
|
+
kind: "compose-provider",
|
|
3614
|
+
module: joinProjectPath(normalizeProjectRoot(where), `${layout2.name}-layout`),
|
|
3615
|
+
providers: [
|
|
3616
|
+
{
|
|
3617
|
+
line: `provideLayout(${symbol}),`,
|
|
3618
|
+
shell: ["provideLayout"],
|
|
3619
|
+
own: [symbol],
|
|
3620
|
+
unless: "provideLayout("
|
|
3621
|
+
}
|
|
3622
|
+
],
|
|
3623
|
+
without: "Without it the workbench keeps the layout it has, and this one is never drawn."
|
|
3624
|
+
}
|
|
3625
|
+
];
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3371
3628
|
// ../devkit/src/recipes/layout/scaffold.ts
|
|
3372
3629
|
var layoutScaffold = {
|
|
3373
3630
|
name: "layout",
|
|
@@ -3376,13 +3633,41 @@ var layoutScaffold = {
|
|
|
3376
3633
|
{
|
|
3377
3634
|
name: "name",
|
|
3378
3635
|
type: "string",
|
|
3379
|
-
description: "Layout name
|
|
3636
|
+
description: "Layout name, kebab-case; the file is <name>-layout.ts.",
|
|
3637
|
+
pattern: KEBAB_ID_PATTERN,
|
|
3638
|
+
default: "base"
|
|
3380
3639
|
},
|
|
3381
|
-
APP_OPTION
|
|
3640
|
+
APP_OPTION,
|
|
3641
|
+
DIRECTORY_OPTION
|
|
3382
3642
|
],
|
|
3383
|
-
build: (values) => generate(layout, { name: stringValue(values, "name") })
|
|
3643
|
+
build: (values) => generate(layout, { name: stringValue(values, "name") }),
|
|
3644
|
+
amend: (values) => layoutAmendments({ name: stringValue(values, "name") }, stringValue(values, "directory"))
|
|
3384
3645
|
};
|
|
3385
3646
|
|
|
3647
|
+
// ../devkit/src/recipes/settings-store/amendments.ts
|
|
3648
|
+
function settingsStoreAmendments(input, where) {
|
|
3649
|
+
if (where === void 0 || where === "") {
|
|
3650
|
+
return [];
|
|
3651
|
+
}
|
|
3652
|
+
const store = resolveSettingsStoreInput(input);
|
|
3653
|
+
const symbol = `${store.className}SettingsStore`;
|
|
3654
|
+
return [
|
|
3655
|
+
{
|
|
3656
|
+
kind: "compose-provider",
|
|
3657
|
+
module: joinProjectPath(normalizeProjectRoot(where), `${store.name}-settings-store`),
|
|
3658
|
+
providers: [
|
|
3659
|
+
{
|
|
3660
|
+
line: `provideSettingsStore(new ${symbol}()),`,
|
|
3661
|
+
shell: ["provideSettingsStore"],
|
|
3662
|
+
own: [symbol],
|
|
3663
|
+
unless: "provideSettingsStore("
|
|
3664
|
+
}
|
|
3665
|
+
],
|
|
3666
|
+
without: "Without it settings stay in the browser's local storage and never reach your API."
|
|
3667
|
+
}
|
|
3668
|
+
];
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3386
3671
|
// ../devkit/src/recipes/settings-store/scaffold.ts
|
|
3387
3672
|
var settingsStoreScaffold = {
|
|
3388
3673
|
name: "settings-store",
|
|
@@ -3395,12 +3680,40 @@ var settingsStoreScaffold = {
|
|
|
3395
3680
|
required: true,
|
|
3396
3681
|
pattern: KEBAB_ID_PATTERN
|
|
3397
3682
|
},
|
|
3398
|
-
APP_OPTION
|
|
3683
|
+
APP_OPTION,
|
|
3684
|
+
DIRECTORY_OPTION
|
|
3399
3685
|
],
|
|
3400
|
-
build: (values) => generate(settingsStore, { name: stringValue(values, "name") ?? "" })
|
|
3686
|
+
build: (values) => generate(settingsStore, { name: stringValue(values, "name") ?? "" }),
|
|
3687
|
+
amend: (values) => settingsStoreAmendments(
|
|
3688
|
+
{ name: stringValue(values, "name") ?? "" },
|
|
3689
|
+
stringValue(values, "directory")
|
|
3690
|
+
)
|
|
3401
3691
|
};
|
|
3402
3692
|
|
|
3693
|
+
// ../devkit/src/recipes/theme/amendments.ts
|
|
3694
|
+
var SHELL_STYLES = "@loomweaver/shell/styles/";
|
|
3695
|
+
function themeAmendments(input, where) {
|
|
3696
|
+
if (where === void 0 || where === "") {
|
|
3697
|
+
return [];
|
|
3698
|
+
}
|
|
3699
|
+
const theme2 = resolveThemeInput(input);
|
|
3700
|
+
return [
|
|
3701
|
+
{
|
|
3702
|
+
kind: "stylesheet-import",
|
|
3703
|
+
file: joinProjectPath(normalizeProjectRoot(where), `${theme2.name}.css`),
|
|
3704
|
+
after: SHELL_STYLES,
|
|
3705
|
+
without: "Without it the theme is never loaded and changes nothing."
|
|
3706
|
+
}
|
|
3707
|
+
];
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3403
3710
|
// ../devkit/src/recipes/theme/scaffold.ts
|
|
3711
|
+
function themeInput(values) {
|
|
3712
|
+
return {
|
|
3713
|
+
name: stringValue(values, "name") ?? "",
|
|
3714
|
+
preset: stringValue(values, "preset") ?? "literal"
|
|
3715
|
+
};
|
|
3716
|
+
}
|
|
3404
3717
|
var themeScaffold = {
|
|
3405
3718
|
name: "theme",
|
|
3406
3719
|
summary: "a token-override stylesheet in @layer lw-tenant-theme",
|
|
@@ -3419,12 +3732,11 @@ var themeScaffold = {
|
|
|
3419
3732
|
choices: THEME_PRESETS,
|
|
3420
3733
|
default: "literal"
|
|
3421
3734
|
},
|
|
3422
|
-
APP_OPTION
|
|
3735
|
+
APP_OPTION,
|
|
3736
|
+
DIRECTORY_OPTION
|
|
3423
3737
|
],
|
|
3424
|
-
build: (values) => generate(theme,
|
|
3425
|
-
|
|
3426
|
-
preset: stringValue(values, "preset") ?? "literal"
|
|
3427
|
-
})
|
|
3738
|
+
build: (values) => generate(theme, themeInput(values)),
|
|
3739
|
+
amend: (values) => themeAmendments(themeInput(values), stringValue(values, "directory"))
|
|
3428
3740
|
};
|
|
3429
3741
|
|
|
3430
3742
|
// ../devkit/src/lib/scaffolds/scaffolds.ts
|
|
@@ -3493,30 +3805,11 @@ function validateI18nParity(bundles) {
|
|
|
3493
3805
|
return findings;
|
|
3494
3806
|
}
|
|
3495
3807
|
|
|
3496
|
-
// ../devkit/src/lib/validate/catalog.ts
|
|
3497
|
-
var CATALOG_ENTRY_KEYS = [
|
|
3498
|
-
"id",
|
|
3499
|
-
"name",
|
|
3500
|
-
"entryUrl",
|
|
3501
|
-
"capabilities",
|
|
3502
|
-
"version",
|
|
3503
|
-
"iconUrl",
|
|
3504
|
-
"description",
|
|
3505
|
-
"icon",
|
|
3506
|
-
"category",
|
|
3507
|
-
"author",
|
|
3508
|
-
"downloads",
|
|
3509
|
-
"updated",
|
|
3510
|
-
"repository",
|
|
3511
|
-
"readmeUrl"
|
|
3512
|
-
];
|
|
3808
|
+
// ../devkit/src/lib/validate/catalog-urls.ts
|
|
3513
3809
|
var SAME_ORIGIN_FIELDS = ["entryUrl", "iconUrl", "readmeUrl"];
|
|
3514
3810
|
function pathOf(index, field) {
|
|
3515
3811
|
return field ? `catalog[${index}].${field}` : `catalog[${index}]`;
|
|
3516
3812
|
}
|
|
3517
|
-
function isPlainObject(raw) {
|
|
3518
|
-
return typeof raw === "object" && raw !== null && !Array.isArray(raw);
|
|
3519
|
-
}
|
|
3520
3813
|
function requiredUrlFinding(value, index, field) {
|
|
3521
3814
|
if (typeof value !== "string" || value.length === 0) {
|
|
3522
3815
|
return {
|
|
@@ -3540,6 +3833,14 @@ function optionalUrlFinding(value, index, field) {
|
|
|
3540
3833
|
return schemeFinding(value, index, field, "ignores the field");
|
|
3541
3834
|
}
|
|
3542
3835
|
function schemeFinding(value, index, field, refusal) {
|
|
3836
|
+
if (value.startsWith("//")) {
|
|
3837
|
+
return {
|
|
3838
|
+
level: "error",
|
|
3839
|
+
code: "catalog.url.foreign",
|
|
3840
|
+
message: `${pathOf(index, field)} is protocol-relative, so it names another host. The host accepts same-origin URLs only and ${refusal}.`,
|
|
3841
|
+
path: pathOf(index, field)
|
|
3842
|
+
};
|
|
3843
|
+
}
|
|
3543
3844
|
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(value)?.[1]?.toLowerCase();
|
|
3544
3845
|
if (scheme && scheme !== "http" && scheme !== "https") {
|
|
3545
3846
|
return {
|
|
@@ -3559,6 +3860,56 @@ function schemeFinding(value, index, field, refusal) {
|
|
|
3559
3860
|
}
|
|
3560
3861
|
return void 0;
|
|
3561
3862
|
}
|
|
3863
|
+
function validateSameOriginUrls(raw, index) {
|
|
3864
|
+
const findings = [];
|
|
3865
|
+
for (const field of SAME_ORIGIN_FIELDS) {
|
|
3866
|
+
const finding = sameOriginUrlFinding(raw, index, field);
|
|
3867
|
+
if (finding) {
|
|
3868
|
+
findings.push(finding);
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
return findings;
|
|
3872
|
+
}
|
|
3873
|
+
function sameOriginUrlFinding(raw, index, field) {
|
|
3874
|
+
if (field === "entryUrl") {
|
|
3875
|
+
return requiredUrlFinding(raw[field], index, field);
|
|
3876
|
+
}
|
|
3877
|
+
return raw[field] === void 0 ? void 0 : optionalUrlFinding(raw[field], index, field);
|
|
3878
|
+
}
|
|
3879
|
+
function isHttpUrl(raw) {
|
|
3880
|
+
if (typeof raw !== "string") {
|
|
3881
|
+
return false;
|
|
3882
|
+
}
|
|
3883
|
+
try {
|
|
3884
|
+
const url = new URL(raw);
|
|
3885
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
3886
|
+
} catch {
|
|
3887
|
+
return false;
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3890
|
+
|
|
3891
|
+
// ../devkit/src/lib/validate/catalog.ts
|
|
3892
|
+
var CATALOG_ENTRY_KEYS = [
|
|
3893
|
+
"id",
|
|
3894
|
+
"name",
|
|
3895
|
+
"entryUrl",
|
|
3896
|
+
"capabilities",
|
|
3897
|
+
"version",
|
|
3898
|
+
"iconUrl",
|
|
3899
|
+
"description",
|
|
3900
|
+
"icon",
|
|
3901
|
+
"category",
|
|
3902
|
+
"author",
|
|
3903
|
+
"downloads",
|
|
3904
|
+
"updated",
|
|
3905
|
+
"repository",
|
|
3906
|
+
"readmeUrl",
|
|
3907
|
+
"level",
|
|
3908
|
+
"deployed"
|
|
3909
|
+
];
|
|
3910
|
+
function isPlainObject(raw) {
|
|
3911
|
+
return typeof raw === "object" && raw !== null && !Array.isArray(raw);
|
|
3912
|
+
}
|
|
3562
3913
|
function validateCapabilities2(value, index, known) {
|
|
3563
3914
|
if (value === void 0) {
|
|
3564
3915
|
return [
|
|
@@ -3623,22 +3974,6 @@ function validateId(raw, index) {
|
|
|
3623
3974
|
}
|
|
3624
3975
|
];
|
|
3625
3976
|
}
|
|
3626
|
-
function validateSameOriginUrls(raw, index) {
|
|
3627
|
-
const findings = [];
|
|
3628
|
-
for (const field of SAME_ORIGIN_FIELDS) {
|
|
3629
|
-
const finding = sameOriginUrlFinding(raw, index, field);
|
|
3630
|
-
if (finding) {
|
|
3631
|
-
findings.push(finding);
|
|
3632
|
-
}
|
|
3633
|
-
}
|
|
3634
|
-
return findings;
|
|
3635
|
-
}
|
|
3636
|
-
function sameOriginUrlFinding(raw, index, field) {
|
|
3637
|
-
if (field === "entryUrl") {
|
|
3638
|
-
return requiredUrlFinding(raw[field], index, field);
|
|
3639
|
-
}
|
|
3640
|
-
return raw[field] === void 0 ? void 0 : optionalUrlFinding(raw[field], index, field);
|
|
3641
|
-
}
|
|
3642
3977
|
function validateNameAndVersion(raw, index) {
|
|
3643
3978
|
const findings = [];
|
|
3644
3979
|
if (raw["name"] === void 0) {
|
|
@@ -3666,8 +4001,28 @@ function validateNameAndVersion(raw, index) {
|
|
|
3666
4001
|
}
|
|
3667
4002
|
return findings;
|
|
3668
4003
|
}
|
|
4004
|
+
var TEXT_FIELDS = [
|
|
4005
|
+
{ field: "description", loss: "the store shows no description" },
|
|
4006
|
+
{ field: "icon", loss: "the install dialog shows the default icon" },
|
|
4007
|
+
{ field: "category", loss: "the entry carries no category badge and the search cannot match one" },
|
|
4008
|
+
{ field: "author", loss: "the store names no author" },
|
|
4009
|
+
{
|
|
4010
|
+
field: "version",
|
|
4011
|
+
loss: "update detection has nothing to compare, so the store can never offer an update"
|
|
4012
|
+
}
|
|
4013
|
+
];
|
|
4014
|
+
function validateTextFields(raw, index) {
|
|
4015
|
+
return TEXT_FIELDS.filter(
|
|
4016
|
+
({ field }) => raw[field] !== void 0 && (typeof raw[field] !== "string" || raw[field] === "")
|
|
4017
|
+
).map(({ field, loss }) => ({
|
|
4018
|
+
level: "warning",
|
|
4019
|
+
code: "catalog.text",
|
|
4020
|
+
message: `${pathOf(index, field)} is not a non-empty string, so the host drops it and ${loss}.`,
|
|
4021
|
+
path: pathOf(index, field)
|
|
4022
|
+
}));
|
|
4023
|
+
}
|
|
3669
4024
|
function validateEntryMetadata(raw, index) {
|
|
3670
|
-
const findings = [];
|
|
4025
|
+
const findings = [...validateTextFields(raw, index)];
|
|
3671
4026
|
if (raw["downloads"] !== void 0 && (typeof raw["downloads"] !== "number" || raw["downloads"] < 0)) {
|
|
3672
4027
|
findings.push({
|
|
3673
4028
|
level: "warning",
|
|
@@ -3676,7 +4031,14 @@ function validateEntryMetadata(raw, index) {
|
|
|
3676
4031
|
path: pathOf(index, "downloads")
|
|
3677
4032
|
});
|
|
3678
4033
|
}
|
|
3679
|
-
if (raw["updated"] !== void 0 &&
|
|
4034
|
+
if (raw["updated"] !== void 0 && (typeof raw["updated"] !== "string" || raw["updated"] === "")) {
|
|
4035
|
+
findings.push({
|
|
4036
|
+
level: "warning",
|
|
4037
|
+
code: "catalog.updated",
|
|
4038
|
+
message: `${pathOf(index, "updated")} is not a non-empty string, so the host drops it and the store shows no date.`,
|
|
4039
|
+
path: pathOf(index, "updated")
|
|
4040
|
+
});
|
|
4041
|
+
} else if (raw["updated"] !== void 0 && !isRenderableDate(raw["updated"])) {
|
|
3680
4042
|
findings.push({
|
|
3681
4043
|
level: "warning",
|
|
3682
4044
|
code: "catalog.updated",
|
|
@@ -3684,6 +4046,7 @@ function validateEntryMetadata(raw, index) {
|
|
|
3684
4046
|
path: pathOf(index, "updated")
|
|
3685
4047
|
});
|
|
3686
4048
|
}
|
|
4049
|
+
findings.push(...validatePlacement(raw, index));
|
|
3687
4050
|
if (raw["repository"] !== void 0 && !isHttpUrl(raw["repository"])) {
|
|
3688
4051
|
findings.push({
|
|
3689
4052
|
level: "warning",
|
|
@@ -3694,6 +4057,26 @@ function validateEntryMetadata(raw, index) {
|
|
|
3694
4057
|
}
|
|
3695
4058
|
return findings;
|
|
3696
4059
|
}
|
|
4060
|
+
function validatePlacement(raw, index) {
|
|
4061
|
+
const findings = [];
|
|
4062
|
+
if (raw["deployed"] !== void 0 && typeof raw["deployed"] !== "boolean") {
|
|
4063
|
+
findings.push({
|
|
4064
|
+
level: "warning",
|
|
4065
|
+
code: "catalog.deployed",
|
|
4066
|
+
message: `${pathOf(index, "deployed")} must be true or false. The host deploys an entry only when it is exactly true, so this one is merely offered.`,
|
|
4067
|
+
path: pathOf(index, "deployed")
|
|
4068
|
+
});
|
|
4069
|
+
}
|
|
4070
|
+
if (raw["level"] !== void 0 && raw["level"] !== "embedded" && raw["level"] !== "isolated") {
|
|
4071
|
+
findings.push({
|
|
4072
|
+
level: "warning",
|
|
4073
|
+
code: "catalog.level",
|
|
4074
|
+
message: `${pathOf(index, "level")} must be "embedded" or "isolated". The host ignores anything else and runs the plugin at the level the distribution chooses.`,
|
|
4075
|
+
path: pathOf(index, "level")
|
|
4076
|
+
});
|
|
4077
|
+
}
|
|
4078
|
+
return findings;
|
|
4079
|
+
}
|
|
3697
4080
|
function validateEntryKeys(raw, index) {
|
|
3698
4081
|
const findings = [];
|
|
3699
4082
|
for (const key of Object.keys(raw)) {
|
|
@@ -3708,16 +4091,11 @@ function validateEntryKeys(raw, index) {
|
|
|
3708
4091
|
}
|
|
3709
4092
|
return findings;
|
|
3710
4093
|
}
|
|
3711
|
-
function
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
const url = new URL(raw);
|
|
3717
|
-
return url.protocol === "http:" || url.protocol === "https:";
|
|
3718
|
-
} catch {
|
|
3719
|
-
return false;
|
|
3720
|
-
}
|
|
4094
|
+
function dropsEntry(findings, index) {
|
|
4095
|
+
const fatal = /* @__PURE__ */ new Set([pathOf(index), pathOf(index, "id"), pathOf(index, "entryUrl")]);
|
|
4096
|
+
return findings.some(
|
|
4097
|
+
(finding) => finding.level === "error" && finding.path !== void 0 && fatal.has(finding.path)
|
|
4098
|
+
);
|
|
3721
4099
|
}
|
|
3722
4100
|
function isRenderableDate(raw) {
|
|
3723
4101
|
return typeof raw === "string" && !Number.isNaN(new Date(raw).getTime());
|
|
@@ -3734,20 +4112,24 @@ function validateCatalog(catalog, known = KNOWN_CAPABILITIES) {
|
|
|
3734
4112
|
];
|
|
3735
4113
|
}
|
|
3736
4114
|
const findings = [];
|
|
3737
|
-
const seen = /* @__PURE__ */ new
|
|
4115
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3738
4116
|
for (const [index, entry] of catalog.entries()) {
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
if (
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
4117
|
+
const own = validateEntry(entry, index, known);
|
|
4118
|
+
findings.push(...own);
|
|
4119
|
+
if (!isPlainObject(entry) || dropsEntry(own, index)) {
|
|
4120
|
+
continue;
|
|
4121
|
+
}
|
|
4122
|
+
const id = entry["id"];
|
|
4123
|
+
const first = seen.get(id);
|
|
4124
|
+
if (first === void 0) {
|
|
4125
|
+
seen.set(id, index);
|
|
4126
|
+
} else {
|
|
4127
|
+
findings.push({
|
|
4128
|
+
level: "warning",
|
|
4129
|
+
code: "catalog.id.duplicate",
|
|
4130
|
+
message: `${pathOf(index, "id")} repeats "${id}" from ${pathOf(first)}. The host keeps the first usable entry with an id and drops this one.`,
|
|
4131
|
+
path: pathOf(index, "id")
|
|
4132
|
+
});
|
|
3751
4133
|
}
|
|
3752
4134
|
}
|
|
3753
4135
|
return findings;
|
|
@@ -3758,26 +4140,100 @@ var RUNTIME_NOTE = "Grants, access and the window decide what is finally offered
|
|
|
3758
4140
|
function validateCommands(sources, ts) {
|
|
3759
4141
|
const findings = [];
|
|
3760
4142
|
for (const source of sources) {
|
|
3761
|
-
const file = ts.createSourceFile(source.path, source.text, ts.ScriptTarget.Latest, true, ts.
|
|
4143
|
+
const file = ts.createSourceFile(source.path, source.text, ts.ScriptTarget.Latest, true, scriptKind(ts, source.path));
|
|
4144
|
+
const constants = stringConstants(ts, file);
|
|
3762
4145
|
for (const call of registerCommandCalls(ts, file)) {
|
|
3763
|
-
findings.push(...findingsFor(readRegistration(ts, file, call)));
|
|
4146
|
+
findings.push(...findingsFor(readRegistration(ts, file, call, constants)));
|
|
3764
4147
|
}
|
|
3765
4148
|
}
|
|
3766
4149
|
findings.push({ level: "info", code: "commands.runtime", message: RUNTIME_NOTE });
|
|
3767
4150
|
return findings;
|
|
3768
4151
|
}
|
|
4152
|
+
var REGISTER = "registerCommand";
|
|
4153
|
+
function scriptKind(ts, path) {
|
|
4154
|
+
if (/\.[cm]?js$/.test(path)) {
|
|
4155
|
+
return ts.ScriptKind.JS;
|
|
4156
|
+
}
|
|
4157
|
+
if (path.endsWith(".jsx")) {
|
|
4158
|
+
return ts.ScriptKind.JSX;
|
|
4159
|
+
}
|
|
4160
|
+
return path.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
4161
|
+
}
|
|
4162
|
+
function walk(ts, file, visit) {
|
|
4163
|
+
const step = (node) => {
|
|
4164
|
+
visit(node);
|
|
4165
|
+
ts.forEachChild(node, step);
|
|
4166
|
+
};
|
|
4167
|
+
step(file);
|
|
4168
|
+
}
|
|
4169
|
+
function stringConstants(ts, file) {
|
|
4170
|
+
const constants = /* @__PURE__ */ new Map();
|
|
4171
|
+
walk(ts, file, (node) => {
|
|
4172
|
+
if (ts.isVariableDeclarationList(node) && (node.flags & ts.NodeFlags.Const) !== 0) {
|
|
4173
|
+
for (const declaration of node.declarations) {
|
|
4174
|
+
const value = unwrapped(ts, declaration.initializer);
|
|
4175
|
+
if (ts.isIdentifier(declaration.name) && value && ts.isStringLiteralLike(value)) {
|
|
4176
|
+
constants.set(declaration.name.text, value.text);
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
});
|
|
4181
|
+
return constants;
|
|
4182
|
+
}
|
|
4183
|
+
function registerAliases(ts, file) {
|
|
4184
|
+
const aliases = /* @__PURE__ */ new Set([REGISTER]);
|
|
4185
|
+
walk(ts, file, (node) => {
|
|
4186
|
+
if (ts.isBindingElement(node) && ts.isIdentifier(node.name)) {
|
|
4187
|
+
const property = node.propertyName ?? node.name;
|
|
4188
|
+
if (ts.isIdentifier(property) && property.text === REGISTER) {
|
|
4189
|
+
aliases.add(node.name.text);
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && namesRegister(ts, node.initializer)) {
|
|
4193
|
+
aliases.add(node.name.text);
|
|
4194
|
+
}
|
|
4195
|
+
});
|
|
4196
|
+
return aliases;
|
|
4197
|
+
}
|
|
4198
|
+
function namesRegister(ts, value) {
|
|
4199
|
+
let target = unwrapped(ts, value);
|
|
4200
|
+
while (target && ts.isCallExpression(target) && ts.isPropertyAccessExpression(target.expression) && target.expression.name.text === "bind") {
|
|
4201
|
+
target = unwrapped(ts, target.expression.expression);
|
|
4202
|
+
}
|
|
4203
|
+
return !!target && ts.isPropertyAccessExpression(target) && target.name.text === REGISTER;
|
|
4204
|
+
}
|
|
3769
4205
|
function registerCommandCalls(ts, file) {
|
|
4206
|
+
const aliases = registerAliases(ts, file);
|
|
3770
4207
|
const calls = [];
|
|
3771
|
-
|
|
3772
|
-
if (ts.isCallExpression(node)
|
|
4208
|
+
walk(ts, file, (node) => {
|
|
4209
|
+
if (!ts.isCallExpression(node)) {
|
|
4210
|
+
return;
|
|
4211
|
+
}
|
|
4212
|
+
const callee = node.expression;
|
|
4213
|
+
if (ts.isPropertyAccessExpression(callee) && callee.name.text === REGISTER || ts.isIdentifier(callee) && aliases.has(callee.text)) {
|
|
3773
4214
|
calls.push(node);
|
|
3774
4215
|
}
|
|
3775
|
-
|
|
3776
|
-
};
|
|
3777
|
-
visit(file);
|
|
4216
|
+
});
|
|
3778
4217
|
return calls;
|
|
3779
4218
|
}
|
|
3780
|
-
function
|
|
4219
|
+
function stringOf(ts, value, constants) {
|
|
4220
|
+
const node = unwrapped(ts, value);
|
|
4221
|
+
if (!node) {
|
|
4222
|
+
return void 0;
|
|
4223
|
+
}
|
|
4224
|
+
if (ts.isStringLiteralLike(node)) {
|
|
4225
|
+
return node.text;
|
|
4226
|
+
}
|
|
4227
|
+
return ts.isIdentifier(node) ? constants.get(node.text) : void 0;
|
|
4228
|
+
}
|
|
4229
|
+
function isDeclared(ts, value) {
|
|
4230
|
+
const node = unwrapped(ts, value);
|
|
4231
|
+
if (!node) {
|
|
4232
|
+
return false;
|
|
4233
|
+
}
|
|
4234
|
+
return !(ts.isIdentifier(node) && node.text === "undefined" || ts.isVoidExpression(node));
|
|
4235
|
+
}
|
|
4236
|
+
function readRegistration(ts, file, call, constants) {
|
|
3781
4237
|
const line = file.getLineAndCharacterOfPosition(call.getStart(file)).line + 1;
|
|
3782
4238
|
const at = `${file.fileName}:${line}`;
|
|
3783
4239
|
const literal = call.arguments[0];
|
|
@@ -3792,31 +4248,28 @@ function readRegistration(ts, file, call) {
|
|
|
3792
4248
|
}
|
|
3793
4249
|
if (ts.isPropertyAssignment(property) && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))) {
|
|
3794
4250
|
properties.set(property.name.text, property.initializer);
|
|
4251
|
+
} else if (ts.isShorthandPropertyAssignment(property)) {
|
|
4252
|
+
properties.set(property.name.text, property.name);
|
|
3795
4253
|
} else if (ts.isMethodDeclaration(property) && ts.isIdentifier(property.name)) {
|
|
3796
4254
|
properties.set(property.name.text, property);
|
|
3797
4255
|
}
|
|
3798
4256
|
}
|
|
3799
|
-
const id = properties.get("id");
|
|
3800
|
-
if (
|
|
3801
|
-
return { ...empty, unreadable: "its id is
|
|
4257
|
+
const id = stringOf(ts, properties.get("id"), constants);
|
|
4258
|
+
if (id === void 0) {
|
|
4259
|
+
return { ...empty, unreadable: "its id is neither a string literal nor a string constant declared in the file" };
|
|
3802
4260
|
}
|
|
3803
|
-
const
|
|
3804
|
-
const consent = properties.get("agentConsent");
|
|
4261
|
+
const consent = stringOf(ts, properties.get("agentConsent"), constants);
|
|
3805
4262
|
return {
|
|
3806
4263
|
at,
|
|
3807
|
-
id
|
|
3808
|
-
callable: callable?.kind === ts.SyntaxKind.TrueKeyword,
|
|
3809
|
-
described: properties.
|
|
3810
|
-
answers: properties.
|
|
4264
|
+
id,
|
|
4265
|
+
callable: unwrapped(ts, properties.get("callable"))?.kind === ts.SyntaxKind.TrueKeyword,
|
|
4266
|
+
described: isDeclared(ts, properties.get("description")),
|
|
4267
|
+
answers: isDeclared(ts, properties.get("answers")),
|
|
3811
4268
|
returnsValue: returnsValue(ts, properties.get("run")),
|
|
3812
|
-
arguments: readArguments(ts, properties.get("arguments")),
|
|
3813
|
-
...
|
|
4269
|
+
arguments: readArguments(ts, properties.get("arguments"), constants),
|
|
4270
|
+
...consent !== void 0 && { agentConsent: consent }
|
|
3814
4271
|
};
|
|
3815
4272
|
}
|
|
3816
|
-
function consentOf(ts, declared) {
|
|
3817
|
-
const value = unwrapped(ts, declared);
|
|
3818
|
-
return value && ts.isStringLiteral(value) ? { agentConsent: value.text } : {};
|
|
3819
|
-
}
|
|
3820
4273
|
function unwrapped(ts, value) {
|
|
3821
4274
|
if (!value) {
|
|
3822
4275
|
return void 0;
|
|
@@ -3845,7 +4298,8 @@ function consentLine(consent) {
|
|
|
3845
4298
|
}
|
|
3846
4299
|
return CONSENT_LINES.get(consent) ?? `declares "${consent}" about an agent's word, which is none of ${STATED_CONSENTS.join(", ")}, so nothing reads it.`;
|
|
3847
4300
|
}
|
|
3848
|
-
function readArguments(ts,
|
|
4301
|
+
function readArguments(ts, declared, constants) {
|
|
4302
|
+
const value = unwrapped(ts, declared);
|
|
3849
4303
|
if (!value || !ts.isArrayLiteralExpression(value)) {
|
|
3850
4304
|
return [];
|
|
3851
4305
|
}
|
|
@@ -3859,17 +4313,17 @@ function readArguments(ts, value) {
|
|
|
3859
4313
|
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
|
|
3860
4314
|
continue;
|
|
3861
4315
|
}
|
|
3862
|
-
if (property.name.text === "name"
|
|
3863
|
-
name = property.initializer
|
|
3864
|
-
}
|
|
3865
|
-
|
|
3866
|
-
described = true;
|
|
4316
|
+
if (property.name.text === "name") {
|
|
4317
|
+
name = stringOf(ts, property.initializer, constants) ?? name;
|
|
4318
|
+
} else if (property.name.text === "description") {
|
|
4319
|
+
described = isDeclared(ts, property.initializer);
|
|
3867
4320
|
}
|
|
3868
4321
|
}
|
|
3869
4322
|
return { name, described };
|
|
3870
4323
|
});
|
|
3871
4324
|
}
|
|
3872
|
-
function returnsValue(ts,
|
|
4325
|
+
function returnsValue(ts, declared) {
|
|
4326
|
+
const run2 = unwrapped(ts, declared);
|
|
3873
4327
|
if (!run2) {
|
|
3874
4328
|
return false;
|
|
3875
4329
|
}
|
|
@@ -3979,32 +4433,20 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
3979
4433
|
import { dirname, relative, resolve as resolve2, sep } from "node:path";
|
|
3980
4434
|
|
|
3981
4435
|
// src/lib/angular-config.ts
|
|
3982
|
-
function asObject2(value) {
|
|
3983
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
3984
|
-
}
|
|
3985
4436
|
function buildProjects(config) {
|
|
3986
|
-
const projects =
|
|
4437
|
+
const projects = asObject(asObject(config)?.["projects"]) ?? {};
|
|
3987
4438
|
return Object.entries(projects).filter(([, project]) => buildTargetIn(project) !== void 0).map(([name, project]) => ({
|
|
3988
4439
|
name,
|
|
3989
|
-
root: normaliseRoot(
|
|
3990
|
-
prefix: declaredPrefix(
|
|
4440
|
+
root: normaliseRoot(asObject(project)?.["root"]),
|
|
4441
|
+
prefix: declaredPrefix(asObject(project)?.["prefix"])
|
|
3991
4442
|
}));
|
|
3992
4443
|
}
|
|
3993
4444
|
function buildTargetOf(config, project) {
|
|
3994
|
-
return buildTargetIn(
|
|
3995
|
-
}
|
|
3996
|
-
function entryStylesheetOf(target) {
|
|
3997
|
-
const styles = asObject2(asObject2(target)?.["options"])?.["styles"];
|
|
3998
|
-
if (!Array.isArray(styles)) {
|
|
3999
|
-
return void 0;
|
|
4000
|
-
}
|
|
4001
|
-
return styles.map((style) => typeof style === "string" ? style : asObject2(style)?.["input"]).find(
|
|
4002
|
-
(input) => typeof input === "string" && input.endsWith(".css")
|
|
4003
|
-
);
|
|
4445
|
+
return buildTargetIn(asObject(asObject(config)?.["projects"])?.[project]);
|
|
4004
4446
|
}
|
|
4005
4447
|
function buildTargetIn(project) {
|
|
4006
4448
|
for (const key of ["architect", "targets"]) {
|
|
4007
|
-
const targets =
|
|
4449
|
+
const targets = asObject(asObject(project)?.[key]);
|
|
4008
4450
|
if (targets?.["build"] !== void 0) {
|
|
4009
4451
|
return {
|
|
4010
4452
|
value: targets["build"],
|
|
@@ -4247,7 +4689,7 @@ import { basename, join, relative as relative3, sep as sep2 } from "node:path";
|
|
|
4247
4689
|
var SKIPPED = /* @__PURE__ */ new Set(["node_modules", "dist", "tmp", "coverage"]);
|
|
4248
4690
|
function nxApplications(workspaceRoot) {
|
|
4249
4691
|
const found = [];
|
|
4250
|
-
const
|
|
4692
|
+
const walk2 = (folder) => {
|
|
4251
4693
|
const project = join(folder, "project.json");
|
|
4252
4694
|
if (existsSync2(project)) {
|
|
4253
4695
|
const config = readJsonFile(project);
|
|
@@ -4263,10 +4705,10 @@ function nxApplications(workspaceRoot) {
|
|
|
4263
4705
|
if (!entry.isDirectory() || SKIPPED.has(entry.name) || entry.name.startsWith(".")) {
|
|
4264
4706
|
continue;
|
|
4265
4707
|
}
|
|
4266
|
-
|
|
4708
|
+
walk2(join(folder, entry.name));
|
|
4267
4709
|
}
|
|
4268
4710
|
};
|
|
4269
|
-
|
|
4711
|
+
walk2(workspaceRoot);
|
|
4270
4712
|
return found.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
4271
4713
|
}
|
|
4272
4714
|
|
|
@@ -4328,10 +4770,12 @@ function runScriptCommand(manager, script) {
|
|
|
4328
4770
|
}
|
|
4329
4771
|
|
|
4330
4772
|
// src/lib/init/init-plan.ts
|
|
4331
|
-
var
|
|
4773
|
+
var PLATFORM_PACKAGES = [
|
|
4332
4774
|
"@loomweaver/shell",
|
|
4333
4775
|
"@loomweaver/plugin-sdk",
|
|
4334
|
-
"@loomweaver/frame-kit"
|
|
4776
|
+
"@loomweaver/frame-kit"
|
|
4777
|
+
];
|
|
4778
|
+
var PEER_PACKAGES = [
|
|
4335
4779
|
"@angular/cdk",
|
|
4336
4780
|
"@jsverse/transloco",
|
|
4337
4781
|
"@ng-icons/heroicons"
|
|
@@ -4358,6 +4802,12 @@ function planInit(args, deps) {
|
|
|
4358
4802
|
const app = workspace.kind === "nx" ? chooseApplication(workspace.root, stringFlag(args, "app")) : void 0;
|
|
4359
4803
|
const name = app?.name ?? packageNameToId(manifest2.name ?? basename2(workspace.root));
|
|
4360
4804
|
const styles = stringFlag(args, "styles") ?? "tailwind";
|
|
4805
|
+
const title = stringFlag(args, "title") ?? toTitleCase(name);
|
|
4806
|
+
const weaver = weaverId(args);
|
|
4807
|
+
resolveDistributionInput({ name, title, styles });
|
|
4808
|
+
if (weaver !== void 0) {
|
|
4809
|
+
resolveWeaverInput({ id: weaver });
|
|
4810
|
+
}
|
|
4361
4811
|
const declared = { ...manifest2.dependencies, ...manifest2.devDependencies };
|
|
4362
4812
|
const missing = (names) => names.filter((candidate) => !Object.hasOwn(declared, candidate));
|
|
4363
4813
|
return {
|
|
@@ -4365,16 +4815,17 @@ function planInit(args, deps) {
|
|
|
4365
4815
|
manager: override ?? detected.manager,
|
|
4366
4816
|
lockfile: override ? void 0 : detected.lockfile,
|
|
4367
4817
|
name,
|
|
4368
|
-
title
|
|
4818
|
+
title,
|
|
4369
4819
|
styles,
|
|
4370
|
-
weaver
|
|
4820
|
+
weaver,
|
|
4371
4821
|
runtime: [
|
|
4372
|
-
...missing(
|
|
4822
|
+
...missing(PLATFORM_PACKAGES).map((pkg) => platformSpec(pkg, deps.version)),
|
|
4823
|
+
...missing(PEER_PACKAGES),
|
|
4373
4824
|
...Object.hasOwn(declared, SERVICE_WORKER) ? [] : [serviceWorkerSpec(workspace.root, manifest2)]
|
|
4374
4825
|
],
|
|
4375
4826
|
dev: [
|
|
4376
4827
|
...styles === "tailwind" ? missing(STYLE_PACKAGES) : [],
|
|
4377
|
-
...workspace.kind === "nx" && !Object.hasOwn(declared, NX_COLLECTION) ? [
|
|
4828
|
+
...workspace.kind === "nx" && !Object.hasOwn(declared, NX_COLLECTION) ? [platformSpec(NX_COLLECTION, deps.version)] : []
|
|
4378
4829
|
],
|
|
4379
4830
|
app,
|
|
4380
4831
|
dryRun: boolFlag(args, "dry-run") === true
|
|
@@ -4430,8 +4881,8 @@ function serviceWorkerSpec(root, manifest2) {
|
|
|
4430
4881
|
const declared = manifest2.dependencies?.["@angular/core"];
|
|
4431
4882
|
return declared ? `${SERVICE_WORKER}@${declared}` : SERVICE_WORKER;
|
|
4432
4883
|
}
|
|
4433
|
-
function
|
|
4434
|
-
return version === UNBUNDLED_VERSION ?
|
|
4884
|
+
function platformSpec(name, version) {
|
|
4885
|
+
return `${name}@${version === UNBUNDLED_VERSION ? "latest" : version}`;
|
|
4435
4886
|
}
|
|
4436
4887
|
function packageNameToId(value) {
|
|
4437
4888
|
const unscoped = value.includes("/") ? value.slice(value.lastIndexOf("/") + 1) : value;
|
|
@@ -4673,7 +5124,7 @@ var AmendLog = class {
|
|
|
4673
5124
|
|
|
4674
5125
|
// src/lib/scaffold/project-wiring.ts
|
|
4675
5126
|
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
|
|
4676
|
-
import { dirname as dirname2, posix, resolve as resolve6 } from "node:path";
|
|
5127
|
+
import { dirname as dirname2, posix as posix4, resolve as resolve6 } from "node:path";
|
|
4677
5128
|
var ProjectWiring = class {
|
|
4678
5129
|
constructor(workspace, target, log) {
|
|
4679
5130
|
this.workspace = workspace;
|
|
@@ -4686,6 +5137,7 @@ var ProjectWiring = class {
|
|
|
4686
5137
|
configAdded = [];
|
|
4687
5138
|
config;
|
|
4688
5139
|
projectResolved = false;
|
|
5140
|
+
pendingCss = /* @__PURE__ */ new Map();
|
|
4689
5141
|
project;
|
|
4690
5142
|
plan(amendment) {
|
|
4691
5143
|
const project = this.resolveProject();
|
|
@@ -4701,8 +5153,16 @@ var ProjectWiring = class {
|
|
|
4701
5153
|
this.planStylesheetSource(amendment, project);
|
|
4702
5154
|
return;
|
|
4703
5155
|
}
|
|
5156
|
+
case "stylesheet-import": {
|
|
5157
|
+
this.planStylesheetImport(amendment, project);
|
|
5158
|
+
return;
|
|
5159
|
+
}
|
|
4704
5160
|
case "compose-plugin": {
|
|
4705
5161
|
this.planComposePlugin(amendment, project);
|
|
5162
|
+
return;
|
|
5163
|
+
}
|
|
5164
|
+
case "compose-provider": {
|
|
5165
|
+
this.planComposeProviders(amendment, project);
|
|
4706
5166
|
}
|
|
4707
5167
|
}
|
|
4708
5168
|
}
|
|
@@ -4745,7 +5205,7 @@ var ProjectWiring = class {
|
|
|
4745
5205
|
if (!usesTailwind(css)) {
|
|
4746
5206
|
return;
|
|
4747
5207
|
}
|
|
4748
|
-
const source =
|
|
5208
|
+
const source = posix4.relative(this.log.displayName(dirname2(entry)), amendment.sourceRoot) || ".";
|
|
4749
5209
|
const next = ensureStylesheetSource(css, source);
|
|
4750
5210
|
if (next === css) {
|
|
4751
5211
|
return;
|
|
@@ -4780,9 +5240,58 @@ var ProjectWiring = class {
|
|
|
4780
5240
|
[
|
|
4781
5241
|
`${amendment.symbol}, its translations and its capability grants`,
|
|
4782
5242
|
...(amendment.providers ?? []).filter((provider) => !result.kept.includes(provider.line)).map((provider) => provider.line.replace(/,$/, "")),
|
|
4783
|
-
...result.kept.map(
|
|
4784
|
-
|
|
4785
|
-
|
|
5243
|
+
...result.kept.map((line) => keptNote(line))
|
|
5244
|
+
],
|
|
5245
|
+
result.source
|
|
5246
|
+
);
|
|
5247
|
+
}
|
|
5248
|
+
planStylesheetImport(amendment, project) {
|
|
5249
|
+
const entry = this.entryStylesheet(project);
|
|
5250
|
+
if (!entry || !existsSync6(entry)) {
|
|
5251
|
+
this.log.note(
|
|
5252
|
+
`No entry stylesheet is wired for ${project.name}, so add it yourself: ${describeAmendment(amendment)}`
|
|
5253
|
+
);
|
|
5254
|
+
return;
|
|
5255
|
+
}
|
|
5256
|
+
const css = this.pendingCss.get(entry) ?? readFileSync4(entry, "utf8");
|
|
5257
|
+
const displayed = this.log.displayName(entry);
|
|
5258
|
+
const result = ensureStylesheetImport(
|
|
5259
|
+
css,
|
|
5260
|
+
importSpecifier(displayed, amendment.file),
|
|
5261
|
+
amendment.after
|
|
5262
|
+
);
|
|
5263
|
+
if (!result.anchored) {
|
|
5264
|
+
this.log.note(
|
|
5265
|
+
`${displayed} does not import ${amendment.after}, so ${amendment.file} was imported after its last import instead; check that it still comes after the shell's styles.`
|
|
5266
|
+
);
|
|
5267
|
+
}
|
|
5268
|
+
if (result.css === css) {
|
|
5269
|
+
return;
|
|
5270
|
+
}
|
|
5271
|
+
this.pendingCss.set(entry, result.css);
|
|
5272
|
+
this.log.plan(entry, [`@import '${importSpecifier(displayed, amendment.file)}'`], result.css);
|
|
5273
|
+
}
|
|
5274
|
+
planComposeProviders(amendment, project) {
|
|
5275
|
+
const root = resolve6(this.workspace.root, project.root, "src/app/app.config.ts");
|
|
5276
|
+
const importPath = moduleImport(this.log.displayName(dirname2(root)), amendment.module);
|
|
5277
|
+
const source = existsSync6(root) ? readFileSync4(root, "utf8") : void 0;
|
|
5278
|
+
const result = source === void 0 ? void 0 : composeProviders(source, amendment, importPath);
|
|
5279
|
+
if (!result?.composed) {
|
|
5280
|
+
this.log.note(
|
|
5281
|
+
`The composition root no longer presents the shape this scaffold generated, so these were NOT added. ${amendment.without} Add them yourself: ${providerLines(amendment, importPath).join(" ")}`
|
|
5282
|
+
);
|
|
5283
|
+
return;
|
|
5284
|
+
}
|
|
5285
|
+
const kept = result.kept.map((line) => keptNote(line));
|
|
5286
|
+
if (result.source === source) {
|
|
5287
|
+
this.log.note(...kept.map((note) => `${note}; replace it to use the generated one.`));
|
|
5288
|
+
return;
|
|
5289
|
+
}
|
|
5290
|
+
this.log.plan(
|
|
5291
|
+
root,
|
|
5292
|
+
[
|
|
5293
|
+
...amendment.providers.filter((provider) => !result.kept.includes(provider.line)).map((provider) => provider.line.replace(/,$/, "")),
|
|
5294
|
+
...kept
|
|
4786
5295
|
],
|
|
4787
5296
|
result.source
|
|
4788
5297
|
);
|
|
@@ -4799,37 +5308,20 @@ var ProjectWiring = class {
|
|
|
4799
5308
|
return this.project;
|
|
4800
5309
|
}
|
|
4801
5310
|
readConfig() {
|
|
4802
|
-
this.config ??=
|
|
5311
|
+
this.config ??= asObject(readJsonFile(this.workspace.configFile)) ?? {};
|
|
4803
5312
|
return this.config;
|
|
4804
5313
|
}
|
|
4805
5314
|
entryStylesheet(project) {
|
|
4806
|
-
const
|
|
4807
|
-
|
|
4808
|
-
);
|
|
5315
|
+
const target = buildTargetOf(this.readConfig(), project.name)?.value;
|
|
5316
|
+
const entry = entryStylesheet(asObject(asObject(target)?.["options"])?.["styles"]);
|
|
4809
5317
|
return entry === void 0 ? void 0 : resolve6(this.workspace.root, entry);
|
|
4810
5318
|
}
|
|
4811
5319
|
};
|
|
4812
5320
|
function composeNote(amendment, importPath) {
|
|
4813
5321
|
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(" ");
|
|
4814
5322
|
}
|
|
4815
|
-
function usesTailwind(css) {
|
|
4816
|
-
return css.split("\n").some((line) => {
|
|
4817
|
-
const directive = line.trimStart();
|
|
4818
|
-
return /^@import\s+['"]tailwindcss['"]/.test(directive) || /^@source\s/.test(directive);
|
|
4819
|
-
});
|
|
4820
|
-
}
|
|
4821
|
-
function relativeImport(fromDir, sourceRoot) {
|
|
4822
|
-
const path = posix.relative(fromDir, sourceRoot);
|
|
4823
|
-
return path.startsWith(".") ? path : `./${path}`;
|
|
4824
|
-
}
|
|
4825
5323
|
|
|
4826
5324
|
// src/lib/scaffold/amend.ts
|
|
4827
|
-
var JS_POSTCSS_CONFIGS = [
|
|
4828
|
-
"postcss.config.js",
|
|
4829
|
-
"postcss.config.mjs",
|
|
4830
|
-
"postcss.config.cjs",
|
|
4831
|
-
".postcssrc.js"
|
|
4832
|
-
];
|
|
4833
5325
|
function planAmend(amendments2, target) {
|
|
4834
5326
|
if (amendments2.length === 0) {
|
|
4835
5327
|
return { amendments: [], remaining: [] };
|
|
@@ -4894,13 +5386,12 @@ var Amender = class {
|
|
|
4894
5386
|
this.wiring.plan(amendment);
|
|
4895
5387
|
}
|
|
4896
5388
|
planPostcss(amendment) {
|
|
4897
|
-
const
|
|
4898
|
-
(name) => existsSync7(resolve7(this.workspace.root, name))
|
|
5389
|
+
const writtenAsCode = postcssWrittenAsCode(
|
|
5390
|
+
(name) => existsSync7(resolve7(this.workspace.root, name)),
|
|
5391
|
+
amendment
|
|
4899
5392
|
);
|
|
4900
|
-
if (
|
|
4901
|
-
this.log.note(
|
|
4902
|
-
`${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.`
|
|
4903
|
-
);
|
|
5393
|
+
if (writtenAsCode) {
|
|
5394
|
+
this.log.note(writtenAsCode);
|
|
4904
5395
|
return;
|
|
4905
5396
|
}
|
|
4906
5397
|
const file = resolve7(this.workspace.root, amendment.file);
|
|
@@ -5027,14 +5518,10 @@ function reportAmendments(io, amend, done) {
|
|
|
5027
5518
|
}
|
|
5028
5519
|
}
|
|
5029
5520
|
}
|
|
5521
|
+
var SCAFFOLD_FLAGS = ["out", "dry-run", "force"];
|
|
5030
5522
|
function scaffold(args, io) {
|
|
5031
5523
|
const descriptor = findScaffold2(args.command);
|
|
5032
|
-
rejectUnknownFlags(args, [
|
|
5033
|
-
...allowedFlagsFor(descriptor),
|
|
5034
|
-
"out",
|
|
5035
|
-
"dry-run",
|
|
5036
|
-
"force"
|
|
5037
|
-
]);
|
|
5524
|
+
rejectUnknownFlags(args, [...allowedFlagsFor(descriptor), ...SCAFFOLD_FLAGS]);
|
|
5038
5525
|
const values = scaffoldValues(descriptor, args);
|
|
5039
5526
|
const files = descriptor.build(values);
|
|
5040
5527
|
const out = stringFlag(args, "out") ?? ".";
|
|
@@ -5093,8 +5580,14 @@ function reportFindings(io, findings, strict) {
|
|
|
5093
5580
|
}
|
|
5094
5581
|
return strict && gating.length > 0 ? 1 : 0;
|
|
5095
5582
|
}
|
|
5583
|
+
var VALIDATE_FLAGS = {
|
|
5584
|
+
"validate-manifest": ["id", "name", "capabilities", "strict"],
|
|
5585
|
+
"validate-i18n": ["dir", "strict"],
|
|
5586
|
+
"validate-commands": ["dir", "strict"],
|
|
5587
|
+
"validate-catalog": ["file", "strict"]
|
|
5588
|
+
};
|
|
5096
5589
|
function validateManifestCommand(args, io) {
|
|
5097
|
-
rejectUnknownFlags(args, ["
|
|
5590
|
+
rejectUnknownFlags(args, VALIDATE_FLAGS["validate-manifest"]);
|
|
5098
5591
|
const capabilities = (stringFlag(args, "capabilities") ?? "").split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
5099
5592
|
return reportFindings(
|
|
5100
5593
|
io,
|
|
@@ -5127,40 +5620,45 @@ function readBundles(dir) {
|
|
|
5127
5620
|
return bundles;
|
|
5128
5621
|
}
|
|
5129
5622
|
function validateI18nCommand(args, io) {
|
|
5130
|
-
rejectUnknownFlags(args, ["
|
|
5623
|
+
rejectUnknownFlags(args, VALIDATE_FLAGS["validate-i18n"]);
|
|
5131
5624
|
return reportFindings(
|
|
5132
5625
|
io,
|
|
5133
5626
|
validateI18nParity(readBundles(requiredFlag(args, "dir"))),
|
|
5134
5627
|
boolFlag(args, "strict") === true
|
|
5135
5628
|
);
|
|
5136
5629
|
}
|
|
5630
|
+
var SOURCE = /\.(?:[cm]?[jt]s|[jt]sx)$/;
|
|
5631
|
+
var NOT_SOURCE = /\.(?:d|spec|test)\.[cm]?[jt]sx?$/;
|
|
5632
|
+
function isCommandSource(name) {
|
|
5633
|
+
return SOURCE.test(name) && !NOT_SOURCE.test(name);
|
|
5634
|
+
}
|
|
5137
5635
|
function readSources(dir) {
|
|
5138
5636
|
const sources = [];
|
|
5139
|
-
const
|
|
5637
|
+
const walk2 = (folder) => {
|
|
5140
5638
|
for (const entry of readdirSync2(folder, { withFileTypes: true })) {
|
|
5141
5639
|
if (entry.name === "node_modules" || entry.name === "dist" || entry.name.startsWith(".")) {
|
|
5142
5640
|
continue;
|
|
5143
5641
|
}
|
|
5144
5642
|
const path = join4(folder, entry.name);
|
|
5145
5643
|
if (entry.isDirectory()) {
|
|
5146
|
-
|
|
5147
|
-
} else if (
|
|
5644
|
+
walk2(path);
|
|
5645
|
+
} else if (isCommandSource(entry.name)) {
|
|
5148
5646
|
sources.push({ path, text: readFileSync5(path, "utf8") });
|
|
5149
5647
|
}
|
|
5150
5648
|
}
|
|
5151
5649
|
};
|
|
5152
5650
|
try {
|
|
5153
|
-
|
|
5651
|
+
walk2(dir);
|
|
5154
5652
|
} catch (error) {
|
|
5155
5653
|
throw new ArgError(`Cannot read ${dir}: ${error.message}`);
|
|
5156
5654
|
}
|
|
5157
5655
|
if (sources.length === 0) {
|
|
5158
|
-
throw new ArgError(`No TypeScript sources found under ${dir}.`);
|
|
5656
|
+
throw new ArgError(`No TypeScript or JavaScript sources found under ${dir}.`);
|
|
5159
5657
|
}
|
|
5160
5658
|
return sources;
|
|
5161
5659
|
}
|
|
5162
5660
|
function validateCommandsCommand(args, io) {
|
|
5163
|
-
rejectUnknownFlags(args, ["
|
|
5661
|
+
rejectUnknownFlags(args, VALIDATE_FLAGS["validate-commands"]);
|
|
5164
5662
|
const dir = requiredFlag(args, "dir");
|
|
5165
5663
|
const ts = loadTypeScript(dir);
|
|
5166
5664
|
if (!ts) {
|
|
@@ -5190,7 +5688,7 @@ function readCatalog(file) {
|
|
|
5190
5688
|
}
|
|
5191
5689
|
}
|
|
5192
5690
|
function validateCatalogCommand(args, io) {
|
|
5193
|
-
rejectUnknownFlags(args, ["
|
|
5691
|
+
rejectUnknownFlags(args, VALIDATE_FLAGS["validate-catalog"]);
|
|
5194
5692
|
return reportFindings(
|
|
5195
5693
|
io,
|
|
5196
5694
|
validateCatalog(readCatalog(requiredFlag(args, "file"))),
|
|
@@ -5199,7 +5697,7 @@ function validateCatalogCommand(args, io) {
|
|
|
5199
5697
|
}
|
|
5200
5698
|
|
|
5201
5699
|
// src/lib/run.ts
|
|
5202
|
-
var VERSION = "0.
|
|
5700
|
+
var VERSION = "0.17.1";
|
|
5203
5701
|
var COMMANDS = /* @__PURE__ */ new Map([
|
|
5204
5702
|
["init", init],
|
|
5205
5703
|
["list", list],
|