@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,537 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ts = require('typescript');
|
|
4
|
+
const {
|
|
5
|
+
isIdentifierNamed,
|
|
6
|
+
extractFetchChain,
|
|
7
|
+
recognizeManagedHandlerWrapper,
|
|
8
|
+
recognizeHandlerSurface,
|
|
9
|
+
sourceModelForCanonicalRecognition
|
|
10
|
+
} = require('./handler-surface-authority.js');
|
|
11
|
+
const { normalizeManagedHandler } = require('./async-surface-normalizer.js');
|
|
12
|
+
const {
|
|
13
|
+
positionFor,
|
|
14
|
+
createCanonicalDiagnostic,
|
|
15
|
+
createHandlerDiagnostic
|
|
16
|
+
} = require('./diagnostic-authority.js');
|
|
17
|
+
|
|
18
|
+
function loadCanonicalSchemaCodecTools() {
|
|
19
|
+
try { return require('@pulse-compute/wasm-schema-json/compiler/canonical-schema-codecs'); }
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) return require('../../../schema-json/src/compiler/canonical-schema-codecs.js');
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const canonicalSchemaCodecTools = loadCanonicalSchemaCodecTools();
|
|
27
|
+
|
|
28
|
+
const PLAIN_HANDLER_FRONTEND_VERSION = 'pulse.plain-handler-frontend.v1';
|
|
29
|
+
const SUPPORTED_FETCH_DECODERS = Object.freeze(['json', 'text']);
|
|
30
|
+
const ALLOWED_IMPORTS = Object.freeze(['@pulse-compute/runtime']);
|
|
31
|
+
|
|
32
|
+
class CanonicalCompileError extends Error {
|
|
33
|
+
constructor(message, diagnostics = []) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = 'CanonicalCompileError';
|
|
36
|
+
this.code = 'PULSE_CANONICAL_COMPILE_FAILED';
|
|
37
|
+
this.diagnostics = diagnostics;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function staticResource(node) {
|
|
42
|
+
if (!node) return Object.freeze({ kind: 'none' });
|
|
43
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
44
|
+
const value = node.text;
|
|
45
|
+
let origin;
|
|
46
|
+
try { origin = new URL(value).origin; } catch (_) { origin = undefined; }
|
|
47
|
+
return Object.freeze({ kind: 'literal', value, origin });
|
|
48
|
+
}
|
|
49
|
+
return Object.freeze({ kind: 'dynamic' });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function diagnostic(sourceFile, node, code, message, detail = {}) {
|
|
53
|
+
return createCanonicalDiagnostic({
|
|
54
|
+
frontend: 'canonical-source',
|
|
55
|
+
sourceFile,
|
|
56
|
+
node,
|
|
57
|
+
code,
|
|
58
|
+
message,
|
|
59
|
+
detail
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
function literalStringValue(node) {
|
|
67
|
+
return node && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) ? node.text : undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function propertyNameValue(node) {
|
|
71
|
+
if (!node) return undefined;
|
|
72
|
+
if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) return String(node.text);
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function objectLiteralPropertyValue(node, propertyName) {
|
|
77
|
+
if (!node || !ts.isObjectLiteralExpression(node)) return undefined;
|
|
78
|
+
for (const property of node.properties) {
|
|
79
|
+
if (!ts.isPropertyAssignment(property) || propertyNameValue(property.name) !== propertyName) continue;
|
|
80
|
+
return Object.freeze({ property, value: property.initializer });
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isTypeOnlyImportDeclaration(statement) {
|
|
86
|
+
if (!statement || !ts.isImportDeclaration(statement)) return false;
|
|
87
|
+
const clause = statement.importClause;
|
|
88
|
+
if (!clause) return true;
|
|
89
|
+
if (clause.isTypeOnly) return true;
|
|
90
|
+
return Boolean(!clause.name && clause.namedBindings && ts.isNamedImports(clause.namedBindings)
|
|
91
|
+
&& clause.namedBindings.elements.length > 0
|
|
92
|
+
&& clause.namedBindings.elements.every((element) => element.isTypeOnly));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function effectiveSchemaBundle(input) {
|
|
96
|
+
if (input && input.registry && typeof input.declarationSource === 'string') return input;
|
|
97
|
+
return canonicalSchemaCodecTools.buildCanonicalSchemaBundle([]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function analyzeHandler(sourceFile, handler, ctxName, diagnostics, schemaBundleInput, options = {}) {
|
|
101
|
+
const schemaBundle = effectiveSchemaBundle(schemaBundleInput);
|
|
102
|
+
const realizedSchemaIds = new Set((schemaBundle.registry.schemas || []).map((schema) => String(schema.id)));
|
|
103
|
+
const schemaIds = new Set(
|
|
104
|
+
Array.isArray(schemaBundle.declaredSchemaIds)
|
|
105
|
+
? schemaBundle.declaredSchemaIds.map(String)
|
|
106
|
+
: realizedSchemaIds
|
|
107
|
+
);
|
|
108
|
+
const responseCases = new Map((schemaBundle.registry.responses || []).map((entry) => [String(entry.id), entry]));
|
|
109
|
+
const strictJson = options.strict !== false && schemaIds.size > 0;
|
|
110
|
+
const schemaReferences = [];
|
|
111
|
+
const capabilities = new Set();
|
|
112
|
+
const providerOperations = [];
|
|
113
|
+
const fetchAliases = new Set();
|
|
114
|
+
let operationIndex = 0;
|
|
115
|
+
let fetchCount = 0;
|
|
116
|
+
let genericJsonCount = 0;
|
|
117
|
+
let schemaBoundJsonCount = 0;
|
|
118
|
+
const forbiddenCalls = new Set(['require', 'setTimeout', 'setInterval', 'setImmediate', 'queueMicrotask', 'eval', 'Function']);
|
|
119
|
+
const forbiddenRoots = new Set(['process', 'global', 'globalThis', 'Deno', 'Bun', 'Buffer', 'console', 'WebSocket', 'XMLHttpRequest', 'EventSource', 'Worker']);
|
|
120
|
+
|
|
121
|
+
for (const effect of options.packageEffects || []) {
|
|
122
|
+
if (effect && effect.capability) capabilities.add(String(effect.capability));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function reject(node, code, message, detail = {}) {
|
|
126
|
+
diagnostics.push(diagnostic(sourceFile, node, code, message, detail));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function recordProviderOperation(node, kind, operation, capability, resourceNode) {
|
|
130
|
+
operationIndex += 1;
|
|
131
|
+
providerOperations.push(Object.freeze({
|
|
132
|
+
id: `${kind}-${operationIndex}`,
|
|
133
|
+
kind,
|
|
134
|
+
operation,
|
|
135
|
+
capability,
|
|
136
|
+
resource: staticResource(resourceNode),
|
|
137
|
+
position: positionFor(sourceFile, node)
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function rootIdentifier(expression) {
|
|
142
|
+
let current = expression;
|
|
143
|
+
while (ts.isPropertyAccessExpression(current) || ts.isElementAccessExpression(current)) current = current.expression;
|
|
144
|
+
return ts.isIdentifier(current) ? current.text : undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function collectFetchAliases(node) {
|
|
148
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
149
|
+
const chain = extractFetchChain(node.initializer, ctxName);
|
|
150
|
+
if (chain) fetchAliases.add(node.name.text);
|
|
151
|
+
}
|
|
152
|
+
ts.forEachChild(node, collectFetchAliases);
|
|
153
|
+
}
|
|
154
|
+
collectFetchAliases(handler.body || handler);
|
|
155
|
+
|
|
156
|
+
function recordKnownSchemaReference(call, schemaId, usage, detail = {}) {
|
|
157
|
+
if (!schemaIds.has(schemaId)) {
|
|
158
|
+
reject(detail.node || call, 'PULSE_CANONICAL_SCHEMA_MISSING', `Schema ${JSON.stringify(schemaId)} is not declared by pulse.schema.`, { usage, schemaId, declared: [...schemaIds].sort() });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (!realizedSchemaIds.has(schemaId)) {
|
|
162
|
+
reject(
|
|
163
|
+
detail.node || call,
|
|
164
|
+
'PULSE_SCHEMA_CODEC_REALIZATION_PENDING',
|
|
165
|
+
`Schema ${JSON.stringify(schemaId)} is registered, but its codec shape is not realized on both targets.`,
|
|
166
|
+
{
|
|
167
|
+
usage,
|
|
168
|
+
schemaId,
|
|
169
|
+
registryVersion: schemaBundle.registryIrVersion,
|
|
170
|
+
fullCodecRealization: false,
|
|
171
|
+
automaticFallback: false
|
|
172
|
+
}
|
|
173
|
+
);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const encode = usage === 'response-encode' || usage === 'fetch-encode';
|
|
177
|
+
schemaReferences.push(Object.freeze({
|
|
178
|
+
id: schemaId,
|
|
179
|
+
usage,
|
|
180
|
+
capability: encode ? 'schema.encode' : 'schema.decode',
|
|
181
|
+
...(detail.responseCaseId ? { responseCaseId: detail.responseCaseId } : {}),
|
|
182
|
+
position: positionFor(sourceFile, call)
|
|
183
|
+
}));
|
|
184
|
+
capabilities.add(encode ? 'schema.encode' : 'schema.decode');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function recordSchemaReference(call, argument, usage) {
|
|
188
|
+
const schemaId = literalStringValue(argument);
|
|
189
|
+
if (schemaId === undefined) {
|
|
190
|
+
reject(argument || call, 'PULSE_CANONICAL_SCHEMA_ID_LITERAL_REQUIRED', 'Schema IDs must be string literals so the project compiler can link codecs before provider lowering.', { usage });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
recordKnownSchemaReference(call, schemaId, usage, { node: argument });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function validateDecoderCall(call, usage) {
|
|
197
|
+
if (!ts.isCallExpression(call) || !ts.isPropertyAccessExpression(call.expression)) return;
|
|
198
|
+
const method = call.expression.name.text;
|
|
199
|
+
if (method === 'text' && call.arguments.length > 0) {
|
|
200
|
+
reject(call, 'PULSE_CANONICAL_DECODER_ARGUMENTS_UNSUPPORTED', 'text() does not accept a schema ID or other decoder arguments.', { usage, method, arguments: call.arguments.length });
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (method !== 'json') return;
|
|
204
|
+
if (call.arguments.length > 1) {
|
|
205
|
+
reject(call, 'PULSE_CANONICAL_DECODER_ARGUMENTS_UNSUPPORTED', 'json() accepts zero arguments for generic JSON or one declared schema ID.', { usage, method, arguments: call.arguments.length });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (call.arguments.length === 1) {
|
|
209
|
+
recordSchemaReference(call, call.arguments[0], usage);
|
|
210
|
+
schemaBoundJsonCount += 1;
|
|
211
|
+
} else {
|
|
212
|
+
genericJsonCount += 1;
|
|
213
|
+
if (strictJson) {
|
|
214
|
+
reject(call, 'PULSE_SCHEMA_REQUIRED', `Schema-less ${usage === 'request-decode' ? 'request' : 'fetched-response'} JSON requires pulse.strict to be false.`, {
|
|
215
|
+
usage,
|
|
216
|
+
strict: true
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function validateResponseSchema(call) {
|
|
223
|
+
if (call.arguments.length > 2) {
|
|
224
|
+
reject(call, 'PULSE_RESPONSE_DESCRIPTOR_INVALID', 'ctx.json accepts a value and at most one response descriptor or response-case ID.', {
|
|
225
|
+
arguments: call.arguments.length
|
|
226
|
+
});
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const descriptor = call.arguments[1];
|
|
230
|
+
if (!descriptor) {
|
|
231
|
+
if (strictJson) reject(call, 'PULSE_SCHEMA_REQUIRED', 'Strict Pulse JSON responses require a schema descriptor or registered response-case ID.', { usage: 'response-encode', strict: true });
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const responseCaseId = literalStringValue(descriptor);
|
|
235
|
+
if (responseCaseId !== undefined) {
|
|
236
|
+
const responseCase = responseCases.get(responseCaseId);
|
|
237
|
+
if (!responseCase) {
|
|
238
|
+
reject(descriptor, 'PULSE_RESPONSE_CASE_MISSING', `Response case ${JSON.stringify(responseCaseId)} is not declared by pulse.schema.`, {
|
|
239
|
+
responseCaseId,
|
|
240
|
+
declared: [...responseCases.keys()].sort()
|
|
241
|
+
});
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
recordKnownSchemaReference(call, String(responseCase.schemaId), 'response-encode', {
|
|
245
|
+
node: descriptor,
|
|
246
|
+
responseCaseId
|
|
247
|
+
});
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (!ts.isObjectLiteralExpression(descriptor)) {
|
|
251
|
+
reject(descriptor, 'PULSE_RESPONSE_DESCRIPTOR_LITERAL_REQUIRED', 'ctx.json response metadata must be an object literal or a registered response-case string literal.', {
|
|
252
|
+
usage: 'response-encode'
|
|
253
|
+
});
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const schemaProperty = objectLiteralPropertyValue(descriptor, 'schema');
|
|
257
|
+
if (schemaProperty) {
|
|
258
|
+
recordSchemaReference(call, schemaProperty.value, 'response-encode');
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (strictJson) reject(descriptor, 'PULSE_SCHEMA_REQUIRED', 'Strict Pulse JSON responses require a schema property or registered response-case ID.', { usage: 'response-encode', strict: true });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function validateFetchSchema(call) {
|
|
265
|
+
const init = call.arguments[1];
|
|
266
|
+
if (!init || !ts.isObjectLiteralExpression(init)) return;
|
|
267
|
+
const jsonProperty = objectLiteralPropertyValue(init, 'json');
|
|
268
|
+
const schemaProperty = objectLiteralPropertyValue(init, 'schema');
|
|
269
|
+
if (schemaProperty && !jsonProperty) {
|
|
270
|
+
reject(schemaProperty.property, 'PULSE_FETCH_SCHEMA_WITHOUT_JSON', 'Pulse fetch schema is only valid together with the semantic json field.', {
|
|
271
|
+
usage: 'fetch-encode'
|
|
272
|
+
});
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (!jsonProperty) return;
|
|
276
|
+
if (schemaProperty) {
|
|
277
|
+
recordSchemaReference(call, schemaProperty.value, 'fetch-encode');
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (strictJson) reject(jsonProperty.property, 'PULSE_SCHEMA_REQUIRED', 'Strict outbound fetch JSON requires a registered literal schema ID.', {
|
|
281
|
+
usage: 'fetch-encode',
|
|
282
|
+
strict: true
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function visit(node) {
|
|
287
|
+
if (typeof ts.isTypeNode === 'function' && ts.isTypeNode(node)) return;
|
|
288
|
+
if (ts.isNewExpression(node) && ts.isIdentifier(node.expression)) {
|
|
289
|
+
if (node.expression.text === 'Promise') diagnostics.push(createHandlerDiagnostic({
|
|
290
|
+
frontend: 'canonical-source',
|
|
291
|
+
issue: 'handler.promise.unsupported',
|
|
292
|
+
sourceFile,
|
|
293
|
+
node,
|
|
294
|
+
values: { construction: true }
|
|
295
|
+
}));
|
|
296
|
+
if (node.expression.text === 'Date' && (!node.arguments || node.arguments.length === 0)) reject(node, 'PULSE_CANONICAL_NONDETERMINISM_UNSUPPORTED', 'Ambient clock access is not available in canonical user scope.');
|
|
297
|
+
if (forbiddenRoots.has(node.expression.text) || node.expression.text === 'Function') reject(node, 'PULSE_CANONICAL_AMBIENT_AUTHORITY_UNSUPPORTED', `Ambient ${node.expression.text} authority is not available in canonical user scope.`, { identifier: node.expression.text });
|
|
298
|
+
}
|
|
299
|
+
if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
|
|
300
|
+
const root = rootIdentifier(node);
|
|
301
|
+
if (root && forbiddenRoots.has(root)) reject(node, 'PULSE_CANONICAL_AMBIENT_AUTHORITY_UNSUPPORTED', `Ambient ${root} authority is not available in canonical user scope.`, { identifier: root });
|
|
302
|
+
}
|
|
303
|
+
if (ts.isCallExpression(node)) {
|
|
304
|
+
if (node.expression.kind === ts.SyntaxKind.ImportKeyword) reject(node, 'PULSE_CANONICAL_DYNAMIC_IMPORT_UNSUPPORTED', 'Dynamic import is not available in canonical user scope.');
|
|
305
|
+
if (ts.isIdentifier(node.expression)) {
|
|
306
|
+
const name = node.expression.text;
|
|
307
|
+
if (name === 'fetch') reject(node, 'PULSE_CANONICAL_AMBIENT_FETCH_UNSUPPORTED', 'Use ctx.fetch; ambient global fetch is not available in user scope.');
|
|
308
|
+
if (name === 'Promise') diagnostics.push(createHandlerDiagnostic({
|
|
309
|
+
frontend: 'canonical-source',
|
|
310
|
+
issue: 'handler.promise.unsupported',
|
|
311
|
+
sourceFile,
|
|
312
|
+
node,
|
|
313
|
+
values: { construction: false }
|
|
314
|
+
}));
|
|
315
|
+
if (forbiddenCalls.has(name)) reject(node, 'PULSE_CANONICAL_AMBIENT_AUTHORITY_UNSUPPORTED', `Ambient ${name} is not available in canonical user scope.`, { identifier: name });
|
|
316
|
+
}
|
|
317
|
+
if (ts.isPropertyAccessExpression(node.expression)) {
|
|
318
|
+
const root = rootIdentifier(node.expression);
|
|
319
|
+
if (root && forbiddenRoots.has(root)) reject(node, 'PULSE_CANONICAL_AMBIENT_AUTHORITY_UNSUPPORTED', `Ambient ${root} authority is not available in canonical user scope.`, { identifier: root });
|
|
320
|
+
if (root === 'Date' && node.expression.name.text === 'now') reject(node, 'PULSE_CANONICAL_NONDETERMINISM_UNSUPPORTED', 'Ambient clock access is not available in canonical user scope.');
|
|
321
|
+
if (root === 'Math' && node.expression.name.text === 'random') reject(node, 'PULSE_CANONICAL_NONDETERMINISM_UNSUPPORTED', 'Ambient randomness is not available in canonical user scope.');
|
|
322
|
+
if (root === 'crypto') reject(node, 'PULSE_CANONICAL_AMBIENT_AUTHORITY_UNSUPPORTED', 'Ambient crypto authority is not available in canonical user scope.', { identifier: 'crypto' });
|
|
323
|
+
|
|
324
|
+
const target = node.expression;
|
|
325
|
+
const surface = recognizeHandlerSurface(node, { ctxName });
|
|
326
|
+
if (surface) {
|
|
327
|
+
if (surface.surfaceId === 'ctx.fetch.opaque-return' && surface.detail.fetch.fetchCall === node) {
|
|
328
|
+
capabilities.add('fetch');
|
|
329
|
+
fetchCount += 1;
|
|
330
|
+
validateFetchSchema(node);
|
|
331
|
+
} else if (surface.surfaceId === 'ctx.fetch.projected') {
|
|
332
|
+
if (surface.detail.fetch.fetchCall === node) {
|
|
333
|
+
capabilities.add('fetch');
|
|
334
|
+
fetchCount += 1;
|
|
335
|
+
validateFetchSchema(node);
|
|
336
|
+
}
|
|
337
|
+
if (surface.detail.fetch.decoderCall === node) validateDecoderCall(node, 'fetch-decode');
|
|
338
|
+
} else if (['ctx.json', 'ctx.text', 'ctx.response'].includes(surface.surfaceId)) {
|
|
339
|
+
const method = surface.surfaceId.slice('ctx.'.length);
|
|
340
|
+
capabilities.add(`response.${method}`);
|
|
341
|
+
if (surface.surfaceId === 'ctx.json') validateResponseSchema(node);
|
|
342
|
+
} else if (surface.surfaceId === 'ctx.req.json.schema' || surface.surfaceId === 'ctx.req.json.generic') {
|
|
343
|
+
capabilities.add('request.json');
|
|
344
|
+
validateDecoderCall(node, 'request-decode');
|
|
345
|
+
} else if (surface.surfaceId === 'ctx.req.text') {
|
|
346
|
+
capabilities.add('request.text');
|
|
347
|
+
validateDecoderCall(node, 'request-decode');
|
|
348
|
+
} else if (surface.surfaceId === 'ctx.state.get' || surface.surfaceId === 'ctx.state.set') {
|
|
349
|
+
capabilities.add(surface.surfaceId.slice('ctx.'.length));
|
|
350
|
+
} else if (surface.surfaceId.startsWith('ctx.log.')) {
|
|
351
|
+
capabilities.add('logging');
|
|
352
|
+
} else if (surface.detail.provider) {
|
|
353
|
+
const operation = surface.detail.provider;
|
|
354
|
+
if (operation.providerKind === 'kv') capabilities.add('kv');
|
|
355
|
+
capabilities.add(operation.capability);
|
|
356
|
+
recordProviderOperation(node, operation.providerKind, operation.operation, operation.capability, operation.resource);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (isIdentifierNamed(target.expression, ctxName) && target.name.text === 'kv') capabilities.add('kv');
|
|
360
|
+
if (ts.isIdentifier(target.expression) && fetchAliases.has(target.expression.text) && SUPPORTED_FETCH_DECODERS.includes(target.name.text)) validateDecoderCall(node, 'fetch-decode');
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
ts.forEachChild(node, visit);
|
|
364
|
+
}
|
|
365
|
+
visit(handler.body || handler);
|
|
366
|
+
return Object.freeze({
|
|
367
|
+
capabilities: Object.freeze([...capabilities].sort()),
|
|
368
|
+
providerOperations: Object.freeze(providerOperations),
|
|
369
|
+
fetchCount,
|
|
370
|
+
genericJsonCount,
|
|
371
|
+
schemaBoundJsonCount,
|
|
372
|
+
schemaReferences: Object.freeze(schemaReferences)
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function findCanonicalHandler(sourceFile, diagnostics, options = {}) {
|
|
377
|
+
const allowedRuntimeImports = new Set([...ALLOWED_IMPORTS, ...(options.allowedRuntimeImports || []).map(String)]);
|
|
378
|
+
for (const statement of sourceFile.statements) {
|
|
379
|
+
if (!ts.isImportDeclaration(statement)) continue;
|
|
380
|
+
const moduleName = statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier.text : '';
|
|
381
|
+
const typeOnlyImport = isTypeOnlyImportDeclaration(statement);
|
|
382
|
+
if (!allowedRuntimeImports.has(moduleName) && !typeOnlyImport) diagnostics.push(diagnostic(sourceFile, statement, 'PULSE_CANONICAL_IMPORT_UNSUPPORTED', `Canonical handlers may use type-only imports and runtime imports owned by declared package lowerers; project-relative runtime values must pass through the reachable module graph.`, { moduleName, allowedRuntimeImports: [...allowedRuntimeImports] }));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
for (const statement of sourceFile.statements) {
|
|
386
|
+
if (ts.isVariableStatement(statement)) diagnostics.push(diagnostic(sourceFile, statement, 'PULSE_CANONICAL_AMBIENT_STATE_UNSUPPORTED', 'Top-level runtime state is not supported; keep request state inside the handler.'));
|
|
387
|
+
if (ts.isClassDeclaration(statement)) diagnostics.push(diagnostic(sourceFile, statement, 'PULSE_CANONICAL_TOP_LEVEL_CLASS_UNSUPPORTED', 'Top-level classes are not part of the canonical alpha handler subset.'));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
let handler;
|
|
391
|
+
for (const statement of sourceFile.statements) {
|
|
392
|
+
if (ts.isFunctionDeclaration(statement)) {
|
|
393
|
+
const hasDefault = (statement.modifiers || []).some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword);
|
|
394
|
+
const hasExport = (statement.modifiers || []).some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
395
|
+
if (hasDefault && hasExport) { handler = statement; break; }
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (!handler) {
|
|
400
|
+
for (const statement of sourceFile.statements) {
|
|
401
|
+
if (!ts.isExportAssignment(statement) || statement.isExportEquals) continue;
|
|
402
|
+
const expression = statement.expression;
|
|
403
|
+
if (ts.isCallExpression(expression) && ts.isIdentifier(expression.expression) && expression.expression.text === 'defineHandler') {
|
|
404
|
+
diagnostics.push(diagnostic(sourceFile, expression, 'PULSE_CANONICAL_DEFINE_HANDLER_UNSUPPORTED', 'defineHandler is not part of the public runtime contract. Export a plain default handler function or a static Router.'));
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) { handler = expression; break; }
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (handler) {
|
|
412
|
+
for (const statement of sourceFile.statements) {
|
|
413
|
+
if (ts.isImportDeclaration(statement)) {
|
|
414
|
+
const moduleName = statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier.text : '';
|
|
415
|
+
const clause = statement.importClause;
|
|
416
|
+
const typeOnly = isTypeOnlyImportDeclaration(statement);
|
|
417
|
+
const packageLowererImport = moduleName !== '@pulse-compute/runtime' && allowedRuntimeImports.has(moduleName);
|
|
418
|
+
if (!typeOnly && !packageLowererImport) diagnostics.push(diagnostic(sourceFile, statement, 'PULSE_CANONICAL_RUNTIME_IMPORT_UNSUPPORTED', 'Canonical handlers may use type-only imports from @pulse-compute/runtime and runtime imports owned by discovered package lowerers.'));
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (statement === handler || ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement) || ts.isEmptyStatement(statement)) continue;
|
|
422
|
+
if (ts.isExportAssignment(statement)) {
|
|
423
|
+
const expression = statement.expression;
|
|
424
|
+
if (expression === handler) continue;
|
|
425
|
+
}
|
|
426
|
+
diagnostics.push(diagnostic(sourceFile, statement, 'PULSE_CANONICAL_TOP_LEVEL_RUNTIME_UNSUPPORTED', 'Canonical source may contain only type declarations, runtime-contract imports, and the exported default handler. Move runtime helpers inside the handler until module lowering is implemented.'));
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (!handler) diagnostics.push(diagnostic(sourceFile, sourceFile, 'PULSE_CANONICAL_HANDLER_MISSING', 'Expected an exported default handler function.'));
|
|
431
|
+
if (handler && handler.asteriskToken) diagnostics.push(createHandlerDiagnostic({
|
|
432
|
+
frontend: 'canonical-source',
|
|
433
|
+
issue: 'handler.generator.unsupported',
|
|
434
|
+
sourceFile,
|
|
435
|
+
node: handler
|
|
436
|
+
}));
|
|
437
|
+
if (handler && handler.parameters.length !== 1) diagnostics.push(diagnostic(sourceFile, handler, 'PULSE_CANONICAL_HANDLER_ARITY', 'Canonical handlers must accept exactly one ctx parameter.'));
|
|
438
|
+
if (handler && !handler.body) diagnostics.push(diagnostic(sourceFile, handler, 'PULSE_CANONICAL_HANDLER_BODY_MISSING', 'Canonical handler must have a body.'));
|
|
439
|
+
return handler;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
function sourceFileForRequest(sourceText, options = {}, recognition) {
|
|
444
|
+
const fileName = String(options.fileName || 'app.ts').replace(/\\/g, '/');
|
|
445
|
+
const source = String(sourceText);
|
|
446
|
+
const model = sourceModelForCanonicalRecognition(recognition);
|
|
447
|
+
if (model && model.file === fileName && model.source === source) {
|
|
448
|
+
return Object.freeze({ fileName, source, sourceFile: model.sourceFile, reusedRecognitionAst: true });
|
|
449
|
+
}
|
|
450
|
+
return Object.freeze({
|
|
451
|
+
fileName,
|
|
452
|
+
source,
|
|
453
|
+
sourceFile: ts.createSourceFile(fileName, source, ts.ScriptTarget.ES2022, true, fileName.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS),
|
|
454
|
+
reusedRecognitionAst: false
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function preparePlainHandlerSource(sourceText, options = {}, recognition) {
|
|
459
|
+
const parsed = sourceFileForRequest(sourceText, options, recognition);
|
|
460
|
+
const schemaBundle = effectiveSchemaBundle(options.schemaBundle);
|
|
461
|
+
const diagnostics = Array.from(parsed.sourceFile.parseDiagnostics || [], (entry) => Object.freeze({
|
|
462
|
+
code: 'PULSE_CANONICAL_SYNTAX_ERROR',
|
|
463
|
+
kind: 'CanonicalCompileDiagnostic',
|
|
464
|
+
severity: 'error',
|
|
465
|
+
message: ts.flattenDiagnosticMessageText(entry.messageText, '\n'),
|
|
466
|
+
file: parsed.fileName,
|
|
467
|
+
position: Object.freeze(parsed.sourceFile.getLineAndCharacterOfPosition(entry.start || 0)),
|
|
468
|
+
detail: Object.freeze({ typescriptCode: entry.code })
|
|
469
|
+
}));
|
|
470
|
+
const authoredHandler = findCanonicalHandler(parsed.sourceFile, diagnostics, options);
|
|
471
|
+
const warnings = [];
|
|
472
|
+
let handler = authoredHandler;
|
|
473
|
+
let normalization;
|
|
474
|
+
let analysis = Object.freeze({ capabilities: Object.freeze([]), providerOperations: Object.freeze([]), fetchCount: 0, genericJsonCount: 0, schemaBoundJsonCount: 0, schemaReferences: Object.freeze([]) });
|
|
475
|
+
if (authoredHandler) {
|
|
476
|
+
const ctxParameter = authoredHandler.parameters[0];
|
|
477
|
+
const ctxName = ctxParameter && ts.isIdentifier(ctxParameter.name) ? ctxParameter.name.text : 'ctx';
|
|
478
|
+
const packageEffectsByStart = new Map();
|
|
479
|
+
for (const effect of options.packageEffects || []) {
|
|
480
|
+
const start = effect && effect.range && Number(effect.range.start);
|
|
481
|
+
if (Number.isSafeInteger(start) && !packageEffectsByStart.has(start)) packageEffectsByStart.set(start, effect);
|
|
482
|
+
}
|
|
483
|
+
const normalized = normalizeManagedHandler(authoredHandler, {
|
|
484
|
+
sourceFile: parsed.sourceFile,
|
|
485
|
+
ctxName,
|
|
486
|
+
role: 'handler',
|
|
487
|
+
strict: options.strict === true,
|
|
488
|
+
frontend: 'canonical-source',
|
|
489
|
+
handlerAuthoring: options.handlerAuthoring,
|
|
490
|
+
requireAsync: options.requireAsync === true,
|
|
491
|
+
requireEffectAwait: options.requireEffectAwait === true,
|
|
492
|
+
packageEffectForCall: options.packageEffectForCall || ((call) => packageEffectsByStart.get(call.getStart(parsed.sourceFile)))
|
|
493
|
+
});
|
|
494
|
+
diagnostics.push(...normalized.diagnostics);
|
|
495
|
+
warnings.push(...normalized.warnings);
|
|
496
|
+
handler = normalized.functionNode;
|
|
497
|
+
normalization = (normalized.summary.changed || normalized.summary.userAuthoredAsync || normalized.summary.awaitCount > 0)
|
|
498
|
+
? normalized.summary
|
|
499
|
+
: undefined;
|
|
500
|
+
analysis = analyzeHandler(parsed.sourceFile, handler, ctxName, diagnostics, schemaBundle, options);
|
|
501
|
+
}
|
|
502
|
+
return Object.freeze({
|
|
503
|
+
version: PLAIN_HANDLER_FRONTEND_VERSION,
|
|
504
|
+
frontend: 'canonical-source',
|
|
505
|
+
fileName: parsed.fileName,
|
|
506
|
+
sourceText: parsed.source,
|
|
507
|
+
sourceFile: parsed.sourceFile,
|
|
508
|
+
handler,
|
|
509
|
+
schemaBundle,
|
|
510
|
+
analysis,
|
|
511
|
+
diagnostics,
|
|
512
|
+
warnings: Object.freeze(warnings),
|
|
513
|
+
normalization,
|
|
514
|
+
authoredHandler,
|
|
515
|
+
reusedRecognitionAst: parsed.reusedRecognitionAst
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function assertPlainHandlerDiagnostics(frontend) {
|
|
520
|
+
if (!frontend || frontend.version !== PLAIN_HANDLER_FRONTEND_VERSION) {
|
|
521
|
+
throw new TypeError(`assertPlainHandlerDiagnostics requires ${PLAIN_HANDLER_FRONTEND_VERSION}.`);
|
|
522
|
+
}
|
|
523
|
+
if (frontend.diagnostics.some((entry) => entry.severity !== 'warning')) {
|
|
524
|
+
throw new CanonicalCompileError(`Canonical compilation failed for ${frontend.fileName}.`, frontend.diagnostics);
|
|
525
|
+
}
|
|
526
|
+
return frontend;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
module.exports = Object.freeze({
|
|
530
|
+
PLAIN_HANDLER_FRONTEND_VERSION,
|
|
531
|
+
SUPPORTED_FETCH_DECODERS,
|
|
532
|
+
ALLOWED_IMPORTS,
|
|
533
|
+
CanonicalCompileError,
|
|
534
|
+
effectiveSchemaBundle,
|
|
535
|
+
preparePlainHandlerSource,
|
|
536
|
+
assertPlainHandlerDiagnostics
|
|
537
|
+
});
|