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