@sculptor/cli 0.3.2 → 0.3.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.
@@ -60,6 +60,23 @@ const inferFileType = (filePath) => {
60
60
  }
61
61
  return "type";
62
62
  };
63
+ const isRegistryTrackableFile = (filePath) => {
64
+ const normalized = normalizeRelativePath(filePath);
65
+ return (/\.(controller|service|repository|middleware|module|dto|route|type|types)\.ts$/.test(normalized) ||
66
+ normalized.endsWith(".route.handler.ts"));
67
+ };
68
+ const createPackageFileRecord = (type, filePath, registered = false, tags = []) => ({
69
+ type,
70
+ path: normalizeRelativePath(filePath),
71
+ registered,
72
+ tags: [...tags]
73
+ });
74
+ const normalizeFileRecord = (record) => ({
75
+ type: record.type ?? inferFileType(record.path),
76
+ path: normalizeRelativePath(record.path),
77
+ registered: record.registered ?? true,
78
+ tags: Array.isArray(record.tags) ? record.tags.map((tag) => String(tag)) : []
79
+ });
63
80
  const inferSymbolFromFile = (filePath) => {
64
81
  const base = path.posix.basename(filePath).replace(/\.ts$/, "");
65
82
  if (base.endsWith(".route.handler")) {
@@ -128,16 +145,71 @@ const getPackageDecorator = (node) => {
128
145
  }
129
146
  return undefined;
130
147
  };
148
+ const getPackageCallExpression = (node) => {
149
+ const visit = (current) => {
150
+ if (ts.isCallExpression(current)) {
151
+ const callee = current.expression;
152
+ if (ts.isIdentifier(callee) && callee.text === "Package") {
153
+ return current;
154
+ }
155
+ }
156
+ return ts.forEachChild(current, visit);
157
+ };
158
+ return visit(node);
159
+ };
160
+ const getFunctionalPackageDefinition = (sourceFile) => {
161
+ for (const statement of sourceFile.statements) {
162
+ if (!ts.isVariableStatement(statement)) {
163
+ continue;
164
+ }
165
+ for (const declaration of statement.declarationList.declarations) {
166
+ if (!ts.isIdentifier(declaration.name)) {
167
+ continue;
168
+ }
169
+ const identifier = declaration.name.text;
170
+ if (!identifier.endsWith("PackageDefinition")) {
171
+ continue;
172
+ }
173
+ const initializer = declaration.initializer;
174
+ if (initializer && ts.isObjectLiteralExpression(initializer)) {
175
+ return initializer;
176
+ }
177
+ }
178
+ }
179
+ return undefined;
180
+ };
181
+ const resolvePackageDefinitionByName = (sourceFile, identifier) => {
182
+ for (const statement of sourceFile.statements) {
183
+ if (!ts.isVariableStatement(statement)) {
184
+ continue;
185
+ }
186
+ for (const declaration of statement.declarationList.declarations) {
187
+ if (!ts.isIdentifier(declaration.name) || declaration.name.text !== identifier) {
188
+ continue;
189
+ }
190
+ if (declaration.initializer && ts.isObjectLiteralExpression(declaration.initializer)) {
191
+ return declaration.initializer;
192
+ }
193
+ }
194
+ }
195
+ return undefined;
196
+ };
131
197
  const parsePackageMetadata = (rootDir, filePath) => {
132
198
  const sourceText = fs.readFileSync(filePath, "utf8");
133
199
  const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
134
200
  const classes = sourceFile.statements.filter(ts.isClassDeclaration);
135
201
  const decoratedClass = classes.find((node) => getPackageDecorator(node));
136
- if (!decoratedClass) {
202
+ const decorator = decoratedClass ? getPackageDecorator(decoratedClass) : undefined;
203
+ const functionalCall = decorator ? undefined : getPackageCallExpression(sourceFile);
204
+ const functionalDefinition = decorator || functionalCall ? undefined : getFunctionalPackageDefinition(sourceFile);
205
+ if (!decorator && !functionalCall && !functionalDefinition) {
137
206
  return undefined;
138
207
  }
139
- const decorator = getPackageDecorator(decoratedClass);
140
- const argument = decorator?.arguments[0];
208
+ const packageCallArgument = functionalCall?.arguments[0];
209
+ const resolvedFunctionalArgument = packageCallArgument && ts.isIdentifier(packageCallArgument)
210
+ ? resolvePackageDefinitionByName(sourceFile, packageCallArgument.text)
211
+ : packageCallArgument;
212
+ const argument = decorator?.arguments[0] ?? resolvedFunctionalArgument ?? functionalDefinition;
141
213
  if (!argument || !ts.isObjectLiteralExpression(argument)) {
142
214
  throw new Error(`Malformed @Package() metadata in ${filePath}.`);
143
215
  }
@@ -156,7 +228,19 @@ const parsePackageMetadata = (rootDir, filePath) => {
156
228
  }
157
229
  return value.text;
158
230
  };
159
- const name = getString("name", decoratedClass.name?.text ?? "");
231
+ const inferredName = decoratedClass?.name?.text ??
232
+ (sourceFile.statements.find(ts.isVariableStatement)?.declarationList.declarations.find((declaration) => {
233
+ if (!ts.isIdentifier(declaration.name)) {
234
+ return false;
235
+ }
236
+ const initializer = declaration.initializer;
237
+ if (!initializer || !ts.isCallExpression(initializer)) {
238
+ return false;
239
+ }
240
+ const callee = initializer.expression;
241
+ return ts.isIdentifier(callee) && callee.text === "Package";
242
+ })?.name.getText(sourceFile) ?? "");
243
+ const name = getString("name", inferredName);
160
244
  const packagePath = getString("path", "");
161
245
  if (!name || !packagePath) {
162
246
  throw new Error(`Malformed @Package() metadata in ${filePath}.`);
@@ -175,8 +259,18 @@ export const loadPackageRegistry = (rootDir) => {
175
259
  return { packages: {}, files: [] };
176
260
  }
177
261
  return {
178
- packages: data.packages ?? {},
179
- files: data.files ?? []
262
+ packages: Object.fromEntries(Object.entries(data.packages ?? {}).map(([name, record]) => [
263
+ name,
264
+ {
265
+ ...record,
266
+ files: (record.files ?? [])
267
+ .map((file) => normalizeFileRecord(file))
268
+ .filter((file) => isRegistryTrackableFile(file.path))
269
+ }
270
+ ])),
271
+ files: (data.files ?? [])
272
+ .map((file) => normalizeFileRecord(file))
273
+ .filter((file) => isRegistryTrackableFile(file.path))
180
274
  };
181
275
  };
182
276
  export const savePackageRegistry = (rootDir, registry) => {
@@ -185,6 +279,16 @@ export const savePackageRegistry = (rootDir, registry) => {
185
279
  export const scanPackageRegistry = (rootDir) => {
186
280
  const srcRoot = getSrcRoot(rootDir);
187
281
  const sourceRoot = path.join(rootDir, srcRoot);
282
+ const existingRegistry = loadPackageRegistry(rootDir);
283
+ const existingFileState = new Map();
284
+ for (const record of Object.values(existingRegistry.packages)) {
285
+ for (const file of record.files) {
286
+ existingFileState.set(file.path, file);
287
+ }
288
+ }
289
+ for (const file of existingRegistry.files) {
290
+ existingFileState.set(file.path, file);
291
+ }
188
292
  const packages = [];
189
293
  const visit = (dir) => {
190
294
  if (!fs.existsSync(dir)) {
@@ -213,24 +317,31 @@ export const scanPackageRegistry = (rootDir) => {
213
317
  const allTsFiles = collectTsFiles(sourceRoot, rootDir);
214
318
  const ownedFiles = new Set();
215
319
  for (const file of allTsFiles) {
216
- if (file === PACKAGE_REGISTRY_FILE || file.endsWith(".d.ts") || file.endsWith("index.ts")) {
320
+ if (file === PACKAGE_REGISTRY_FILE ||
321
+ file.endsWith(".d.ts") ||
322
+ file.endsWith("index.ts") ||
323
+ !isRegistryTrackableFile(file)) {
217
324
  continue;
218
325
  }
219
326
  const owner = [...packages]
220
327
  .filter((pkg) => fileBelongsToPackage(pkg.path, file))
221
328
  .sort((left, right) => right.path.length - left.path.length)[0];
222
- const nextFile = {
223
- type: inferFileType(file),
224
- path: file
225
- };
329
+ const existingFile = existingFileState.get(file);
330
+ const nextFile = createPackageFileRecord(inferFileType(file), file, owner ? existingFile?.registered ?? true : existingFile?.registered ?? false, existingFile?.tags ?? []);
226
331
  if (owner) {
227
- owner.files.push(nextFile);
332
+ owner.files.push({
333
+ ...nextFile,
334
+ registered: existingFile?.registered ?? true
335
+ });
228
336
  ownedFiles.add(file);
229
337
  }
230
338
  }
231
339
  const unpackaged = [];
232
340
  for (const file of allTsFiles) {
233
- if (file === PACKAGE_REGISTRY_FILE || file.endsWith(".d.ts") || file.endsWith("index.ts")) {
341
+ if (file === PACKAGE_REGISTRY_FILE ||
342
+ file.endsWith(".d.ts") ||
343
+ file.endsWith("index.ts") ||
344
+ !isRegistryTrackableFile(file)) {
234
345
  continue;
235
346
  }
236
347
  if (ownedFiles.has(file)) {
@@ -238,7 +349,9 @@ export const scanPackageRegistry = (rootDir) => {
238
349
  }
239
350
  unpackaged.push({
240
351
  type: inferFileType(file),
241
- path: file
352
+ path: file,
353
+ registered: existingFileState.get(file)?.registered ?? false,
354
+ tags: existingFileState.get(file)?.tags ?? []
242
355
  });
243
356
  }
244
357
  return {
@@ -258,9 +371,9 @@ export const syncPackageRegistry = (rootDir) => {
258
371
  savePackageRegistry(rootDir, registry);
259
372
  return registry;
260
373
  };
261
- const derivePackageIndexView = (record) => {
262
- const ownedFiles = record.files.filter((file) => file.path !== record.index);
263
- const controllerFiles = ownedFiles.filter((file) => file.path.endsWith(".controller.ts"));
374
+ const derivePackageIndexView = (record, style = "decorator") => {
375
+ const ownedFiles = record.files.filter((file) => file.path !== record.index && file.registered);
376
+ const controllerFiles = style === "decorator" ? ownedFiles.filter((file) => file.path.endsWith(".controller.ts")) : [];
264
377
  const serviceFiles = ownedFiles.filter((file) => file.path.endsWith(".service.ts"));
265
378
  const repositoryFiles = ownedFiles.filter((file) => file.path.endsWith(".repository.ts"));
266
379
  const middlewareFiles = ownedFiles.filter((file) => file.path.endsWith(".middleware.ts"));
@@ -268,29 +381,28 @@ const derivePackageIndexView = (record) => {
268
381
  const routeHandlerFiles = ownedFiles.filter((file) => file.path.endsWith(".route.handler.ts"));
269
382
  const dtoFiles = ownedFiles.filter((file) => file.path.endsWith(".dto.ts"));
270
383
  const typeFiles = ownedFiles.filter((file) => file.path.endsWith(".types.ts") || file.path.endsWith(".type.ts"));
271
- const importableFiles = [
272
- ...controllerFiles,
273
- ...serviceFiles,
274
- ...repositoryFiles,
275
- ...middlewareFiles,
276
- ...routeFiles,
277
- ...dtoFiles
278
- ];
279
- const exportFiles = [
280
- ...controllerFiles,
281
- ...serviceFiles,
282
- ...repositoryFiles,
283
- ...middlewareFiles,
284
- ...routeFiles,
285
- ...routeHandlerFiles,
286
- ...dtoFiles
287
- ];
288
- const packageExportFiles = [
289
- ...serviceFiles,
290
- ...repositoryFiles,
291
- ...middlewareFiles,
292
- ...dtoFiles
293
- ];
384
+ const importableFiles = style === "functional"
385
+ ? [...routeFiles]
386
+ : [
387
+ ...controllerFiles,
388
+ ...serviceFiles,
389
+ ...repositoryFiles,
390
+ ...middlewareFiles,
391
+ ...routeFiles,
392
+ ...dtoFiles
393
+ ];
394
+ const exportFiles = style === "functional"
395
+ ? [...serviceFiles, ...repositoryFiles, ...routeFiles, ...routeHandlerFiles, ...typeFiles]
396
+ : [
397
+ ...controllerFiles,
398
+ ...serviceFiles,
399
+ ...repositoryFiles,
400
+ ...middlewareFiles,
401
+ ...routeFiles,
402
+ ...routeHandlerFiles,
403
+ ...dtoFiles
404
+ ];
405
+ const packageExportFiles = style === "functional" ? [] : [...serviceFiles, ...repositoryFiles, ...middlewareFiles, ...dtoFiles];
294
406
  const renderArray = (values) => values.length === 0 ? "[]" : `[\n${values.map((value) => ` ${value}`).join(",\n")}\n ]`;
295
407
  const imports = importableFiles
296
408
  .map((file) => {
@@ -308,7 +420,21 @@ const derivePackageIndexView = (record) => {
308
420
  return `export * from "./${relative}";`;
309
421
  })
310
422
  .join("\n");
311
- const packageBlock = `@Package({
423
+ const packageBlock = style === "functional"
424
+ ? `const ${toPascalCase(record.name)}PackageDefinition = {
425
+ name: "${record.name}",
426
+ path: "${record.path}",
427
+ imports: [],
428
+ exports: [],
429
+ controllers: [],
430
+ services: [],
431
+ repositories: [],
432
+ middlewares: [],
433
+ routes: ${renderArray(routeFiles.map((file) => inferSymbolFromFile(file.path)))}
434
+ };
435
+
436
+ export const ${toPascalCase(record.name)}Package = Package(${toPascalCase(record.name)}PackageDefinition)(() => ${toPascalCase(record.name)}PackageDefinition);`
437
+ : `@Package({
312
438
  name: "${record.name}",
313
439
  path: "${record.path}",
314
440
  imports: [],
@@ -322,9 +448,12 @@ const derivePackageIndexView = (record) => {
322
448
  export class ${toPascalCase(record.name)}Package {}`;
323
449
  return { imports, exports, packageBlock };
324
450
  };
325
- export const renderPackageIndex = (record) => {
326
- const sections = derivePackageIndexView(record);
327
- return `/**\n * @generated true\n */\nimport { Package } from "@sculptor/core";\n\n// [sculptor:imports:start]\n${sections.imports}\n// [sculptor:imports:end]\n\n// [sculptor:exports:start]\n${sections.exports}\n// [sculptor:exports:end]\n\n// [sculptor:package:start]\n${sections.packageBlock}\n// [sculptor:package:end]\n`;
451
+ export const renderPackageIndex = (record, style = "decorator") => {
452
+ const sections = derivePackageIndexView(record, style);
453
+ const importLine = style === "functional"
454
+ ? 'import { Package, type SculptorFunctionalPackage } from "@sculptor/core";'
455
+ : 'import { Package } from "@sculptor/core";';
456
+ return `/**\n * @generated true\n */\n${importLine}\n\n// [sculptor:imports:start]\n${sections.imports}\n// [sculptor:imports:end]\n\n// [sculptor:exports:start]\n${sections.exports}\n// [sculptor:exports:end]\n\n// [sculptor:package:start]\n${sections.packageBlock}\n// [sculptor:package:end]\n`;
328
457
  };
329
458
  export const inferPackageNameFromPath = (packagePath) => normalizeRelativePath(path.posix.basename(packagePath));
330
459
  export const inferPackagePath = (rootDir, packageName, explicitPath) => {
@@ -333,7 +462,7 @@ export const inferPackagePath = (rootDir, packageName, explicitPath) => {
333
462
  if (explicitPath) {
334
463
  return normalizeRelativePath(path.posix.join(normalizeRelativePath(explicitPath), packageFolder));
335
464
  }
336
- return normalizeRelativePath(path.posix.join(srcRoot, packageFolder));
465
+ return normalizeRelativePath(path.posix.join(srcRoot, "app", packageFolder));
337
466
  };
338
467
  export const inferPackageIndexPath = (packagePath) => normalizeRelativePath(path.posix.join(packagePath, "index.ts"));
339
468
  export const buildPackageRecord = (rootDir, packageName, explicitPath) => {
@@ -341,11 +470,11 @@ export const buildPackageRecord = (rootDir, packageName, explicitPath) => {
341
470
  const index = inferPackageIndexPath(packagePath);
342
471
  const normalizedPackageName = normalizeRelativePath(packageName);
343
472
  const files = [
344
- { type: "controller", path: normalizeRelativePath(path.posix.join(packagePath, `${normalizedPackageName}.controller.ts`)) },
345
- { type: "service", path: normalizeRelativePath(path.posix.join(packagePath, `${normalizedPackageName}.service.ts`)) },
346
- { type: "repository", path: normalizeRelativePath(path.posix.join(packagePath, `${normalizedPackageName}.repository.ts`)) },
347
- { type: "dto", path: normalizeRelativePath(path.posix.join(packagePath, `${normalizedPackageName}.dto.ts`)) },
348
- { type: "type", path: normalizeRelativePath(path.posix.join(packagePath, `${normalizedPackageName}.types.ts`)) }
473
+ createPackageFileRecord("controller", path.posix.join(packagePath, `${normalizedPackageName}.controller.ts`), true),
474
+ createPackageFileRecord("service", path.posix.join(packagePath, `${normalizedPackageName}.service.ts`), true),
475
+ createPackageFileRecord("repository", path.posix.join(packagePath, `${normalizedPackageName}.repository.ts`), true),
476
+ createPackageFileRecord("dto", path.posix.join(packagePath, `${normalizedPackageName}.dto.ts`), true),
477
+ createPackageFileRecord("type", path.posix.join(packagePath, `${normalizedPackageName}.types.ts`), true)
349
478
  ];
350
479
  return {
351
480
  name: normalizedPackageName,
@@ -368,52 +497,67 @@ const renderPackageRegistryImport = (srcRoot, record) => {
368
497
  : record.path;
369
498
  return `import { ${toPascalCase(record.name)}Package } from "./${normalizeRelativePath(path.posix.join(packageImportPath, "index.js"))}";`;
370
499
  };
371
- const renderRegistryRootFile = (srcRoot, records) => {
372
- const imports = records.map((record) => renderPackageRegistryImport(srcRoot, record)).join("\n");
373
- const packageEntries = records.map((record) => `${toPascalCase(record.name)}Package`).join(", ");
374
- return `${imports}${imports ? "\n\n" : ""}export const registry = {\n packages: [${packageEntries}],\n controllers: [],\n routes: [],\n services: [],\n repositories: [],\n middlewares: []\n};\n`;
500
+ const renderDirectRegistryImport = (srcRoot, file) => {
501
+ if (!["controller", "service", "repository", "middleware", "route"].includes(file.type)) {
502
+ return undefined;
503
+ }
504
+ const sourcePath = file.path.startsWith(`${srcRoot}/`)
505
+ ? file.path.slice(`${srcRoot}/`.length)
506
+ : file.path;
507
+ const symbol = inferSymbolFromFile(file.path);
508
+ return `import { ${symbol} } from "./${normalizeRelativePath(path.posix.join(sourcePath.replace(/\.ts$/, ".js")))}";`;
509
+ };
510
+ const renderRegistryRootFile = (rootDir, registry) => {
511
+ const srcRoot = getSrcRoot(rootDir);
512
+ const packageRecords = Object.values(registry.packages).sort((left, right) => left.name.localeCompare(right.name));
513
+ const directFiles = registry.files
514
+ .filter((file) => file.registered)
515
+ .sort((left, right) => left.path.localeCompare(right.path));
516
+ const packageImports = packageRecords.map((record) => renderPackageRegistryImport(srcRoot, record));
517
+ const directImports = directFiles
518
+ .map((file) => renderDirectRegistryImport(srcRoot, file))
519
+ .filter((value) => Boolean(value));
520
+ const directControllers = directFiles
521
+ .filter((file) => file.type === "controller")
522
+ .map((file) => inferSymbolFromFile(file.path));
523
+ const directServices = directFiles
524
+ .filter((file) => file.type === "service")
525
+ .map((file) => inferSymbolFromFile(file.path));
526
+ const directRepositories = directFiles
527
+ .filter((file) => file.type === "repository")
528
+ .map((file) => inferSymbolFromFile(file.path));
529
+ const directMiddlewares = directFiles
530
+ .filter((file) => file.type === "middleware")
531
+ .map((file) => inferSymbolFromFile(file.path));
532
+ const directRoutes = directFiles
533
+ .filter((file) => file.type === "route")
534
+ .map((file) => inferSymbolFromFile(file.path));
535
+ const packageEntries = packageRecords.map((record) => `${toPascalCase(record.name)}Package`);
536
+ return `/**
537
+ * @generated true
538
+ */
539
+ ${[...packageImports, ...directImports].join("\n")}
540
+
541
+ export const registry = {
542
+ packages: [${packageEntries.join(", ")}],
543
+ controllers: [${directControllers.join(", ")}],
544
+ routes: [${directRoutes.join(", ")}],
545
+ services: [${directServices.join(", ")}],
546
+ repositories: [${directRepositories.join(", ")}],
547
+ middlewares: [${directMiddlewares.join(", ")}]
548
+ };
549
+ `;
375
550
  };
376
551
  export const syncRootRegistryForPackages = (rootDir) => {
377
552
  const srcRoot = getSrcRoot(rootDir);
378
553
  const registryPath = path.join(rootDir, srcRoot, "registry.ts");
379
554
  const registry = loadPackageRegistry(rootDir);
380
- const packageRecords = Object.values(registry.packages).sort((left, right) => left.name.localeCompare(right.name));
381
- if (packageRecords.length === 0 && !fs.existsSync(registryPath)) {
555
+ if (Object.keys(registry.packages).length === 0 && registry.files.length === 0 && !fs.existsSync(registryPath)) {
382
556
  return registryPath;
383
557
  }
384
558
  fs.mkdirSync(path.dirname(registryPath), { recursive: true });
385
- if (!fs.existsSync(registryPath)) {
386
- const next = renderRegistryRootFile(srcRoot, packageRecords);
387
- fs.writeFileSync(registryPath, next, "utf8");
388
- return registryPath;
389
- }
390
- const current = fs.readFileSync(registryPath, "utf8");
391
- const packageImportPattern = /^import \{ [^}]+Package \} from "\.\/.*\/index\.js";\n?/gm;
392
- const hasPackageRegistryShape = packageImportPattern.test(current) || /packages:\s*\[[^\]]*\]/m.test(current);
393
- packageImportPattern.lastIndex = 0;
394
- if (packageRecords.length === 0 && !hasPackageRegistryShape) {
395
- return registryPath;
396
- }
397
- const imports = packageRecords.map((record) => renderPackageRegistryImport(srcRoot, record)).join("\n");
398
- const packageEntries = packageRecords.map((record) => `${toPascalCase(record.name)}Package`).join(", ");
399
- const strippedImports = current.replace(packageImportPattern, "");
400
- const withImports = `${imports}${imports ? "\n\n" : ""}${strippedImports}`.replace(/^\n+/, "");
401
- const packagePattern = /packages:\s*\[[^\]]*\]/m;
402
- if (packagePattern.test(withImports)) {
403
- const next = withImports.replace(packagePattern, `packages: [${packageEntries}]`);
404
- if (next !== current) {
405
- fs.writeFileSync(registryPath, next, "utf8");
406
- }
407
- return registryPath;
408
- }
409
- const registryOpenPattern = /(export const registry = \{\n)/;
410
- if (!registryOpenPattern.test(withImports)) {
411
- throw new Error(`Unable to update ${registryPath} safely. registry.ts is malformed.`);
412
- }
413
- const next = withImports.replace(registryOpenPattern, `$1 packages: [${packageEntries}],\n`);
414
- if (next !== current) {
415
- fs.writeFileSync(registryPath, next, "utf8");
416
- }
559
+ const next = renderRegistryRootFile(rootDir, registry);
560
+ fs.writeFileSync(registryPath, next, "utf8");
417
561
  return registryPath;
418
562
  };
419
563
  export const upsertFileIntoRegistry = (registry, filePath) => {
@@ -427,10 +571,7 @@ export const upsertFileIntoRegistry = (registry, filePath) => {
427
571
  continue;
428
572
  }
429
573
  const existingIndex = record.files.findIndex((entry) => entry.path === normalizedPath);
430
- const nextFile = {
431
- type: fileType,
432
- path: normalizedPath
433
- };
574
+ const nextFile = createPackageFileRecord(fileType, normalizedPath, true);
434
575
  if (existingIndex >= 0) {
435
576
  record.files[existingIndex] = nextFile;
436
577
  }
@@ -440,10 +581,7 @@ export const upsertFileIntoRegistry = (registry, filePath) => {
440
581
  return registry;
441
582
  }
442
583
  const existing = registry.files.findIndex((entry) => entry.path === normalizedPath);
443
- const nextFile = {
444
- type: fileType,
445
- path: normalizedPath
446
- };
584
+ const nextFile = createPackageFileRecord(fileType, normalizedPath, true);
447
585
  if (existing >= 0) {
448
586
  registry.files[existing] = nextFile;
449
587
  }
@@ -452,7 +590,39 @@ export const upsertFileIntoRegistry = (registry, filePath) => {
452
590
  }
453
591
  return registry;
454
592
  };
455
- export const removeFileFromRegistry = (registry, filePath) => {
593
+ export const unregisterFileFromRegistry = (registry, filePath) => {
594
+ const normalizedPath = normalizeRelativePath(filePath);
595
+ for (const record of Object.values(registry.packages)) {
596
+ if (normalizedPath === record.index) {
597
+ return registry;
598
+ }
599
+ if (!isFileOwnedByPackage(record, normalizedPath)) {
600
+ continue;
601
+ }
602
+ const existingIndex = record.files.findIndex((entry) => entry.path === normalizedPath);
603
+ if (existingIndex >= 0) {
604
+ record.files[existingIndex] = {
605
+ ...record.files[existingIndex],
606
+ registered: false
607
+ };
608
+ }
609
+ else {
610
+ record.files.push(createPackageFileRecord(inferFileType(normalizedPath), normalizedPath, false));
611
+ }
612
+ return registry;
613
+ }
614
+ const existingIndex = registry.files.findIndex((entry) => entry.path === normalizedPath);
615
+ if (existingIndex >= 0) {
616
+ registry.files[existingIndex] = {
617
+ ...registry.files[existingIndex],
618
+ registered: false
619
+ };
620
+ return registry;
621
+ }
622
+ registry.files.push(createPackageFileRecord(inferFileType(normalizedPath), normalizedPath, false));
623
+ return registry;
624
+ };
625
+ export const deleteFileFromRegistry = (registry, filePath) => {
456
626
  const normalizedPath = normalizeRelativePath(filePath);
457
627
  for (const record of Object.values(registry.packages)) {
458
628
  if (normalizedPath === record.index) {
@@ -471,7 +641,7 @@ export const getOwningPackage = (registry, filePath) => {
471
641
  const normalizedPath = normalizeRelativePath(filePath);
472
642
  return Object.values(registry.packages).find((record) => isFileOwnedByPackage(record, normalizedPath));
473
643
  };
474
- export const renderPackageIndexForRecord = (record) => renderPackageIndex(record);
644
+ export const renderPackageIndexForRecord = (record) => renderPackageIndex(record, inferPackageIndexStyle(record));
475
645
  const generatedMarkers = {
476
646
  imports: {
477
647
  start: "// [sculptor:imports:start]",
@@ -498,8 +668,17 @@ const replaceMarkerBlock = (source, section, content) => {
498
668
  }
499
669
  return `${source.slice(0, startIndex + start.length)}\n${content}\n${source.slice(endIndex)}`;
500
670
  };
501
- const renderPackageIndexSections = (record) => {
502
- const sections = derivePackageIndexView(record);
671
+ const inferPackageIndexStyle = (record, sourceText) => {
672
+ if (sourceText?.includes("SculptorFunctionalPackage")) {
673
+ return "functional";
674
+ }
675
+ if (record.files.some((file) => file.path.endsWith(".controller.ts"))) {
676
+ return "decorator";
677
+ }
678
+ return "functional";
679
+ };
680
+ const renderPackageIndexSections = (record, style = "decorator") => {
681
+ const sections = derivePackageIndexView(record, style);
503
682
  return {
504
683
  imports: sections.imports,
505
684
  exports: sections.exports,
@@ -507,20 +686,20 @@ const renderPackageIndexSections = (record) => {
507
686
  };
508
687
  };
509
688
  export const updatePackageIndexForRecord = (filePath, record) => {
510
- const next = renderPackageIndex(record);
511
689
  if (!fs.existsSync(filePath)) {
512
690
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
513
- fs.writeFileSync(filePath, next, "utf8");
691
+ fs.writeFileSync(filePath, renderPackageIndex(record, inferPackageIndexStyle(record)), "utf8");
514
692
  return;
515
693
  }
516
694
  const current = fs.readFileSync(filePath, "utf8");
695
+ const style = inferPackageIndexStyle(record, current);
517
696
  for (const section of Object.keys(generatedMarkers)) {
518
697
  if (!current.includes(generatedMarkers[section].start) || !current.includes(generatedMarkers[section].end)) {
519
698
  throw new Error(`Package index markers are missing or malformed in ${filePath}. Refusing to rewrite the file unsafely.`);
520
699
  }
521
700
  }
522
701
  let updated = current;
523
- const sections = renderPackageIndexSections(record);
702
+ const sections = renderPackageIndexSections(record, style);
524
703
  updated = replaceMarkerBlock(updated, "imports", sections.imports);
525
704
  updated = replaceMarkerBlock(updated, "exports", sections.exports);
526
705
  updated = replaceMarkerBlock(updated, "package", sections.package);