@clearlydev/nx-archweave 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +1 -1
  2. package/dist/eslint/fsd-boundaries.d.ts +25 -8
  3. package/dist/eslint/fsd-policy.d.ts +38 -0
  4. package/dist/eslint/index.d.ts +10 -9
  5. package/dist/eslint/index.js +227 -34
  6. package/dist/eslint/slice-groups.d.ts +12 -0
  7. package/dist/generators/add-agent-rules/generator.js +1175 -23
  8. package/dist/generators/add-backend/generator.js +1127 -75
  9. package/dist/generators/add-fsd-lite/generator.js +1274 -96
  10. package/dist/generators/add-review/files/dependabot.yml +7 -0
  11. package/dist/generators/add-review/generator.js +1100 -46
  12. package/dist/generators/doctor/generator.js +780 -96
  13. package/dist/generators/entity/generator.js +251 -32
  14. package/dist/generators/feature/generator.js +251 -32
  15. package/dist/generators/init/generator.js +1955 -549
  16. package/dist/generators/page/generator.js +251 -32
  17. package/dist/generators/shared/generator.js +251 -32
  18. package/dist/generators/widget/generator.js +251 -32
  19. package/package.json +4 -1
  20. package/src/generators/add-agent-rules/schema.json +5 -0
  21. package/src/generators/add-fsd-lite/schema.json +2 -2
  22. package/src/generators/entity/schema.json +11 -1
  23. package/src/generators/feature/schema.json +11 -1
  24. package/src/generators/init/schema.json +13 -2
  25. package/src/generators/page/schema.json +15 -3
  26. package/src/generators/shared/schema.json +7 -1
  27. package/src/generators/widget/schema.json +11 -1
  28. package/dist/eslint/fsd-boundaries.js +0 -144
  29. package/dist/eslint/tsconfig-aliases.js +0 -114
  30. package/dist/generators/add-agent-rules/render.js +0 -46
  31. package/dist/generators/add-backend/render.js +0 -102
  32. package/dist/generators/add-fsd-lite/render.js +0 -72
  33. package/dist/generators/add-github-templates/render.js +0 -26
  34. package/dist/generators/add-integration/render.js +0 -34
  35. package/dist/generators/add-review/render.js +0 -96
  36. package/dist/generators/allow-builds/render.js +0 -82
  37. package/dist/generators/doctor/integration-report.js +0 -70
  38. package/dist/generators/doctor/render.js +0 -10
  39. package/dist/generators/init/render.js +0 -125
  40. package/dist/generators/init/validation.js +0 -74
  41. package/dist/generators/install-integration/render.js +0 -128
  42. package/dist/generators/record-integration/render.js +0 -31
  43. package/dist/generators/shared/dry-run.js +0 -12
  44. package/dist/generators/shared/frontend-app.js +0 -57
  45. package/dist/generators/shared/project-type.js +0 -39
  46. package/dist/generators/shared/record-pipeline.js +0 -34
  47. package/dist/generators/shared/run-nx.js +0 -24
  48. package/dist/generators/shared/template-files.js +0 -38
  49. package/dist/generators/shared/toolkit-config-file.js +0 -110
  50. package/dist/generators/slice/core.js +0 -185
  51. package/dist/generators/slice/render.js +0 -70
  52. package/dist/lib/nx-plugin.js +0 -7
  53. package/dist/runtime/integrations-composition-root.js +0 -20
  54. package/dist/runtime/node-command-runner.js +0 -81
@@ -1,3 +1,6 @@
1
+ // src/generators/add-fsd-lite/generator.ts
2
+ import { posix } from "node:path";
3
+
1
4
  // ../shared/dist/lib/toolkit-config.js
2
5
  function readFsdEnforcement(fsd) {
3
6
  if (typeof fsd !== "object" || fsd === null)
@@ -52,6 +55,24 @@ function packageManagerCommands(pm, flavor = "classic") {
52
55
  }
53
56
 
54
57
  // ../template-engine/dist/backend/eslint-boundaries.js
58
+ var DEFAULT_BOUNDARY_TAGS = { frontend: "scope:frontend", backend: "scope:backend", shared: "scope:shared" };
59
+ function readBoundaryTags(config) {
60
+ const boundaries = config?.["boundaries"];
61
+ const tags = typeof boundaries === "object" && boundaries !== null ? boundaries["tags"] : void 0;
62
+ const pick = (role) => {
63
+ const value = typeof tags === "object" && tags !== null ? tags[role] : void 0;
64
+ return typeof value === "string" && value.trim() !== "" ? value : DEFAULT_BOUNDARY_TAGS[role];
65
+ };
66
+ return { frontend: pick("frontend"), backend: pick("backend"), shared: pick("shared") };
67
+ }
68
+ function scopeConstraints(tags = DEFAULT_BOUNDARY_TAGS) {
69
+ return [
70
+ { sourceTag: tags.frontend, notDependOnLibsWithTags: [tags.backend] },
71
+ { sourceTag: tags.backend, notDependOnLibsWithTags: [tags.frontend] },
72
+ { sourceTag: tags.shared, notDependOnLibsWithTags: [tags.frontend, tags.backend] }
73
+ ];
74
+ }
75
+ var SCOPE_CONSTRAINTS = scopeConstraints();
55
76
  var ESLINT_CONFIG_CANDIDATES = ["eslint.config.mjs", "eslint.config.js", "eslint.config.cjs"];
56
77
  function maskComments(text) {
57
78
  const out = text.split("");
@@ -102,20 +123,21 @@ var FSD_ESLINT_MODULE = "@clearlydev/nx-archweave/eslint";
102
123
  function buildFsdEslintImportLine() {
103
124
  return `import { fsdBoundaries } from '${FSD_ESLINT_MODULE}';`;
104
125
  }
105
- function buildFsdEslintEntry(severity) {
106
- return `...fsdBoundaries({ severity: '${severity}' }),`;
126
+ function buildFsdEslintEntry(severity, root = "src", framework = {}) {
127
+ const list = (key, values) => values === void 0 || values.length === 0 ? "" : `, ${key}: [${values.map((value) => `'${value}'`).join(", ")}]`;
128
+ return `...fsdBoundaries({ severity: '${severity}', cwd: import.meta.dirname, root: '${root}'${list("platformSuffixes", framework.platformSuffixes)}${list("routerDirs", framework.routerDirs)} }),`;
107
129
  }
108
- function buildFsdEslintSnippet({ severity, locale }) {
130
+ function buildFsdEslintSnippet({ severity, locale, root, framework }) {
109
131
  const lines = locale === "ko" ? [
110
132
  "// nx-archweave FSD \uB808\uC774\uC5B4 \uADDC\uCE59 \u2014 \uD504\uB860\uD2B8\uC5D4\uB4DC \uC571\uC758 eslint.config.mjs\uC5D0 \uC9C1\uC811 \uB123\uC73C\uC138\uC694:",
111
133
  "// 1) \uB9C8\uC9C0\uB9C9 import \uB4A4\uC5D0 \uC544\uB798 import\uB97C,",
112
- "// 2) export default [...] \uBC30\uC5F4\uC758 \uB9C8\uC9C0\uB9C9 \uD56D\uBAA9\uC73C\uB85C \uC544\uB798 spread\uB97C \uCD94\uAC00\uD569\uB2C8\uB2E4."
134
+ "// 2) \uC124\uC815 \uBC30\uC5F4(export default [...], \uB610\uB294 export default \uD558\uB294 \uBCC0\uC218\uC758 \uBC30\uC5F4)\uC758 \uB9C8\uC9C0\uB9C9 \uD56D\uBAA9\uC73C\uB85C \uC544\uB798 spread\uB97C \uCD94\uAC00\uD569\uB2C8\uB2E4."
113
135
  ] : [
114
136
  "// nx-archweave FSD layer rules \u2014 add these to the frontend app's eslint.config.mjs yourself:",
115
137
  "// 1) the import after the last import,",
116
- "// 2) the spread as the last element of the export default [...] array."
138
+ "// 2) the spread as the last element of the config array (export default [...], or the array your config exports by name)."
117
139
  ];
118
- return [...lines, buildFsdEslintImportLine(), buildFsdEslintEntry(severity)].join("\n");
140
+ return [...lines, buildFsdEslintImportLine(), buildFsdEslintEntry(severity, root, framework)].join("\n");
119
141
  }
120
142
  var LAYER_GUIDE_EN = {
121
143
  app: "composition root: providers, routing, global styles (the framework entry point lives here)",
@@ -161,7 +183,7 @@ var FSD_CHECK_TARGET = "fsd-check";
161
183
  function buildFsdCheckTarget(sourceRootName = "src") {
162
184
  return { command: `steiger ${sourceRootName}`, options: { cwd: "{projectRoot}" } };
163
185
  }
164
- function buildSteigerConfig({ frontend, locale }) {
186
+ function buildSteigerConfig({ renamedLayerDirs, locale }) {
165
187
  const ko = locale === "ko";
166
188
  const lines = [
167
189
  "import { defineConfig } from 'steiger';",
@@ -174,8 +196,8 @@ function buildSteigerConfig({ frontend, locale }) {
174
196
  " rules: {",
175
197
  ko ? " // \uC544\uBB34\uB3C4 \uCC38\uC870\uD558\uC9C0 \uC54A\uB294 \uC2AC\uB77C\uC774\uC2A4 = \uBC29\uAE08 \uC0DD\uC131\uD55C \uC2AC\uB77C\uC774\uC2A4. \uACBD\uACE0\uB85C \uBCF4\uC774\uB418 \uAC80\uC0AC\uB97C \uB9C9\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4." : " // a slice nothing references yet is every freshly generated slice: visible, not blocking",
176
198
  " 'fsd/insignificant-slice': 'warn',",
177
- ...frontend === "next" ? [
178
- ko ? " // Next.js: pages \uB808\uC774\uC5B4\uB294 _pages/ (FSD \uACF5\uC2DD Next.js \uAC00\uC774\uB4DC) \u2014 steiger 0.7\uC740 \uC544\uC9C1 \uC624\uD0C0\uB85C \uBD05\uB2C8\uB2E4." : " // Next.js: the pages layer is _pages/ (FSD's own Next.js guide) \u2014 steiger 0.7 still reads it as a typo",
199
+ ...renamedLayerDirs.length > 0 ? [
200
+ ko ? ` // ${renamedLayerDirs.join(", ")}: \uD504\uB808\uC784\uC6CC\uD06C \uB77C\uC6B0\uD130\uC640 \uACB9\uCE58\uC9C0 \uC54A\uAC8C \uC774\uB984\uC744 \uBC14\uAFBC \uB808\uC774\uC5B4 (FSD \uACF5\uC2DD \uBA85\uBA85) \u2014 steiger 0.7\uC740 \uC544\uC9C1 \uC624\uD0C0\uB85C \uBD05\uB2C8\uB2E4.` : ` // ${renamedLayerDirs.join(", ")}: layers renamed out of the framework router's way (FSD's own naming) \u2014 steiger 0.7 still reads them as typos`,
179
201
  " 'fsd/typo-in-layer-name': 'off',"
180
202
  ] : [],
181
203
  " },",
@@ -186,6 +208,93 @@ function buildSteigerConfig({ frontend, locale }) {
186
208
  return lines.join("\n");
187
209
  }
188
210
 
211
+ // ../template-engine/dist/frameworks/profiles.js
212
+ var ALL_LAYERS = ["app", "pages", "features", "entities", "shared"];
213
+ var NEXT = {
214
+ id: "next",
215
+ recognised: true,
216
+ // Next.js's own app/ (App Router) directory is the composition root; FSD's Next.js guide
217
+ // names the FSD app and pages layers _app and _pages, since Next.js reads src/app and
218
+ // src/pages as its routers.
219
+ layers: ["pages", "features", "entities", "shared"],
220
+ layerDirs: { app: "_app", pages: "_pages" },
221
+ buildOutputDirs: [".next", "out"],
222
+ platformSuffixes: [],
223
+ routerDirs: ["pages"]
224
+ };
225
+ var REACT_VITE = {
226
+ id: "react-vite",
227
+ recognised: true,
228
+ layers: ALL_LAYERS,
229
+ layerDirs: {},
230
+ buildOutputDirs: ["dist"],
231
+ platformSuffixes: [],
232
+ routerDirs: []
233
+ };
234
+ var EXPO_ROUTER = {
235
+ id: "expo",
236
+ recognised: true,
237
+ layers: ALL_LAYERS,
238
+ layerDirs: { app: "_app" },
239
+ buildOutputDirs: [".expo", "dist"],
240
+ platformSuffixes: ["ios", "android", "native", "web"],
241
+ routerDirs: []
242
+ };
243
+ var EXPO = { ...EXPO_ROUTER, layerDirs: {} };
244
+ var GENERIC = {
245
+ id: "generic",
246
+ recognised: false,
247
+ layers: ALL_LAYERS,
248
+ layerDirs: {},
249
+ buildOutputDirs: ["dist", "build"],
250
+ platformSuffixes: [],
251
+ routerDirs: []
252
+ };
253
+ var PROFILES = { next: NEXT, "react-vite": REACT_VITE, expo: EXPO_ROUTER, generic: GENERIC };
254
+ function profileById(id) {
255
+ return PROFILES[id];
256
+ }
257
+ var NEXT_CONFIGS = ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"];
258
+ var VITE_CONFIGS = ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"];
259
+ var EXPO_CONFIGS = ["app.config.ts", "app.config.js", "app.config.mjs"];
260
+ function hasExpoKey(input, file) {
261
+ const text = input.read(file);
262
+ if (typeof text !== "string")
263
+ return false;
264
+ try {
265
+ return typeof JSON.parse(text)["expo"] === "object";
266
+ } catch {
267
+ return false;
268
+ }
269
+ }
270
+ function dependsOn(input, manifest, name) {
271
+ const text = input.read(manifest);
272
+ if (typeof text !== "string")
273
+ return false;
274
+ try {
275
+ const parsed = JSON.parse(text);
276
+ return ["dependencies", "devDependencies", "peerDependencies"].some((field) => parsed[field]?.[name] !== void 0);
277
+ } catch {
278
+ return false;
279
+ }
280
+ }
281
+ function detectFramework(input) {
282
+ const prefix = input.projectRoot === "" || input.projectRoot === "." ? "" : `${input.projectRoot}/`;
283
+ const executors = input.executors ?? [];
284
+ if (NEXT_CONFIGS.some((file) => input.exists(`${prefix}${file}`)) || executors.some((executor) => executor.startsWith("@nx/next:"))) {
285
+ return NEXT;
286
+ }
287
+ const dependsAnywhere = (name) => dependsOn(input, `${prefix}package.json`, name) || dependsOn(input, "package.json", name);
288
+ const usesExpo = executors.some((executor) => executor.startsWith("@nx/expo:")) || hasExpoKey(input, `${prefix}app.json`) || EXPO_CONFIGS.some((file) => input.exists(`${prefix}${file}`)) || dependsOn(input, `${prefix}package.json`, "expo");
289
+ if (usesExpo)
290
+ return dependsAnywhere("expo-router") ? EXPO_ROUTER : EXPO;
291
+ const usesReact = dependsAnywhere("react");
292
+ const usesVite = VITE_CONFIGS.some((file) => input.exists(`${prefix}${file}`)) || executors.some((executor) => executor.startsWith("@nx/vite:"));
293
+ if (usesVite && usesReact)
294
+ return REACT_VITE;
295
+ return GENERIC;
296
+ }
297
+
189
298
  // ../template-engine/dist/fsd/fsd-eslint-config.js
190
299
  function classifyFsdEslintConfig(configText) {
191
300
  return maskComments(configText).includes(FSD_ESLINT_MODULE) ? "applied" : "absent";
@@ -193,21 +302,47 @@ function classifyFsdEslintConfig(configText) {
193
302
  var EXPORT_DEFAULT_ARRAY = /export\s+default\s+\[/g;
194
303
  var IMPORT_STATEMENT = /^import\b[^;]*?\bfrom\s+['"][^'"]+['"]\s*;?[ \t]*$/gm;
195
304
  var CLOSING_BRACKET = /\]\s*;?\s*$/;
196
- function applyFsdEslintConfig(configText, severity) {
305
+ function closingBracketOf(masked, open) {
306
+ let depth = 0;
307
+ for (let i = open; i < masked.length; i++) {
308
+ const ch = masked[i];
309
+ if (ch === '"' || ch === "'" || ch === "`") {
310
+ for (i++; i < masked.length && masked[i] !== ch; i++)
311
+ if (masked[i] === "\\")
312
+ i++;
313
+ continue;
314
+ }
315
+ if (ch === "[")
316
+ depth++;
317
+ else if (ch === "]" && --depth === 0)
318
+ return i;
319
+ }
320
+ return -1;
321
+ }
322
+ function namedConfigArrayEnd(masked) {
323
+ const exported = [...masked.matchAll(/export\s+default\s+([A-Za-z_$][\w$]*)\s*;?/g)];
324
+ if (exported.length !== 1)
325
+ return -1;
326
+ const declaration = new RegExp(`(?:const|let|var)\\s+${exported[0][1].replace(/\$/g, "\\$")}\\s*=\\s*(?:defineConfig\\s*\\(\\s*)?\\[`).exec(masked);
327
+ if (declaration === null)
328
+ return -1;
329
+ return closingBracketOf(masked, declaration.index + declaration[0].length - 1);
330
+ }
331
+ function applyFsdEslintConfig(configText, severity, root = "src", framework = {}) {
197
332
  if (classifyFsdEslintConfig(configText) === "applied")
198
333
  return { status: "already-applied" };
199
334
  const masked = maskComments(configText);
200
335
  EXPORT_DEFAULT_ARRAY.lastIndex = 0;
201
336
  const exportMatches = masked.match(EXPORT_DEFAULT_ARRAY) ?? [];
202
337
  const closing = CLOSING_BRACKET.exec(masked);
203
- if (exportMatches.length !== 1 || closing === null)
338
+ const closingIndex = exportMatches.length === 1 && closing !== null ? closing.index : namedConfigArrayEnd(masked);
339
+ if (closingIndex === -1)
204
340
  return { status: "unrecognized" };
205
341
  const eol = configText.includes("\r\n") ? "\r\n" : "\n";
206
- const closingIndex = closing.index;
207
342
  const before = configText.slice(0, closingIndex);
208
343
  const lastNonSpace = before.replace(/\s+$/, "");
209
344
  const needsComma = !lastNonSpace.endsWith(",") && !lastNonSpace.endsWith("[");
210
- const entryText = `${needsComma ? "," : ""}${eol} ${buildFsdEslintEntry(severity)}${eol}`;
345
+ const entryText = `${needsComma ? "," : ""}${eol} ${buildFsdEslintEntry(severity, root, framework)}${eol}`;
211
346
  const withEntry = `${lastNonSpace}${entryText}${configText.slice(closingIndex)}`;
212
347
  IMPORT_STATEMENT.lastIndex = 0;
213
348
  let lastImportEnd = -1;
@@ -219,20 +354,12 @@ function applyFsdEslintConfig(configText, severity) {
219
354
  const text = lastImportEnd === -1 ? `${importLine}${eol}${eol}${withEntry}` : `${withEntry.slice(0, lastImportEnd)}${eol}${importLine}${withEntry.slice(lastImportEnd)}`;
220
355
  return { status: "applied", text };
221
356
  }
222
- function buildFsdEslintPasteSnippet(severity, locale) {
223
- return buildFsdEslintSnippet({ severity, locale });
224
- }
225
- var REACT_VITE_LAYERS = ["app", "pages", "features", "entities", "shared"];
226
- var NEXT_LAYERS = ["pages", "features", "entities", "shared"];
227
- function fsdLayerDirName(frontend, layer) {
228
- return frontend === "next" && (layer === "pages" || layer === "app") ? `_${layer}` : layer;
229
- }
230
- function fsdLayersFor(frontend) {
231
- return frontend === "next" ? [...NEXT_LAYERS] : [...REACT_VITE_LAYERS];
357
+ function buildFsdEslintPasteSnippet(severity, locale, root = "src", framework = {}) {
358
+ return buildFsdEslintSnippet({ severity, locale, root, framework });
232
359
  }
233
- function composeFsdLayerGuide({ frontend, sourceRoot, locale }) {
234
- const layers = fsdLayersFor(frontend);
235
- return buildFsdLayerGuide({ layers, directories: layers.map((layer) => fsdLayerDirName(frontend, layer)), sourceRoot, locale });
360
+ function composeFsdLayerGuide({ profile, sourceRoot, locale }) {
361
+ const layers = [...profile.layers];
362
+ return buildFsdLayerGuide({ layers, directories: layers.map((layer) => profile.layerDirs[layer] ?? layer), sourceRoot, locale });
236
363
  }
237
364
 
238
365
  // src/generators/shared/dry-run.ts
@@ -243,69 +370,933 @@ function resolveDryRun(options) {
243
370
  // src/generators/shared/frontend-app.ts
244
371
  import { getProjects } from "@nx/devkit";
245
372
 
246
- // src/generators/shared/project-type.ts
247
- import { parseJson } from "@nx/devkit";
248
- function resolveProjectType(tree, root, declared) {
249
- if (declared) return declared === "library" ? "library" : "application";
250
- const prefix = root === "" || root === "." ? "" : `${root}/`;
251
- if (tree.exists(`${prefix}tsconfig.lib.json`)) return "library";
252
- if (tree.exists(`${prefix}tsconfig.app.json`)) return "application";
253
- const raw = tree.read(`${prefix}package.json`, "utf-8");
254
- if (raw === null) return "library";
255
- try {
256
- const manifest = parseJson(raw);
257
- if (manifest && !["exports", "main", "module", "bin"].some((key) => Boolean(manifest[key]))) return "application";
258
- } catch {
373
+ // ../../node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
374
+ function createScanner(text, ignoreTrivia = false) {
375
+ const len = text.length;
376
+ let pos = 0, value = "", tokenOffset = 0, token = 16, lineNumber = 0, lineStartOffset = 0, tokenLineStartOffset = 0, prevTokenLineStartOffset = 0, scanError = 0;
377
+ function scanHexDigits(count, exact) {
378
+ let digits = 0;
379
+ let value2 = 0;
380
+ while (digits < count || !exact) {
381
+ let ch = text.charCodeAt(pos);
382
+ if (ch >= 48 && ch <= 57) {
383
+ value2 = value2 * 16 + ch - 48;
384
+ } else if (ch >= 65 && ch <= 70) {
385
+ value2 = value2 * 16 + ch - 65 + 10;
386
+ } else if (ch >= 97 && ch <= 102) {
387
+ value2 = value2 * 16 + ch - 97 + 10;
388
+ } else {
389
+ break;
390
+ }
391
+ pos++;
392
+ digits++;
393
+ }
394
+ if (digits < count) {
395
+ value2 = -1;
396
+ }
397
+ return value2;
259
398
  }
260
- return "library";
399
+ function setPosition(newPosition) {
400
+ pos = newPosition;
401
+ value = "";
402
+ tokenOffset = 0;
403
+ token = 16;
404
+ scanError = 0;
405
+ }
406
+ function scanNumber() {
407
+ let start = pos;
408
+ if (text.charCodeAt(pos) === 48) {
409
+ pos++;
410
+ } else {
411
+ pos++;
412
+ while (pos < text.length && isDigit(text.charCodeAt(pos))) {
413
+ pos++;
414
+ }
415
+ }
416
+ if (pos < text.length && text.charCodeAt(pos) === 46) {
417
+ pos++;
418
+ if (pos < text.length && isDigit(text.charCodeAt(pos))) {
419
+ pos++;
420
+ while (pos < text.length && isDigit(text.charCodeAt(pos))) {
421
+ pos++;
422
+ }
423
+ } else {
424
+ scanError = 3;
425
+ return text.substring(start, pos);
426
+ }
427
+ }
428
+ let end = pos;
429
+ if (pos < text.length && (text.charCodeAt(pos) === 69 || text.charCodeAt(pos) === 101)) {
430
+ pos++;
431
+ if (pos < text.length && text.charCodeAt(pos) === 43 || text.charCodeAt(pos) === 45) {
432
+ pos++;
433
+ }
434
+ if (pos < text.length && isDigit(text.charCodeAt(pos))) {
435
+ pos++;
436
+ while (pos < text.length && isDigit(text.charCodeAt(pos))) {
437
+ pos++;
438
+ }
439
+ end = pos;
440
+ } else {
441
+ scanError = 3;
442
+ }
443
+ }
444
+ return text.substring(start, end);
445
+ }
446
+ function scanString() {
447
+ let result = "", start = pos;
448
+ while (true) {
449
+ if (pos >= len) {
450
+ result += text.substring(start, pos);
451
+ scanError = 2;
452
+ break;
453
+ }
454
+ const ch = text.charCodeAt(pos);
455
+ if (ch === 34) {
456
+ result += text.substring(start, pos);
457
+ pos++;
458
+ break;
459
+ }
460
+ if (ch === 92) {
461
+ result += text.substring(start, pos);
462
+ pos++;
463
+ if (pos >= len) {
464
+ scanError = 2;
465
+ break;
466
+ }
467
+ const ch2 = text.charCodeAt(pos++);
468
+ switch (ch2) {
469
+ case 34:
470
+ result += '"';
471
+ break;
472
+ case 92:
473
+ result += "\\";
474
+ break;
475
+ case 47:
476
+ result += "/";
477
+ break;
478
+ case 98:
479
+ result += "\b";
480
+ break;
481
+ case 102:
482
+ result += "\f";
483
+ break;
484
+ case 110:
485
+ result += "\n";
486
+ break;
487
+ case 114:
488
+ result += "\r";
489
+ break;
490
+ case 116:
491
+ result += " ";
492
+ break;
493
+ case 117:
494
+ const ch3 = scanHexDigits(4, true);
495
+ if (ch3 >= 0) {
496
+ result += String.fromCharCode(ch3);
497
+ } else {
498
+ scanError = 4;
499
+ }
500
+ break;
501
+ default:
502
+ scanError = 5;
503
+ }
504
+ start = pos;
505
+ continue;
506
+ }
507
+ if (ch >= 0 && ch <= 31) {
508
+ if (isLineBreak(ch)) {
509
+ result += text.substring(start, pos);
510
+ scanError = 2;
511
+ break;
512
+ } else {
513
+ scanError = 6;
514
+ }
515
+ }
516
+ pos++;
517
+ }
518
+ return result;
519
+ }
520
+ function scanNext() {
521
+ value = "";
522
+ scanError = 0;
523
+ tokenOffset = pos;
524
+ lineStartOffset = lineNumber;
525
+ prevTokenLineStartOffset = tokenLineStartOffset;
526
+ if (pos >= len) {
527
+ tokenOffset = len;
528
+ return token = 17;
529
+ }
530
+ let code = text.charCodeAt(pos);
531
+ if (isWhiteSpace(code)) {
532
+ do {
533
+ pos++;
534
+ value += String.fromCharCode(code);
535
+ code = text.charCodeAt(pos);
536
+ } while (isWhiteSpace(code));
537
+ return token = 15;
538
+ }
539
+ if (isLineBreak(code)) {
540
+ pos++;
541
+ value += String.fromCharCode(code);
542
+ if (code === 13 && text.charCodeAt(pos) === 10) {
543
+ pos++;
544
+ value += "\n";
545
+ }
546
+ lineNumber++;
547
+ tokenLineStartOffset = pos;
548
+ return token = 14;
549
+ }
550
+ switch (code) {
551
+ // tokens: []{}:,
552
+ case 123:
553
+ pos++;
554
+ return token = 1;
555
+ case 125:
556
+ pos++;
557
+ return token = 2;
558
+ case 91:
559
+ pos++;
560
+ return token = 3;
561
+ case 93:
562
+ pos++;
563
+ return token = 4;
564
+ case 58:
565
+ pos++;
566
+ return token = 6;
567
+ case 44:
568
+ pos++;
569
+ return token = 5;
570
+ // strings
571
+ case 34:
572
+ pos++;
573
+ value = scanString();
574
+ return token = 10;
575
+ // comments
576
+ case 47:
577
+ const start = pos - 1;
578
+ if (text.charCodeAt(pos + 1) === 47) {
579
+ pos += 2;
580
+ while (pos < len) {
581
+ if (isLineBreak(text.charCodeAt(pos))) {
582
+ break;
583
+ }
584
+ pos++;
585
+ }
586
+ value = text.substring(start, pos);
587
+ return token = 12;
588
+ }
589
+ if (text.charCodeAt(pos + 1) === 42) {
590
+ pos += 2;
591
+ const safeLength = len - 1;
592
+ let commentClosed = false;
593
+ while (pos < safeLength) {
594
+ const ch = text.charCodeAt(pos);
595
+ if (ch === 42 && text.charCodeAt(pos + 1) === 47) {
596
+ pos += 2;
597
+ commentClosed = true;
598
+ break;
599
+ }
600
+ pos++;
601
+ if (isLineBreak(ch)) {
602
+ if (ch === 13 && text.charCodeAt(pos) === 10) {
603
+ pos++;
604
+ }
605
+ lineNumber++;
606
+ tokenLineStartOffset = pos;
607
+ }
608
+ }
609
+ if (!commentClosed) {
610
+ pos++;
611
+ scanError = 1;
612
+ }
613
+ value = text.substring(start, pos);
614
+ return token = 13;
615
+ }
616
+ value += String.fromCharCode(code);
617
+ pos++;
618
+ return token = 16;
619
+ // numbers
620
+ case 45:
621
+ value += String.fromCharCode(code);
622
+ pos++;
623
+ if (pos === len || !isDigit(text.charCodeAt(pos))) {
624
+ return token = 16;
625
+ }
626
+ // found a minus, followed by a number so
627
+ // we fall through to proceed with scanning
628
+ // numbers
629
+ case 48:
630
+ case 49:
631
+ case 50:
632
+ case 51:
633
+ case 52:
634
+ case 53:
635
+ case 54:
636
+ case 55:
637
+ case 56:
638
+ case 57:
639
+ value += scanNumber();
640
+ return token = 11;
641
+ // literals and unknown symbols
642
+ default:
643
+ while (pos < len && isUnknownContentCharacter(code)) {
644
+ pos++;
645
+ code = text.charCodeAt(pos);
646
+ }
647
+ if (tokenOffset !== pos) {
648
+ value = text.substring(tokenOffset, pos);
649
+ switch (value) {
650
+ case "true":
651
+ return token = 8;
652
+ case "false":
653
+ return token = 9;
654
+ case "null":
655
+ return token = 7;
656
+ }
657
+ return token = 16;
658
+ }
659
+ value += String.fromCharCode(code);
660
+ pos++;
661
+ return token = 16;
662
+ }
663
+ }
664
+ function isUnknownContentCharacter(code) {
665
+ if (isWhiteSpace(code) || isLineBreak(code)) {
666
+ return false;
667
+ }
668
+ switch (code) {
669
+ case 125:
670
+ case 93:
671
+ case 123:
672
+ case 91:
673
+ case 34:
674
+ case 58:
675
+ case 44:
676
+ case 47:
677
+ return false;
678
+ }
679
+ return true;
680
+ }
681
+ function scanNextNonTrivia() {
682
+ let result;
683
+ do {
684
+ result = scanNext();
685
+ } while (result >= 12 && result <= 15);
686
+ return result;
687
+ }
688
+ return {
689
+ setPosition,
690
+ getPosition: () => pos,
691
+ scan: ignoreTrivia ? scanNextNonTrivia : scanNext,
692
+ getToken: () => token,
693
+ getTokenValue: () => value,
694
+ getTokenOffset: () => tokenOffset,
695
+ getTokenLength: () => pos - tokenOffset,
696
+ getTokenStartLine: () => lineStartOffset,
697
+ getTokenStartCharacter: () => tokenOffset - prevTokenLineStartOffset,
698
+ getTokenError: () => scanError
699
+ };
261
700
  }
262
-
263
- // src/generators/shared/frontend-app.ts
264
- var baseNameOf = (root) => root.split("/").pop() ?? root;
265
- function isEndToEnd(root) {
266
- const dir = baseNameOf(root);
267
- return dir === "e2e" || dir.endsWith("-e2e");
701
+ function isWhiteSpace(ch) {
702
+ return ch === 32 || ch === 9;
268
703
  }
269
- function isBackend(root, config) {
270
- return baseNameOf(root) === "api" || (config.tags ?? []).includes("scope:backend");
704
+ function isLineBreak(ch) {
705
+ return ch === 10 || ch === 13;
271
706
  }
272
- function sourceRootOf(config) {
273
- if (config.sourceRoot) return config.sourceRoot;
274
- return config.root === "" || config.root === "." ? "src" : `${config.root}/src`;
707
+ function isDigit(ch) {
708
+ return ch >= 48 && ch <= 57;
275
709
  }
276
- function resolveFrontendApp(tree, explicitProject) {
277
- let projects;
278
- try {
279
- projects = getProjects(tree);
280
- } catch (error) {
281
- const message = error instanceof Error ? error.message : String(error);
282
- const pathMatch = /Cannot parse ([^:]+):/.exec(message);
283
- return { kind: "invalid-project-json", detail: pathMatch ? pathMatch[1] : message };
710
+ var CharacterCodes;
711
+ (function(CharacterCodes2) {
712
+ CharacterCodes2[CharacterCodes2["lineFeed"] = 10] = "lineFeed";
713
+ CharacterCodes2[CharacterCodes2["carriageReturn"] = 13] = "carriageReturn";
714
+ CharacterCodes2[CharacterCodes2["space"] = 32] = "space";
715
+ CharacterCodes2[CharacterCodes2["_0"] = 48] = "_0";
716
+ CharacterCodes2[CharacterCodes2["_1"] = 49] = "_1";
717
+ CharacterCodes2[CharacterCodes2["_2"] = 50] = "_2";
718
+ CharacterCodes2[CharacterCodes2["_3"] = 51] = "_3";
719
+ CharacterCodes2[CharacterCodes2["_4"] = 52] = "_4";
720
+ CharacterCodes2[CharacterCodes2["_5"] = 53] = "_5";
721
+ CharacterCodes2[CharacterCodes2["_6"] = 54] = "_6";
722
+ CharacterCodes2[CharacterCodes2["_7"] = 55] = "_7";
723
+ CharacterCodes2[CharacterCodes2["_8"] = 56] = "_8";
724
+ CharacterCodes2[CharacterCodes2["_9"] = 57] = "_9";
725
+ CharacterCodes2[CharacterCodes2["a"] = 97] = "a";
726
+ CharacterCodes2[CharacterCodes2["b"] = 98] = "b";
727
+ CharacterCodes2[CharacterCodes2["c"] = 99] = "c";
728
+ CharacterCodes2[CharacterCodes2["d"] = 100] = "d";
729
+ CharacterCodes2[CharacterCodes2["e"] = 101] = "e";
730
+ CharacterCodes2[CharacterCodes2["f"] = 102] = "f";
731
+ CharacterCodes2[CharacterCodes2["g"] = 103] = "g";
732
+ CharacterCodes2[CharacterCodes2["h"] = 104] = "h";
733
+ CharacterCodes2[CharacterCodes2["i"] = 105] = "i";
734
+ CharacterCodes2[CharacterCodes2["j"] = 106] = "j";
735
+ CharacterCodes2[CharacterCodes2["k"] = 107] = "k";
736
+ CharacterCodes2[CharacterCodes2["l"] = 108] = "l";
737
+ CharacterCodes2[CharacterCodes2["m"] = 109] = "m";
738
+ CharacterCodes2[CharacterCodes2["n"] = 110] = "n";
739
+ CharacterCodes2[CharacterCodes2["o"] = 111] = "o";
740
+ CharacterCodes2[CharacterCodes2["p"] = 112] = "p";
741
+ CharacterCodes2[CharacterCodes2["q"] = 113] = "q";
742
+ CharacterCodes2[CharacterCodes2["r"] = 114] = "r";
743
+ CharacterCodes2[CharacterCodes2["s"] = 115] = "s";
744
+ CharacterCodes2[CharacterCodes2["t"] = 116] = "t";
745
+ CharacterCodes2[CharacterCodes2["u"] = 117] = "u";
746
+ CharacterCodes2[CharacterCodes2["v"] = 118] = "v";
747
+ CharacterCodes2[CharacterCodes2["w"] = 119] = "w";
748
+ CharacterCodes2[CharacterCodes2["x"] = 120] = "x";
749
+ CharacterCodes2[CharacterCodes2["y"] = 121] = "y";
750
+ CharacterCodes2[CharacterCodes2["z"] = 122] = "z";
751
+ CharacterCodes2[CharacterCodes2["A"] = 65] = "A";
752
+ CharacterCodes2[CharacterCodes2["B"] = 66] = "B";
753
+ CharacterCodes2[CharacterCodes2["C"] = 67] = "C";
754
+ CharacterCodes2[CharacterCodes2["D"] = 68] = "D";
755
+ CharacterCodes2[CharacterCodes2["E"] = 69] = "E";
756
+ CharacterCodes2[CharacterCodes2["F"] = 70] = "F";
757
+ CharacterCodes2[CharacterCodes2["G"] = 71] = "G";
758
+ CharacterCodes2[CharacterCodes2["H"] = 72] = "H";
759
+ CharacterCodes2[CharacterCodes2["I"] = 73] = "I";
760
+ CharacterCodes2[CharacterCodes2["J"] = 74] = "J";
761
+ CharacterCodes2[CharacterCodes2["K"] = 75] = "K";
762
+ CharacterCodes2[CharacterCodes2["L"] = 76] = "L";
763
+ CharacterCodes2[CharacterCodes2["M"] = 77] = "M";
764
+ CharacterCodes2[CharacterCodes2["N"] = 78] = "N";
765
+ CharacterCodes2[CharacterCodes2["O"] = 79] = "O";
766
+ CharacterCodes2[CharacterCodes2["P"] = 80] = "P";
767
+ CharacterCodes2[CharacterCodes2["Q"] = 81] = "Q";
768
+ CharacterCodes2[CharacterCodes2["R"] = 82] = "R";
769
+ CharacterCodes2[CharacterCodes2["S"] = 83] = "S";
770
+ CharacterCodes2[CharacterCodes2["T"] = 84] = "T";
771
+ CharacterCodes2[CharacterCodes2["U"] = 85] = "U";
772
+ CharacterCodes2[CharacterCodes2["V"] = 86] = "V";
773
+ CharacterCodes2[CharacterCodes2["W"] = 87] = "W";
774
+ CharacterCodes2[CharacterCodes2["X"] = 88] = "X";
775
+ CharacterCodes2[CharacterCodes2["Y"] = 89] = "Y";
776
+ CharacterCodes2[CharacterCodes2["Z"] = 90] = "Z";
777
+ CharacterCodes2[CharacterCodes2["asterisk"] = 42] = "asterisk";
778
+ CharacterCodes2[CharacterCodes2["backslash"] = 92] = "backslash";
779
+ CharacterCodes2[CharacterCodes2["closeBrace"] = 125] = "closeBrace";
780
+ CharacterCodes2[CharacterCodes2["closeBracket"] = 93] = "closeBracket";
781
+ CharacterCodes2[CharacterCodes2["colon"] = 58] = "colon";
782
+ CharacterCodes2[CharacterCodes2["comma"] = 44] = "comma";
783
+ CharacterCodes2[CharacterCodes2["dot"] = 46] = "dot";
784
+ CharacterCodes2[CharacterCodes2["doubleQuote"] = 34] = "doubleQuote";
785
+ CharacterCodes2[CharacterCodes2["minus"] = 45] = "minus";
786
+ CharacterCodes2[CharacterCodes2["openBrace"] = 123] = "openBrace";
787
+ CharacterCodes2[CharacterCodes2["openBracket"] = 91] = "openBracket";
788
+ CharacterCodes2[CharacterCodes2["plus"] = 43] = "plus";
789
+ CharacterCodes2[CharacterCodes2["slash"] = 47] = "slash";
790
+ CharacterCodes2[CharacterCodes2["formFeed"] = 12] = "formFeed";
791
+ CharacterCodes2[CharacterCodes2["tab"] = 9] = "tab";
792
+ })(CharacterCodes || (CharacterCodes = {}));
793
+
794
+ // ../../node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/string-intern.js
795
+ var cachedSpaces = new Array(20).fill(0).map((_, index) => {
796
+ return " ".repeat(index);
797
+ });
798
+ var maxCachedValues = 200;
799
+ var cachedBreakLinesWithSpaces = {
800
+ " ": {
801
+ "\n": new Array(maxCachedValues).fill(0).map((_, index) => {
802
+ return "\n" + " ".repeat(index);
803
+ }),
804
+ "\r": new Array(maxCachedValues).fill(0).map((_, index) => {
805
+ return "\r" + " ".repeat(index);
806
+ }),
807
+ "\r\n": new Array(maxCachedValues).fill(0).map((_, index) => {
808
+ return "\r\n" + " ".repeat(index);
809
+ })
810
+ },
811
+ " ": {
812
+ "\n": new Array(maxCachedValues).fill(0).map((_, index) => {
813
+ return "\n" + " ".repeat(index);
814
+ }),
815
+ "\r": new Array(maxCachedValues).fill(0).map((_, index) => {
816
+ return "\r" + " ".repeat(index);
817
+ }),
818
+ "\r\n": new Array(maxCachedValues).fill(0).map((_, index) => {
819
+ return "\r\n" + " ".repeat(index);
820
+ })
284
821
  }
285
- const found = (project2, config2) => ({
286
- kind: "found",
287
- project: project2,
288
- root: config2.root,
289
- sourceRoot: sourceRootOf(config2)
290
- });
291
- if (explicitProject !== void 0) {
292
- const config2 = projects.get(explicitProject);
293
- return config2 ? found(explicitProject, config2) : { kind: "not-found", project: explicitProject };
822
+ };
823
+
824
+ // ../../node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/parser.js
825
+ var ParseOptions;
826
+ (function(ParseOptions2) {
827
+ ParseOptions2.DEFAULT = {
828
+ allowTrailingComma: false
829
+ };
830
+ })(ParseOptions || (ParseOptions = {}));
831
+ function parseTree(text, errors = [], options = ParseOptions.DEFAULT) {
832
+ let currentParent = { type: "array", offset: -1, length: -1, children: [], parent: void 0 };
833
+ function ensurePropertyComplete(endOffset) {
834
+ if (currentParent.type === "property") {
835
+ currentParent.length = endOffset - currentParent.offset;
836
+ currentParent = currentParent.parent;
837
+ }
838
+ }
839
+ function onValue(valueNode) {
840
+ currentParent.children.push(valueNode);
841
+ return valueNode;
842
+ }
843
+ const visitor = {
844
+ onObjectBegin: (offset) => {
845
+ currentParent = onValue({ type: "object", offset, length: -1, parent: currentParent, children: [] });
846
+ },
847
+ onObjectProperty: (name, offset, length) => {
848
+ currentParent = onValue({ type: "property", offset, length: -1, parent: currentParent, children: [] });
849
+ currentParent.children.push({ type: "string", value: name, offset, length, parent: currentParent });
850
+ },
851
+ onObjectEnd: (offset, length) => {
852
+ ensurePropertyComplete(offset + length);
853
+ currentParent.length = offset + length - currentParent.offset;
854
+ currentParent = currentParent.parent;
855
+ ensurePropertyComplete(offset + length);
856
+ },
857
+ onArrayBegin: (offset, length) => {
858
+ currentParent = onValue({ type: "array", offset, length: -1, parent: currentParent, children: [] });
859
+ },
860
+ onArrayEnd: (offset, length) => {
861
+ currentParent.length = offset + length - currentParent.offset;
862
+ currentParent = currentParent.parent;
863
+ ensurePropertyComplete(offset + length);
864
+ },
865
+ onLiteralValue: (value, offset, length) => {
866
+ onValue({ type: getNodeType(value), offset, length, parent: currentParent, value });
867
+ ensurePropertyComplete(offset + length);
868
+ },
869
+ onSeparator: (sep, offset, length) => {
870
+ if (currentParent.type === "property") {
871
+ if (sep === ":") {
872
+ currentParent.colonOffset = offset;
873
+ } else if (sep === ",") {
874
+ ensurePropertyComplete(offset);
875
+ }
876
+ }
877
+ },
878
+ onError: (error, offset, length) => {
879
+ errors.push({ error, offset, length });
880
+ }
881
+ };
882
+ visit(text, visitor, options);
883
+ const result = currentParent.children[0];
884
+ if (result) {
885
+ delete result.parent;
886
+ }
887
+ return result;
888
+ }
889
+ function findNodeAtLocation(root, path) {
890
+ if (!root) {
891
+ return void 0;
892
+ }
893
+ let node = root;
894
+ for (let segment of path) {
895
+ if (typeof segment === "string") {
896
+ if (node.type !== "object" || !Array.isArray(node.children)) {
897
+ return void 0;
898
+ }
899
+ let found = false;
900
+ for (const propertyNode of node.children) {
901
+ if (Array.isArray(propertyNode.children) && propertyNode.children[0].value === segment && propertyNode.children.length === 2) {
902
+ node = propertyNode.children[1];
903
+ found = true;
904
+ break;
905
+ }
906
+ }
907
+ if (!found) {
908
+ return void 0;
909
+ }
910
+ } else {
911
+ const index = segment;
912
+ if (node.type !== "array" || index < 0 || !Array.isArray(node.children) || index >= node.children.length) {
913
+ return void 0;
914
+ }
915
+ node = node.children[index];
916
+ }
917
+ }
918
+ return node;
919
+ }
920
+ function visit(text, visitor, options = ParseOptions.DEFAULT) {
921
+ const _scanner = createScanner(text, false);
922
+ const _jsonPath = [];
923
+ let suppressedCallbacks = 0;
924
+ function toNoArgVisit(visitFunction) {
925
+ return visitFunction ? () => suppressedCallbacks === 0 && visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true;
926
+ }
927
+ function toOneArgVisit(visitFunction) {
928
+ return visitFunction ? (arg) => suppressedCallbacks === 0 && visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true;
929
+ }
930
+ function toOneArgVisitWithPath(visitFunction) {
931
+ return visitFunction ? (arg) => suppressedCallbacks === 0 && visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter(), () => _jsonPath.slice()) : () => true;
932
+ }
933
+ function toBeginVisit(visitFunction) {
934
+ return visitFunction ? () => {
935
+ if (suppressedCallbacks > 0) {
936
+ suppressedCallbacks++;
937
+ } else {
938
+ let cbReturn = visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter(), () => _jsonPath.slice());
939
+ if (cbReturn === false) {
940
+ suppressedCallbacks = 1;
941
+ }
942
+ }
943
+ } : () => true;
944
+ }
945
+ function toEndVisit(visitFunction) {
946
+ return visitFunction ? () => {
947
+ if (suppressedCallbacks > 0) {
948
+ suppressedCallbacks--;
949
+ }
950
+ if (suppressedCallbacks === 0) {
951
+ visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter());
952
+ }
953
+ } : () => true;
954
+ }
955
+ const onObjectBegin = toBeginVisit(visitor.onObjectBegin), onObjectProperty = toOneArgVisitWithPath(visitor.onObjectProperty), onObjectEnd = toEndVisit(visitor.onObjectEnd), onArrayBegin = toBeginVisit(visitor.onArrayBegin), onArrayEnd = toEndVisit(visitor.onArrayEnd), onLiteralValue = toOneArgVisitWithPath(visitor.onLiteralValue), onSeparator = toOneArgVisit(visitor.onSeparator), onComment = toNoArgVisit(visitor.onComment), onError = toOneArgVisit(visitor.onError);
956
+ const disallowComments = options && options.disallowComments;
957
+ const allowTrailingComma = options && options.allowTrailingComma;
958
+ function scanNext() {
959
+ while (true) {
960
+ const token = _scanner.scan();
961
+ switch (_scanner.getTokenError()) {
962
+ case 4:
963
+ handleError(
964
+ 14
965
+ /* ParseErrorCode.InvalidUnicode */
966
+ );
967
+ break;
968
+ case 5:
969
+ handleError(
970
+ 15
971
+ /* ParseErrorCode.InvalidEscapeCharacter */
972
+ );
973
+ break;
974
+ case 3:
975
+ handleError(
976
+ 13
977
+ /* ParseErrorCode.UnexpectedEndOfNumber */
978
+ );
979
+ break;
980
+ case 1:
981
+ if (!disallowComments) {
982
+ handleError(
983
+ 11
984
+ /* ParseErrorCode.UnexpectedEndOfComment */
985
+ );
986
+ }
987
+ break;
988
+ case 2:
989
+ handleError(
990
+ 12
991
+ /* ParseErrorCode.UnexpectedEndOfString */
992
+ );
993
+ break;
994
+ case 6:
995
+ handleError(
996
+ 16
997
+ /* ParseErrorCode.InvalidCharacter */
998
+ );
999
+ break;
1000
+ }
1001
+ switch (token) {
1002
+ case 12:
1003
+ case 13:
1004
+ if (disallowComments) {
1005
+ handleError(
1006
+ 10
1007
+ /* ParseErrorCode.InvalidCommentToken */
1008
+ );
1009
+ } else {
1010
+ onComment();
1011
+ }
1012
+ break;
1013
+ case 16:
1014
+ handleError(
1015
+ 1
1016
+ /* ParseErrorCode.InvalidSymbol */
1017
+ );
1018
+ break;
1019
+ case 15:
1020
+ case 14:
1021
+ break;
1022
+ default:
1023
+ return token;
1024
+ }
1025
+ }
1026
+ }
1027
+ function handleError(error, skipUntilAfter = [], skipUntil = []) {
1028
+ onError(error);
1029
+ if (skipUntilAfter.length + skipUntil.length > 0) {
1030
+ let token = _scanner.getToken();
1031
+ while (token !== 17) {
1032
+ if (skipUntilAfter.indexOf(token) !== -1) {
1033
+ scanNext();
1034
+ break;
1035
+ } else if (skipUntil.indexOf(token) !== -1) {
1036
+ break;
1037
+ }
1038
+ token = scanNext();
1039
+ }
1040
+ }
1041
+ }
1042
+ function parseString(isValue) {
1043
+ const value = _scanner.getTokenValue();
1044
+ if (isValue) {
1045
+ onLiteralValue(value);
1046
+ } else {
1047
+ onObjectProperty(value);
1048
+ _jsonPath.push(value);
1049
+ }
1050
+ scanNext();
1051
+ return true;
1052
+ }
1053
+ function parseLiteral() {
1054
+ switch (_scanner.getToken()) {
1055
+ case 11:
1056
+ const tokenValue = _scanner.getTokenValue();
1057
+ let value = Number(tokenValue);
1058
+ if (isNaN(value)) {
1059
+ handleError(
1060
+ 2
1061
+ /* ParseErrorCode.InvalidNumberFormat */
1062
+ );
1063
+ value = 0;
1064
+ }
1065
+ onLiteralValue(value);
1066
+ break;
1067
+ case 7:
1068
+ onLiteralValue(null);
1069
+ break;
1070
+ case 8:
1071
+ onLiteralValue(true);
1072
+ break;
1073
+ case 9:
1074
+ onLiteralValue(false);
1075
+ break;
1076
+ default:
1077
+ return false;
1078
+ }
1079
+ scanNext();
1080
+ return true;
1081
+ }
1082
+ function parseProperty() {
1083
+ if (_scanner.getToken() !== 10) {
1084
+ handleError(3, [], [
1085
+ 2,
1086
+ 5
1087
+ /* SyntaxKind.CommaToken */
1088
+ ]);
1089
+ return false;
1090
+ }
1091
+ parseString(false);
1092
+ if (_scanner.getToken() === 6) {
1093
+ onSeparator(":");
1094
+ scanNext();
1095
+ if (!parseValue()) {
1096
+ handleError(4, [], [
1097
+ 2,
1098
+ 5
1099
+ /* SyntaxKind.CommaToken */
1100
+ ]);
1101
+ }
1102
+ } else {
1103
+ handleError(5, [], [
1104
+ 2,
1105
+ 5
1106
+ /* SyntaxKind.CommaToken */
1107
+ ]);
1108
+ }
1109
+ _jsonPath.pop();
1110
+ return true;
1111
+ }
1112
+ function parseObject() {
1113
+ onObjectBegin();
1114
+ scanNext();
1115
+ let needsComma = false;
1116
+ while (_scanner.getToken() !== 2 && _scanner.getToken() !== 17) {
1117
+ if (_scanner.getToken() === 5) {
1118
+ if (!needsComma) {
1119
+ handleError(4, [], []);
1120
+ }
1121
+ onSeparator(",");
1122
+ scanNext();
1123
+ if (_scanner.getToken() === 2 && allowTrailingComma) {
1124
+ break;
1125
+ }
1126
+ } else if (needsComma) {
1127
+ handleError(6, [], []);
1128
+ }
1129
+ if (!parseProperty()) {
1130
+ handleError(4, [], [
1131
+ 2,
1132
+ 5
1133
+ /* SyntaxKind.CommaToken */
1134
+ ]);
1135
+ }
1136
+ needsComma = true;
1137
+ }
1138
+ onObjectEnd();
1139
+ if (_scanner.getToken() !== 2) {
1140
+ handleError(7, [
1141
+ 2
1142
+ /* SyntaxKind.CloseBraceToken */
1143
+ ], []);
1144
+ } else {
1145
+ scanNext();
1146
+ }
1147
+ return true;
1148
+ }
1149
+ function parseArray() {
1150
+ onArrayBegin();
1151
+ scanNext();
1152
+ let isFirstElement = true;
1153
+ let needsComma = false;
1154
+ while (_scanner.getToken() !== 4 && _scanner.getToken() !== 17) {
1155
+ if (_scanner.getToken() === 5) {
1156
+ if (!needsComma) {
1157
+ handleError(4, [], []);
1158
+ }
1159
+ onSeparator(",");
1160
+ scanNext();
1161
+ if (_scanner.getToken() === 4 && allowTrailingComma) {
1162
+ break;
1163
+ }
1164
+ } else if (needsComma) {
1165
+ handleError(6, [], []);
1166
+ }
1167
+ if (isFirstElement) {
1168
+ _jsonPath.push(0);
1169
+ isFirstElement = false;
1170
+ } else {
1171
+ _jsonPath[_jsonPath.length - 1]++;
1172
+ }
1173
+ if (!parseValue()) {
1174
+ handleError(4, [], [
1175
+ 4,
1176
+ 5
1177
+ /* SyntaxKind.CommaToken */
1178
+ ]);
1179
+ }
1180
+ needsComma = true;
1181
+ }
1182
+ onArrayEnd();
1183
+ if (!isFirstElement) {
1184
+ _jsonPath.pop();
1185
+ }
1186
+ if (_scanner.getToken() !== 4) {
1187
+ handleError(8, [
1188
+ 4
1189
+ /* SyntaxKind.CloseBracketToken */
1190
+ ], []);
1191
+ } else {
1192
+ scanNext();
1193
+ }
1194
+ return true;
1195
+ }
1196
+ function parseValue() {
1197
+ switch (_scanner.getToken()) {
1198
+ case 3:
1199
+ return parseArray();
1200
+ case 1:
1201
+ return parseObject();
1202
+ case 10:
1203
+ return parseString(true);
1204
+ default:
1205
+ return parseLiteral();
1206
+ }
1207
+ }
1208
+ scanNext();
1209
+ if (_scanner.getToken() === 17) {
1210
+ if (options.allowEmptyContent) {
1211
+ return true;
1212
+ }
1213
+ handleError(4, [], []);
1214
+ return false;
1215
+ }
1216
+ if (!parseValue()) {
1217
+ handleError(4, [], []);
1218
+ return false;
1219
+ }
1220
+ if (_scanner.getToken() !== 17) {
1221
+ handleError(9, [], []);
1222
+ }
1223
+ return true;
1224
+ }
1225
+ function getNodeType(value) {
1226
+ switch (typeof value) {
1227
+ case "boolean":
1228
+ return "boolean";
1229
+ case "number":
1230
+ return "number";
1231
+ case "string":
1232
+ return "string";
1233
+ case "object": {
1234
+ if (!value) {
1235
+ return "null";
1236
+ } else if (Array.isArray(value)) {
1237
+ return "array";
1238
+ }
1239
+ return "object";
1240
+ }
1241
+ default:
1242
+ return "null";
294
1243
  }
295
- const eligible = [...projects.entries()].filter(([, config2]) => !isEndToEnd(config2.root) && !isBackend(config2.root, config2));
296
- const declared = eligible.filter(([, config2]) => config2.projectType === "application");
297
- const inferred = eligible.filter(
298
- ([, config2]) => config2.projectType === void 0 && config2.root !== "" && config2.root !== "." && resolveProjectType(tree, config2.root, void 0) === "application"
299
- );
300
- const candidates = (declared.length > 0 ? declared : inferred).sort(([a], [b]) => a.localeCompare(b));
301
- if (candidates.length === 0) return { kind: "none" };
302
- if (candidates.length > 1) return { kind: "ambiguous", candidates: candidates.map(([name]) => name) };
303
- const [project, config] = candidates[0];
304
- return found(project, config);
305
1244
  }
306
1245
 
307
- // src/generators/shared/toolkit-config-file.ts
308
- var TOOLKIT_CONFIG_PATH = "toolkit.config.json";
1246
+ // ../../node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/main.js
1247
+ var ScanError;
1248
+ (function(ScanError2) {
1249
+ ScanError2[ScanError2["None"] = 0] = "None";
1250
+ ScanError2[ScanError2["UnexpectedEndOfComment"] = 1] = "UnexpectedEndOfComment";
1251
+ ScanError2[ScanError2["UnexpectedEndOfString"] = 2] = "UnexpectedEndOfString";
1252
+ ScanError2[ScanError2["UnexpectedEndOfNumber"] = 3] = "UnexpectedEndOfNumber";
1253
+ ScanError2[ScanError2["InvalidUnicode"] = 4] = "InvalidUnicode";
1254
+ ScanError2[ScanError2["InvalidEscapeCharacter"] = 5] = "InvalidEscapeCharacter";
1255
+ ScanError2[ScanError2["InvalidCharacter"] = 6] = "InvalidCharacter";
1256
+ })(ScanError || (ScanError = {}));
1257
+ var SyntaxKind;
1258
+ (function(SyntaxKind2) {
1259
+ SyntaxKind2[SyntaxKind2["OpenBraceToken"] = 1] = "OpenBraceToken";
1260
+ SyntaxKind2[SyntaxKind2["CloseBraceToken"] = 2] = "CloseBraceToken";
1261
+ SyntaxKind2[SyntaxKind2["OpenBracketToken"] = 3] = "OpenBracketToken";
1262
+ SyntaxKind2[SyntaxKind2["CloseBracketToken"] = 4] = "CloseBracketToken";
1263
+ SyntaxKind2[SyntaxKind2["CommaToken"] = 5] = "CommaToken";
1264
+ SyntaxKind2[SyntaxKind2["ColonToken"] = 6] = "ColonToken";
1265
+ SyntaxKind2[SyntaxKind2["NullKeyword"] = 7] = "NullKeyword";
1266
+ SyntaxKind2[SyntaxKind2["TrueKeyword"] = 8] = "TrueKeyword";
1267
+ SyntaxKind2[SyntaxKind2["FalseKeyword"] = 9] = "FalseKeyword";
1268
+ SyntaxKind2[SyntaxKind2["StringLiteral"] = 10] = "StringLiteral";
1269
+ SyntaxKind2[SyntaxKind2["NumericLiteral"] = 11] = "NumericLiteral";
1270
+ SyntaxKind2[SyntaxKind2["LineCommentTrivia"] = 12] = "LineCommentTrivia";
1271
+ SyntaxKind2[SyntaxKind2["BlockCommentTrivia"] = 13] = "BlockCommentTrivia";
1272
+ SyntaxKind2[SyntaxKind2["LineBreakTrivia"] = 14] = "LineBreakTrivia";
1273
+ SyntaxKind2[SyntaxKind2["Trivia"] = 15] = "Trivia";
1274
+ SyntaxKind2[SyntaxKind2["Unknown"] = 16] = "Unknown";
1275
+ SyntaxKind2[SyntaxKind2["EOF"] = 17] = "EOF";
1276
+ })(SyntaxKind || (SyntaxKind = {}));
1277
+ var parseTree2 = parseTree;
1278
+ var findNodeAtLocation2 = findNodeAtLocation;
1279
+ var ParseErrorCode;
1280
+ (function(ParseErrorCode2) {
1281
+ ParseErrorCode2[ParseErrorCode2["InvalidSymbol"] = 1] = "InvalidSymbol";
1282
+ ParseErrorCode2[ParseErrorCode2["InvalidNumberFormat"] = 2] = "InvalidNumberFormat";
1283
+ ParseErrorCode2[ParseErrorCode2["PropertyNameExpected"] = 3] = "PropertyNameExpected";
1284
+ ParseErrorCode2[ParseErrorCode2["ValueExpected"] = 4] = "ValueExpected";
1285
+ ParseErrorCode2[ParseErrorCode2["ColonExpected"] = 5] = "ColonExpected";
1286
+ ParseErrorCode2[ParseErrorCode2["CommaExpected"] = 6] = "CommaExpected";
1287
+ ParseErrorCode2[ParseErrorCode2["CloseBraceExpected"] = 7] = "CloseBraceExpected";
1288
+ ParseErrorCode2[ParseErrorCode2["CloseBracketExpected"] = 8] = "CloseBracketExpected";
1289
+ ParseErrorCode2[ParseErrorCode2["EndOfFileExpected"] = 9] = "EndOfFileExpected";
1290
+ ParseErrorCode2[ParseErrorCode2["InvalidCommentToken"] = 10] = "InvalidCommentToken";
1291
+ ParseErrorCode2[ParseErrorCode2["UnexpectedEndOfComment"] = 11] = "UnexpectedEndOfComment";
1292
+ ParseErrorCode2[ParseErrorCode2["UnexpectedEndOfString"] = 12] = "UnexpectedEndOfString";
1293
+ ParseErrorCode2[ParseErrorCode2["UnexpectedEndOfNumber"] = 13] = "UnexpectedEndOfNumber";
1294
+ ParseErrorCode2[ParseErrorCode2["InvalidUnicode"] = 14] = "InvalidUnicode";
1295
+ ParseErrorCode2[ParseErrorCode2["InvalidEscapeCharacter"] = 15] = "InvalidEscapeCharacter";
1296
+ ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
1297
+ })(ParseErrorCode || (ParseErrorCode = {}));
1298
+
1299
+ // src/generators/shared/json-edit.ts
309
1300
  function detectIndent(raw) {
310
1301
  for (const line of raw.split("\n")) {
311
1302
  const match = /^(\t+| +)/.exec(line);
@@ -313,6 +1304,81 @@ function detectIndent(raw) {
313
1304
  }
314
1305
  return " ";
315
1306
  }
1307
+ function styleOf(raw) {
1308
+ return { eol: raw.includes("\r\n") ? "\r\n" : "\n", unit: detectIndent(raw) };
1309
+ }
1310
+ function lineIndentAt(raw, offset) {
1311
+ const lineStart = raw.lastIndexOf("\n", offset - 1) + 1;
1312
+ return /^[ \t]*/.exec(raw.slice(lineStart))?.[0] ?? "";
1313
+ }
1314
+ function isMultiline(raw, node) {
1315
+ return raw.slice(node.offset, node.offset + node.length).includes("\n");
1316
+ }
1317
+ function isPrimitive(value) {
1318
+ return value === null || ["string", "number", "boolean"].includes(typeof value);
1319
+ }
1320
+ function renderValue(value, indent, style) {
1321
+ if (isPrimitive(value)) return JSON.stringify(value);
1322
+ if (Array.isArray(value)) {
1323
+ if (value.every(isPrimitive)) return `[${value.map((item) => JSON.stringify(item)).join(", ")}]`;
1324
+ const inner2 = `${indent}${style.unit}`;
1325
+ return `[${style.eol}${value.map((item) => `${inner2}${renderValue(item, inner2, style)}`).join(`,${style.eol}`)}${style.eol}${indent}]`;
1326
+ }
1327
+ const entries = Object.entries(value);
1328
+ if (entries.length === 0) return "{}";
1329
+ const inner = `${indent}${style.unit}`;
1330
+ const body = entries.map(([key, item]) => `${inner}${JSON.stringify(key)}: ${renderValue(item, inner, style)}`).join(`,${style.eol}`);
1331
+ return `{${style.eol}${body}${style.eol}${indent}}`;
1332
+ }
1333
+ function splice(raw, start, end, text) {
1334
+ return raw.slice(0, start) + text + raw.slice(end);
1335
+ }
1336
+ function nest(keys, value) {
1337
+ return keys.reduceRight((inner, key) => ({ [key]: inner }), value);
1338
+ }
1339
+ function insertProperty(raw, object, key, value, style) {
1340
+ const properties = object.children ?? [];
1341
+ const objectIndent = lineIndentAt(raw, object.offset);
1342
+ if (properties.length === 0) {
1343
+ const inner = `${objectIndent}${style.unit}`;
1344
+ const text = `{${style.eol}${inner}${JSON.stringify(String(key))}: ${renderValue(value, inner, style)}${style.eol}${objectIndent}}`;
1345
+ return splice(raw, object.offset, object.offset + object.length, text);
1346
+ }
1347
+ const last = properties[properties.length - 1];
1348
+ const end = last.offset + last.length;
1349
+ if (!isMultiline(raw, object)) {
1350
+ return splice(raw, end, end, `, ${JSON.stringify(String(key))}: ${renderValue(value, "", style)}`);
1351
+ }
1352
+ const indent = lineIndentAt(raw, last.offset);
1353
+ return splice(raw, end, end, `,${style.eol}${indent}${JSON.stringify(String(key))}: ${renderValue(value, indent, style)}`);
1354
+ }
1355
+ function setJsonValue(raw, path, value) {
1356
+ const style = styleOf(raw);
1357
+ const root = parseTree2(raw);
1358
+ if (root === void 0) throw new Error("setJsonValue: not a JSON document");
1359
+ const existing = findNodeAtLocation2(root, path);
1360
+ if (existing !== void 0) {
1361
+ return splice(raw, existing.offset, existing.offset + existing.length, renderValue(value, lineIndentAt(raw, existing.offset), style));
1362
+ }
1363
+ for (let depth = path.length - 1; depth >= 0; depth--) {
1364
+ const parent = depth === 0 ? root : findNodeAtLocation2(root, path.slice(0, depth));
1365
+ if (parent === void 0) continue;
1366
+ if (parent.type !== "object") throw new Error(`setJsonValue: ${path.slice(0, depth).join(".") || "the root"} is not an object`);
1367
+ return insertProperty(raw, parent, path[depth], nest(path.slice(depth + 1), value), style);
1368
+ }
1369
+ throw new Error("setJsonValue: unreachable");
1370
+ }
1371
+ function applyJsonEdits(raw, edits) {
1372
+ if (raw === null) {
1373
+ let text = "{}\n";
1374
+ for (const [path, value] of edits) text = setJsonValue(text, path, value);
1375
+ return text;
1376
+ }
1377
+ return edits.reduce((text, [path, value]) => setJsonValue(text, path, value), raw);
1378
+ }
1379
+
1380
+ // src/generators/shared/toolkit-config-file.ts
1381
+ var TOOLKIT_CONFIG_PATH = "toolkit.config.json";
316
1382
  function readToolkitConfigFile(tree) {
317
1383
  const raw = tree.read(TOOLKIT_CONFIG_PATH, "utf-8");
318
1384
  if (raw === null) return { raw: null, parsed: null };
@@ -338,16 +1404,16 @@ function recordedChoice(config, key, allowed) {
338
1404
  const value = config?.[key];
339
1405
  return typeof value === "string" && allowed.includes(value) ? value : void 0;
340
1406
  }
341
- var RECORDED_FRONTENDS = ["react-vite", "next", "none"];
1407
+ var RECORDED_FRONTENDS = ["react-vite", "next", "expo", "generic", "none"];
342
1408
  var RECORDED_PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun"];
343
1409
  function resolveFrontendOption(option, config, generator) {
344
1410
  const frontend = option ?? recordedChoice(config, "frontend", RECORDED_FRONTENDS);
345
1411
  if (frontend === void 0) {
346
- throw new Error(`${generator}: pass --frontend=react-vite|next (toolkit.config.json records none \u2014 init writes it).`);
1412
+ throw new Error(`${generator}: pass --frontend=react-vite|next|expo|generic (toolkit.config.json records none \u2014 init writes it).`);
347
1413
  }
348
1414
  if (frontend === "none") {
349
1415
  throw new Error(
350
- `${generator}: this is a backend-only workspace (toolkit.config.json frontend: none) \u2014 FSD structures a frontend app. Pass --frontend=react-vite|next if you added one.`
1416
+ `${generator}: this is a backend-only workspace (toolkit.config.json frontend: none) \u2014 FSD structures a frontend app. Pass --frontend=react-vite|next|expo|generic if you added one.`
351
1417
  );
352
1418
  }
353
1419
  return frontend;
@@ -359,6 +1425,106 @@ function treePackageManager(tree, config) {
359
1425
  return workspacePackageManager((p) => tree.exists(p), (p) => tree.read(p, "utf-8"), config);
360
1426
  }
361
1427
 
1428
+ // src/generators/shared/project-type.ts
1429
+ import { parseJson } from "@nx/devkit";
1430
+ function resolveProjectType(tree, root, declared) {
1431
+ if (declared) return declared === "library" ? "library" : "application";
1432
+ const prefix = root === "" || root === "." ? "" : `${root}/`;
1433
+ if (tree.exists(`${prefix}tsconfig.lib.json`)) return "library";
1434
+ if (tree.exists(`${prefix}tsconfig.app.json`)) return "application";
1435
+ const raw = tree.read(`${prefix}package.json`, "utf-8");
1436
+ if (raw === null) return "library";
1437
+ try {
1438
+ const manifest = parseJson(raw);
1439
+ if (manifest && !["exports", "main", "module", "bin"].some((key) => Boolean(manifest[key]))) return "application";
1440
+ } catch {
1441
+ }
1442
+ return "library";
1443
+ }
1444
+
1445
+ // src/generators/shared/frontend-app.ts
1446
+ var baseNameOf = (root) => root.split("/").pop() ?? root;
1447
+ function isEndToEnd(root) {
1448
+ const dir = baseNameOf(root);
1449
+ return dir === "e2e" || dir.endsWith("-e2e");
1450
+ }
1451
+ function isBackend(root, config, backendTag) {
1452
+ return baseNameOf(root) === "api" || (config.tags ?? []).includes(backendTag);
1453
+ }
1454
+ function prefixOf(root) {
1455
+ return root === "" || root === "." ? "" : `${root}/`;
1456
+ }
1457
+ function sourceRootOf(tree, config) {
1458
+ const prefix = prefixOf(config.root);
1459
+ const declared = config.sourceRoot?.replace(/\/$/, "");
1460
+ const atRoot = declared !== void 0 && (declared === config.root || prefix === "" && (declared === "." || declared === ""));
1461
+ if (declared && !atRoot) return declared;
1462
+ if (atRoot && !tree.exists(`${prefix}src`)) return prefix === "" ? "." : config.root;
1463
+ return `${prefix}src`;
1464
+ }
1465
+ function frameworkOf(tree, config) {
1466
+ return detectFramework({
1467
+ exists: (path) => tree.exists(path),
1468
+ read: (path) => tree.read(path, "utf-8"),
1469
+ projectRoot: config.root,
1470
+ executors: Object.values(config.targets ?? {}).flatMap((target) => target.executor ? [target.executor] : [])
1471
+ });
1472
+ }
1473
+ function resolveFrontendApp(tree, explicitProject) {
1474
+ let projects;
1475
+ try {
1476
+ projects = getProjects(tree);
1477
+ } catch (error) {
1478
+ const message = error instanceof Error ? error.message : String(error);
1479
+ const pathMatch = /Cannot parse ([^:]+):/.exec(message);
1480
+ return { kind: "invalid-project-json", detail: pathMatch ? pathMatch[1] : message };
1481
+ }
1482
+ const found = (project2, config2) => {
1483
+ const profile = frameworkOf(tree, config2);
1484
+ return { kind: "found", project: project2, root: config2.root, sourceRoot: sourceRootOf(tree, config2), framework: profile.id, profile };
1485
+ };
1486
+ if (explicitProject !== void 0) {
1487
+ const config2 = projects.get(explicitProject);
1488
+ if (config2) return found(explicitProject, config2);
1489
+ const rootApp = inferredRootApp(tree, projects);
1490
+ return rootApp?.kind === "found" && rootApp.project === explicitProject ? rootApp : { kind: "not-found", project: explicitProject };
1491
+ }
1492
+ const backendTag = readBoundaryTags(readToolkitConfig(tree)).backend;
1493
+ const eligible = [...projects.entries()].filter(([, config2]) => !isEndToEnd(config2.root) && !isBackend(config2.root, config2, backendTag));
1494
+ const declared = eligible.filter(([, config2]) => config2.projectType === "application");
1495
+ const inferred = eligible.filter(([, config2]) => {
1496
+ if (config2.projectType !== void 0) return false;
1497
+ if (config2.root === "" || config2.root === ".") return frameworkOf(tree, config2).recognised;
1498
+ return resolveProjectType(tree, config2.root, void 0) === "application";
1499
+ });
1500
+ const candidates = (declared.length > 0 ? declared : inferred).sort(([a], [b]) => a.localeCompare(b));
1501
+ if (candidates.length === 0) {
1502
+ const rootApp = inferredRootApp(tree, projects);
1503
+ if (rootApp !== null) return rootApp;
1504
+ const otherProjects = [...projects.keys()].filter((name) => !isEndToEnd(projects.get(name)?.root ?? "")).sort();
1505
+ return { kind: "none", otherProjects };
1506
+ }
1507
+ if (candidates.length > 1) return { kind: "ambiguous", candidates: candidates.map(([name]) => name) };
1508
+ const [project, config] = candidates[0];
1509
+ return found(project, config);
1510
+ }
1511
+ function inferredRootApp(tree, projects) {
1512
+ if ([...projects.values()].some((config2) => config2.root === "" || config2.root === ".")) return null;
1513
+ const raw = tree.read("package.json", "utf-8");
1514
+ if (raw === null) return null;
1515
+ let name;
1516
+ try {
1517
+ name = JSON.parse(raw).name;
1518
+ } catch {
1519
+ return null;
1520
+ }
1521
+ if (typeof name !== "string" || name === "") return null;
1522
+ const config = { name, root: "." };
1523
+ const framework = frameworkOf(tree, config);
1524
+ if (!framework.recognised) return null;
1525
+ return { kind: "found", project: name, root: ".", sourceRoot: sourceRootOf(tree, config), framework: framework.id, profile: framework, inferredRoot: true };
1526
+ }
1527
+
362
1528
  // src/generators/add-fsd-lite/render.ts
363
1529
  function renderAddFsdLiteReport(report) {
364
1530
  const lines = [];
@@ -375,8 +1541,11 @@ function renderAddFsdLiteReport(report) {
375
1541
  for (const name of refusal.candidates) lines.push(` - ${name}`);
376
1542
  } else if (refusal.reason === "invalid-project-json") {
377
1543
  lines.push(` a project.json could not be parsed (${refusal.detail}) \u2014 fix it, then rerun.`);
1544
+ } else if (refusal.otherProjects.length > 0) {
1545
+ lines.push(" no project was recognised as a frontend app. If one of these is it, rerun with --project=<name>:");
1546
+ for (const name of refusal.otherProjects) lines.push(` - ${name}`);
378
1547
  } else {
379
- lines.push(" the workspace has no frontend application \u2014 create one (e.g. nx g @nx/react:app) or pass --project.");
1548
+ lines.push(" the workspace has no projects \u2014 create one (e.g. nx g @nx/react:app or nx g @nx/next:app), then rerun.");
380
1549
  }
381
1550
  }
382
1551
  return lines.join("\n");
@@ -386,6 +1555,9 @@ function renderAddFsdLiteReport(report) {
386
1555
  lines.push("");
387
1556
  }
388
1557
  lines.push(`FSD layers (in ${report.project}, ${report.sourceRoot}):`);
1558
+ if (report.layerGuide !== null && report.frontend === "generic") {
1559
+ lines.push("Frontend: framework not recognised \u2014 the common FSD rules apply, with no framework-specific layer names.");
1560
+ }
389
1561
  if (report.layerGuide !== null) lines.push(report.layerGuide);
390
1562
  lines.push("");
391
1563
  const where = report.eslintPath ?? `${report.project}'s eslint.config.mjs`;
@@ -437,26 +1609,31 @@ function runAddFsdLite(tree, options) {
437
1609
  const frontend = resolveFrontendOption(options.frontend, config, "add-fsd-lite");
438
1610
  const dryRun = options.dryRun ?? false;
439
1611
  const applyEslint = options.applyEslint ?? false;
440
- const snippet = buildFsdEslintPasteSnippet(enforcement, locale);
441
1612
  const app = resolveFrontendApp(tree, options.project);
442
1613
  if (app.kind !== "found") {
443
- const appRefusal = app.kind === "not-found" ? { reason: "project-not-found", project: app.project } : app.kind === "ambiguous" ? { reason: "ambiguous-frontend", candidates: app.candidates } : app.kind === "invalid-project-json" ? { reason: "invalid-project-json", detail: app.detail } : { reason: "no-frontend-app" };
444
- return { dryRun, aborted: true, appRefusal, project: null, sourceRoot: null, enforcement, eslint: "not-applied", eslintPath: null, snippet, layerGuide: null, steiger: null };
1614
+ const snippet2 = buildFsdEslintPasteSnippet(enforcement, locale);
1615
+ const appRefusal = app.kind === "not-found" ? { reason: "project-not-found", project: app.project } : app.kind === "ambiguous" ? { reason: "ambiguous-frontend", candidates: app.candidates } : app.kind === "invalid-project-json" ? { reason: "invalid-project-json", detail: app.detail } : { reason: "no-frontend-app", otherProjects: app.otherProjects };
1616
+ return { dryRun, aborted: true, appRefusal, project: null, sourceRoot: null, enforcement, frontend, eslint: "not-applied", eslintPath: null, snippet: snippet2, layerGuide: null, steiger: null };
445
1617
  }
446
1618
  const appDir = app.root === "" || app.root === "." ? "" : `${app.root}/`;
1619
+ const eslintRoot = posix.relative(app.root === "" ? "." : app.root, app.sourceRoot) || ".";
1620
+ const profile = app.framework === frontend ? app.profile : profileById(frontend);
1621
+ const { platformSuffixes, routerDirs } = profile;
1622
+ const framework = { platformSuffixes, routerDirs };
1623
+ const snippet = buildFsdEslintPasteSnippet(enforcement, locale, eslintRoot, framework);
447
1624
  const eslintPath = ESLINT_CONFIG_CANDIDATES.map((name) => `${appDir}${name}`).find((path) => tree.exists(path)) ?? null;
448
1625
  const configText = eslintPath === null ? null : tree.read(eslintPath, "utf-8");
449
1626
  let eslint;
450
1627
  if (!applyEslint) {
451
- eslint = configText !== null && applyFsdEslintConfig(configText, enforcement).status === "already-applied" ? "already-applied" : "snippet-only";
1628
+ eslint = configText !== null && applyFsdEslintConfig(configText, enforcement, eslintRoot, framework).status === "already-applied" ? "already-applied" : "snippet-only";
452
1629
  } else if (configText === null) {
453
1630
  eslint = "degraded-unrecognized";
454
1631
  } else {
455
- const result = applyFsdEslintConfig(configText, enforcement);
1632
+ const result = applyFsdEslintConfig(configText, enforcement, eslintRoot, framework);
456
1633
  eslint = result.status === "applied" ? "applied" : result.status === "already-applied" ? "already-applied" : "degraded-unrecognized";
457
1634
  if (result.status === "applied" && !dryRun && eslintPath !== null) tree.write(eslintPath, result.text);
458
1635
  }
459
- const steiger = options.steiger ? addSteiger(tree, { appDir, appRoot: app.root, sourceRoot: app.sourceRoot, frontend, locale, write: !dryRun, config }) : null;
1636
+ const steiger = options.steiger ? addSteiger(tree, { appDir, appRoot: app.root, sourceRoot: app.sourceRoot, profile, locale, write: !dryRun, config }) : null;
460
1637
  return {
461
1638
  dryRun,
462
1639
  aborted: false,
@@ -465,10 +1642,11 @@ function runAddFsdLite(tree, options) {
465
1642
  sourceRoot: app.sourceRoot,
466
1643
  steiger,
467
1644
  enforcement,
1645
+ frontend,
468
1646
  eslint,
469
1647
  eslintPath,
470
1648
  snippet: eslint === "applied" || eslint === "already-applied" ? null : snippet,
471
- layerGuide: composeFsdLayerGuide({ frontend, sourceRoot: app.sourceRoot, locale })
1649
+ layerGuide: composeFsdLayerGuide({ profile, sourceRoot: app.sourceRoot, locale })
472
1650
  };
473
1651
  }
474
1652
  function readJsonObject(tree, path) {
@@ -483,7 +1661,7 @@ function readJsonObject(tree, path) {
483
1661
  }
484
1662
  function addSteiger(tree, input) {
485
1663
  const configPath = `${input.appDir}${STEIGER_CONFIG_FILE}`;
486
- const configText = buildSteigerConfig({ frontend: input.frontend, locale: input.locale });
1664
+ const configText = buildSteigerConfig({ renamedLayerDirs: Object.values(input.profile.layerDirs), locale: input.locale });
487
1665
  const existing = tree.read(configPath, "utf-8");
488
1666
  let configAction;
489
1667
  if (existing === null) {
@@ -510,8 +1688,8 @@ function addSteiger(tree, input) {
510
1688
  } else {
511
1689
  targets[FSD_CHECK_TARGET] = target;
512
1690
  targetAction = "added";
513
- if (input.write) tree.write(candidate, `${JSON.stringify(file.parsed, null, detectIndent(file.raw))}
514
- `);
1691
+ const path = candidate.endsWith("project.json") ? ["targets", FSD_CHECK_TARGET] : ["nx", "targets", FSD_CHECK_TARGET];
1692
+ if (input.write) tree.write(candidate, setJsonValue(file.raw, path, target));
515
1693
  }
516
1694
  break;
517
1695
  }
@@ -526,8 +1704,8 @@ function addSteiger(tree, input) {
526
1704
  } else {
527
1705
  for (const [name, range] of missing) devDependencies[name] = range;
528
1706
  dependenciesAction = "added";
529
- if (input.write) tree.write("package.json", `${JSON.stringify(rootPackage.parsed, null, detectIndent(rootPackage.raw))}
530
- `);
1707
+ const edited = applyJsonEdits(rootPackage.raw, missing.map(([name, range]) => [["devDependencies", name], range]));
1708
+ if (input.write) tree.write("package.json", edited);
531
1709
  }
532
1710
  }
533
1711
  const pm = treePackageManager(tree, input.config) ?? "pnpm";