@pulse-compute/wasm-compiler 0.0.0 → 1.0.0-beta.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/README.md +52 -1
- package/bin/provider-proof-composition.js +34 -0
- package/bin/pulsewasm-extract.js +20 -0
- package/package.json +60 -5
- package/src/artifacts-dir.js +13 -0
- package/src/ast-json.js +52 -0
- package/src/build-manifest.js +354 -0
- package/src/canonical-api-compiler.js +245 -0
- package/src/canonical-native-compiler.js +411 -0
- package/src/canonical-native-plan.js +1477 -0
- package/src/canonical-project-compiler.js +1218 -0
- package/src/canonical-router-compiler.js +25 -0
- package/src/cli-intents.js +1235 -0
- package/src/cli.js +1927 -0
- package/src/codegen/assemblyscript-compile.js +3 -0
- package/src/codegen/assemblyscript-core.js +3 -0
- package/src/codegen/assemblyscript-shape.js +3 -0
- package/src/codegen/assemblyscript-wasm-smoke.js +3 -0
- package/src/codegen/backend-capabilities.js +3 -0
- package/src/codegen/channel-broadcaster.js +3 -0
- package/src/codegen/compiled-handlers.js +3 -0
- package/src/codegen/compiled-wasm-runtime.js +3 -0
- package/src/codegen/config-references.js +248 -0
- package/src/codegen/dispatch-ts.js +145 -0
- package/src/codegen/effect-composition.js +331 -0
- package/src/codegen/effect-runtime.js +418 -0
- package/src/codegen/execution-harness-ts.js +467 -0
- package/src/codegen/handler-bindings-ts.js +298 -0
- package/src/codegen/handler-library-contracts.js +3 -0
- package/src/codegen/host-capabilities.js +3 -0
- package/src/codegen/host-runtime-kernel.js +3 -0
- package/src/codegen/integrated-compiled-app.js +3 -0
- package/src/codegen/json-body.js +3 -0
- package/src/codegen/library-sidecars.js +3 -0
- package/src/codegen/local-harness-ts.js +453 -0
- package/src/codegen/pulse-wrapper.js +217 -0
- package/src/codegen/request-result-headers.js +3 -0
- package/src/codegen/schema-json-compile.js +3 -0
- package/src/codegen/schema-json-sidecar-v2.js +3 -0
- package/src/codegen/schema-json-sidecar.js +3 -0
- package/src/codegen/streaming-passthrough.js +3 -0
- package/src/codegen/wasm-host-abi.js +3 -0
- package/src/codegen/wasm-host-bridge.js +3 -0
- package/src/compiled-wasm-host-runtime-kv.js +3 -0
- package/src/config-resolver.js +813 -0
- package/src/crypto-requirement-planner.js +89 -0
- package/src/definitions/config-schema.js +14 -0
- package/src/definitions/handler-roles.js +14 -0
- package/src/definitions/path-grammar.js +14 -0
- package/src/definitions/router-api.js +14 -0
- package/src/diagnostics/codes.js +14 -0
- package/src/diagnostics/reporter.js +14 -0
- package/src/diagnostics.js +14 -0
- package/src/dispatch-table.js +400 -0
- package/src/events/event-emit.js +265 -0
- package/src/events/event-topology.js +127 -0
- package/src/execution-plan.js +463 -0
- package/src/extractor.js +2816 -0
- package/src/handler-eval.js +1154 -0
- package/src/handler-table.js +326 -0
- package/src/index.js +19 -0
- package/src/javascript-application-plan.js +181 -0
- package/src/kv-provider.js +3 -0
- package/src/path-table.js +60 -0
- package/src/path.js +14 -0
- package/src/patterns/config-define.js +30 -0
- package/src/patterns/dependency-call.js +18 -0
- package/src/patterns/env-lookup.js +13 -0
- package/src/patterns/handler-reference.js +29 -0
- package/src/patterns/path-literal.js +35 -0
- package/src/patterns/result.js +15 -0
- package/src/patterns/router-chain-call.js +50 -0
- package/src/patterns/router-construction.js +19 -0
- package/src/project/package-reachability.js +782 -0
- package/src/project/reachable-graph-builder.js +992 -0
- package/src/project/reachable-graph-contract.js +54 -0
- package/src/project/reachable-graph-implementation.js +36 -0
- package/src/project/router-module-linker.js +710 -0
- package/src/project-config-compiler.js +222 -0
- package/src/project-target-support.js +500 -0
- package/src/provider-toolchain.js +299 -0
- package/src/spine/async-surface-normalizer.js +312 -0
- package/src/spine/canonical-handler-ir.js +335 -0
- package/src/spine/canonical-native-module.js +87 -0
- package/src/spine/canonical-native-plan.js +89 -0
- package/src/spine/canonical-project.js +76 -0
- package/src/spine/canonical-router.js +155 -0
- package/src/spine/canonical-source.js +165 -0
- package/src/spine/diagnostic-authority.js +290 -0
- package/src/spine/equivalence.js +262 -0
- package/src/spine/guest-unit-stage.js +87 -0
- package/src/spine/handler-ir-emitter.js +454 -0
- package/src/spine/handler-ir-managed.js +1597 -0
- package/src/spine/handler-ir.js +797 -0
- package/src/spine/handler-surface-authority.js +585 -0
- package/src/spine/package-operation-seam.js +1015 -0
- package/src/spine/pipeline.js +202 -0
- package/src/spine/plain-handler-frontend.js +537 -0
- package/src/spine/provider-requirement-authority.js +208 -0
- package/src/spine/router-control-contract.js +17 -0
- package/src/spine/router-handler-frontend.js +513 -0
- package/src/spine/router-handler-ir.js +372 -0
- package/src/spine/router-topology-frontend.js +635 -0
- package/src/stable-id.js +14 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { PACKAGE_VERSION, normalizeArtifact, normalizeDiagnostic } = require('../diagnostics.js');
|
|
4
|
+
const {
|
|
5
|
+
CONFIG_REFERENCES_VERSION,
|
|
6
|
+
RUNTIME_CONFIG_REFERENCES_VERSION,
|
|
7
|
+
PLATFORM_STORE_MAP_VERSION,
|
|
8
|
+
CONFIG_AUTHORING_SHAPE_VERSION,
|
|
9
|
+
CONFIG_REFERENCES_PHASE: PHASE,
|
|
10
|
+
collectSymbolicRefs,
|
|
11
|
+
groupSymbolicRefs,
|
|
12
|
+
isSymbolicRef,
|
|
13
|
+
normalizeSymbolicRef,
|
|
14
|
+
configAuthoringShape
|
|
15
|
+
} = loadContractsSymbolicRefs();
|
|
16
|
+
|
|
17
|
+
function loadContractsSymbolicRefs() {
|
|
18
|
+
try {
|
|
19
|
+
return require('@pulse-compute/wasm-contracts/config/symbolic-refs');
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
|
|
22
|
+
return require('../../../contracts/src/config/symbolic-refs.js');
|
|
23
|
+
}
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function plainObject(value) {
|
|
29
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function configArtifact(options = {}) {
|
|
33
|
+
return options.resolvedConfig?.artifact || options.resolvedConfig || {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeDiagnostic(code, message, hint, details) {
|
|
37
|
+
return normalizeDiagnostic({
|
|
38
|
+
severity: 'error',
|
|
39
|
+
phase: 'config-references',
|
|
40
|
+
code,
|
|
41
|
+
message,
|
|
42
|
+
hint,
|
|
43
|
+
details,
|
|
44
|
+
loc: { file: '<config>' }
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function validateRefs(refs) {
|
|
49
|
+
const diagnostics = [];
|
|
50
|
+
const keys = new Set();
|
|
51
|
+
for (const ref of refs) {
|
|
52
|
+
const path = ref.path.join('.');
|
|
53
|
+
if (ref.kind === 'mixed') {
|
|
54
|
+
diagnostics.push(makeDiagnostic(
|
|
55
|
+
'PULSEWASM_CONFIG_REF_MIXED_KIND',
|
|
56
|
+
`Config reference at ${path} declares both $config and $secret.`,
|
|
57
|
+
'Use exactly one symbolic reference marker: { $config: "NAME" } or { $secret: "NAME" }.',
|
|
58
|
+
{ path, raw: ref.raw }
|
|
59
|
+
));
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (typeof ref.key !== 'string' || ref.key.trim() === '') {
|
|
63
|
+
diagnostics.push(makeDiagnostic(
|
|
64
|
+
'PULSEWASM_CONFIG_REF_KEY_INVALID',
|
|
65
|
+
`Config reference at ${path} must use a non-empty string key.`,
|
|
66
|
+
'Use a literal key such as { $config: "USERS_API_BASE_URL" } or { $secret: "USERS_API_TOKEN" }.',
|
|
67
|
+
{ path, raw: ref.raw }
|
|
68
|
+
));
|
|
69
|
+
}
|
|
70
|
+
const allowedKeys = new Set([ref.kind === 'config' ? '$config' : '$secret']);
|
|
71
|
+
for (const key of Object.keys(ref.raw || {})) {
|
|
72
|
+
if (!allowedKeys.has(key)) {
|
|
73
|
+
diagnostics.push(makeDiagnostic(
|
|
74
|
+
'PULSEWASM_CONFIG_REF_EXTRA_KEY',
|
|
75
|
+
`Config reference at ${path} contains unsupported property ${key}.`,
|
|
76
|
+
'Symbolic refs are intentionally tiny. Use only { $config: "NAME" } or { $secret: "NAME" }.',
|
|
77
|
+
{ path, property: key }
|
|
78
|
+
));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
keys.add(`${ref.kind}:${ref.key}`);
|
|
82
|
+
}
|
|
83
|
+
return diagnostics;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildConfigReferenceContract(generatedBy) {
|
|
87
|
+
return {
|
|
88
|
+
version: CONFIG_REFERENCES_VERSION,
|
|
89
|
+
generatedBy,
|
|
90
|
+
phase: PHASE,
|
|
91
|
+
status: 'locked',
|
|
92
|
+
canonicalRefs: {
|
|
93
|
+
config: { authoring: '{ $config: "KEY" }', secret: false, compiledIntoBinary: false },
|
|
94
|
+
secret: { authoring: '{ $secret: "KEY" }', secret: true, compiledIntoBinary: false }
|
|
95
|
+
},
|
|
96
|
+
authoringPolicy: {
|
|
97
|
+
envHelperRequired: false,
|
|
98
|
+
envHelperCanonical: false,
|
|
99
|
+
profileSelection: 'external PULSE_PROFILE / CLI profile, not defineConfig env() helper',
|
|
100
|
+
verboseMappings: 'generated-artifact-only',
|
|
101
|
+
rootTopologyOnly: ['entry', 'rootRouter', 'profiles'],
|
|
102
|
+
runtimeSettingsLiveUnder: 'profiles.<name>.runtime'
|
|
103
|
+
},
|
|
104
|
+
nonGoals: [
|
|
105
|
+
'no platform store lookup implementation',
|
|
106
|
+
'no Fastly config/secret hostcalls',
|
|
107
|
+
'no automatic env fallback',
|
|
108
|
+
'no secret values compiled into Wasm'
|
|
109
|
+
]
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function buildMarkdown(artifact, runtimeRefs, platformMap, authoringShape) {
|
|
114
|
+
const lines = [];
|
|
115
|
+
lines.push('# PulseWasm Static Config References Contract v1');
|
|
116
|
+
lines.push('');
|
|
117
|
+
lines.push(`Generated by: \`${artifact.generatedBy}\``);
|
|
118
|
+
lines.push(`Phase: \`${artifact.phase}\``);
|
|
119
|
+
lines.push('');
|
|
120
|
+
lines.push('## Locked position');
|
|
121
|
+
lines.push('');
|
|
122
|
+
lines.push('- Author configs use symbolic refs: `{ $config: "KEY" }` and `{ $secret: "KEY" }`.');
|
|
123
|
+
lines.push('- `env()` is not required for canonical config authoring.');
|
|
124
|
+
lines.push('- `PULSE_PROFILE` / CLI `--profile` selects the profile externally.');
|
|
125
|
+
lines.push('- Verbose provider mappings are generated artifacts only.');
|
|
126
|
+
lines.push('- Secrets are symbolic and never compiled into the Wasm binary.');
|
|
127
|
+
lines.push('');
|
|
128
|
+
lines.push('## Current refs');
|
|
129
|
+
lines.push('');
|
|
130
|
+
if (runtimeRefs.refs.length === 0) {
|
|
131
|
+
lines.push('No symbolic config refs were found in the selected profile runtime.');
|
|
132
|
+
} else {
|
|
133
|
+
for (const ref of runtimeRefs.refs) {
|
|
134
|
+
lines.push(`- \`${ref.kind}\` \`${ref.key}\` at \`${ref.path.join('.')}\` → provider \`${ref.provider}\`${ref.store ? ` store \`${ref.store}\`` : ''}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
lines.push('');
|
|
138
|
+
lines.push('## Fastly store mapping');
|
|
139
|
+
lines.push('');
|
|
140
|
+
lines.push(`- config store: ${platformMap.fastly.configStore.name ? `\`${platformMap.fastly.configStore.name}\`` : '`<unmapped>`'}`);
|
|
141
|
+
lines.push(`- secret store: ${platformMap.fastly.secretStore.name ? `\`${platformMap.fastly.secretStore.name}\`` : '`<unmapped>`'}`);
|
|
142
|
+
lines.push('');
|
|
143
|
+
lines.push('## Authoring shape');
|
|
144
|
+
lines.push('');
|
|
145
|
+
lines.push(`Root allowed keys: ${authoringShape.root.allowedKeys.map((key) => `\`${key}\``).join(', ')}`);
|
|
146
|
+
lines.push(`Profile runtime keys: ${authoringShape.profile.runtimeKeys.map((key) => `\`${key}\``).join(', ')}`);
|
|
147
|
+
return lines.join('\n');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function buildConfigReferences(options = {}) {
|
|
151
|
+
const generatedBy = options.generatedBy || PACKAGE_VERSION;
|
|
152
|
+
const config = configArtifact(options);
|
|
153
|
+
const runtime = plainObject(config.runtime);
|
|
154
|
+
const rawRefs = collectSymbolicRefs(runtime);
|
|
155
|
+
const refs = rawRefs.map((ref) => normalizeSymbolicRef(ref, runtime));
|
|
156
|
+
const diagnostics = validateRefs(rawRefs);
|
|
157
|
+
const platformMap = groupSymbolicRefs(rawRefs, runtime);
|
|
158
|
+
|
|
159
|
+
const missingFastlyConfigStore = refs.some((ref) => ref.kind === 'config' && ref.provider === 'unmapped.configStore');
|
|
160
|
+
const missingFastlySecretStore = refs.some((ref) => ref.kind === 'secret' && ref.provider === 'unmapped.secretStore');
|
|
161
|
+
if (missingFastlyConfigStore) {
|
|
162
|
+
diagnostics.push(makeDiagnostic(
|
|
163
|
+
'PULSEWASM_CONFIG_REF_FASTLY_CONFIG_STORE_UNMAPPED',
|
|
164
|
+
'One or more $config refs are present but runtime.platform.fastly.configStore is not configured.',
|
|
165
|
+
'Set profiles.<name>.runtime.platform.fastly.configStore to the Fastly config store name.',
|
|
166
|
+
{ refs: refs.filter((ref) => ref.kind === 'config') }
|
|
167
|
+
));
|
|
168
|
+
}
|
|
169
|
+
if (missingFastlySecretStore) {
|
|
170
|
+
diagnostics.push(makeDiagnostic(
|
|
171
|
+
'PULSEWASM_CONFIG_REF_FASTLY_SECRET_STORE_UNMAPPED',
|
|
172
|
+
'One or more $secret refs are present but runtime.platform.fastly.secretStore is not configured.',
|
|
173
|
+
'Set profiles.<name>.runtime.platform.fastly.secretStore to the Fastly secret store name.',
|
|
174
|
+
{ refs: refs.filter((ref) => ref.kind === 'secret') }
|
|
175
|
+
));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const runtimeRefs = {
|
|
179
|
+
version: RUNTIME_CONFIG_REFERENCES_VERSION,
|
|
180
|
+
generatedBy,
|
|
181
|
+
phase: PHASE,
|
|
182
|
+
profile: config.profile || null,
|
|
183
|
+
refs,
|
|
184
|
+
summary: {
|
|
185
|
+
refs: refs.length,
|
|
186
|
+
configRefs: refs.filter((ref) => ref.kind === 'config').length,
|
|
187
|
+
secretRefs: refs.filter((ref) => ref.kind === 'secret').length,
|
|
188
|
+
compiledIntoBinary: refs.filter((ref) => ref.compiledIntoBinary).length,
|
|
189
|
+
diagnostics: diagnostics.length
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const platformStoreMap = {
|
|
194
|
+
version: PLATFORM_STORE_MAP_VERSION,
|
|
195
|
+
generatedBy,
|
|
196
|
+
phase: PHASE,
|
|
197
|
+
profile: config.profile || null,
|
|
198
|
+
policy: {
|
|
199
|
+
verboseMappingsAreGeneratedOnly: true,
|
|
200
|
+
configValuesAreNotSecrets: true,
|
|
201
|
+
secretValuesNeverCompileIntoBinary: true,
|
|
202
|
+
fastlyIsProviderNotAuthoringShape: true
|
|
203
|
+
},
|
|
204
|
+
stores: platformMap,
|
|
205
|
+
summary: {
|
|
206
|
+
fastlyConfigStoreReady: platformMap.fastly.configStore.ready,
|
|
207
|
+
fastlySecretStoreReady: platformMap.fastly.secretStore.ready,
|
|
208
|
+
localProviderReserved: true
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const authoringShape = configAuthoringShape(generatedBy, PHASE);
|
|
213
|
+
|
|
214
|
+
const artifact = buildConfigReferenceContract(generatedBy);
|
|
215
|
+
artifact.profile = config.profile || null;
|
|
216
|
+
artifact.summary = {
|
|
217
|
+
refs: refs.length,
|
|
218
|
+
configRefs: runtimeRefs.summary.configRefs,
|
|
219
|
+
secretRefs: runtimeRefs.summary.secretRefs,
|
|
220
|
+
fastlyConfigStoreReady: platformStoreMap.summary.fastlyConfigStoreReady,
|
|
221
|
+
fastlySecretStoreReady: platformStoreMap.summary.fastlySecretStoreReady,
|
|
222
|
+
diagnostics: diagnostics.length
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const files = [{
|
|
226
|
+
file: 'generated/host/config-references.md',
|
|
227
|
+
text: buildMarkdown(artifact, runtimeRefs, platformMap, authoringShape)
|
|
228
|
+
}];
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
artifact: normalizeArtifact(artifact, options.cwd),
|
|
232
|
+
runtimeReferences: normalizeArtifact(runtimeRefs, options.cwd),
|
|
233
|
+
platformStoreMap: normalizeArtifact(platformStoreMap, options.cwd),
|
|
234
|
+
authoringShape: normalizeArtifact(authoringShape, options.cwd),
|
|
235
|
+
diagnostics,
|
|
236
|
+
files
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
module.exports = {
|
|
241
|
+
CONFIG_REFERENCES_VERSION,
|
|
242
|
+
RUNTIME_CONFIG_REFERENCES_VERSION,
|
|
243
|
+
PLATFORM_STORE_MAP_VERSION,
|
|
244
|
+
CONFIG_AUTHORING_SHAPE_VERSION,
|
|
245
|
+
buildConfigReferences,
|
|
246
|
+
collectSymbolicRefs,
|
|
247
|
+
isSymbolicRef
|
|
248
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const { PACKAGE_VERSION, normalizeArtifact } = require('../diagnostics.js');
|
|
5
|
+
|
|
6
|
+
const DISPATCH_TS_VERSION = 'pulsewasm.dispatch-ts.v3';
|
|
7
|
+
|
|
8
|
+
function sha256(text) {
|
|
9
|
+
return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function tsJson(value) {
|
|
13
|
+
return JSON.stringify(value, null, 2)
|
|
14
|
+
.replace(/</g, '\\u003c')
|
|
15
|
+
.replace(/>/g, '\\u003e')
|
|
16
|
+
.replace(/&/g, '\\u0026');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function header() {
|
|
20
|
+
return [
|
|
21
|
+
'/*',
|
|
22
|
+
' * Generated by PulseWasm Phase 8A/8B/8C/8E.',
|
|
23
|
+
' * Do not edit by hand.',
|
|
24
|
+
' *',
|
|
25
|
+
' * This file is generated from validated PulseWasm dispatch metadata.',
|
|
26
|
+
' * It must not re-read user source or re-infer route semantics.',
|
|
27
|
+
' */',
|
|
28
|
+
''
|
|
29
|
+
].join('\n');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildTypesSource() {
|
|
33
|
+
return `${header()}export type DispatchParamMap = Record<string, string>\n\nexport type DispatchSegment =\n | { readonly kind: 'literal'; readonly value: string }\n | { readonly kind: 'param'; readonly name: string }\n | { readonly kind: 'wildcard' }\n\nexport type DispatchPathPattern = {\n readonly grammarVersion: string\n readonly normalized: string\n readonly scoped: boolean\n readonly wildcard: boolean\n readonly segments: readonly DispatchSegment[]\n readonly params: readonly string[]\n}\n\nexport type HandlerUse = {\n readonly slot: number | null\n readonly id?: string\n readonly handlerId?: string\n readonly name?: string\n readonly role?: string\n readonly kind?: string\n readonly index?: number\n readonly order?: number\n readonly evalStatus?: string\n readonly unresolved?: boolean\n}\n\nexport type DispatchRoute = {\n readonly stableId: string\n readonly runtimeId: number\n readonly id: number\n readonly method: string\n readonly path: string\n readonly params: readonly string[]\n readonly pattern: DispatchPathPattern\n readonly handlers: {\n readonly middleware: readonly HandlerUse[]\n readonly route: HandlerUse\n readonly error: readonly HandlerUse[]\n readonly lifecycle: {\n readonly connect: readonly HandlerUse[]\n readonly disconnect: readonly HandlerUse[]\n }\n readonly channel?: HandlerUse | { readonly kind: 'static'; readonly value: string }\n }\n readonly execution: {\n readonly normal: readonly HandlerUse[]\n readonly error: readonly HandlerUse[]\n readonly lifecycle: {\n readonly connect: readonly HandlerUse[]\n readonly disconnect: readonly HandlerUse[]\n }\n readonly channel?: HandlerUse | { readonly kind: 'static'; readonly value: string }\n }\n}\n\nexport type DispatchMatch = {\n readonly stableId: string\n readonly runtimeId: number\n readonly id: number\n readonly method: string\n readonly path: string\n readonly requestPath: string\n readonly params: DispatchParamMap\n readonly rest: string\n readonly handlers: DispatchRoute['handlers']\n readonly execution: DispatchRoute['execution']\n readonly route: DispatchRoute\n}\n`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildRouteTableSource(dispatchTable) {
|
|
37
|
+
const routes = dispatchTable.routes || [];
|
|
38
|
+
const methodBuckets = dispatchTable.methodBuckets || {};
|
|
39
|
+
const handlerSlots = dispatchTable.handlerSlots || [];
|
|
40
|
+
const summary = dispatchTable.summary || {};
|
|
41
|
+
const routeCases = routes
|
|
42
|
+
.map((route, index) => ` case ${Number(route.runtimeId)}: return ROUTES[${index}]`)
|
|
43
|
+
.join('\n');
|
|
44
|
+
const routeSwitchBody = routeCases ? `${routeCases}\n default: return undefined` : ' default: return undefined';
|
|
45
|
+
|
|
46
|
+
return `${header()}import type { DispatchRoute } from './types'\n\nexport const DISPATCH_TABLE_VERSION = ${tsJson(dispatchTable.version)} as const\nexport const DISPATCH_GENERATED_BY = ${tsJson(dispatchTable.generatedBy)} as const\n\nexport const METHOD_BUCKETS = ${tsJson(methodBuckets)} as const\n\nexport const HANDLER_SLOTS = ${tsJson(handlerSlots)} as const\n\nexport const ROUTES = ${tsJson(routes)} as const\n\nexport const DISPATCH_SUMMARY = ${tsJson(summary)} as const\n\nexport function getRouteByRuntimeId(runtimeId: number): DispatchRoute | undefined {\n switch (runtimeId) {\n${routeSwitchBody}\n }\n}\n`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildDispatchSource() {
|
|
50
|
+
return `${header()}import { METHOD_BUCKETS, getRouteByRuntimeId } from './route-table'\nimport type { DispatchMatch, DispatchParamMap, DispatchPathPattern, DispatchRoute } from './types'\n\nfunction ensureLeadingSlash(value: string): string {\n return value.startsWith('/') ? value : '/' + value\n}\n\nfunction trimTrailingSlash(value: string): string {\n let out = value\n while (out.length > 1 && out.endsWith('/')) {\n out = out.slice(0, -1)\n }\n return out\n}\n\nexport function normalizeRoutePath(input: string): string {\n const text = String(input ?? '').trim()\n return trimTrailingSlash(ensureLeadingSlash(text || '/'))\n}\n\nfunction rawSegments(input: string): string[] {\n const normalized = normalizeRoutePath(input)\n if (normalized === '/') return []\n const body = normalized.slice(1)\n const segments = body.split('/')\n if (segments.some((segment) => segment.length === 0)) {\n throw new Error('Duplicate or empty path segment is unsupported: ' + normalized)\n }\n return segments\n}\n\nexport function matchPathPattern(pattern: DispatchPathPattern, requestPath: string): { params: DispatchParamMap; rest: string } | null {\n const pathSegments = rawSegments(requestPath)\n const params: DispatchParamMap = {}\n let pathIndex = 0\n\n for (const segment of pattern.segments) {\n if (segment.kind === 'wildcard') {\n const restSegments = pathSegments.slice(pathIndex)\n return { params, rest: restSegments.length === 0 ? '/' : '/' + restSegments.join('/') }\n }\n\n const value = pathSegments[pathIndex]\n if (value == null) return null\n\n if (segment.kind === 'literal') {\n if (segment.value !== value) return null\n } else if (segment.kind === 'param') {\n params[segment.name] = value\n } else {\n return null\n }\n\n pathIndex += 1\n }\n\n if (pathIndex !== pathSegments.length) return null\n return { params, rest: '/' }\n}\n\nexport function matchRoute(route: DispatchRoute, requestPath: string): DispatchMatch | null {\n const matched = matchPathPattern(route.pattern, requestPath)\n if (!matched) return null\n return {\n stableId: route.stableId,\n runtimeId: route.runtimeId,\n id: route.id,\n method: route.method,\n path: route.path,\n requestPath,\n params: matched.params,\n rest: matched.rest,\n handlers: route.handlers,\n execution: route.execution,\n route\n }\n}\n\nexport function match(method: string, requestPath: string): DispatchMatch | null {\n const normalizedMethod = String(method || '').toUpperCase()\n const bucket = (METHOD_BUCKETS as Record<string, readonly number[]>)[normalizedMethod] || []\n\n for (const runtimeId of bucket) {\n const route = getRouteByRuntimeId(runtimeId)\n if (!route) continue\n const matched = matchRoute(route, requestPath)\n if (matched) return matched\n }\n\n return null\n}\n\nexport const dispatch = match\n`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildIndexSource(options = {}) {
|
|
54
|
+
const handlerBindingsExport = options.includeHandlerBindings ? "export * from './handler-slots'\n" : '';
|
|
55
|
+
const executionHarnessExport = options.includeExecutionHarness ? "export * from './execution-plan'\nexport * from './execution-harness'\n" : '';
|
|
56
|
+
const localHarnessExport = options.includeLocalHarness ? "export * from './local-harness'\n" : '';
|
|
57
|
+
return `${header()}export * from './types'\nexport * from './route-table'\nexport * from './dispatch'\n${handlerBindingsExport}${executionHarnessExport}${localHarnessExport}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function makeFile(file, text) {
|
|
61
|
+
return {
|
|
62
|
+
file,
|
|
63
|
+
bytes: Buffer.byteLength(text, 'utf8'),
|
|
64
|
+
sha256: sha256(text),
|
|
65
|
+
text
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildTypeScriptDispatch(dispatchTable, options = {}) {
|
|
70
|
+
const cwd = options.cwd || process.cwd();
|
|
71
|
+
const handlerBindings = options.handlerBindings;
|
|
72
|
+
const executionHarness = options.executionHarness;
|
|
73
|
+
const localHarness = options.localHarness;
|
|
74
|
+
const includeHandlerBindings = Boolean(handlerBindings);
|
|
75
|
+
const includeExecutionHarness = Boolean(executionHarness);
|
|
76
|
+
const includeLocalHarness = Boolean(localHarness);
|
|
77
|
+
const files = [
|
|
78
|
+
makeFile('generated/types.ts', buildTypesSource()),
|
|
79
|
+
makeFile('generated/route-table.ts', buildRouteTableSource(dispatchTable)),
|
|
80
|
+
makeFile('generated/dispatch.ts', buildDispatchSource())
|
|
81
|
+
]
|
|
82
|
+
.concat(includeHandlerBindings ? handlerBindings.files || [] : [])
|
|
83
|
+
.concat(includeExecutionHarness ? executionHarness.files || [] : [])
|
|
84
|
+
.concat(includeLocalHarness ? localHarness.files || [] : [])
|
|
85
|
+
.concat([makeFile('generated/index.ts', buildIndexSource({ includeHandlerBindings, includeExecutionHarness, includeLocalHarness }))]);
|
|
86
|
+
|
|
87
|
+
const manifest = normalizeArtifact({
|
|
88
|
+
version: DISPATCH_TS_VERSION,
|
|
89
|
+
generatedBy: options.generatedBy || PACKAGE_VERSION,
|
|
90
|
+
source: dispatchTable?.source,
|
|
91
|
+
sourceDispatchTableVersion: dispatchTable?.version,
|
|
92
|
+
routePlanVersion: dispatchTable?.routePlanVersion,
|
|
93
|
+
handlerTableVersion: dispatchTable?.handlerTableVersion,
|
|
94
|
+
handlerEvalVersion: dispatchTable?.handlerEvalVersion,
|
|
95
|
+
pathTableVersion: dispatchTable?.pathTableVersion,
|
|
96
|
+
handlerBindingsVersion: handlerBindings?.artifact?.version,
|
|
97
|
+
executionHarnessVersion: executionHarness?.artifact?.version,
|
|
98
|
+
localHarnessVersion: localHarness?.artifact?.version,
|
|
99
|
+
output: {
|
|
100
|
+
language: 'typescript',
|
|
101
|
+
module: 'esm',
|
|
102
|
+
directory: 'generated',
|
|
103
|
+
files: files.map(({ file, bytes, sha256: hash }) => ({ file, bytes, sha256: hash }))
|
|
104
|
+
},
|
|
105
|
+
policy: {
|
|
106
|
+
phase: includeLocalHarness ? '9D' : includeExecutionHarness ? '8E' : includeHandlerBindings ? '8B' : '8A',
|
|
107
|
+
sourceOfTruth: includeLocalHarness
|
|
108
|
+
? 'dispatch-table.json + execution-plan.json + handler-table.json + handler-bindings.json + local-harness.json'
|
|
109
|
+
: includeExecutionHarness
|
|
110
|
+
? 'dispatch-table.json + execution-plan.json + handler-table.json + handler-bindings.json'
|
|
111
|
+
: includeHandlerBindings
|
|
112
|
+
? 'dispatch-table.json + handler-table.json'
|
|
113
|
+
: 'dispatch-table.json',
|
|
114
|
+
reReadsSource: false,
|
|
115
|
+
reInfersRouteSemantics: false,
|
|
116
|
+
handlerInvocation: includeLocalHarness
|
|
117
|
+
? 'local harness emitted; result ABI, Map state, lifecycle entrypoints, and broadcaster adaptor are available'
|
|
118
|
+
: includeExecutionHarness
|
|
119
|
+
? 'execution harness emitted; generated entry scanner and handler slot bindings required'
|
|
120
|
+
: includeHandlerBindings
|
|
121
|
+
? 'handler slot imports emitted; invocation harness reserved for Phase 8C'
|
|
122
|
+
: 'not emitted in Phase 8A; handler slot bindings are reserved for Phase 8B',
|
|
123
|
+
handlerBindings: includeHandlerBindings,
|
|
124
|
+
executionHarness: includeExecutionHarness,
|
|
125
|
+
localHarness: includeLocalHarness,
|
|
126
|
+
wasmCodegen: false
|
|
127
|
+
},
|
|
128
|
+
summary: {
|
|
129
|
+
routes: Array.isArray(dispatchTable?.routes) ? dispatchTable.routes.length : 0,
|
|
130
|
+
methods: Object.keys(dispatchTable?.methodBuckets || {}).sort(),
|
|
131
|
+
handlerSlots: Array.isArray(dispatchTable?.handlerSlots) ? dispatchTable.handlerSlots.length : 0,
|
|
132
|
+
handlerBindings: includeHandlerBindings ? handlerBindings?.artifact?.summary?.bindings || 0 : 0,
|
|
133
|
+
executionHarness: includeExecutionHarness ? executionHarness?.artifact?.summary : undefined,
|
|
134
|
+
localHarness: includeLocalHarness ? localHarness?.artifact?.summary : undefined,
|
|
135
|
+
files: files.length
|
|
136
|
+
}
|
|
137
|
+
}, cwd);
|
|
138
|
+
|
|
139
|
+
return { version: DISPATCH_TS_VERSION, manifest, files, diagnostics: [] };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = {
|
|
143
|
+
DISPATCH_TS_VERSION,
|
|
144
|
+
buildTypeScriptDispatch
|
|
145
|
+
};
|