@happyvertical/smrt-core 0.52.0 → 0.53.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/AGENTS.md +1 -0
- package/README.md +8 -2
- package/agents/model-moves.md +114 -0
- package/agents/object-runtime.md +3 -1
- package/agents/run-once.md +16 -7
- package/dist/cascade.d.ts.map +1 -1
- package/dist/cascade.js +4 -2
- package/dist/cascade.js.map +1 -1
- package/dist/generators/preflight-route.d.ts.map +1 -1
- package/dist/generators/preflight-route.js +1 -1
- package/dist/generators/preflight-route.js.map +1 -1
- package/dist/knowledge.d.ts.map +1 -1
- package/dist/knowledge.js +3 -0
- package/dist/knowledge.js.map +1 -1
- package/dist/manifest/static-manifest.js +1 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +2 -1
- package/dist/migrations/qualified-name-aliases.d.ts +134 -0
- package/dist/migrations/qualified-name-aliases.d.ts.map +1 -0
- package/dist/migrations/qualified-name-aliases.js +294 -0
- package/dist/migrations/qualified-name-aliases.js.map +1 -0
- package/dist/migrations.js +2 -1
- package/dist/polymorphic-association.d.ts +18 -0
- package/dist/polymorphic-association.d.ts.map +1 -1
- package/dist/polymorphic-association.js +27 -2
- package/dist/polymorphic-association.js.map +1 -1
- package/dist/registry/class-registration.d.ts.map +1 -1
- package/dist/registry/class-registration.js +27 -4
- package/dist/registry/class-registration.js.map +1 -1
- package/dist/registry/name-resolver.d.ts +8 -2
- package/dist/registry/name-resolver.d.ts.map +1 -1
- package/dist/registry/name-resolver.js +17 -8
- package/dist/registry/name-resolver.js.map +1 -1
- package/dist/registry/qualified-name-aliases.d.ts +109 -0
- package/dist/registry/qualified-name-aliases.d.ts.map +1 -0
- package/dist/registry/qualified-name-aliases.js +286 -0
- package/dist/registry/qualified-name-aliases.js.map +1 -0
- package/dist/registry/relationship-graph.d.ts.map +1 -1
- package/dist/registry/relationship-graph.js +1 -1
- package/dist/registry/relationship-graph.js.map +1 -1
- package/dist/registry/types.d.ts +26 -0
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry.d.ts +59 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +115 -6
- package/dist/registry.js.map +1 -1
- package/dist/scanner/manifest-generator.d.ts +11 -0
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +36 -1
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/schema/conflict-target.js +8 -1
- package/dist/schema/conflict-target.js.map +1 -1
- package/dist/scripts/browser-plain-json-regression.mjs +4 -1
- package/dist/smrt-knowledge.json +15 -9
- package/dist/utils/qualified-names.d.ts +11 -0
- package/dist/utils/qualified-names.d.ts.map +1 -1
- package/dist/utils/qualified-names.js +21 -1
- package/dist/utils/qualified-names.js.map +1 -1
- package/package.json +11 -11
|
@@ -35,6 +35,8 @@ export declare function hasClassCaseInsensitive(name: string): boolean;
|
|
|
35
35
|
*
|
|
36
36
|
* Lookup priority:
|
|
37
37
|
* 1. Direct hit on classes map (works for qualified names as keys)
|
|
38
|
+
* 1b. A deprecated qualified name declared in a class's
|
|
39
|
+
* `previousQualifiedNames` resolves to that class (#3338)
|
|
38
40
|
* 2. If input contains ':', prefer direct qualified lookup, then fall back
|
|
39
41
|
* to an exact/simple registration when runtime source imports registered
|
|
40
42
|
* the class before package-qualified promotion happened
|
|
@@ -42,7 +44,7 @@ export declare function hasClassCaseInsensitive(name: string): boolean;
|
|
|
42
44
|
* - Unambiguous (1 match) → return it
|
|
43
45
|
* - Ambiguous (>1 matches) → log warning, return first
|
|
44
46
|
*/
|
|
45
|
-
export declare function findClass(name: string): RegisteredClass | undefined;
|
|
47
|
+
export declare function findClass(name: string, source?: string): RegisteredClass | undefined;
|
|
46
48
|
/**
|
|
47
49
|
* Strict class lookup with package-aware disambiguation.
|
|
48
50
|
*
|
|
@@ -70,8 +72,12 @@ export declare function getClass(name: string): RegisteredClass | undefined;
|
|
|
70
72
|
export declare function getClassByConstructor(ctor: SmrtObjectConstructor): RegisteredClass | undefined;
|
|
71
73
|
/**
|
|
72
74
|
* Get a registered class by its qualified name (O(1) direct lookup).
|
|
75
|
+
*
|
|
76
|
+
* A deprecated qualified name declared in a class's `previousQualifiedNames`
|
|
77
|
+
* resolves to that class with a one-time deprecation warning naming
|
|
78
|
+
* `source` (#3338).
|
|
73
79
|
*/
|
|
74
|
-
export declare function getClassByQualifiedName(qualifiedName: string): RegisteredClass | undefined;
|
|
80
|
+
export declare function getClassByQualifiedName(qualifiedName: string, source?: string): RegisteredClass | undefined;
|
|
75
81
|
/**
|
|
76
82
|
* Get a registered class by package name and class name.
|
|
77
83
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"name-resolver.d.ts","sourceRoot":"","sources":["../../src/registry/name-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAQH,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"name-resolver.d.ts","sourceRoot":"","sources":["../../src/registry/name-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAQH,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAS9E,OAAO,KAAK,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAyFtE;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKtE;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE7D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,MAAM,GACd,eAAe,GAAG,SAAS,CA+D7B;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,WAAW,CAAC,EAAE,MAAM,GACnB,eAAe,GAAG,SAAS,CA0C7B;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,MAAM,EACpB,cAAc,EAAE,MAAM,GACrB,MAAM,CAyDR;AAID;;GAEG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAElE;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,qBAAqB,GAC1B,eAAe,GAAG,SAAS,CAM7B;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,aAAa,EAAE,MAAM,EACrB,MAAM,CAAC,EAAE,MAAM,GACd,eAAe,GAAG,SAAS,CAK7B;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB,eAAe,GAAG,SAAS,CAG7B;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,eAAe,EAAE,CActE;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,kBAAkB,CAiCjE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,GAClB,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAU9B;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,cAAc,GACzB,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAW9B;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAE/D;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAE5D;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,EAAE,CAGxC;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAqBjD;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE9C"}
|
|
@@ -3,6 +3,7 @@ import { getRegistryGeneration } from "./generation.js";
|
|
|
3
3
|
import { createQualifiedName, isQualifiedName, parseQualifiedName } from "../utils/qualified-names.js";
|
|
4
4
|
import { getClasses, getConstructorIndex, verboseLog } from "./shared-state.js";
|
|
5
5
|
import { getLocalTestManifestCache, getManifestCache, getTestManifestCache } from "../manifest/store.js";
|
|
6
|
+
import { resolveQualifiedNameAlias } from "./qualified-name-aliases.js";
|
|
6
7
|
//#region src/registry/name-resolver.ts
|
|
7
8
|
/**
|
|
8
9
|
* Name resolution module for the SMRT ObjectRegistry.
|
|
@@ -106,6 +107,8 @@ function hasClassCaseInsensitive(name) {
|
|
|
106
107
|
*
|
|
107
108
|
* Lookup priority:
|
|
108
109
|
* 1. Direct hit on classes map (works for qualified names as keys)
|
|
110
|
+
* 1b. A deprecated qualified name declared in a class's
|
|
111
|
+
* `previousQualifiedNames` resolves to that class (#3338)
|
|
109
112
|
* 2. If input contains ':', prefer direct qualified lookup, then fall back
|
|
110
113
|
* to an exact/simple registration when runtime source imports registered
|
|
111
114
|
* the class before package-qualified promotion happened
|
|
@@ -113,10 +116,12 @@ function hasClassCaseInsensitive(name) {
|
|
|
113
116
|
* - Unambiguous (1 match) → return it
|
|
114
117
|
* - Ambiguous (>1 matches) → log warning, return first
|
|
115
118
|
*/
|
|
116
|
-
function findClass(name) {
|
|
119
|
+
function findClass(name, source) {
|
|
117
120
|
const classes = getClasses();
|
|
118
121
|
const registered = classes.get(name);
|
|
119
122
|
if (registered) return registered;
|
|
123
|
+
const aliased = resolveQualifiedNameAlias(name, source);
|
|
124
|
+
if (aliased) return aliased;
|
|
120
125
|
if (isQualifiedName(name)) {
|
|
121
126
|
const { packageName, className } = parseQualifiedName(name);
|
|
122
127
|
const keys = registryKeysBySimpleName(className);
|
|
@@ -151,7 +156,7 @@ function findClassStrict(name, fromPackage) {
|
|
|
151
156
|
const classes = getClasses();
|
|
152
157
|
const registered = classes.get(name);
|
|
153
158
|
if (registered) return registered;
|
|
154
|
-
if (isQualifiedName(name)) return;
|
|
159
|
+
if (isQualifiedName(name)) return resolveQualifiedNameAlias(name);
|
|
155
160
|
if (fromPackage) {
|
|
156
161
|
const qualifiedAttempt = createQualifiedName(fromPackage, name);
|
|
157
162
|
const byQualified = classes.get(qualifiedAttempt);
|
|
@@ -206,16 +211,19 @@ function getClassByConstructor(ctor) {
|
|
|
206
211
|
}
|
|
207
212
|
/**
|
|
208
213
|
* Get a registered class by its qualified name (O(1) direct lookup).
|
|
214
|
+
*
|
|
215
|
+
* A deprecated qualified name declared in a class's `previousQualifiedNames`
|
|
216
|
+
* resolves to that class with a one-time deprecation warning naming
|
|
217
|
+
* `source` (#3338).
|
|
209
218
|
*/
|
|
210
|
-
function getClassByQualifiedName(qualifiedName) {
|
|
211
|
-
return getClasses().get(qualifiedName);
|
|
219
|
+
function getClassByQualifiedName(qualifiedName, source) {
|
|
220
|
+
return getClasses().get(qualifiedName) ?? resolveQualifiedNameAlias(qualifiedName, source);
|
|
212
221
|
}
|
|
213
222
|
/**
|
|
214
223
|
* Get a registered class by package name and class name.
|
|
215
224
|
*/
|
|
216
225
|
function getClassInPackage(packageName, className) {
|
|
217
|
-
|
|
218
|
-
return getClasses().get(qualifiedName);
|
|
226
|
+
return getClassByQualifiedName(createQualifiedName(packageName, className));
|
|
219
227
|
}
|
|
220
228
|
/**
|
|
221
229
|
* Find all registered classes with a given simple class name.
|
|
@@ -235,8 +243,9 @@ function findClassesByName(className) {
|
|
|
235
243
|
*/
|
|
236
244
|
function resolveType(shortName) {
|
|
237
245
|
if (shortName.includes(":") && shortName.startsWith("@")) {
|
|
238
|
-
|
|
239
|
-
|
|
246
|
+
const registered = getClassByQualifiedName(shortName);
|
|
247
|
+
if (!registered) throw new Error(`Class "${shortName}" is not registered. Make sure the package is installed and the class is decorated with @smrt().`);
|
|
248
|
+
return registered.qualifiedName ?? shortName;
|
|
240
249
|
}
|
|
241
250
|
const matches = findClassesByName(shortName);
|
|
242
251
|
if (matches.length === 0) throw new Error(`Class "${shortName}" is not registered. Make sure the package is installed and the class is decorated with @smrt().`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"name-resolver.js","names":[],"sources":["../../src/registry/name-resolver.ts"],"sourcesContent":["/**\n * Name resolution module for the SMRT ObjectRegistry.\n *\n * Handles class lookup, disambiguation, and qualified name resolution.\n * All functions operate on the shared globalThis state.\n *\n * Release B (#1133): classNameMap is gone — the eagerly-maintained\n * lowercase-simple-name → qualified-key[] index was removed in favor of\n * on-demand iteration over the `classes` Map. Production SMRT apps carry\n * a few hundred classes at most, so the linear scan is negligible and\n * removes an entire class of cache-sync bugs (#584, #847, #951).\n *\n * #3047: hydration resolves names per row, so the scan now runs once per\n * registry generation into a derived (never hand-maintained) index; see\n * `getSimpleNameIndex()` and `registry/generation.ts`.\n *\n * Extracted from registry.ts as part of issue #1006.\n * @see https://github.com/happyvertical/smrt/issues/1006\n * @see https://github.com/happyvertical/smrt/issues/1133\n */\n\nimport { ConfigurationError } from '../errors';\nimport {\n getLocalTestManifestCache,\n getManifestCache,\n getTestManifestCache,\n} from '../manifest/store.js';\nimport type { QualifiedClassName, SmrtVisibility } from '../scanner/types.js';\nimport {\n createQualifiedName,\n isQualifiedName,\n parseQualifiedName,\n} from '../utils/qualified-names.js';\nimport { getRegistryGeneration } from './generation';\nimport { getClasses, getConstructorIndex, verboseLog } from './shared-state';\nimport type { RegisteredClass, SmrtObjectConstructor } from './types';\n\n// ── Simple-name iteration (replaces classNameMap reads) ─────────────\n\n/**\n * Return every registry key whose registered class has the given simple\n * name (case-insensitive). Replaces the old eagerly-maintained\n * `__smrtRegistryClassNameMap` lookup: we iterate `classes` instead.\n *\n * Cost is O(n) over the classes map; production SMRT apps hold low-\n * hundreds of entries so this is trivially fast. Returned keys preserve\n * insertion order, which makes the \"first match wins\" behavior of the old\n * map-based lookup deterministic in the same way.\n *\n * De-duplicates by RegisteredClass object identity so a class registered\n * under both a simple key and a promoted qualified key (a transitional\n * state during manifest merge) counts as a single entry — matching the\n * old classNameMap's behavior of storing each canonical qualified key once.\n * When that happens, the qualified key wins over the simple one.\n */\nfunction registryKeysBySimpleName(simpleName: string): string[] {\n const keys = getSimpleNameIndex().get(simpleName.toLowerCase());\n return keys ? [...keys] : [];\n}\n\n/**\n * Lower-cased simple name → registry keys, rebuilt once per registry\n * generation (#3047).\n *\n * #1133 removed an eagerly-maintained name index because every writer had to\n * keep it in sync. This one is derived, not maintained: it is rebuilt from\n * the `classes` map whenever the registry generation moves, and every\n * registry mutation bumps the generation (the map itself is\n * generation-tracked; in-place identity changes bump explicitly). Hydration\n * resolves names several times per row, so the O(registered classes) scan\n * per lookup dominated large result sets.\n */\nlet simpleNameIndex:\n | {\n generation: number;\n classes: Map<string, RegisteredClass>;\n keysByName: Map<string, string[]>;\n }\n | undefined;\n\nfunction getSimpleNameIndex(): Map<string, string[]> {\n const classes = getClasses();\n const generation = getRegistryGeneration();\n if (\n simpleNameIndex &&\n simpleNameIndex.generation === generation &&\n simpleNameIndex.classes === classes\n ) {\n return simpleNameIndex.keysByName;\n }\n\n const byName = new Map<string, Map<RegisteredClass, string>>();\n for (const [key, value] of classes.entries()) {\n const lower = value.name?.toLowerCase();\n if (!lower) continue;\n\n let seen = byName.get(lower);\n if (!seen) {\n seen = new Map<RegisteredClass, string>();\n byName.set(lower, seen);\n }\n\n const existing = seen.get(value);\n if (!existing) {\n seen.set(value, key);\n continue;\n }\n\n // Two keys for the same object: prefer the qualified form.\n if (key.includes(':') && !existing.includes(':')) {\n seen.set(value, key);\n }\n }\n\n const keysByName = new Map<string, string[]>();\n for (const [lower, seen] of byName) {\n keysByName.set(lower, [...seen.values()]);\n }\n simpleNameIndex = { generation, classes, keysByName };\n return keysByName;\n}\n\n// ── Lookup functions ────────────────────────────────────────\n\n/**\n * Check if a class is already registered (case-insensitive).\n * Returns the canonical name if found, undefined otherwise. Returns\n * undefined if the simple name is ambiguous across multiple packages.\n */\nexport function getCanonicalClassName(name: string): string | undefined {\n const keys = registryKeysBySimpleName(name);\n if (keys.length === 1) return keys[0];\n // Zero matches, or ambiguous with >1 entries\n return undefined;\n}\n\n/**\n * Check if a class exists by name (case-insensitive).\n */\nexport function hasClassCaseInsensitive(name: string): boolean {\n return getSimpleNameIndex().has(name.toLowerCase());\n}\n\n/**\n * Helper for class lookup with qualified name support.\n *\n * Lookup priority:\n * 1. Direct hit on classes map (works for qualified names as keys)\n * 2. If input contains ':', prefer direct qualified lookup, then fall back\n * to an exact/simple registration when runtime source imports registered\n * the class before package-qualified promotion happened\n * 3. Simple-name iteration by lowercase\n * - Unambiguous (1 match) → return it\n * - Ambiguous (>1 matches) → log warning, return first\n */\nexport function findClass(name: string): RegisteredClass | undefined {\n const classes = getClasses();\n\n // 1. Direct hit on classes map (fast path, works for qualified keys)\n const registered = classes.get(name);\n if (registered) {\n return registered;\n }\n\n // 2. Qualified lookup fallback for source-registered classes.\n // In workspace/dev mode a package can be imported from source before a\n // manifest-promoted qualified key exists. If we already have exactly the\n // requested package or a single unqualified registration for this class\n // name, treat it as the same class instead of forcing node_modules manifest\n // discovery.\n if (isQualifiedName(name)) {\n const { packageName, className } = parseQualifiedName(name);\n const keys = registryKeysBySimpleName(className);\n\n if (keys.length > 0) {\n const exactPackageMatch = keys\n .map((key) => classes.get(key))\n .find((candidate) => candidate?.packageName === packageName);\n\n if (exactPackageMatch) {\n return exactPackageMatch;\n }\n\n if (keys.length === 1) {\n const fallback = classes.get(keys[0]);\n if (fallback && !fallback.packageName) {\n return fallback;\n }\n }\n }\n\n return undefined;\n }\n\n // 3. Simple-name lookup\n const keys = registryKeysBySimpleName(name);\n if (keys.length > 0) {\n if (keys.length === 1) {\n return classes.get(keys[0]);\n }\n // Ambiguous — multiple packages define this class name\n // Return first match but log a warning (use resolveType() for strict behavior)\n verboseLog(\n `[registry] findClass(\"${name}\") is ambiguous — ${keys.length} matches. ` +\n `Use qualified name (e.g., ${keys[0]}) for precision.`,\n );\n return classes.get(keys[0]);\n }\n\n return undefined;\n}\n\n/**\n * Strict class lookup with package-aware disambiguation.\n *\n * Unlike `findClass()` which silently returns the first match when a simple\n * name is ambiguous, this method throws a `ConfigurationError` — making it\n * safe for inheritance-critical paths.\n *\n * @throws {ConfigurationError} When simple name is ambiguous and no package context resolves it\n * @see https://github.com/happyvertical/smrt/issues/1005\n */\nexport function findClassStrict(\n name: string,\n fromPackage?: string,\n): RegisteredClass | undefined {\n const classes = getClasses();\n\n // 1. Direct hit on classes map (fast path, works for qualified keys)\n const registered = classes.get(name);\n if (registered) {\n return registered;\n }\n\n // 2. If input is a qualified name, no fallback — it's not found\n if (isQualifiedName(name)) {\n return undefined;\n }\n\n // 3. If fromPackage provided, try constructing qualified name for direct lookup\n if (fromPackage) {\n const qualifiedAttempt = createQualifiedName(fromPackage, name);\n const byQualified = classes.get(qualifiedAttempt);\n if (byQualified) {\n return byQualified;\n }\n }\n\n // 4. Simple-name lookup\n const keys = registryKeysBySimpleName(name);\n if (keys.length > 0) {\n if (keys.length === 1) {\n return classes.get(keys[0]);\n }\n // Ambiguous — multiple packages define this class name\n // In strict mode, throw instead of silently returning first match\n throw new ConfigurationError(\n `Ambiguous class name \"${name}\" — found in ${keys.length} packages: ` +\n `${keys.join(', ')}. ` +\n `Use a qualified name (e.g., ${keys[0]}) to disambiguate.`,\n 'CONFIG_AMBIGUOUS_CLASS',\n { className: name, candidates: keys },\n );\n }\n\n return undefined;\n}\n\n/**\n * Qualify an `extends` value with the parent class's package name.\n *\n * @see https://github.com/happyvertical/smrt/issues/1004\n */\nexport function qualifyExtendsName(\n extendsValue: string,\n currentPackage: string,\n): string {\n const classes = getClasses();\n\n // Already qualified → pass through\n if (isQualifiedName(extendsValue)) {\n return extendsValue;\n }\n\n // Skip framework base classes (never registered with qualified names)\n if (\n extendsValue === 'SmrtObject' ||\n extendsValue === 'SmrtClass' ||\n extendsValue === 'SmrtCollection'\n ) {\n return extendsValue;\n }\n\n // Try same-package first (common case: child and parent in same package)\n const samePackageQualified = createQualifiedName(\n currentPackage,\n extendsValue,\n );\n if (classes.has(samePackageQualified)) {\n return samePackageQualified;\n }\n\n // The package's own manifest declares the parent but it is not registered\n // yet (manifest entries register in file order, so a subclass can precede\n // its base). It is still this package's class: another package's\n // same-named registration must not become the parent (#3098).\n const ownManifests = [\n getManifestCache().get(currentPackage),\n getLocalTestManifestCache(),\n getTestManifestCache(),\n ];\n if (\n ownManifests.some((manifest) => {\n if (manifest?.packageName !== currentPackage) return false;\n const entry = manifest.objects?.[samePackageQualified];\n // An entry that itself extends this simple name is the subclass being\n // registered (a same-named child of another package's base, e.g.\n // support's ServiceTimeEntry), not a base of its own package.\n const entryParent = entry?.extends?.split(':').pop();\n return !!entry && entryParent !== extendsValue;\n })\n ) {\n return samePackageQualified;\n }\n\n // Try to find the parent in any registered package\n const keys = registryKeysBySimpleName(extendsValue);\n if (keys.length === 1) {\n return keys[0];\n }\n\n // Fallback: return unmodified (backward compat)\n return extendsValue;\n}\n\n// ── Public lookup functions ─────────────────────────────────\n\n/**\n * Get a registered class by name (case-insensitive).\n */\nexport function getClass(name: string): RegisteredClass | undefined {\n return findClass(name);\n}\n\n/**\n * Get a registered class by its constructor reference (O(1) WeakMap lookup).\n */\nexport function getClassByConstructor(\n ctor: SmrtObjectConstructor,\n): RegisteredClass | undefined {\n const registeredName = getConstructorIndex().get(ctor);\n if (registeredName) {\n return getClasses().get(registeredName);\n }\n return undefined;\n}\n\n/**\n * Get a registered class by its qualified name (O(1) direct lookup).\n */\nexport function getClassByQualifiedName(\n qualifiedName: string,\n): RegisteredClass | undefined {\n return getClasses().get(qualifiedName);\n}\n\n/**\n * Get a registered class by package name and class name.\n */\nexport function getClassInPackage(\n packageName: string,\n className: string,\n): RegisteredClass | undefined {\n const qualifiedName = createQualifiedName(packageName, className);\n return getClasses().get(qualifiedName);\n}\n\n/**\n * Find all registered classes with a given simple class name.\n */\nexport function findClassesByName(className: string): RegisteredClass[] {\n // A source registration can retain its simple key while manifest hydration\n // adds its canonical qualified key. Both aliases designate one class, so\n // ambiguity means distinct RegisteredClass identities — which is what the\n // index's per-name keys already are (one key per identity).\n const classes = getClasses();\n const matches: RegisteredClass[] = [];\n for (const key of getSimpleNameIndex().get(className.toLowerCase()) ?? []) {\n const registered = classes.get(key);\n if (registered) {\n matches.push(registered);\n }\n }\n return matches;\n}\n\n/**\n * Resolve a short class name to its qualified name.\n * @throws {Error} If no class or ambiguous classes registered\n */\nexport function resolveType(shortName: string): QualifiedClassName {\n // If already qualified, validate and return\n if (shortName.includes(':') && shortName.startsWith('@')) {\n const registered = getClassByQualifiedName(shortName as QualifiedClassName);\n if (!registered) {\n throw new Error(\n `Class \"${shortName}\" is not registered. ` +\n `Make sure the package is installed and the class is decorated with @smrt().`,\n );\n }\n return shortName as QualifiedClassName;\n }\n\n // Find all classes with this short name\n const matches = findClassesByName(shortName);\n\n if (matches.length === 0) {\n throw new Error(\n `Class \"${shortName}\" is not registered. ` +\n `Make sure the package is installed and the class is decorated with @smrt().`,\n );\n }\n\n if (matches.length > 1) {\n const packageList = matches.map((m) => ` - ${m.qualifiedName}`).join('\\n');\n throw new Error(\n `\"${shortName}\" is ambiguous. Found in multiple packages:\\n${packageList}\\n` +\n `Use the fully qualified name instead.`,\n );\n }\n\n return matches[0].qualifiedName as QualifiedClassName;\n}\n\n/**\n * Get all registered classes from a specific package.\n */\nexport function getClassesByPackage(\n packageName: string,\n): Map<string, RegisteredClass> {\n const result = new Map<string, RegisteredClass>();\n\n for (const [name, registered] of getClasses().entries()) {\n if (registered.packageName === packageName) {\n result.set(name, registered);\n }\n }\n\n return result;\n}\n\n/**\n * Get all registered classes with a specific visibility level.\n */\nexport function getClassesByVisibility(\n visibility: SmrtVisibility,\n): Map<string, RegisteredClass> {\n const result = new Map<string, RegisteredClass>();\n\n for (const [name, registered] of getClasses().entries()) {\n const classVisibility = registered.visibility || 'public';\n if (classVisibility === visibility) {\n result.set(name, registered);\n }\n }\n\n return result;\n}\n\n/**\n * Get all public registered classes.\n */\nexport function getPublicClasses(): Map<string, RegisteredClass> {\n return getClassesByVisibility('public');\n}\n\n/**\n * Get all registered classes.\n */\nexport function getAllClasses(): Map<string, RegisteredClass> {\n return new Map(getClasses());\n}\n\n/**\n * Get class names (simple names, deduplicated).\n */\nexport function getClassNames(): string[] {\n const names = Array.from(getClasses().values()).map((entry) => entry.name);\n return Array.from(new Set(names));\n}\n\n/**\n * Get lookup names for every registered class without collapsing\n * cross-package simple-name collisions.\n *\n * Prefer each registration's qualified name when available, fall back to the\n * registry key, then to the simple class name for package-less registrations.\n * De-duplicates aliases that point to the same RegisteredClass object while\n * preserving distinct packages that intentionally share a simple class name.\n */\nexport function getQualifiedClassNames(): string[] {\n const names: string[] = [];\n const seenRegistrations = new Set<RegisteredClass>();\n const seenNames = new Set<string>();\n\n for (const [key, entry] of getClasses().entries()) {\n if (seenRegistrations.has(entry)) {\n continue;\n }\n seenRegistrations.add(entry);\n\n const lookupName = entry.qualifiedName || key || entry.name;\n if (seenNames.has(lookupName)) {\n continue;\n }\n\n seenNames.add(lookupName);\n names.push(lookupName);\n }\n\n return names;\n}\n\n/**\n * Check if a class is registered (case-insensitive).\n */\nexport function hasClass(name: string): boolean {\n return findClass(name) !== undefined;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"name-resolver.js","names":[],"sources":["../../src/registry/name-resolver.ts"],"sourcesContent":["/**\n * Name resolution module for the SMRT ObjectRegistry.\n *\n * Handles class lookup, disambiguation, and qualified name resolution.\n * All functions operate on the shared globalThis state.\n *\n * Release B (#1133): classNameMap is gone — the eagerly-maintained\n * lowercase-simple-name → qualified-key[] index was removed in favor of\n * on-demand iteration over the `classes` Map. Production SMRT apps carry\n * a few hundred classes at most, so the linear scan is negligible and\n * removes an entire class of cache-sync bugs (#584, #847, #951).\n *\n * #3047: hydration resolves names per row, so the scan now runs once per\n * registry generation into a derived (never hand-maintained) index; see\n * `getSimpleNameIndex()` and `registry/generation.ts`.\n *\n * Extracted from registry.ts as part of issue #1006.\n * @see https://github.com/happyvertical/smrt/issues/1006\n * @see https://github.com/happyvertical/smrt/issues/1133\n */\n\nimport { ConfigurationError } from '../errors';\nimport {\n getLocalTestManifestCache,\n getManifestCache,\n getTestManifestCache,\n} from '../manifest/store.js';\nimport type { QualifiedClassName, SmrtVisibility } from '../scanner/types.js';\nimport {\n createQualifiedName,\n isQualifiedName,\n parseQualifiedName,\n} from '../utils/qualified-names.js';\nimport { getRegistryGeneration } from './generation';\nimport { resolveQualifiedNameAlias } from './qualified-name-aliases';\nimport { getClasses, getConstructorIndex, verboseLog } from './shared-state';\nimport type { RegisteredClass, SmrtObjectConstructor } from './types';\n\n// ── Simple-name iteration (replaces classNameMap reads) ─────────────\n\n/**\n * Return every registry key whose registered class has the given simple\n * name (case-insensitive). Replaces the old eagerly-maintained\n * `__smrtRegistryClassNameMap` lookup: we iterate `classes` instead.\n *\n * Cost is O(n) over the classes map; production SMRT apps hold low-\n * hundreds of entries so this is trivially fast. Returned keys preserve\n * insertion order, which makes the \"first match wins\" behavior of the old\n * map-based lookup deterministic in the same way.\n *\n * De-duplicates by RegisteredClass object identity so a class registered\n * under both a simple key and a promoted qualified key (a transitional\n * state during manifest merge) counts as a single entry — matching the\n * old classNameMap's behavior of storing each canonical qualified key once.\n * When that happens, the qualified key wins over the simple one.\n */\nfunction registryKeysBySimpleName(simpleName: string): string[] {\n const keys = getSimpleNameIndex().get(simpleName.toLowerCase());\n return keys ? [...keys] : [];\n}\n\n/**\n * Lower-cased simple name → registry keys, rebuilt once per registry\n * generation (#3047).\n *\n * #1133 removed an eagerly-maintained name index because every writer had to\n * keep it in sync. This one is derived, not maintained: it is rebuilt from\n * the `classes` map whenever the registry generation moves, and every\n * registry mutation bumps the generation (the map itself is\n * generation-tracked; in-place identity changes bump explicitly). Hydration\n * resolves names several times per row, so the O(registered classes) scan\n * per lookup dominated large result sets.\n */\nlet simpleNameIndex:\n | {\n generation: number;\n classes: Map<string, RegisteredClass>;\n keysByName: Map<string, string[]>;\n }\n | undefined;\n\nfunction getSimpleNameIndex(): Map<string, string[]> {\n const classes = getClasses();\n const generation = getRegistryGeneration();\n if (\n simpleNameIndex &&\n simpleNameIndex.generation === generation &&\n simpleNameIndex.classes === classes\n ) {\n return simpleNameIndex.keysByName;\n }\n\n const byName = new Map<string, Map<RegisteredClass, string>>();\n for (const [key, value] of classes.entries()) {\n const lower = value.name?.toLowerCase();\n if (!lower) continue;\n\n let seen = byName.get(lower);\n if (!seen) {\n seen = new Map<RegisteredClass, string>();\n byName.set(lower, seen);\n }\n\n const existing = seen.get(value);\n if (!existing) {\n seen.set(value, key);\n continue;\n }\n\n // Two keys for the same object: prefer the qualified form.\n if (key.includes(':') && !existing.includes(':')) {\n seen.set(value, key);\n }\n }\n\n const keysByName = new Map<string, string[]>();\n for (const [lower, seen] of byName) {\n keysByName.set(lower, [...seen.values()]);\n }\n simpleNameIndex = { generation, classes, keysByName };\n return keysByName;\n}\n\n// ── Lookup functions ────────────────────────────────────────\n\n/**\n * Check if a class is already registered (case-insensitive).\n * Returns the canonical name if found, undefined otherwise. Returns\n * undefined if the simple name is ambiguous across multiple packages.\n */\nexport function getCanonicalClassName(name: string): string | undefined {\n const keys = registryKeysBySimpleName(name);\n if (keys.length === 1) return keys[0];\n // Zero matches, or ambiguous with >1 entries\n return undefined;\n}\n\n/**\n * Check if a class exists by name (case-insensitive).\n */\nexport function hasClassCaseInsensitive(name: string): boolean {\n return getSimpleNameIndex().has(name.toLowerCase());\n}\n\n/**\n * Helper for class lookup with qualified name support.\n *\n * Lookup priority:\n * 1. Direct hit on classes map (works for qualified names as keys)\n * 1b. A deprecated qualified name declared in a class's\n * `previousQualifiedNames` resolves to that class (#3338)\n * 2. If input contains ':', prefer direct qualified lookup, then fall back\n * to an exact/simple registration when runtime source imports registered\n * the class before package-qualified promotion happened\n * 3. Simple-name iteration by lowercase\n * - Unambiguous (1 match) → return it\n * - Ambiguous (>1 matches) → log warning, return first\n */\nexport function findClass(\n name: string,\n source?: string,\n): RegisteredClass | undefined {\n const classes = getClasses();\n\n // 1. Direct hit on classes map (fast path, works for qualified keys)\n const registered = classes.get(name);\n if (registered) {\n return registered;\n }\n\n // 1b. Deprecated qualified-name alias (#3338). Checked before the\n // simple-name heuristics below: a declared alias is exact, and\n // registration guarantees it never names a live class.\n const aliased = resolveQualifiedNameAlias(name, source);\n if (aliased) {\n return aliased;\n }\n\n // 2. Qualified lookup fallback for source-registered classes.\n // In workspace/dev mode a package can be imported from source before a\n // manifest-promoted qualified key exists. If we already have exactly the\n // requested package or a single unqualified registration for this class\n // name, treat it as the same class instead of forcing node_modules manifest\n // discovery.\n if (isQualifiedName(name)) {\n const { packageName, className } = parseQualifiedName(name);\n const keys = registryKeysBySimpleName(className);\n\n if (keys.length > 0) {\n const exactPackageMatch = keys\n .map((key) => classes.get(key))\n .find((candidate) => candidate?.packageName === packageName);\n\n if (exactPackageMatch) {\n return exactPackageMatch;\n }\n\n if (keys.length === 1) {\n const fallback = classes.get(keys[0]);\n if (fallback && !fallback.packageName) {\n return fallback;\n }\n }\n }\n\n return undefined;\n }\n\n // 3. Simple-name lookup\n const keys = registryKeysBySimpleName(name);\n if (keys.length > 0) {\n if (keys.length === 1) {\n return classes.get(keys[0]);\n }\n // Ambiguous — multiple packages define this class name\n // Return first match but log a warning (use resolveType() for strict behavior)\n verboseLog(\n `[registry] findClass(\"${name}\") is ambiguous — ${keys.length} matches. ` +\n `Use qualified name (e.g., ${keys[0]}) for precision.`,\n );\n return classes.get(keys[0]);\n }\n\n return undefined;\n}\n\n/**\n * Strict class lookup with package-aware disambiguation.\n *\n * Unlike `findClass()` which silently returns the first match when a simple\n * name is ambiguous, this method throws a `ConfigurationError` — making it\n * safe for inheritance-critical paths.\n *\n * @throws {ConfigurationError} When simple name is ambiguous and no package context resolves it\n * @see https://github.com/happyvertical/smrt/issues/1005\n */\nexport function findClassStrict(\n name: string,\n fromPackage?: string,\n): RegisteredClass | undefined {\n const classes = getClasses();\n\n // 1. Direct hit on classes map (fast path, works for qualified keys)\n const registered = classes.get(name);\n if (registered) {\n return registered;\n }\n\n // 2. If input is a qualified name, the only fallback is a declared\n // deprecated alias (#3338) — never a simple-name guess.\n if (isQualifiedName(name)) {\n return resolveQualifiedNameAlias(name);\n }\n\n // 3. If fromPackage provided, try constructing qualified name for direct lookup\n if (fromPackage) {\n const qualifiedAttempt = createQualifiedName(fromPackage, name);\n const byQualified = classes.get(qualifiedAttempt);\n if (byQualified) {\n return byQualified;\n }\n }\n\n // 4. Simple-name lookup\n const keys = registryKeysBySimpleName(name);\n if (keys.length > 0) {\n if (keys.length === 1) {\n return classes.get(keys[0]);\n }\n // Ambiguous — multiple packages define this class name\n // In strict mode, throw instead of silently returning first match\n throw new ConfigurationError(\n `Ambiguous class name \"${name}\" — found in ${keys.length} packages: ` +\n `${keys.join(', ')}. ` +\n `Use a qualified name (e.g., ${keys[0]}) to disambiguate.`,\n 'CONFIG_AMBIGUOUS_CLASS',\n { className: name, candidates: keys },\n );\n }\n\n return undefined;\n}\n\n/**\n * Qualify an `extends` value with the parent class's package name.\n *\n * @see https://github.com/happyvertical/smrt/issues/1004\n */\nexport function qualifyExtendsName(\n extendsValue: string,\n currentPackage: string,\n): string {\n const classes = getClasses();\n\n // Already qualified → pass through\n if (isQualifiedName(extendsValue)) {\n return extendsValue;\n }\n\n // Skip framework base classes (never registered with qualified names)\n if (\n extendsValue === 'SmrtObject' ||\n extendsValue === 'SmrtClass' ||\n extendsValue === 'SmrtCollection'\n ) {\n return extendsValue;\n }\n\n // Try same-package first (common case: child and parent in same package)\n const samePackageQualified = createQualifiedName(\n currentPackage,\n extendsValue,\n );\n if (classes.has(samePackageQualified)) {\n return samePackageQualified;\n }\n\n // The package's own manifest declares the parent but it is not registered\n // yet (manifest entries register in file order, so a subclass can precede\n // its base). It is still this package's class: another package's\n // same-named registration must not become the parent (#3098).\n const ownManifests = [\n getManifestCache().get(currentPackage),\n getLocalTestManifestCache(),\n getTestManifestCache(),\n ];\n if (\n ownManifests.some((manifest) => {\n if (manifest?.packageName !== currentPackage) return false;\n const entry = manifest.objects?.[samePackageQualified];\n // An entry that itself extends this simple name is the subclass being\n // registered (a same-named child of another package's base, e.g.\n // support's ServiceTimeEntry), not a base of its own package.\n const entryParent = entry?.extends?.split(':').pop();\n return !!entry && entryParent !== extendsValue;\n })\n ) {\n return samePackageQualified;\n }\n\n // Try to find the parent in any registered package\n const keys = registryKeysBySimpleName(extendsValue);\n if (keys.length === 1) {\n return keys[0];\n }\n\n // Fallback: return unmodified (backward compat)\n return extendsValue;\n}\n\n// ── Public lookup functions ─────────────────────────────────\n\n/**\n * Get a registered class by name (case-insensitive).\n */\nexport function getClass(name: string): RegisteredClass | undefined {\n return findClass(name);\n}\n\n/**\n * Get a registered class by its constructor reference (O(1) WeakMap lookup).\n */\nexport function getClassByConstructor(\n ctor: SmrtObjectConstructor,\n): RegisteredClass | undefined {\n const registeredName = getConstructorIndex().get(ctor);\n if (registeredName) {\n return getClasses().get(registeredName);\n }\n return undefined;\n}\n\n/**\n * Get a registered class by its qualified name (O(1) direct lookup).\n *\n * A deprecated qualified name declared in a class's `previousQualifiedNames`\n * resolves to that class with a one-time deprecation warning naming\n * `source` (#3338).\n */\nexport function getClassByQualifiedName(\n qualifiedName: string,\n source?: string,\n): RegisteredClass | undefined {\n return (\n getClasses().get(qualifiedName) ??\n resolveQualifiedNameAlias(qualifiedName, source)\n );\n}\n\n/**\n * Get a registered class by package name and class name.\n */\nexport function getClassInPackage(\n packageName: string,\n className: string,\n): RegisteredClass | undefined {\n const qualifiedName = createQualifiedName(packageName, className);\n return getClassByQualifiedName(qualifiedName);\n}\n\n/**\n * Find all registered classes with a given simple class name.\n */\nexport function findClassesByName(className: string): RegisteredClass[] {\n // A source registration can retain its simple key while manifest hydration\n // adds its canonical qualified key. Both aliases designate one class, so\n // ambiguity means distinct RegisteredClass identities — which is what the\n // index's per-name keys already are (one key per identity).\n const classes = getClasses();\n const matches: RegisteredClass[] = [];\n for (const key of getSimpleNameIndex().get(className.toLowerCase()) ?? []) {\n const registered = classes.get(key);\n if (registered) {\n matches.push(registered);\n }\n }\n return matches;\n}\n\n/**\n * Resolve a short class name to its qualified name.\n * @throws {Error} If no class or ambiguous classes registered\n */\nexport function resolveType(shortName: string): QualifiedClassName {\n // If already qualified, validate and return\n if (shortName.includes(':') && shortName.startsWith('@')) {\n const registered = getClassByQualifiedName(shortName as QualifiedClassName);\n if (!registered) {\n throw new Error(\n `Class \"${shortName}\" is not registered. ` +\n `Make sure the package is installed and the class is decorated with @smrt().`,\n );\n }\n // A deprecated alias resolves to the class's CURRENT name (#3338).\n return (registered.qualifiedName ?? shortName) as QualifiedClassName;\n }\n\n // Find all classes with this short name\n const matches = findClassesByName(shortName);\n\n if (matches.length === 0) {\n throw new Error(\n `Class \"${shortName}\" is not registered. ` +\n `Make sure the package is installed and the class is decorated with @smrt().`,\n );\n }\n\n if (matches.length > 1) {\n const packageList = matches.map((m) => ` - ${m.qualifiedName}`).join('\\n');\n throw new Error(\n `\"${shortName}\" is ambiguous. Found in multiple packages:\\n${packageList}\\n` +\n `Use the fully qualified name instead.`,\n );\n }\n\n return matches[0].qualifiedName as QualifiedClassName;\n}\n\n/**\n * Get all registered classes from a specific package.\n */\nexport function getClassesByPackage(\n packageName: string,\n): Map<string, RegisteredClass> {\n const result = new Map<string, RegisteredClass>();\n\n for (const [name, registered] of getClasses().entries()) {\n if (registered.packageName === packageName) {\n result.set(name, registered);\n }\n }\n\n return result;\n}\n\n/**\n * Get all registered classes with a specific visibility level.\n */\nexport function getClassesByVisibility(\n visibility: SmrtVisibility,\n): Map<string, RegisteredClass> {\n const result = new Map<string, RegisteredClass>();\n\n for (const [name, registered] of getClasses().entries()) {\n const classVisibility = registered.visibility || 'public';\n if (classVisibility === visibility) {\n result.set(name, registered);\n }\n }\n\n return result;\n}\n\n/**\n * Get all public registered classes.\n */\nexport function getPublicClasses(): Map<string, RegisteredClass> {\n return getClassesByVisibility('public');\n}\n\n/**\n * Get all registered classes.\n */\nexport function getAllClasses(): Map<string, RegisteredClass> {\n return new Map(getClasses());\n}\n\n/**\n * Get class names (simple names, deduplicated).\n */\nexport function getClassNames(): string[] {\n const names = Array.from(getClasses().values()).map((entry) => entry.name);\n return Array.from(new Set(names));\n}\n\n/**\n * Get lookup names for every registered class without collapsing\n * cross-package simple-name collisions.\n *\n * Prefer each registration's qualified name when available, fall back to the\n * registry key, then to the simple class name for package-less registrations.\n * De-duplicates aliases that point to the same RegisteredClass object while\n * preserving distinct packages that intentionally share a simple class name.\n */\nexport function getQualifiedClassNames(): string[] {\n const names: string[] = [];\n const seenRegistrations = new Set<RegisteredClass>();\n const seenNames = new Set<string>();\n\n for (const [key, entry] of getClasses().entries()) {\n if (seenRegistrations.has(entry)) {\n continue;\n }\n seenRegistrations.add(entry);\n\n const lookupName = entry.qualifiedName || key || entry.name;\n if (seenNames.has(lookupName)) {\n continue;\n }\n\n seenNames.add(lookupName);\n names.push(lookupName);\n }\n\n return names;\n}\n\n/**\n * Check if a class is registered (case-insensitive).\n */\nexport function hasClass(name: string): boolean {\n return findClass(name) !== undefined;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deprecated qualified-name aliases (#3338).
|
|
3
|
+
*
|
|
4
|
+
* A class that moves package (or is renamed) changes its qualified name, and
|
|
5
|
+
* every persisted or declared reference to the old name — a polymorphic
|
|
6
|
+
* `metaType`, a `@crossPackageRef` target, a playbook `model:` — would stop
|
|
7
|
+
* resolving silently. `@smrt({ previousQualifiedNames })` declares the old
|
|
8
|
+
* names; this module turns them into lookup bridges.
|
|
9
|
+
*
|
|
10
|
+
* An alias is never a registry key. The `classes` map — and so
|
|
11
|
+
* `getAllClasses()`, schema/DDL, MCP, CLI and REST generation, and every other
|
|
12
|
+
* iteration — still holds exactly one entry per class. Qualified-name lookups
|
|
13
|
+
* consult the derived alias index only after a direct miss, so a live class
|
|
14
|
+
* always wins and an alias can never shadow one; registration refuses the
|
|
15
|
+
* collisions that would make that ambiguous.
|
|
16
|
+
*
|
|
17
|
+
* Like the simple-name index in `name-resolver.ts`, the alias index is
|
|
18
|
+
* derived, not maintained: it is rebuilt from `classes` once per registry
|
|
19
|
+
* generation (#3047).
|
|
20
|
+
*/
|
|
21
|
+
import type { SmartObjectManifest } from '../scanner/types.js';
|
|
22
|
+
import type { RegisteredClass } from './types';
|
|
23
|
+
export { isQualifiedNameAliasFormat, readPreviousQualifiedNames, } from '../utils/qualified-names.js';
|
|
24
|
+
/** Error code for an alias that collides with a live class or another alias. */
|
|
25
|
+
export declare const QUALIFIED_NAME_ALIAS_COLLISION = "CONFIG_QUALIFIED_NAME_ALIAS_COLLISION";
|
|
26
|
+
/** Error code for a malformed `previousQualifiedNames` declaration. */
|
|
27
|
+
export declare const QUALIFIED_NAME_ALIAS_INVALID = "CONFIG_QUALIFIED_NAME_ALIAS_INVALID";
|
|
28
|
+
/** Diagnostic code recorded (once per alias) when a lookup uses an old name. */
|
|
29
|
+
export declare const QUALIFIED_NAME_ALIAS_DEPRECATED = "QUALIFIED_NAME_ALIAS_DEPRECATED";
|
|
30
|
+
declare global {
|
|
31
|
+
var __smrtRegistryWarnedQualifiedNameAliases: Set<string> | undefined;
|
|
32
|
+
}
|
|
33
|
+
/** Forget which aliases already warned (`ObjectRegistry.clear()`, tests). */
|
|
34
|
+
export declare function resetQualifiedNameAliasWarnings(): void;
|
|
35
|
+
/**
|
|
36
|
+
* The class a deprecated qualified name now resolves to, WITHOUT warning.
|
|
37
|
+
* Returns `undefined` for live names and unknown names alike.
|
|
38
|
+
*/
|
|
39
|
+
export declare function lookupQualifiedNameAlias(name: string): RegisteredClass | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a deprecated qualified name to its current class, logging the
|
|
42
|
+
* one-time deprecation warning. `source` names the resolution site for that
|
|
43
|
+
* warning (e.g. `SmrtPolymorphicAssociation.hydrate`).
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveQualifiedNameAlias(name: string, source?: string): RegisteredClass | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Warn — once per old name per process — that a lookup used a deprecated
|
|
48
|
+
* qualified name. Also recorded as a `warn` registry diagnostic so apps and
|
|
49
|
+
* tests can inspect it through `ObjectRegistry.getDiagnostics()`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function warnDeprecatedQualifiedName(alias: string, current: string, source: string): void;
|
|
52
|
+
/** Every declared alias → its class's current qualified name, sorted. */
|
|
53
|
+
export declare function getQualifiedNameAliasMap(): Map<string, string>;
|
|
54
|
+
/**
|
|
55
|
+
* Refuse a registration whose aliases would make a qualified name resolve to
|
|
56
|
+
* two classes. Called before a class is stored in `classes` — and, for an
|
|
57
|
+
* update of a live entry, with the PROPOSED entry and `replacing` set to the
|
|
58
|
+
* live one, before anything is mutated, so a refused update changes nothing.
|
|
59
|
+
*
|
|
60
|
+
* - every alias is a scoped `@scope/package:ClassName` string, distinct from the
|
|
61
|
+
* class's own name, and declared once;
|
|
62
|
+
* - no alias is the key of a different live class;
|
|
63
|
+
* - no alias is also declared by a different class;
|
|
64
|
+
* - the class's own name is not another class's alias.
|
|
65
|
+
*
|
|
66
|
+
* @throws {ConfigurationError} `CONFIG_QUALIFIED_NAME_ALIAS_INVALID` or
|
|
67
|
+
* `CONFIG_QUALIFIED_NAME_ALIAS_COLLISION`.
|
|
68
|
+
*/
|
|
69
|
+
export declare function assertQualifiedNameAliasesAvailable(registrationKey: string, entry: RegisteredClass, replacing?: RegisteredClass): void;
|
|
70
|
+
/**
|
|
71
|
+
* What a set of manifests says about qualified names (#3338): which classes
|
|
72
|
+
* claim each old name, and which qualified names the manifests DEFINE as
|
|
73
|
+
* classes of their own (installed but possibly unregistered).
|
|
74
|
+
*/
|
|
75
|
+
export interface ManifestAliasInventory {
|
|
76
|
+
/** Old name → distinct current names claiming it. */
|
|
77
|
+
claims: Map<string, Set<string>>;
|
|
78
|
+
/** Every qualified name a manifest defines as a class. */
|
|
79
|
+
defined: Set<string>;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Index `manifests` (one manifest may appear twice, e.g. cached and static;
|
|
83
|
+
* the same class claiming twice is one claim). A re-exported constructor is
|
|
84
|
+
* not a definition: manifests list only classes a package declares.
|
|
85
|
+
*/
|
|
86
|
+
export declare function collectManifestAliasInventory(manifests: Iterable<SmartObjectManifest | null | undefined>): ManifestAliasInventory;
|
|
87
|
+
/**
|
|
88
|
+
* THE manifest rule for old names (#3338). Throws when any of `aliases`:
|
|
89
|
+
*
|
|
90
|
+
* - is defined as a class by a manifest in `inventory` — an installed
|
|
91
|
+
* (possibly stale) package still ships the old class, so the old name is
|
|
92
|
+
* live, not deprecated;
|
|
93
|
+
* - is claimed by a class other than `owner` (or, with no `owner`, by more
|
|
94
|
+
* than one class).
|
|
95
|
+
*
|
|
96
|
+
* @throws {ConfigurationError} `CONFIG_QUALIFIED_NAME_ALIAS_COLLISION`
|
|
97
|
+
*/
|
|
98
|
+
export declare function assertNoCompetingManifestClaims(aliases: Iterable<string>, inventory: ManifestAliasInventory, owner?: string): void;
|
|
99
|
+
/**
|
|
100
|
+
* The single current owner of `alias` in an inventory, or `undefined` —
|
|
101
|
+
* also when a manifest defines `alias` as a class of its own.
|
|
102
|
+
* Two distinct claimants are refused rather than resolved first-match: an
|
|
103
|
+
* old name can resolve to only one class, on the lazy path as on the eager
|
|
104
|
+
* one.
|
|
105
|
+
*
|
|
106
|
+
* @throws {ConfigurationError} `CONFIG_QUALIFIED_NAME_ALIAS_COLLISION`
|
|
107
|
+
*/
|
|
108
|
+
export declare function resolveManifestQualifiedNameAlias(inventory: ManifestAliasInventory, alias: string): string | undefined;
|
|
109
|
+
//# sourceMappingURL=qualified-name-aliases.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"qualified-name-aliases.d.ts","sourceRoot":"","sources":["../../src/registry/qualified-name-aliases.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAUH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAQ/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,OAAO,EACL,0BAA0B,EAC1B,0BAA0B,GAC3B,MAAM,6BAA6B,CAAC;AAErC,gFAAgF;AAChF,eAAO,MAAM,8BAA8B,0CACF,CAAC;AAE1C,uEAAuE;AACvE,eAAO,MAAM,4BAA4B,wCACF,CAAC;AAExC,gFAAgF;AAChF,eAAO,MAAM,+BAA+B,oCACT,CAAC;AAIpC,OAAO,CAAC,MAAM,CAAC;IAEb,IAAI,wCAAwC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;CACvE;AASD,6EAA6E;AAC7E,wBAAgB,+BAA+B,IAAI,IAAI,CAEtD;AAyDD;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,GACX,eAAe,GAAG,SAAS,CAG7B;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,MAAM,SAAyC,GAC9C,eAAe,GAAG,SAAS,CAU7B;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,GACb,IAAI,CAcN;AAED,yEAAyE;AACzE,wBAAgB,wBAAwB,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAO9D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mCAAmC,CACjD,eAAe,EAAE,MAAM,EACvB,KAAK,EAAE,eAAe,EACtB,SAAS,CAAC,EAAE,eAAe,GAC1B,IAAI,CAmGN;AAyBD;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,qDAAqD;IACrD,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IACjC,0DAA0D;IAC1D,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC3C,SAAS,EAAE,QAAQ,CAAC,mBAAmB,GAAG,IAAI,GAAG,SAAS,CAAC,GAC1D,sBAAsB,CAmBxB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,EACzB,SAAS,EAAE,sBAAsB,EACjC,KAAK,CAAC,EAAE,MAAM,GACb,IAAI,CAsBN;AAYD;;;;;;;;GAQG;AACH,wBAAgB,iCAAiC,CAC/C,SAAS,EAAE,sBAAsB,EACjC,KAAK,EAAE,MAAM,GACZ,MAAM,GAAG,SAAS,CAKpB"}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { ConfigurationError } from "../errors.js";
|
|
2
|
+
import { getRegistryGeneration } from "./generation.js";
|
|
3
|
+
import { isQualifiedNameAliasFormat, readPreviousQualifiedNames } from "../utils/qualified-names.js";
|
|
4
|
+
import { getClasses } from "./shared-state.js";
|
|
5
|
+
import { recordRegistryDiagnostic } from "./diagnostics.js";
|
|
6
|
+
import { getLocalTestManifestCache, getManifestCache, getStaticManifestCache, getTestManifestCache } from "../manifest/store.js";
|
|
7
|
+
import { createLogger } from "@happyvertical/logger";
|
|
8
|
+
//#region src/registry/qualified-name-aliases.ts
|
|
9
|
+
/**
|
|
10
|
+
* Deprecated qualified-name aliases (#3338).
|
|
11
|
+
*
|
|
12
|
+
* A class that moves package (or is renamed) changes its qualified name, and
|
|
13
|
+
* every persisted or declared reference to the old name — a polymorphic
|
|
14
|
+
* `metaType`, a `@crossPackageRef` target, a playbook `model:` — would stop
|
|
15
|
+
* resolving silently. `@smrt({ previousQualifiedNames })` declares the old
|
|
16
|
+
* names; this module turns them into lookup bridges.
|
|
17
|
+
*
|
|
18
|
+
* An alias is never a registry key. The `classes` map — and so
|
|
19
|
+
* `getAllClasses()`, schema/DDL, MCP, CLI and REST generation, and every other
|
|
20
|
+
* iteration — still holds exactly one entry per class. Qualified-name lookups
|
|
21
|
+
* consult the derived alias index only after a direct miss, so a live class
|
|
22
|
+
* always wins and an alias can never shadow one; registration refuses the
|
|
23
|
+
* collisions that would make that ambiguous.
|
|
24
|
+
*
|
|
25
|
+
* Like the simple-name index in `name-resolver.ts`, the alias index is
|
|
26
|
+
* derived, not maintained: it is rebuilt from `classes` once per registry
|
|
27
|
+
* generation (#3047).
|
|
28
|
+
*/
|
|
29
|
+
/** Error code for an alias that collides with a live class or another alias. */
|
|
30
|
+
var QUALIFIED_NAME_ALIAS_COLLISION = "CONFIG_QUALIFIED_NAME_ALIAS_COLLISION";
|
|
31
|
+
/** Error code for a malformed `previousQualifiedNames` declaration. */
|
|
32
|
+
var QUALIFIED_NAME_ALIAS_INVALID = "CONFIG_QUALIFIED_NAME_ALIAS_INVALID";
|
|
33
|
+
/** Diagnostic code recorded (once per alias) when a lookup uses an old name. */
|
|
34
|
+
var QUALIFIED_NAME_ALIAS_DEPRECATED = "QUALIFIED_NAME_ALIAS_DEPRECATED";
|
|
35
|
+
var logger = createLogger({ level: "info" });
|
|
36
|
+
function getWarnedAliases() {
|
|
37
|
+
if (!globalThis.__smrtRegistryWarnedQualifiedNameAliases) globalThis.__smrtRegistryWarnedQualifiedNameAliases = /* @__PURE__ */ new Set();
|
|
38
|
+
return globalThis.__smrtRegistryWarnedQualifiedNameAliases;
|
|
39
|
+
}
|
|
40
|
+
/** Forget which aliases already warned (`ObjectRegistry.clear()`, tests). */
|
|
41
|
+
function resetQualifiedNameAliasWarnings() {
|
|
42
|
+
getWarnedAliases().clear();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Two registrations describe the same class: one object, one constructor, or
|
|
46
|
+
* one qualified name (a manifest stub later replaced by its real class, or
|
|
47
|
+
* an HMR reload).
|
|
48
|
+
*/
|
|
49
|
+
function sameClass(left, right) {
|
|
50
|
+
return left === right || left.constructor === right.constructor || !!left.qualifiedName && left.qualifiedName === right.qualifiedName;
|
|
51
|
+
}
|
|
52
|
+
function ownName(entry, registrationKey) {
|
|
53
|
+
return entry.qualifiedName ?? registrationKey ?? entry.name;
|
|
54
|
+
}
|
|
55
|
+
var aliasIndex;
|
|
56
|
+
/** Alias → registered class, rebuilt once per registry generation. */
|
|
57
|
+
function getAliasIndex() {
|
|
58
|
+
const classes = getClasses();
|
|
59
|
+
const generation = getRegistryGeneration();
|
|
60
|
+
if (aliasIndex && aliasIndex.generation === generation && aliasIndex.classes === classes) return aliasIndex.byAlias;
|
|
61
|
+
const byAlias = /* @__PURE__ */ new Map();
|
|
62
|
+
for (const [key, entry] of classes) for (const alias of readPreviousQualifiedNames(entry.config)) {
|
|
63
|
+
if (classes.has(alias)) continue;
|
|
64
|
+
const existing = byAlias.get(alias);
|
|
65
|
+
if (!existing || sameClass(existing, entry) && key.includes(":")) byAlias.set(alias, entry);
|
|
66
|
+
}
|
|
67
|
+
aliasIndex = {
|
|
68
|
+
generation,
|
|
69
|
+
classes,
|
|
70
|
+
byAlias
|
|
71
|
+
};
|
|
72
|
+
return byAlias;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The class a deprecated qualified name now resolves to, WITHOUT warning.
|
|
76
|
+
* Returns `undefined` for live names and unknown names alike.
|
|
77
|
+
*/
|
|
78
|
+
function lookupQualifiedNameAlias(name) {
|
|
79
|
+
if (!name.includes(":")) return void 0;
|
|
80
|
+
return getAliasIndex().get(name);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolve a deprecated qualified name to its current class, logging the
|
|
84
|
+
* one-time deprecation warning. `source` names the resolution site for that
|
|
85
|
+
* warning (e.g. `SmrtPolymorphicAssociation.hydrate`).
|
|
86
|
+
*/
|
|
87
|
+
function resolveQualifiedNameAlias(name, source = "ObjectRegistry qualified-name lookup") {
|
|
88
|
+
const registered = lookupQualifiedNameAlias(name);
|
|
89
|
+
if (registered) warnDeprecatedQualifiedName(name, registered.qualifiedName ?? registered.name, source);
|
|
90
|
+
return registered;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Warn — once per old name per process — that a lookup used a deprecated
|
|
94
|
+
* qualified name. Also recorded as a `warn` registry diagnostic so apps and
|
|
95
|
+
* tests can inspect it through `ObjectRegistry.getDiagnostics()`.
|
|
96
|
+
*/
|
|
97
|
+
function warnDeprecatedQualifiedName(alias, current, source) {
|
|
98
|
+
const warned = getWarnedAliases();
|
|
99
|
+
if (warned.has(alias)) return;
|
|
100
|
+
warned.add(alias);
|
|
101
|
+
const message = `Qualified name "${alias}" is deprecated; it now resolves to "${current}" (resolved from ${source}). Store and declare "${current}" instead — the alias will be removed in a later breaking release. \`smrt doctor --db\` counts stored references that still use it.`;
|
|
102
|
+
const context = {
|
|
103
|
+
alias,
|
|
104
|
+
current,
|
|
105
|
+
source
|
|
106
|
+
};
|
|
107
|
+
recordRegistryDiagnostic("warn", QUALIFIED_NAME_ALIAS_DEPRECATED, message, { ...context });
|
|
108
|
+
logger.warn(`[smrt:${QUALIFIED_NAME_ALIAS_DEPRECATED}] ${message}`, context);
|
|
109
|
+
}
|
|
110
|
+
/** Every declared alias → its class's current qualified name, sorted. */
|
|
111
|
+
function getQualifiedNameAliasMap() {
|
|
112
|
+
const entries = [...getAliasIndex()].map(([alias, registered]) => [alias, registered.qualifiedName ?? registered.name]);
|
|
113
|
+
entries.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
|
|
114
|
+
return new Map(entries);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Refuse a registration whose aliases would make a qualified name resolve to
|
|
118
|
+
* two classes. Called before a class is stored in `classes` — and, for an
|
|
119
|
+
* update of a live entry, with the PROPOSED entry and `replacing` set to the
|
|
120
|
+
* live one, before anything is mutated, so a refused update changes nothing.
|
|
121
|
+
*
|
|
122
|
+
* - every alias is a scoped `@scope/package:ClassName` string, distinct from the
|
|
123
|
+
* class's own name, and declared once;
|
|
124
|
+
* - no alias is the key of a different live class;
|
|
125
|
+
* - no alias is also declared by a different class;
|
|
126
|
+
* - the class's own name is not another class's alias.
|
|
127
|
+
*
|
|
128
|
+
* @throws {ConfigurationError} `CONFIG_QUALIFIED_NAME_ALIAS_INVALID` or
|
|
129
|
+
* `CONFIG_QUALIFIED_NAME_ALIAS_COLLISION`.
|
|
130
|
+
*/
|
|
131
|
+
function assertQualifiedNameAliasesAvailable(registrationKey, entry, replacing) {
|
|
132
|
+
const self = ownName(entry, registrationKey);
|
|
133
|
+
const declared = entry.config?.previousQualifiedNames;
|
|
134
|
+
if (declared !== void 0 && !Array.isArray(declared)) throw new ConfigurationError(`${self}: previousQualifiedNames must be an array of qualified names ("@package/name:ClassName").`, QUALIFIED_NAME_ALIAS_INVALID, {
|
|
135
|
+
className: self,
|
|
136
|
+
previousQualifiedNames: declared
|
|
137
|
+
});
|
|
138
|
+
const aliases = declared ?? [];
|
|
139
|
+
const seen = /* @__PURE__ */ new Set();
|
|
140
|
+
for (const alias of aliases) {
|
|
141
|
+
if (!isQualifiedNameAliasFormat(alias)) throw new ConfigurationError(`${self}: previousQualifiedNames entry ${JSON.stringify(alias)} is not a qualified name ("@package/name:ClassName").`, QUALIFIED_NAME_ALIAS_INVALID, {
|
|
142
|
+
className: self,
|
|
143
|
+
alias
|
|
144
|
+
});
|
|
145
|
+
if (alias === self) throw new ConfigurationError(`${self}: previousQualifiedNames lists the class's own current name "${alias}".`, QUALIFIED_NAME_ALIAS_INVALID, {
|
|
146
|
+
className: self,
|
|
147
|
+
alias
|
|
148
|
+
});
|
|
149
|
+
if (alias === registrationKey) throw collision(`${self} declares previousQualifiedNames "${alias}", but a class is being registered under "${alias}". An alias may only name a class that no longer exists; remove the alias or the stale class/manifest.`, {
|
|
150
|
+
className: self,
|
|
151
|
+
alias
|
|
152
|
+
});
|
|
153
|
+
if (seen.has(alias)) throw new ConfigurationError(`${self}: previousQualifiedNames lists "${alias}" more than once.`, QUALIFIED_NAME_ALIAS_INVALID, {
|
|
154
|
+
className: self,
|
|
155
|
+
alias
|
|
156
|
+
});
|
|
157
|
+
seen.add(alias);
|
|
158
|
+
}
|
|
159
|
+
const classes = getClasses();
|
|
160
|
+
const identities = (other, key) => [
|
|
161
|
+
ownName(other, key),
|
|
162
|
+
key,
|
|
163
|
+
...other.packageName ? [`${other.packageName}:${other.name}`] : []
|
|
164
|
+
];
|
|
165
|
+
const selfIdentities = new Set(identities(entry, registrationKey));
|
|
166
|
+
for (const [key, other] of classes) {
|
|
167
|
+
if (other === replacing || sameClass(other, entry)) continue;
|
|
168
|
+
const otherName = ownName(other, key);
|
|
169
|
+
for (const identity of identities(other, key)) if (seen.has(identity)) throw collision(`${self} declares previousQualifiedNames "${identity}", but "${identity}" is a live registered class. An alias may only name a class that no longer exists; remove the alias or the stale class/manifest.`, {
|
|
170
|
+
className: self,
|
|
171
|
+
alias: identity,
|
|
172
|
+
liveClass: otherName
|
|
173
|
+
});
|
|
174
|
+
const otherAliases = readPreviousQualifiedNames(other.config);
|
|
175
|
+
if (otherAliases.length === 0) continue;
|
|
176
|
+
for (const alias of otherAliases) {
|
|
177
|
+
if (seen.has(alias)) throw collision(`previousQualifiedNames "${alias}" is declared by both ${self} and ${otherName}; an old name can resolve to only one class.`, {
|
|
178
|
+
className: self,
|
|
179
|
+
alias,
|
|
180
|
+
otherClass: otherName
|
|
181
|
+
});
|
|
182
|
+
if (selfIdentities.has(alias)) throw collision(`${otherName} declares previousQualifiedNames "${alias}", but ${self} is registered under that name. An alias may only name a class that no longer exists; remove the alias or the stale class/manifest.`, {
|
|
183
|
+
className: self,
|
|
184
|
+
alias,
|
|
185
|
+
otherClass: otherName
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (seen.size > 0) assertNoCompetingManifestClaims(seen, collectManifestAliasInventory(loadedManifests()), entry.qualifiedName ?? self);
|
|
190
|
+
}
|
|
191
|
+
function collision(message, details) {
|
|
192
|
+
return new ConfigurationError(message, QUALIFIED_NAME_ALIAS_COLLISION, details);
|
|
193
|
+
}
|
|
194
|
+
/** The current qualified name a manifest object registers under. */
|
|
195
|
+
function manifestObjectQualifiedName(manifest, key, objectDef) {
|
|
196
|
+
if (objectDef.qualifiedName) return objectDef.qualifiedName;
|
|
197
|
+
if (key.includes(":")) return key;
|
|
198
|
+
const packageName = objectDef.packageName ?? manifest.packageName;
|
|
199
|
+
return packageName ? `${packageName}:${objectDef.className}` : void 0;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Index `manifests` (one manifest may appear twice, e.g. cached and static;
|
|
203
|
+
* the same class claiming twice is one claim). A re-exported constructor is
|
|
204
|
+
* not a definition: manifests list only classes a package declares.
|
|
205
|
+
*/
|
|
206
|
+
function collectManifestAliasInventory(manifests) {
|
|
207
|
+
const claims = /* @__PURE__ */ new Map();
|
|
208
|
+
const defined = /* @__PURE__ */ new Set();
|
|
209
|
+
for (const manifest of manifests) {
|
|
210
|
+
if (!manifest?.objects) continue;
|
|
211
|
+
for (const [key, objectDef] of Object.entries(manifest.objects)) {
|
|
212
|
+
const current = manifestObjectQualifiedName(manifest, key, objectDef);
|
|
213
|
+
if (!current) continue;
|
|
214
|
+
defined.add(current);
|
|
215
|
+
for (const alias of readPreviousQualifiedNames(objectDef?.decoratorConfig)) {
|
|
216
|
+
const owners = claims.get(alias) ?? /* @__PURE__ */ new Set();
|
|
217
|
+
owners.add(current);
|
|
218
|
+
claims.set(alias, owners);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
claims,
|
|
224
|
+
defined
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* THE manifest rule for old names (#3338). Throws when any of `aliases`:
|
|
229
|
+
*
|
|
230
|
+
* - is defined as a class by a manifest in `inventory` — an installed
|
|
231
|
+
* (possibly stale) package still ships the old class, so the old name is
|
|
232
|
+
* live, not deprecated;
|
|
233
|
+
* - is claimed by a class other than `owner` (or, with no `owner`, by more
|
|
234
|
+
* than one class).
|
|
235
|
+
*
|
|
236
|
+
* @throws {ConfigurationError} `CONFIG_QUALIFIED_NAME_ALIAS_COLLISION`
|
|
237
|
+
*/
|
|
238
|
+
function assertNoCompetingManifestClaims(aliases, inventory, owner) {
|
|
239
|
+
for (const alias of aliases) {
|
|
240
|
+
if (inventory.defined.has(alias)) {
|
|
241
|
+
const claimants = [...inventory.claims.get(alias) ?? []];
|
|
242
|
+
if (owner && !claimants.includes(owner)) claimants.push(owner);
|
|
243
|
+
throw collision(`previousQualifiedNames "${alias}" is still defined as a class by an installed manifest (claimed by ${claimants.sort().join(", ") || "no class"}). An alias may only name a class that no longer exists; remove the stale package/manifest or the alias.`, {
|
|
244
|
+
alias,
|
|
245
|
+
claimants: claimants.sort(),
|
|
246
|
+
definedByManifest: true
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
const owners = new Set(inventory.claims.get(alias) ?? []);
|
|
250
|
+
if (owner) owners.add(owner);
|
|
251
|
+
if (owners.size > 1) {
|
|
252
|
+
const sorted = [...owners].sort();
|
|
253
|
+
throw collision(`previousQualifiedNames "${alias}" is declared by more than one class across installed manifests (${sorted.join(", ")}); an old name can resolve to only one class.`, {
|
|
254
|
+
alias,
|
|
255
|
+
claimants: sorted
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/** Every manifest this process has loaded (package, static, test caches). */
|
|
261
|
+
function loadedManifests() {
|
|
262
|
+
return [
|
|
263
|
+
...getManifestCache().values(),
|
|
264
|
+
getStaticManifestCache(),
|
|
265
|
+
getTestManifestCache(),
|
|
266
|
+
getLocalTestManifestCache()
|
|
267
|
+
];
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* The single current owner of `alias` in an inventory, or `undefined` —
|
|
271
|
+
* also when a manifest defines `alias` as a class of its own.
|
|
272
|
+
* Two distinct claimants are refused rather than resolved first-match: an
|
|
273
|
+
* old name can resolve to only one class, on the lazy path as on the eager
|
|
274
|
+
* one.
|
|
275
|
+
*
|
|
276
|
+
* @throws {ConfigurationError} `CONFIG_QUALIFIED_NAME_ALIAS_COLLISION`
|
|
277
|
+
*/
|
|
278
|
+
function resolveManifestQualifiedNameAlias(inventory, alias) {
|
|
279
|
+
if (inventory.defined.has(alias)) return void 0;
|
|
280
|
+
assertNoCompetingManifestClaims([alias], inventory);
|
|
281
|
+
return [...inventory.claims.get(alias) ?? []][0];
|
|
282
|
+
}
|
|
283
|
+
//#endregion
|
|
284
|
+
export { QUALIFIED_NAME_ALIAS_COLLISION, QUALIFIED_NAME_ALIAS_DEPRECATED, QUALIFIED_NAME_ALIAS_INVALID, assertNoCompetingManifestClaims, assertQualifiedNameAliasesAvailable, collectManifestAliasInventory, getQualifiedNameAliasMap, lookupQualifiedNameAlias, readPreviousQualifiedNames, resetQualifiedNameAliasWarnings, resolveManifestQualifiedNameAlias, resolveQualifiedNameAlias, warnDeprecatedQualifiedName };
|
|
285
|
+
|
|
286
|
+
//# sourceMappingURL=qualified-name-aliases.js.map
|