@workos/oagen-emitters 0.16.0 → 0.16.1
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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +7 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/{plugin-DuB1UozS.mjs → plugin-CpO8rePT.mjs} +1164 -490
- package/dist/plugin-CpO8rePT.mjs.map +1 -0
- package/dist/plugin.mjs +1 -1
- package/package.json +7 -7
- package/src/node/enums.ts +17 -4
- package/src/node/index.ts +264 -4
- package/src/node/live-surface.ts +309 -0
- package/src/node/models.ts +69 -3
- package/src/node/naming.ts +204 -23
- package/src/node/resources.ts +39 -3
- package/src/node/utils.ts +140 -22
- package/test/node/enums.test.ts +239 -2
- package/test/node/live-surface.test.ts +771 -1
- package/test/node/models.test.ts +738 -3
- package/test/node/naming.test.ts +159 -0
- package/test/node/resources.test.ts +464 -0
- package/test/node/utils.test.ts +157 -2
- package/dist/plugin-DuB1UozS.mjs.map +0 -1
package/dist/plugin.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as workosEmittersPlugin } from "./plugin-
|
|
1
|
+
import { t as workosEmittersPlugin } from "./plugin-CpO8rePT.mjs";
|
|
2
2
|
export { workosEmittersPlugin };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workos/oagen-emitters",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "WorkOS' oagen emitters",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "WorkOS",
|
|
@@ -40,12 +40,12 @@
|
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@commitlint/cli": "^21.0.2",
|
|
42
42
|
"@commitlint/config-conventional": "^21.0.2",
|
|
43
|
-
"@types/node": "^25.9.
|
|
43
|
+
"@types/node": "^25.9.3",
|
|
44
44
|
"husky": "^9.1.7",
|
|
45
|
-
"oxfmt": "^0.
|
|
46
|
-
"oxlint": "^1.
|
|
47
|
-
"prettier": "^3.8.
|
|
48
|
-
"tsdown": "^0.22.
|
|
45
|
+
"oxfmt": "^0.54.0",
|
|
46
|
+
"oxlint": "^1.69.0",
|
|
47
|
+
"prettier": "^3.8.4",
|
|
48
|
+
"tsdown": "^0.22.2",
|
|
49
49
|
"tsx": "^4.22.4",
|
|
50
50
|
"typescript": "^6.0.3",
|
|
51
51
|
"vitest": "^4.1.8"
|
|
@@ -54,6 +54,6 @@
|
|
|
54
54
|
"node": ">=24.10.0"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@workos/oagen": "^0.22.
|
|
57
|
+
"@workos/oagen": "^0.22.5"
|
|
58
58
|
}
|
|
59
59
|
}
|
package/src/node/enums.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { Enum, EmitterContext, GeneratedFile, Model, Service } from '@workos/oagen';
|
|
2
|
-
import {
|
|
2
|
+
import { collectFieldDependencies, toPascalCase, walkTypeRef } from '@workos/oagen';
|
|
3
3
|
import { fileName, resolveServiceDir, buildServiceNameMap } from './naming.js';
|
|
4
|
-
import { docComment } from './utils.js';
|
|
4
|
+
import { docComment, assignModelsToEmittableServices } from './utils.js';
|
|
5
5
|
import { isInlineEnum } from './type-map.js';
|
|
6
|
+
import { isNodeOwnedService } from './options.js';
|
|
6
7
|
import { liveSurfaceConstEnumMembers, liveSurfaceInterfacePath } from './live-surface.js';
|
|
7
8
|
|
|
8
9
|
export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
|
|
@@ -30,7 +31,15 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
|
|
|
30
31
|
if (dirName === 'common' && !baselineSourceFile && (ctx.outputDir || ctx.targetDir || ctx.apiSurface)) {
|
|
31
32
|
continue;
|
|
32
33
|
}
|
|
33
|
-
|
|
34
|
+
// The declared-elsewhere skip must not fire for OWNED services: the
|
|
35
|
+
// "elsewhere" is typically the very interface file the owned-service
|
|
36
|
+
// regeneration is simultaneously overwriting (e.g. legacy enums declared
|
|
37
|
+
// inline in organization-domain.interface.ts), so skipping here leaves
|
|
38
|
+
// the names referenced by generated code but declared nowhere. Under
|
|
39
|
+
// ownership the canonical per-service module is the source of truth —
|
|
40
|
+
// emit it; the model emitter plans its import from the same path.
|
|
41
|
+
const isOwnedEnumService = isNodeOwnedService(ctx, service, service ? serviceNameMap.get(service) : undefined);
|
|
42
|
+
if (baselineSourceFile && baselineSourceFile !== generatedPath && !isOwnedEnumService) {
|
|
34
43
|
continue;
|
|
35
44
|
}
|
|
36
45
|
|
|
@@ -160,7 +169,11 @@ export function assignEnumsToServices(
|
|
|
160
169
|
}
|
|
161
170
|
|
|
162
171
|
if (models.length > 0) {
|
|
163
|
-
|
|
172
|
+
// Use the emittable-services assignment (not the raw engine one) so an
|
|
173
|
+
// enum referenced through a model that was re-homed into an owned
|
|
174
|
+
// service directory follows the model — `generateEnums` emission and the
|
|
175
|
+
// model's enum imports must agree on the directory.
|
|
176
|
+
const modelToService = assignModelsToEmittableServices(models, services, ctx);
|
|
164
177
|
for (const model of models) {
|
|
165
178
|
const service = modelToService.get(model.name);
|
|
166
179
|
if (!service) continue;
|
package/src/node/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
Enum,
|
|
9
9
|
Service,
|
|
10
10
|
} from '@workos/oagen';
|
|
11
|
+
import { toPascalCase } from '@workos/oagen';
|
|
11
12
|
import * as fs from 'node:fs';
|
|
12
13
|
import * as path from 'node:path';
|
|
13
14
|
|
|
@@ -18,10 +19,17 @@ import { generateClient } from './client.js';
|
|
|
18
19
|
import { generateTests as generateTestFiles } from './tests.js';
|
|
19
20
|
import { enrichModelsFromSpec, getSyntheticEnums } from '../shared/model-utils.js';
|
|
20
21
|
import { planDiscriminatedModels, generateDiscriminatedFiles } from './discriminated-models.js';
|
|
21
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
buildLiveSurface,
|
|
24
|
+
emptyLiveSurface,
|
|
25
|
+
mergeGeneratedClassMethodsIntoExisting,
|
|
26
|
+
setActiveLiveSurface,
|
|
27
|
+
type LiveSurface,
|
|
28
|
+
} from './live-surface.js';
|
|
22
29
|
import {
|
|
23
30
|
setBaselineSerializedNames,
|
|
24
31
|
setBaselineInterfaceNames,
|
|
32
|
+
setBaselineDeclaredNames,
|
|
25
33
|
setAdoptedModelNames,
|
|
26
34
|
setDiscriminatedModelNames,
|
|
27
35
|
setStructurallyRenamedDomainNames,
|
|
@@ -31,7 +39,7 @@ import { withNodeOperationOverrides } from './node-overrides.js';
|
|
|
31
39
|
import { isNodeOwnedService, nodeOptions } from './options.js';
|
|
32
40
|
import { setInlineEnumUnions, setDomainNameResolver } from './type-map.js';
|
|
33
41
|
import { groupByMount } from '../shared/resolved-ops.js';
|
|
34
|
-
import { assignModelsToServices, createServiceDirResolver } from './utils.js';
|
|
42
|
+
import { assignModelsToServices, createServiceDirResolver, relativeImport } from './utils.js';
|
|
35
43
|
import { fileName } from './naming.js';
|
|
36
44
|
|
|
37
45
|
/**
|
|
@@ -58,6 +66,24 @@ function getEmittedPaths(ctx: EmitterContext): Set<string> {
|
|
|
58
66
|
return set;
|
|
59
67
|
}
|
|
60
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Every `GeneratedFile` the node emitter has produced so far in this ctx,
|
|
71
|
+
* keyed by path. All emitter hooks share one ctx (and the engine only reads
|
|
72
|
+
* file contents after the last hook returns), so the final hook can run a
|
|
73
|
+
* whole-run pass over files emitted by earlier hooks — see
|
|
74
|
+
* `enforceEmittedImportInvariant`.
|
|
75
|
+
*/
|
|
76
|
+
const emittedFilesCache = new WeakMap<EmitterContext, Map<string, GeneratedFile>>();
|
|
77
|
+
|
|
78
|
+
function getEmittedFiles(ctx: EmitterContext): Map<string, GeneratedFile> {
|
|
79
|
+
let map = emittedFilesCache.get(ctx);
|
|
80
|
+
if (!map) {
|
|
81
|
+
map = new Map();
|
|
82
|
+
emittedFilesCache.set(ctx, map);
|
|
83
|
+
}
|
|
84
|
+
return map;
|
|
85
|
+
}
|
|
86
|
+
|
|
61
87
|
function getSurface(ctx: EmitterContext): LiveSurface {
|
|
62
88
|
let surface = surfaceCache.get(ctx);
|
|
63
89
|
if (surface) return surface;
|
|
@@ -102,6 +128,18 @@ function getSurface(ctx: EmitterContext): LiveSurface {
|
|
|
102
128
|
for (const name of surface.interfaces.keys()) allInterfaces.add(name);
|
|
103
129
|
setBaselineInterfaceNames(allInterfaces);
|
|
104
130
|
|
|
131
|
+
// Every DECLARED baseline name — interfaces and type aliases, from both
|
|
132
|
+
// the api-surface JSON and the disk walk (whose `interfaces` map already
|
|
133
|
+
// includes `export type` aliases). `resolveInterfaceName` uses this to
|
|
134
|
+
// let exact-name declarations preempt structural renames: an alias-form
|
|
135
|
+
// file (`export type X = Y;`) carries no fields, so the engine's
|
|
136
|
+
// structural matcher would otherwise re-point IR model X at a similarly
|
|
137
|
+
// shaped interface and emit renamed duplicates that flip the file's form
|
|
138
|
+
// on every regeneration.
|
|
139
|
+
const declaredNames = new Set<string>(allInterfaces);
|
|
140
|
+
for (const name of Object.keys(ctx.apiSurface?.typeAliases ?? {})) declaredNames.add(name);
|
|
141
|
+
setBaselineDeclaredNames(declaredNames);
|
|
142
|
+
|
|
105
143
|
// Inline-enum optimization is intentionally disabled. workos-node emits the
|
|
106
144
|
// dual `const X = {...} as const; type X = ...` pattern so callers can use
|
|
107
145
|
// members at runtime (e.g. `GenerateLinkIntent.SSO`). Inlining the type
|
|
@@ -342,6 +380,16 @@ function isOwnedPath(relPath: string, policy: LiveSurfacePolicy): boolean {
|
|
|
342
380
|
return dir !== undefined && policy.ownedServiceDirs.has(dir);
|
|
343
381
|
}
|
|
344
382
|
|
|
383
|
+
/** Read the current on-disk content of a live-surface file, if present. */
|
|
384
|
+
function readExistingSurfaceFile(surface: LiveSurface, relPath: string): string | null {
|
|
385
|
+
if (!surface.rootDir) return null;
|
|
386
|
+
try {
|
|
387
|
+
return fs.readFileSync(path.join(surface.rootDir, relPath), 'utf8');
|
|
388
|
+
} catch {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
345
393
|
function extractRelativeImportPaths(content: string, fromPath: string): string[] {
|
|
346
394
|
const dir = path.dirname(fromPath);
|
|
347
395
|
const paths: string[] = [];
|
|
@@ -438,7 +486,26 @@ function applyLiveSurface(files: GeneratedFile[], ctx: EmitterContext, surface:
|
|
|
438
486
|
// `@oagen-ignore-start`/`@oagen-ignore-end` regions inside the file
|
|
439
487
|
// are still preserved by `overwriteWithPreservedRegions` in the
|
|
440
488
|
// engine.
|
|
489
|
+
//
|
|
490
|
+
// Exception: a NOT-owned, NOT-adopted service can receive a PARTIAL
|
|
491
|
+
// resource emission — resources.ts filters out operations already
|
|
492
|
+
// covered by the baseline class, leaving only the new methods (see
|
|
493
|
+
// generateResourceClass). Forcing a full overwrite with that partial
|
|
494
|
+
// class deletes the existing public methods (workos-node's
|
|
495
|
+
// api-keys.ts lost four methods when the spec gained one operation).
|
|
496
|
+
// When the generated class would drop methods that the on-disk class
|
|
497
|
+
// declares, merge instead: keep the existing file text verbatim and
|
|
498
|
+
// append only the new methods plus the imports they need.
|
|
441
499
|
if (surface.autogenFiles.has(f.path) || ownedPath) {
|
|
500
|
+
const dir = topLevelDir(f.path);
|
|
501
|
+
const isAdoptedDirPath = dir !== undefined && policy.adoptedServiceDirs.has(dir);
|
|
502
|
+
if (!ownedPath && !isAdoptedDirPath && surface.autogenFiles.has(f.path)) {
|
|
503
|
+
const existingText = readExistingSurfaceFile(surface, f.path);
|
|
504
|
+
if (existingText !== null) {
|
|
505
|
+
const merged = mergeGeneratedClassMethodsIntoExisting(existingText, f.content);
|
|
506
|
+
if (merged !== null) f.content = merged;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
442
509
|
f.overwriteExisting = true;
|
|
443
510
|
f.skipIfExists = false;
|
|
444
511
|
}
|
|
@@ -449,10 +516,192 @@ function applyLiveSurface(files: GeneratedFile[], ctx: EmitterContext, surface:
|
|
|
449
516
|
out.push(f);
|
|
450
517
|
}
|
|
451
518
|
const emitted = getEmittedPaths(ctx);
|
|
452
|
-
|
|
519
|
+
const emittedFiles = getEmittedFiles(ctx);
|
|
520
|
+
for (const f of out) {
|
|
521
|
+
emitted.add(f.path);
|
|
522
|
+
emittedFiles.set(f.path, f);
|
|
523
|
+
}
|
|
453
524
|
return out;
|
|
454
525
|
}
|
|
455
526
|
|
|
527
|
+
// ---------------------------------------------------------------------------
|
|
528
|
+
// Import-resolution invariant
|
|
529
|
+
// ---------------------------------------------------------------------------
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Matches single-line `import`/`export … from './relative'` statements — the
|
|
533
|
+
* only form the node emitter produces. Captures: keyword, optional ` type`
|
|
534
|
+
* modifier, the binding clause (`* [as ns]` or `{ … }`), and the module path.
|
|
535
|
+
*/
|
|
536
|
+
const RELATIVE_FROM_STMT_RE =
|
|
537
|
+
/^(import|export)(\s+type)?\s+(\*(?:\s+as\s+[\w$]+)?|\{[^}]*\})\s+from\s+['"](\.[^'"]+)['"];?\s*$/;
|
|
538
|
+
|
|
539
|
+
const EXPORTED_DECL_RE =
|
|
540
|
+
/^export\s+(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(?:interface|class|enum|function|const|let|var|type)\s+([A-Za-z_$][\w$]*)/gm;
|
|
541
|
+
const EXPORTED_CLAUSE_RE = /^export\s+(?:type\s+)?\{([^}]*)\}/gm;
|
|
542
|
+
|
|
543
|
+
/** Repo-relative paths a relative import specifier may resolve to. */
|
|
544
|
+
function importTargetCandidates(fromPath: string, spec: string): string[] {
|
|
545
|
+
const base = path.posix.normalize(path.posix.join(path.posix.dirname(fromPath), spec));
|
|
546
|
+
return [base, `${base}.ts`, `${base}/index.ts`];
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Index exported symbol → file path across this run's emitted contents. */
|
|
550
|
+
function indexEmittedExports(files: GeneratedFile[]): Map<string, string> {
|
|
551
|
+
const index = new Map<string, string>();
|
|
552
|
+
for (const f of files) {
|
|
553
|
+
if (!f.path.endsWith('.ts') || !f.content) continue;
|
|
554
|
+
for (const m of f.content.matchAll(EXPORTED_DECL_RE)) {
|
|
555
|
+
if (!index.has(m[1])) index.set(m[1], f.path);
|
|
556
|
+
}
|
|
557
|
+
for (const m of f.content.matchAll(EXPORTED_CLAUSE_RE)) {
|
|
558
|
+
for (const raw of m[1].split(',')) {
|
|
559
|
+
const entry = raw.trim();
|
|
560
|
+
if (!entry) continue;
|
|
561
|
+
const parts = entry.split(/\s+as\s+/);
|
|
562
|
+
const exported = (parts[1] ?? parts[0]).replace(/^type\s+/, '').trim();
|
|
563
|
+
if (exported && !index.has(exported)) index.set(exported, f.path);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return index;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Enforce: every relative import/re-export path in emitted code resolves to
|
|
572
|
+
* either (i) a file emitted in the same run, or (ii) a file that already
|
|
573
|
+
* exists on disk in the target SDK.
|
|
574
|
+
*
|
|
575
|
+
* Violations observed in real generations (all TS2307 in otherwise-valid
|
|
576
|
+
* output): serializer imports pointing at canonical paths while the function
|
|
577
|
+
* lives in a legacy hand serializer under a different filename; barrels
|
|
578
|
+
* re-exporting a module-local enum file whose declaration lives under
|
|
579
|
+
* `src/common/interfaces`; barrels exporting interface files that no run
|
|
580
|
+
* emits at all.
|
|
581
|
+
*
|
|
582
|
+
* Repair strategy, per statement whose target is neither emitted nor on
|
|
583
|
+
* disk:
|
|
584
|
+
* 1. Locate each imported symbol (this run's emissions first, then the
|
|
585
|
+
* live-surface declaration maps) and rewrite the path to where the
|
|
586
|
+
* symbol actually lives — splitting into one statement per location
|
|
587
|
+
* when symbols are spread across files.
|
|
588
|
+
* 2. `export * from` / namespace imports carry no symbol list, so derive
|
|
589
|
+
* the expected symbol from the file stem (`foo-bar.interface` →
|
|
590
|
+
* `FooBar`, `foo.serializer` → `deserializeFoo`/`serializeFoo`).
|
|
591
|
+
* 3. When a symbol exists nowhere, drop the statement and warn — a missing
|
|
592
|
+
* named export fails loudly at the usage site instead of as a phantom
|
|
593
|
+
* module, and a barrel line for a never-emitted file is pure noise.
|
|
594
|
+
*
|
|
595
|
+
* Mutates `f.content` in place and returns the warnings issued.
|
|
596
|
+
*/
|
|
597
|
+
export function enforceEmittedImportInvariant(
|
|
598
|
+
files: Iterable<GeneratedFile>,
|
|
599
|
+
emittedPaths: Set<string>,
|
|
600
|
+
surface: LiveSurface,
|
|
601
|
+
): string[] {
|
|
602
|
+
const fileList = [...files];
|
|
603
|
+
const emittedSymbols = indexEmittedExports(fileList);
|
|
604
|
+
const warnings: string[] = [];
|
|
605
|
+
|
|
606
|
+
const targetExists = (fromPath: string, spec: string): boolean =>
|
|
607
|
+
importTargetCandidates(fromPath, spec).some((p) => emittedPaths.has(p) || surface.files.has(p));
|
|
608
|
+
|
|
609
|
+
const locateSymbol = (name: string): string | undefined =>
|
|
610
|
+
emittedSymbols.get(name) ??
|
|
611
|
+
surface.functions.get(name) ??
|
|
612
|
+
surface.interfaces.get(name)?.filePath ??
|
|
613
|
+
surface.classes.get(name)?.filePath;
|
|
614
|
+
|
|
615
|
+
for (const f of fileList) {
|
|
616
|
+
if (!f.path.endsWith('.ts') || !f.content) continue;
|
|
617
|
+
let changed = false;
|
|
618
|
+
const outLines: string[] = [];
|
|
619
|
+
for (const line of f.content.split('\n')) {
|
|
620
|
+
const m = line.match(RELATIVE_FROM_STMT_RE);
|
|
621
|
+
if (!m || targetExists(f.path, m[4])) {
|
|
622
|
+
outLines.push(line);
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
const [, keyword, typeMod, clause, spec] = m;
|
|
626
|
+
const repaired = repairUnresolvableStatement(f.path, keyword, typeMod ?? '', clause, spec, locateSymbol);
|
|
627
|
+
changed = true;
|
|
628
|
+
outLines.push(...repaired.lines);
|
|
629
|
+
if (repaired.warning) warnings.push(repaired.warning);
|
|
630
|
+
}
|
|
631
|
+
if (changed) f.content = outLines.join('\n');
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
for (const w of warnings) console.warn(w);
|
|
635
|
+
return warnings;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function repairUnresolvableStatement(
|
|
639
|
+
fromPath: string,
|
|
640
|
+
keyword: string,
|
|
641
|
+
typeMod: string,
|
|
642
|
+
clause: string,
|
|
643
|
+
spec: string,
|
|
644
|
+
locateSymbol: (name: string) => string | undefined,
|
|
645
|
+
): { lines: string[]; warning?: string } {
|
|
646
|
+
if (clause.startsWith('{')) {
|
|
647
|
+
const entries = clause
|
|
648
|
+
.slice(1, -1)
|
|
649
|
+
.split(',')
|
|
650
|
+
.map((e) => e.trim())
|
|
651
|
+
.filter(Boolean);
|
|
652
|
+
const byLocation = new Map<string, string[]>();
|
|
653
|
+
const missing: string[] = [];
|
|
654
|
+
for (const entry of entries) {
|
|
655
|
+
const source = entry
|
|
656
|
+
.split(/\s+as\s+/)[0]
|
|
657
|
+
.replace(/^type\s+/, '')
|
|
658
|
+
.trim();
|
|
659
|
+
const location = locateSymbol(source);
|
|
660
|
+
if (!location) {
|
|
661
|
+
missing.push(source);
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
if (location === fromPath) continue; // declared locally — no import needed
|
|
665
|
+
const group = byLocation.get(location);
|
|
666
|
+
if (group) {
|
|
667
|
+
group.push(entry);
|
|
668
|
+
} else {
|
|
669
|
+
byLocation.set(location, [entry]);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
// Emit the symbols we *can* relocate; only the genuinely-missing ones are
|
|
673
|
+
// dropped (and warned about). Returning [] for the whole clause when any
|
|
674
|
+
// one symbol is missing would also discard the resolvable symbols, failing
|
|
675
|
+
// them with TS2305 at their usage sites — the breakage this pass prevents.
|
|
676
|
+
const lines = [...byLocation].map(
|
|
677
|
+
([location, group]) =>
|
|
678
|
+
`${keyword}${typeMod} { ${group.join(', ')} } from '${relativeImport(fromPath, location)}';`,
|
|
679
|
+
);
|
|
680
|
+
const warning =
|
|
681
|
+
missing.length > 0
|
|
682
|
+
? `oagen(node): dropped unresolvable symbol(s) from ${keyword} in ${fromPath}: '${spec}' — found neither in this run's output nor in the target SDK: ${missing.join(', ')}`
|
|
683
|
+
: undefined;
|
|
684
|
+
return { lines, warning };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// `* [as ns]` — no symbol list; derive the expected symbol from the stem.
|
|
688
|
+
const stem = spec.split('/').pop() ?? '';
|
|
689
|
+
let location: string | undefined;
|
|
690
|
+
if (stem.endsWith('.interface')) {
|
|
691
|
+
location = locateSymbol(toPascalCase(stem.slice(0, -'.interface'.length)));
|
|
692
|
+
} else if (stem.endsWith('.serializer')) {
|
|
693
|
+
const base = toPascalCase(stem.slice(0, -'.serializer'.length));
|
|
694
|
+
location = locateSymbol(`deserialize${base}`) ?? locateSymbol(`serialize${base}`);
|
|
695
|
+
}
|
|
696
|
+
if (location && location !== fromPath) {
|
|
697
|
+
return { lines: [`${keyword}${typeMod} ${clause} from '${relativeImport(fromPath, location)}';`] };
|
|
698
|
+
}
|
|
699
|
+
return {
|
|
700
|
+
lines: [],
|
|
701
|
+
warning: `oagen(node): dropped unresolvable ${keyword} in ${fromPath}: '${spec}' — module is neither emitted this run nor present in the target SDK`,
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
456
705
|
/**
|
|
457
706
|
* Re-declare prior-manifest paths that we did not emit this run so manifest
|
|
458
707
|
* pruning can tell "intentionally removed" from "untouched but still managed."
|
|
@@ -593,7 +842,18 @@ export const nodeEmitter: Emitter = {
|
|
|
593
842
|
const testFiles = nodeOptions(nodeCtx).regenerateOwnedTests
|
|
594
843
|
? applyLiveSurface(generateTestFiles(spec, nodeCtx), nodeCtx, surface)
|
|
595
844
|
: [];
|
|
596
|
-
|
|
845
|
+
const result = [...testFiles, ...carryForwardManagedFiles(nodeCtx, surface)];
|
|
846
|
+
|
|
847
|
+
// Final whole-run pass: this is the last generate hook, every hook shares
|
|
848
|
+
// `nodeCtx`, and the engine reads contents only after all hooks return —
|
|
849
|
+
// so the emitted-files cache now covers the complete run and repairs here
|
|
850
|
+
// reach files produced by earlier hooks. Greenfield runs are exempt: with
|
|
851
|
+
// no SDK on disk, "resolves to an existing file" has no meaning and the
|
|
852
|
+
// SDK core (workos.ts, common/) is intentionally not emitted.
|
|
853
|
+
if (managedPathsFor(nodeCtx, surface).size > 0) {
|
|
854
|
+
enforceEmittedImportInvariant(getEmittedFiles(nodeCtx).values(), getEmittedPaths(nodeCtx), surface);
|
|
855
|
+
}
|
|
856
|
+
return result;
|
|
597
857
|
},
|
|
598
858
|
|
|
599
859
|
// No operations map needed — the manifest belongs to the staging+target flow,
|