@forinda/kickjs-cli 3.2.0 → 4.1.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.
- package/README.md +12 -89
- package/dist/cli.mjs +1757 -609
- package/dist/index.d.mts +263 -9
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +755 -489
- package/dist/index.mjs.map +1 -1
- package/dist/{typegen-C30frihW.mjs → typegen-C-H8pg-y.mjs} +450 -11
- package/dist/typegen-C-H8pg-y.mjs.map +1 -0
- package/package.json +9 -14
- package/dist/typegen-C30frihW.mjs.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs-cli
|
|
2
|
+
* @forinda/kickjs-cli v4.1.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,16 +646,149 @@ 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
|
|
514
660
|
};
|
|
515
661
|
}
|
|
516
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
|
|
706
|
+
};
|
|
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
|
+
}
|
|
791
|
+
//#endregion
|
|
517
792
|
//#region src/typegen/generator.ts
|
|
518
793
|
/**
|
|
519
794
|
* Generates `.d.ts` files inside `.kickjs/types/` from the discovered
|
|
@@ -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
|
|
653
|
-
// importing this file (or having it on
|
|
654
|
-
// \`container.resolve()\`,
|
|
655
|
-
//
|
|
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,94 @@ ${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) for (const line of item.description.split("\n")) docLines.push(` * ${line}`);
|
|
1193
|
+
if (item.example) {
|
|
1194
|
+
docLines.push(` * @example`, ` * \`\`\`ts`);
|
|
1195
|
+
for (const line of item.example.split("\n")) docLines.push(` * ${line}`);
|
|
1196
|
+
docLines.push(` * \`\`\``);
|
|
1197
|
+
}
|
|
1198
|
+
docLines.push(` * @see ${item.relativePath}`);
|
|
1199
|
+
blocks.push([
|
|
1200
|
+
"/**",
|
|
1201
|
+
...docLines,
|
|
1202
|
+
" */",
|
|
1203
|
+
`export interface ${item.name}Augmentation {}`
|
|
1204
|
+
].join("\n"));
|
|
1205
|
+
}
|
|
1206
|
+
return `${HEADER}
|
|
1207
|
+
// Catalogue of augmentable interfaces in this project. The interfaces
|
|
1208
|
+
// below are documentation only — augment the source-of-truth interfaces
|
|
1209
|
+
// in your own \`d.ts\` files (the framework declares the actual types).
|
|
1210
|
+
|
|
1211
|
+
${blocks.join("\n\n")}
|
|
1212
|
+
`;
|
|
1213
|
+
}
|
|
852
1214
|
/** Write all generated `.d.ts` files to `outDir` */
|
|
853
1215
|
async function generateTypes(opts) {
|
|
854
|
-
const { classes, routes = [], tokens = [], injects = [], collisions = [], env = null,
|
|
1216
|
+
const { classes, routes = [], tokens = [], injects = [], collisions = [], env = null, pluginsAndAdapters = [], augmentations = [], assets = {
|
|
1217
|
+
entries: [],
|
|
1218
|
+
count: 0
|
|
1219
|
+
}, outDir, allowDuplicates = false, schemaValidator = false } = opts;
|
|
855
1220
|
if (collisions.length > 0 && !allowDuplicates) throw new TokenCollisionError(collisions);
|
|
856
1221
|
await mkdir(outDir, { recursive: true });
|
|
857
1222
|
const registryFile = join(outDir, "registry.d.ts");
|
|
@@ -859,6 +1224,9 @@ async function generateTypes(opts) {
|
|
|
859
1224
|
const modulesFile = join(outDir, "modules.d.ts");
|
|
860
1225
|
const routesFile = join(outDir, "routes.ts");
|
|
861
1226
|
const envFile = join(outDir, "env.ts");
|
|
1227
|
+
const pluginsFile = join(outDir, "plugins.d.ts");
|
|
1228
|
+
const augmentationsFile = join(outDir, "augmentations.d.ts");
|
|
1229
|
+
const assetsFile = join(outDir, "assets.d.ts");
|
|
862
1230
|
const indexFile = join(outDir, "index.d.ts");
|
|
863
1231
|
const collidingNames = new Set(collisions.map((c) => c.className));
|
|
864
1232
|
const registryContent = renderRegistry(classes, registryFile, collidingNames);
|
|
@@ -875,17 +1243,26 @@ async function generateTypes(opts) {
|
|
|
875
1243
|
const modulesContent = renderUnion("ModuleToken", modules, "(no @Module classes discovered — `kick g module <name>` to add one)");
|
|
876
1244
|
const routesContent = renderRoutes(routes, routesFile, schemaValidator);
|
|
877
1245
|
const envContent = renderEnv(env, envFile);
|
|
1246
|
+
const pluginsContent = renderPlugins(pluginsAndAdapters);
|
|
1247
|
+
const augmentationsContent = renderAugmentations(augmentations);
|
|
1248
|
+
const assetsContent = renderAssetTypes(assets);
|
|
878
1249
|
const indexContent = renderIndex(envContent !== null);
|
|
879
1250
|
await writeFile(registryFile, registryContent, "utf-8");
|
|
880
1251
|
await writeFile(servicesFile, servicesContent, "utf-8");
|
|
881
1252
|
await writeFile(modulesFile, modulesContent, "utf-8");
|
|
882
1253
|
await writeFile(routesFile, routesContent, "utf-8");
|
|
1254
|
+
await writeFile(pluginsFile, pluginsContent, "utf-8");
|
|
1255
|
+
await writeFile(augmentationsFile, augmentationsContent, "utf-8");
|
|
1256
|
+
await writeFile(assetsFile, assetsContent, "utf-8");
|
|
883
1257
|
await writeFile(indexFile, indexContent, "utf-8");
|
|
884
1258
|
const written = [
|
|
885
1259
|
registryFile,
|
|
886
1260
|
servicesFile,
|
|
887
1261
|
modulesFile,
|
|
888
1262
|
routesFile,
|
|
1263
|
+
pluginsFile,
|
|
1264
|
+
augmentationsFile,
|
|
1265
|
+
assetsFile,
|
|
889
1266
|
indexFile
|
|
890
1267
|
];
|
|
891
1268
|
if (envContent) {
|
|
@@ -893,17 +1270,62 @@ async function generateTypes(opts) {
|
|
|
893
1270
|
written.push(envFile);
|
|
894
1271
|
}
|
|
895
1272
|
await writeFile(join(dirname(outDir), ".gitignore"), "# Auto-generated by kick typegen\n*\n", "utf-8");
|
|
1273
|
+
const uniquePluginNames = new Set(pluginsAndAdapters.map((p) => p.name)).size;
|
|
1274
|
+
const uniqueAugmentations = new Set(augmentations.map((a) => a.name)).size;
|
|
896
1275
|
return {
|
|
897
1276
|
registryEntries: classTokens.length,
|
|
898
1277
|
serviceTokens: new Set(allServices).size,
|
|
899
1278
|
moduleTokens: modules.length,
|
|
900
1279
|
routeEntries: routes.length,
|
|
1280
|
+
pluginEntries: uniquePluginNames,
|
|
1281
|
+
augmentationEntries: uniqueAugmentations,
|
|
1282
|
+
assetEntries: assets.count,
|
|
901
1283
|
envWritten: envContent !== null,
|
|
902
1284
|
written,
|
|
903
1285
|
resolvedCollisions: collisions.length
|
|
904
1286
|
};
|
|
905
1287
|
}
|
|
906
1288
|
//#endregion
|
|
1289
|
+
//#region src/typegen/token-conventions.ts
|
|
1290
|
+
/**
|
|
1291
|
+
* Regex for the §22.2 token shape. Breakdown:
|
|
1292
|
+
*
|
|
1293
|
+
* - `^(kick\/)?` — optional reserved framework prefix.
|
|
1294
|
+
* - `([a-z][\w-]*\/[A-Z]\w*)` — `<scope>/<PascalKey>`. Scope is
|
|
1295
|
+
* lowercase, key is PascalCase.
|
|
1296
|
+
* - `(\/.+)?` — optional `/suffix` for sub-flavours
|
|
1297
|
+
* (e.g. `mycorp/Cache/redis`).
|
|
1298
|
+
* - `(:[a-z][\w-]+(:[a-z][\w-]+)*)?` — optional `:instance` (and
|
|
1299
|
+
* further `:extra` colon-sections) for `.scoped()` shards.
|
|
1300
|
+
*/
|
|
1301
|
+
const TOKEN_CONVENTION_REGEX = /^(kick\/)?([a-z][\w-]*\/[A-Z]\w*)(\/.+)?(:[a-z][\w-]+(:[a-z][\w-]+)*)?$/;
|
|
1302
|
+
const LEGACY_PREFIX = "kickjs.";
|
|
1303
|
+
function validateTokenConventions(tokens) {
|
|
1304
|
+
const warnings = [];
|
|
1305
|
+
for (const token of tokens) {
|
|
1306
|
+
const name = token.name;
|
|
1307
|
+
if (name.startsWith(LEGACY_PREFIX)) continue;
|
|
1308
|
+
if (TOKEN_CONVENTION_REGEX.test(name)) continue;
|
|
1309
|
+
warnings.push({
|
|
1310
|
+
token: name,
|
|
1311
|
+
variable: token.variable,
|
|
1312
|
+
filePath: token.relativePath,
|
|
1313
|
+
reason: "does not match `<scope>/<PascalKey>[/<suffix>][:<instance>]`",
|
|
1314
|
+
suggestion: suggestRename(name)
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
return warnings;
|
|
1318
|
+
}
|
|
1319
|
+
function suggestRename(name) {
|
|
1320
|
+
if (/^[A-Z]\w*$/.test(name)) return `'<scope>/${name}' (e.g. 'mycorp/${name}')`;
|
|
1321
|
+
if (name.includes(".")) return `consider '<scope>/PascalKey' instead of dotted form`;
|
|
1322
|
+
const slashLower = /^([a-z][\w-]*)\/([a-z]\w*)$/.exec(name);
|
|
1323
|
+
if (slashLower) {
|
|
1324
|
+
const [, scope, key] = slashLower;
|
|
1325
|
+
return `'${scope}/${key.charAt(0).toUpperCase()}${key.slice(1)}'`;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
//#endregion
|
|
907
1329
|
//#region src/typegen/index.ts
|
|
908
1330
|
/**
|
|
909
1331
|
* Public entry point for the KickJS typegen module.
|
|
@@ -944,6 +1366,7 @@ async function runTypegen(opts = {}) {
|
|
|
944
1366
|
cwd,
|
|
945
1367
|
envFile: envFile === false ? void 0 : envFile
|
|
946
1368
|
});
|
|
1369
|
+
const assets = discoverAssets(opts.assetMap, cwd);
|
|
947
1370
|
const result = await generateTypes({
|
|
948
1371
|
classes: scan.classes,
|
|
949
1372
|
routes: scan.routes,
|
|
@@ -951,23 +1374,39 @@ async function runTypegen(opts = {}) {
|
|
|
951
1374
|
injects: scan.injects,
|
|
952
1375
|
collisions: scan.collisions,
|
|
953
1376
|
env: envFile === false ? null : scan.env,
|
|
1377
|
+
pluginsAndAdapters: scan.pluginsAndAdapters,
|
|
1378
|
+
augmentations: scan.augmentations,
|
|
1379
|
+
assets,
|
|
954
1380
|
outDir,
|
|
955
1381
|
allowDuplicates,
|
|
956
1382
|
schemaValidator
|
|
957
1383
|
});
|
|
1384
|
+
const tokenWarnings = validateTokenConventions(scan.tokens);
|
|
958
1385
|
const elapsed = Date.now() - start;
|
|
959
1386
|
if (!silent) {
|
|
960
1387
|
const where = outDir.replace(cwd + "/", "");
|
|
961
1388
|
const collisionNote = result.resolvedCollisions > 0 ? `, ${result.resolvedCollisions} collisions namespaced` : "";
|
|
962
1389
|
const envNote = result.envWritten ? ", env typed" : "";
|
|
963
|
-
|
|
1390
|
+
const pluginNote = result.pluginEntries > 0 ? `, ${result.pluginEntries} plugins/adapters` : "";
|
|
1391
|
+
const augNote = result.augmentationEntries > 0 ? `, ${result.augmentationEntries} augmentations` : "";
|
|
1392
|
+
const assetNote = result.assetEntries > 0 ? `, ${result.assetEntries} assets` : "";
|
|
1393
|
+
console.log(` kick typegen → ${result.serviceTokens} services, ${result.routeEntries} routes, ${result.moduleTokens} modules${pluginNote}${augNote}${assetNote}${envNote}${collisionNote} → ${where} (${elapsed}ms)`);
|
|
1394
|
+
if (tokenWarnings.length > 0) {
|
|
1395
|
+
console.warn(` kick typegen: ${tokenWarnings.length} token(s) don't match the §22.2 convention:`);
|
|
1396
|
+
for (const warning of tokenWarnings) {
|
|
1397
|
+
const variableNote = warning.variable ? ` [${warning.variable}]` : "";
|
|
1398
|
+
console.warn(` '${warning.token}' (${warning.filePath})${variableNote} — ${warning.reason}`);
|
|
1399
|
+
if (warning.suggestion) console.warn(` → suggestion: ${warning.suggestion}`);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
964
1402
|
}
|
|
965
1403
|
return {
|
|
966
1404
|
scan,
|
|
967
|
-
result
|
|
1405
|
+
result,
|
|
1406
|
+
tokenWarnings
|
|
968
1407
|
};
|
|
969
1408
|
}
|
|
970
1409
|
//#endregion
|
|
971
1410
|
export { runTypegen };
|
|
972
1411
|
|
|
973
|
-
//# sourceMappingURL=typegen-
|
|
1412
|
+
//# sourceMappingURL=typegen-C-H8pg-y.mjs.map
|