@forinda/kickjs-cli 3.2.0 → 4.0.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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @forinda/kickjs-cli v3.2.0
2
+ * @forinda/kickjs-cli v4.0.0
3
3
  *
4
4
  * Copyright (c) Felix Orinda
5
5
  *
@@ -8,8 +8,10 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import { dirname, join, relative, resolve, sep } from "node:path";
11
+ import { dirname, extname, join, relative, resolve, sep } from "node:path";
12
12
  import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
13
+ import { statSync } from "node:fs";
14
+ import { globSync } from "glob";
13
15
  //#region src/typegen/scanner.ts
14
16
  /** Decorators that mark a class as DI-managed */
15
17
  const DECORATOR_NAMES = [
@@ -64,6 +66,28 @@ const BARE_CREATE_TOKEN_REGEX = /createToken\s*(?:<[^>]*>)?\s*\(\s*['"`]([^'"`]+
64
66
  /** Match `@Inject('literal')` — only literals; computed args are skipped */
65
67
  const INJECT_LITERAL_REGEX = /@Inject\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
66
68
  /**
69
+ * Match the start of a `defineAdapter(...)` or `definePlugin(...)` call,
70
+ * tolerating optional `<TConfig, TExtra>` generics. Captures the helper
71
+ * name. The callsite's first-arg object is parsed forward via
72
+ * `findBalancedClose` so nested objects/parens don't confuse us.
73
+ */
74
+ const DEFINE_HELPER_START = /\b(defineAdapter|definePlugin)\s*(?:<[^>]*>)?\s*\(/g;
75
+ /**
76
+ * Match a class declaration whose `implements` clause includes `AppAdapter`.
77
+ * Captures the class name. Used to pick up the (rare, post-defineAdapter)
78
+ * legacy class-style adapters so their literal `name = '...'` field can
79
+ * still feed `KickJsPluginRegistry`.
80
+ */
81
+ const APP_ADAPTER_CLASS_REGEX = new RegExp(String.raw`export\s+(?:default\s+)?(?:abstract\s+)?class\s+(\w+)` + String.raw`(?:\s+extends\s+\w+(?:<[^>]*>)?)?` + String.raw`\s+implements\s+[^{]*\bAppAdapter\b`, "g");
82
+ /** Match a string-literal `name = '...'` field on a class body. */
83
+ const CLASS_NAME_FIELD_REGEX = /\bname\s*(?::\s*[^=]+)?=\s*['"`]([^'"`]+)['"`]/;
84
+ /**
85
+ * Match the start of a `defineAugmentation('Name', ...)` call. Captures
86
+ * the literal name. The optional second-arg object is parsed forward so
87
+ * `description` / `example` can be pulled out.
88
+ */
89
+ const DEFINE_AUGMENTATION_START = /\bdefineAugmentation\s*\(\s*['"`]([^'"`]+)['"`]\s*(,\s*\{)?/g;
90
+ /**
67
91
  * Locate the start of a route decorator: `@Get(`, `@Post(`, etc.
68
92
  * Used by `extractRoutesFromSource`; the rest of the route declaration
69
93
  * (balanced parens, stacked decorators, method name) is parsed by walking
@@ -410,6 +434,120 @@ function extractInjectsFromSource(source, filePath, cwd) {
410
434
  return out;
411
435
  }
412
436
  /**
437
+ * Extract the bounds of an object literal that begins at `openBracePos`
438
+ * (the index of the `{` character). Returns the index of the matching `}`
439
+ * or -1 if no match is found. Counts balanced braces only — does not
440
+ * understand string literals so a `{` or `}` inside a string inside the
441
+ * object will skew the depth counter (matches `findBalancedClose`).
442
+ */
443
+ function findBalancedBrace(text, openBracePos) {
444
+ let depth = 1;
445
+ for (let i = openBracePos + 1; i < text.length; i++) {
446
+ const ch = text[i];
447
+ if (ch === "{") depth++;
448
+ else if (ch === "}") {
449
+ depth--;
450
+ if (depth === 0) return i;
451
+ }
452
+ }
453
+ return -1;
454
+ }
455
+ /**
456
+ * Extract plugins/adapters declared via `defineAdapter({ name: '...' })`
457
+ * or `definePlugin({ name: '...' })` calls and via class-style adapters
458
+ * (`class XxxAdapter implements AppAdapter` with a string-literal `name`
459
+ * field).
460
+ *
461
+ * Only the literal `name:` field feeds the result — the symbol on the LHS
462
+ * is irrelevant since `dependsOn` references the runtime name.
463
+ */
464
+ function extractPluginsAndAdaptersFromSource(source, filePath, cwd) {
465
+ const out = [];
466
+ const relPath = toRelative(filePath, cwd);
467
+ const seen = /* @__PURE__ */ new Set();
468
+ DEFINE_HELPER_START.lastIndex = 0;
469
+ let helperMatch;
470
+ while ((helperMatch = DEFINE_HELPER_START.exec(source)) !== null) {
471
+ const helper = helperMatch[1];
472
+ const openParen = DEFINE_HELPER_START.lastIndex - 1;
473
+ const closeParen = findBalancedClose(source, openParen);
474
+ if (closeParen < 0) continue;
475
+ const callArgs = source.slice(openParen + 1, closeParen);
476
+ const nameMatch = /\bname\s*:\s*['"`]([^'"`]+)['"`]/.exec(callArgs);
477
+ if (!nameMatch) continue;
478
+ const name = nameMatch[1];
479
+ const dedupeKey = `${helper}::${name}::${filePath}`;
480
+ if (seen.has(dedupeKey)) continue;
481
+ seen.add(dedupeKey);
482
+ out.push({
483
+ kind: helper === "definePlugin" ? "plugin" : "adapter",
484
+ name,
485
+ filePath,
486
+ relativePath: relPath
487
+ });
488
+ }
489
+ APP_ADAPTER_CLASS_REGEX.lastIndex = 0;
490
+ let classMatch;
491
+ while ((classMatch = APP_ADAPTER_CLASS_REGEX.exec(source)) !== null) {
492
+ const classStart = classMatch.index;
493
+ const bracePos = source.indexOf("{", classStart);
494
+ if (bracePos < 0) continue;
495
+ const closeBrace = findBalancedBrace(source, bracePos);
496
+ if (closeBrace < 0) continue;
497
+ const body = source.slice(bracePos + 1, closeBrace);
498
+ const nameMatch = CLASS_NAME_FIELD_REGEX.exec(body);
499
+ if (!nameMatch) continue;
500
+ const name = nameMatch[1];
501
+ const dedupeKey = `class::${name}::${filePath}`;
502
+ if (seen.has(dedupeKey)) continue;
503
+ seen.add(dedupeKey);
504
+ out.push({
505
+ kind: "adapter",
506
+ name,
507
+ filePath,
508
+ relativePath: relPath
509
+ });
510
+ }
511
+ return out;
512
+ }
513
+ /**
514
+ * Extract `defineAugmentation('Name', { description, example })` calls
515
+ * from a source file. The metadata object is optional — when absent both
516
+ * `description` and `example` resolve to `null`.
517
+ */
518
+ function extractAugmentationsFromSource(source, filePath, cwd) {
519
+ const out = [];
520
+ const relPath = toRelative(filePath, cwd);
521
+ DEFINE_AUGMENTATION_START.lastIndex = 0;
522
+ let match;
523
+ while ((match = DEFINE_AUGMENTATION_START.exec(source)) !== null) {
524
+ const name = match[1];
525
+ let description = null;
526
+ let example = null;
527
+ if (match[2]) {
528
+ const bracePos = source.indexOf("{", match.index + match[0].length - 1);
529
+ if (bracePos >= 0) {
530
+ const closeBrace = findBalancedBrace(source, bracePos);
531
+ if (closeBrace >= 0) {
532
+ const body = source.slice(bracePos + 1, closeBrace);
533
+ const descMatch = /\bdescription\s*:\s*['"`]([^'"`]+)['"`]/.exec(body);
534
+ const exampleMatch = /\bexample\s*:\s*['"`]([^'"`]+)['"`]/.exec(body);
535
+ description = descMatch ? descMatch[1] : null;
536
+ example = exampleMatch ? exampleMatch[1] : null;
537
+ }
538
+ }
539
+ }
540
+ out.push({
541
+ name,
542
+ description,
543
+ example,
544
+ filePath,
545
+ relativePath: relPath
546
+ });
547
+ }
548
+ return out;
549
+ }
550
+ /**
413
551
  * Default search order for the env schema file. Newer projects keep
414
552
  * the schema under `src/config/` so the framework's "config" concept
415
553
  * has a single home; older scaffolds dropped it at `src/env.ts` (kept
@@ -480,6 +618,8 @@ async function scanProject(opts) {
480
618
  const routes = [];
481
619
  const tokens = [];
482
620
  const injects = [];
621
+ const pluginsAndAdapters = [];
622
+ const augmentations = [];
483
623
  const sources = /* @__PURE__ */ new Map();
484
624
  for (const file of files) {
485
625
  let source;
@@ -492,6 +632,8 @@ async function scanProject(opts) {
492
632
  classes.push(...extractClassesFromSource(source, file, opts.cwd));
493
633
  tokens.push(...extractTokensFromSource(source, file, opts.cwd));
494
634
  injects.push(...extractInjectsFromSource(source, file, opts.cwd));
635
+ pluginsAndAdapters.push(...extractPluginsAndAdaptersFromSource(source, file, opts.cwd));
636
+ augmentations.push(...extractAugmentationsFromSource(source, file, opts.cwd));
495
637
  }
496
638
  for (const [file, source] of sources) {
497
639
  const classesInFile = classes.filter((c) => c.filePath === file);
@@ -504,15 +646,148 @@ async function scanProject(opts) {
504
646
  tokens.sort((a, b) => a.name.localeCompare(b.name) || a.relativePath.localeCompare(b.relativePath));
505
647
  injects.sort((a, b) => a.name.localeCompare(b.name) || a.relativePath.localeCompare(b.relativePath));
506
648
  routes.sort((a, b) => a.controller.localeCompare(b.controller) || a.method.localeCompare(b.method));
649
+ pluginsAndAdapters.sort((a, b) => a.name.localeCompare(b.name) || a.relativePath.localeCompare(b.relativePath));
650
+ augmentations.sort((a, b) => a.name.localeCompare(b.name) || a.relativePath.localeCompare(b.relativePath));
507
651
  return {
508
652
  classes,
509
653
  routes,
510
654
  tokens,
511
655
  injects,
512
656
  collisions: findCollisions(classes),
513
- env: await detectEnvFile(opts.cwd, opts.envFile ?? "src/env.ts")
657
+ env: await detectEnvFile(opts.cwd, opts.envFile ?? "src/env.ts"),
658
+ pluginsAndAdapters,
659
+ augmentations
660
+ };
661
+ }
662
+ //#endregion
663
+ //#region src/typegen/asset-types.ts
664
+ /**
665
+ * Walks every `assetMap` entry's source directory + emits a typed
666
+ * `KickAssets` ambient augmentation (assets-plan.md PR 4). Generates
667
+ * `.kickjs/types/assets.d.ts` so adopters get autocomplete on
668
+ * `assets.<namespace>.<key>` and `@Asset('<namespace>/<key>')`.
669
+ *
670
+ * Pure module — no side effects beyond what the caller does with the
671
+ * returned content. Mirrors the shape of `renderPlugins` /
672
+ * `renderRegistry` in the generator so the typegen output stays
673
+ * consistent across surfaces.
674
+ *
675
+ * @module @forinda/kickjs-cli/typegen/asset-types
676
+ */
677
+ function discoverAssets(assetMap, cwd) {
678
+ if (!assetMap) return {
679
+ entries: [],
680
+ count: 0
681
+ };
682
+ const seen = /* @__PURE__ */ new Map();
683
+ for (const [namespace, entry] of Object.entries(assetMap)) {
684
+ if (!entry || typeof entry.src !== "string") continue;
685
+ const srcAbs = resolve(cwd, entry.src);
686
+ if (!isDir(srcAbs)) continue;
687
+ const matches = globSync(entry.glob ?? "**/*", {
688
+ cwd: srcAbs,
689
+ nodir: true,
690
+ dot: false,
691
+ posix: true
692
+ });
693
+ matches.sort();
694
+ for (const rel of matches) {
695
+ const key = stripExt(rel);
696
+ const logical = `${namespace}/${key}`;
697
+ seen.set(logical, {
698
+ namespace,
699
+ key
700
+ });
701
+ }
702
+ }
703
+ return {
704
+ entries: [...seen.values()],
705
+ count: seen.size
514
706
  };
515
707
  }
708
+ function renderAssetTypes(discovered) {
709
+ const HEADER = `/* eslint-disable */
710
+ // AUTO-GENERATED by \`kick typegen\`. DO NOT EDIT.
711
+ // Re-run with \`kick typegen\` or rely on \`kick dev\` to refresh.
712
+ `;
713
+ if (discovered.entries.length === 0) return `${HEADER}
714
+ declare module '@forinda/kickjs' {
715
+ /**
716
+ * Map of every typed asset discovered in the project's assetMap.
717
+ * (No assetMap entries discovered yet — declare with
718
+ * \`assetMap: { name: { src: 'src/...' } }\` in kick.config.ts.)
719
+ */
720
+ interface KickAssets {}
721
+ }
722
+
723
+ export {}
724
+ `;
725
+ const tree = {};
726
+ for (const entry of discovered.entries) {
727
+ const path = `${entry.namespace}/${entry.key}`.split("/");
728
+ let node = tree;
729
+ for (let i = 0; i < path.length - 1; i++) {
730
+ const part = path[i];
731
+ const existing = node[part];
732
+ if (existing === LEAF) {
733
+ const promoted = {};
734
+ node[part] = promoted;
735
+ node = promoted;
736
+ } else {
737
+ if (!existing) node[part] = {};
738
+ node = node[part];
739
+ }
740
+ }
741
+ const leaf = path[path.length - 1];
742
+ if (typeof node[leaf] === "object") continue;
743
+ node[leaf] = LEAF;
744
+ }
745
+ return `${HEADER}
746
+ declare module '@forinda/kickjs' {
747
+ /**
748
+ * Map of every typed asset discovered in the project's assetMap.
749
+ * Each leaf is a \`() => string\` thunk that returns the resolved
750
+ * absolute path for the file in the current run mode (dev → src,
751
+ * prod → dist).
752
+ */
753
+ interface KickAssets {
754
+ ${renderTree(tree, " ")}
755
+ }
756
+ }
757
+
758
+ export {}
759
+ `;
760
+ }
761
+ const LEAF = Symbol("asset-leaf");
762
+ function renderTree(node, indent) {
763
+ const keys = Object.keys(node).sort();
764
+ const lines = [];
765
+ for (const key of keys) {
766
+ const child = node[key];
767
+ const safeKey = isIdentifier(key) ? key : JSON.stringify(key);
768
+ if (child === LEAF) lines.push(`${indent}${safeKey}: () => string`);
769
+ else {
770
+ lines.push(`${indent}${safeKey}: {`);
771
+ lines.push(renderTree(child, `${indent} `));
772
+ lines.push(`${indent}}`);
773
+ }
774
+ }
775
+ return lines.join("\n");
776
+ }
777
+ function isIdentifier(str) {
778
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(str);
779
+ }
780
+ function isDir(path) {
781
+ try {
782
+ return statSync(path).isDirectory();
783
+ } catch {
784
+ return false;
785
+ }
786
+ }
787
+ function stripExt(path) {
788
+ const ext = extname(path);
789
+ return ext ? path.slice(0, -ext.length) : path;
790
+ }
516
791
  //#endregion
517
792
  //#region src/typegen/generator.ts
518
793
  /**
@@ -649,12 +924,17 @@ function renderIndex(includeEnv) {
649
924
  export type { ServiceToken } from './services'
650
925
  export type { ModuleToken } from './modules'
651
926
 
652
- // The registry, routes, and env augmentations are loaded as side-effects —
653
- // importing this file (or having it on tsconfig include) is enough for
654
- // \`container.resolve()\`, \`Ctx<KickRoutes.UserController['getUser']>\`,
655
- // and \`@Value('PORT')\` to resolve.
927
+ // The registry, routes, plugins, assets, and env augmentations are
928
+ // loaded as side-effects — importing this file (or having it on
929
+ // tsconfig include) is enough for \`container.resolve()\`,
930
+ // \`Ctx<KickRoutes.UserController['getUser']>\`,
931
+ // \`dependsOn: ['TenantAdapter']\`, \`assets.mails.welcome()\`, and
932
+ // \`@Value('PORT')\` to resolve.
656
933
  import './registry'
657
934
  import './routes'
935
+ import './plugins'
936
+ import './augmentations'
937
+ import './assets'
658
938
  ${includeEnv ? "import './env'\n" : ""}`;
659
939
  }
660
940
  /**
@@ -849,9 +1129,90 @@ ${interfaces.join("\n")}
849
1129
  export {}
850
1130
  `;
851
1131
  }
1132
+ /**
1133
+ * Render the `KickJsPluginRegistry` augmentation. Each entry maps the
1134
+ * literal `name` field of a plugin/adapter to a marker type (the
1135
+ * registry value isn't load-bearing at runtime — `dependsOn` only cares
1136
+ * about `keyof`, so any non-`never` type works). We emit `'plugin'` /
1137
+ * `'adapter'` strings so DevTools can later read the registry to tell
1138
+ * the kinds apart without a second source of truth.
1139
+ *
1140
+ * When the project has no discoverable plugins/adapters, the augmentation
1141
+ * is intentionally empty rather than skipped so the `keyof` constraint
1142
+ * resolves to `never` (which is harmless — `dependsOn: []` still works).
1143
+ */
1144
+ function renderPlugins(items) {
1145
+ const byName = /* @__PURE__ */ new Map();
1146
+ for (const item of items) if (!byName.has(item.name)) byName.set(item.name, item);
1147
+ const entries = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)).map((item) => ` '${item.name}': '${item.kind}'`).join("\n");
1148
+ return `${HEADER}
1149
+ declare module '@forinda/kickjs' {
1150
+ /**
1151
+ * Map of every plugin/adapter \`name\` discovered in the project. The
1152
+ * value type is the kind tag (\`'plugin'\` or \`'adapter'\`); the
1153
+ * \`keyof\` of this interface narrows \`dependsOn\` so misspelled deps
1154
+ * become compile errors instead of boot-time \`MissingMountDepError\`.
1155
+ */
1156
+ interface KickJsPluginRegistry {
1157
+ ${entries ? entries : " // (no plugins/adapters discovered yet — `defineAdapter`/`definePlugin` calls feed this)"}
1158
+ }
1159
+ }
1160
+
1161
+ export {}
1162
+ `;
1163
+ }
1164
+ /**
1165
+ * Render the augmentation manifest — one block per `defineAugmentation`
1166
+ * call discovered in the project. The output is a `.d.ts` file that
1167
+ * does nothing at runtime but acts as in-IDE documentation: adopters
1168
+ * jumping into it see every interface their plugins offer for
1169
+ * augmentation, alongside any `description` / `example` the plugin
1170
+ * authors provided.
1171
+ */
1172
+ function renderAugmentations(items) {
1173
+ if (items.length === 0) return `${HEADER}
1174
+ // No augmentations discovered.
1175
+ //
1176
+ // Plugins advertise augmentable interfaces via:
1177
+ //
1178
+ // import { defineAugmentation } from '@forinda/kickjs'
1179
+ // defineAugmentation('FeatureFlags', {
1180
+ // description: 'Feature flag shape consumed by FlagsPlugin',
1181
+ // example: '{ beta: boolean; rolloutPercentage: number }',
1182
+ // })
1183
+ //
1184
+ // See \`docs/guide/typegen.md#augmentations\` for the full pattern.
1185
+ export {}
1186
+ `;
1187
+ const byName = /* @__PURE__ */ new Map();
1188
+ for (const item of items) if (!byName.has(item.name)) byName.set(item.name, item);
1189
+ const blocks = [];
1190
+ for (const item of [...byName.values()].sort((a, b) => a.name.localeCompare(b.name))) {
1191
+ const docLines = [];
1192
+ if (item.description) docLines.push(` * ${item.description}`);
1193
+ if (item.example) docLines.push(` * @example`, ` * \`\`\`ts`, ` * ${item.example}`, ` * \`\`\``);
1194
+ docLines.push(` * @see ${item.relativePath}`);
1195
+ blocks.push([
1196
+ "/**",
1197
+ ...docLines,
1198
+ " */",
1199
+ `export interface ${item.name}Augmentation {}`
1200
+ ].join("\n"));
1201
+ }
1202
+ return `${HEADER}
1203
+ // Catalogue of augmentable interfaces in this project. The interfaces
1204
+ // below are documentation only — augment the source-of-truth interfaces
1205
+ // in your own \`d.ts\` files (the framework declares the actual types).
1206
+
1207
+ ${blocks.join("\n\n")}
1208
+ `;
1209
+ }
852
1210
  /** Write all generated `.d.ts` files to `outDir` */
853
1211
  async function generateTypes(opts) {
854
- const { classes, routes = [], tokens = [], injects = [], collisions = [], env = null, outDir, allowDuplicates = false, schemaValidator = false } = opts;
1212
+ const { classes, routes = [], tokens = [], injects = [], collisions = [], env = null, pluginsAndAdapters = [], augmentations = [], assets = {
1213
+ entries: [],
1214
+ count: 0
1215
+ }, outDir, allowDuplicates = false, schemaValidator = false } = opts;
855
1216
  if (collisions.length > 0 && !allowDuplicates) throw new TokenCollisionError(collisions);
856
1217
  await mkdir(outDir, { recursive: true });
857
1218
  const registryFile = join(outDir, "registry.d.ts");
@@ -859,6 +1220,9 @@ async function generateTypes(opts) {
859
1220
  const modulesFile = join(outDir, "modules.d.ts");
860
1221
  const routesFile = join(outDir, "routes.ts");
861
1222
  const envFile = join(outDir, "env.ts");
1223
+ const pluginsFile = join(outDir, "plugins.d.ts");
1224
+ const augmentationsFile = join(outDir, "augmentations.d.ts");
1225
+ const assetsFile = join(outDir, "assets.d.ts");
862
1226
  const indexFile = join(outDir, "index.d.ts");
863
1227
  const collidingNames = new Set(collisions.map((c) => c.className));
864
1228
  const registryContent = renderRegistry(classes, registryFile, collidingNames);
@@ -875,17 +1239,26 @@ async function generateTypes(opts) {
875
1239
  const modulesContent = renderUnion("ModuleToken", modules, "(no @Module classes discovered — `kick g module <name>` to add one)");
876
1240
  const routesContent = renderRoutes(routes, routesFile, schemaValidator);
877
1241
  const envContent = renderEnv(env, envFile);
1242
+ const pluginsContent = renderPlugins(pluginsAndAdapters);
1243
+ const augmentationsContent = renderAugmentations(augmentations);
1244
+ const assetsContent = renderAssetTypes(assets);
878
1245
  const indexContent = renderIndex(envContent !== null);
879
1246
  await writeFile(registryFile, registryContent, "utf-8");
880
1247
  await writeFile(servicesFile, servicesContent, "utf-8");
881
1248
  await writeFile(modulesFile, modulesContent, "utf-8");
882
1249
  await writeFile(routesFile, routesContent, "utf-8");
1250
+ await writeFile(pluginsFile, pluginsContent, "utf-8");
1251
+ await writeFile(augmentationsFile, augmentationsContent, "utf-8");
1252
+ await writeFile(assetsFile, assetsContent, "utf-8");
883
1253
  await writeFile(indexFile, indexContent, "utf-8");
884
1254
  const written = [
885
1255
  registryFile,
886
1256
  servicesFile,
887
1257
  modulesFile,
888
1258
  routesFile,
1259
+ pluginsFile,
1260
+ augmentationsFile,
1261
+ assetsFile,
889
1262
  indexFile
890
1263
  ];
891
1264
  if (envContent) {
@@ -893,17 +1266,62 @@ async function generateTypes(opts) {
893
1266
  written.push(envFile);
894
1267
  }
895
1268
  await writeFile(join(dirname(outDir), ".gitignore"), "# Auto-generated by kick typegen\n*\n", "utf-8");
1269
+ const uniquePluginNames = new Set(pluginsAndAdapters.map((p) => p.name)).size;
1270
+ const uniqueAugmentations = new Set(augmentations.map((a) => a.name)).size;
896
1271
  return {
897
1272
  registryEntries: classTokens.length,
898
1273
  serviceTokens: new Set(allServices).size,
899
1274
  moduleTokens: modules.length,
900
1275
  routeEntries: routes.length,
1276
+ pluginEntries: uniquePluginNames,
1277
+ augmentationEntries: uniqueAugmentations,
1278
+ assetEntries: assets.count,
901
1279
  envWritten: envContent !== null,
902
1280
  written,
903
1281
  resolvedCollisions: collisions.length
904
1282
  };
905
1283
  }
906
1284
  //#endregion
1285
+ //#region src/typegen/token-conventions.ts
1286
+ /**
1287
+ * Regex for the §22.2 token shape. Breakdown:
1288
+ *
1289
+ * - `^(kick\/)?` — optional reserved framework prefix.
1290
+ * - `([a-z][\w-]*\/[A-Z]\w*)` — `<scope>/<PascalKey>`. Scope is
1291
+ * lowercase, key is PascalCase.
1292
+ * - `(\/.+)?` — optional `/suffix` for sub-flavours
1293
+ * (e.g. `mycorp/Cache/redis`).
1294
+ * - `(:[a-z][\w-]+(:[a-z][\w-]+)*)?` — optional `:instance` (and
1295
+ * further `:extra` colon-sections) for `.scoped()` shards.
1296
+ */
1297
+ const TOKEN_CONVENTION_REGEX = /^(kick\/)?([a-z][\w-]*\/[A-Z]\w*)(\/.+)?(:[a-z][\w-]+(:[a-z][\w-]+)*)?$/;
1298
+ const LEGACY_PREFIX = "kickjs.";
1299
+ function validateTokenConventions(tokens) {
1300
+ const warnings = [];
1301
+ for (const token of tokens) {
1302
+ const name = token.name;
1303
+ if (name.startsWith(LEGACY_PREFIX)) continue;
1304
+ if (TOKEN_CONVENTION_REGEX.test(name)) continue;
1305
+ warnings.push({
1306
+ token: name,
1307
+ variable: token.variable,
1308
+ filePath: token.relativePath,
1309
+ reason: "does not match `<scope>/<PascalKey>[/<suffix>][:<instance>]`",
1310
+ suggestion: suggestRename(name)
1311
+ });
1312
+ }
1313
+ return warnings;
1314
+ }
1315
+ function suggestRename(name) {
1316
+ if (/^[A-Z]\w*$/.test(name)) return `'<scope>/${name}' (e.g. 'mycorp/${name}')`;
1317
+ if (name.includes(".")) return `consider '<scope>/PascalKey' instead of dotted form`;
1318
+ const slashLower = /^([a-z][\w-]*)\/([a-z]\w*)$/.exec(name);
1319
+ if (slashLower) {
1320
+ const [, scope, key] = slashLower;
1321
+ return `'${scope}/${key.charAt(0).toUpperCase()}${key.slice(1)}'`;
1322
+ }
1323
+ }
1324
+ //#endregion
907
1325
  //#region src/typegen/index.ts
908
1326
  /**
909
1327
  * Public entry point for the KickJS typegen module.
@@ -944,6 +1362,7 @@ async function runTypegen(opts = {}) {
944
1362
  cwd,
945
1363
  envFile: envFile === false ? void 0 : envFile
946
1364
  });
1365
+ const assets = discoverAssets(opts.assetMap, cwd);
947
1366
  const result = await generateTypes({
948
1367
  classes: scan.classes,
949
1368
  routes: scan.routes,
@@ -951,23 +1370,39 @@ async function runTypegen(opts = {}) {
951
1370
  injects: scan.injects,
952
1371
  collisions: scan.collisions,
953
1372
  env: envFile === false ? null : scan.env,
1373
+ pluginsAndAdapters: scan.pluginsAndAdapters,
1374
+ augmentations: scan.augmentations,
1375
+ assets,
954
1376
  outDir,
955
1377
  allowDuplicates,
956
1378
  schemaValidator
957
1379
  });
1380
+ const tokenWarnings = validateTokenConventions(scan.tokens);
958
1381
  const elapsed = Date.now() - start;
959
1382
  if (!silent) {
960
1383
  const where = outDir.replace(cwd + "/", "");
961
1384
  const collisionNote = result.resolvedCollisions > 0 ? `, ${result.resolvedCollisions} collisions namespaced` : "";
962
1385
  const envNote = result.envWritten ? ", env typed" : "";
963
- console.log(` kick typegen ${result.serviceTokens} services, ${result.routeEntries} routes, ${result.moduleTokens} modules${envNote}${collisionNote} ${where} (${elapsed}ms)`);
1386
+ const pluginNote = result.pluginEntries > 0 ? `, ${result.pluginEntries} plugins/adapters` : "";
1387
+ const augNote = result.augmentationEntries > 0 ? `, ${result.augmentationEntries} augmentations` : "";
1388
+ const assetNote = result.assetEntries > 0 ? `, ${result.assetEntries} assets` : "";
1389
+ console.log(` kick typegen → ${result.serviceTokens} services, ${result.routeEntries} routes, ${result.moduleTokens} modules${pluginNote}${augNote}${assetNote}${envNote}${collisionNote} → ${where} (${elapsed}ms)`);
1390
+ if (tokenWarnings.length > 0) {
1391
+ console.warn(` kick typegen: ${tokenWarnings.length} token(s) don't match the §22.2 convention:`);
1392
+ for (const warning of tokenWarnings) {
1393
+ const variableNote = warning.variable ? ` [${warning.variable}]` : "";
1394
+ console.warn(` '${warning.token}' (${warning.filePath})${variableNote} — ${warning.reason}`);
1395
+ if (warning.suggestion) console.warn(` → suggestion: ${warning.suggestion}`);
1396
+ }
1397
+ }
964
1398
  }
965
1399
  return {
966
1400
  scan,
967
- result
1401
+ result,
1402
+ tokenWarnings
968
1403
  };
969
1404
  }
970
1405
  //#endregion
971
1406
  export { runTypegen };
972
1407
 
973
- //# sourceMappingURL=typegen-C30frihW.mjs.map
1408
+ //# sourceMappingURL=typegen-vI1eqGLK.mjs.map