@pulse-compute/wasm-compiler 0.0.0 → 1.0.0-beta.2

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.
Files changed (104) hide show
  1. package/README.md +53 -1
  2. package/bin/provider-proof-composition.js +34 -0
  3. package/bin/pulsewasm-extract.js +20 -0
  4. package/package.json +60 -5
  5. package/src/artifacts-dir.js +13 -0
  6. package/src/ast-json.js +52 -0
  7. package/src/build-manifest.js +354 -0
  8. package/src/canonical-api-compiler.js +269 -0
  9. package/src/canonical-native-compiler.js +411 -0
  10. package/src/canonical-native-plan.js +1478 -0
  11. package/src/canonical-project-compiler.js +1224 -0
  12. package/src/canonical-router-compiler.js +25 -0
  13. package/src/cli-intents.js +1235 -0
  14. package/src/cli.js +1927 -0
  15. package/src/codegen/assemblyscript-compile.js +3 -0
  16. package/src/codegen/assemblyscript-core.js +3 -0
  17. package/src/codegen/assemblyscript-shape.js +3 -0
  18. package/src/codegen/assemblyscript-wasm-smoke.js +3 -0
  19. package/src/codegen/backend-capabilities.js +3 -0
  20. package/src/codegen/channel-broadcaster.js +3 -0
  21. package/src/codegen/compiled-handlers.js +3 -0
  22. package/src/codegen/compiled-wasm-runtime.js +3 -0
  23. package/src/codegen/config-references.js +248 -0
  24. package/src/codegen/dispatch-ts.js +145 -0
  25. package/src/codegen/effect-composition.js +331 -0
  26. package/src/codegen/effect-runtime.js +418 -0
  27. package/src/codegen/execution-harness-ts.js +467 -0
  28. package/src/codegen/handler-bindings-ts.js +298 -0
  29. package/src/codegen/handler-library-contracts.js +3 -0
  30. package/src/codegen/host-capabilities.js +3 -0
  31. package/src/codegen/host-runtime-kernel.js +3 -0
  32. package/src/codegen/integrated-compiled-app.js +3 -0
  33. package/src/codegen/json-body.js +3 -0
  34. package/src/codegen/library-sidecars.js +3 -0
  35. package/src/codegen/local-harness-ts.js +453 -0
  36. package/src/codegen/pulse-wrapper.js +217 -0
  37. package/src/codegen/request-result-headers.js +3 -0
  38. package/src/codegen/schema-json-compile.js +3 -0
  39. package/src/codegen/schema-json-sidecar-v2.js +3 -0
  40. package/src/codegen/schema-json-sidecar.js +3 -0
  41. package/src/codegen/streaming-passthrough.js +3 -0
  42. package/src/codegen/wasm-host-abi.js +3 -0
  43. package/src/codegen/wasm-host-bridge.js +3 -0
  44. package/src/compiled-wasm-host-runtime-kv.js +3 -0
  45. package/src/config-resolver.js +813 -0
  46. package/src/crypto-requirement-planner.js +89 -0
  47. package/src/definitions/config-schema.js +14 -0
  48. package/src/definitions/handler-roles.js +14 -0
  49. package/src/definitions/path-grammar.js +14 -0
  50. package/src/definitions/router-api.js +14 -0
  51. package/src/diagnostics/codes.js +14 -0
  52. package/src/diagnostics/reporter.js +14 -0
  53. package/src/diagnostics.js +14 -0
  54. package/src/dispatch-table.js +400 -0
  55. package/src/events/event-emit.js +265 -0
  56. package/src/events/event-topology.js +127 -0
  57. package/src/execution-plan.js +463 -0
  58. package/src/extractor.js +2816 -0
  59. package/src/handler-eval.js +1154 -0
  60. package/src/handler-table.js +326 -0
  61. package/src/index.js +19 -0
  62. package/src/javascript-application-plan.js +181 -0
  63. package/src/kv-provider.js +3 -0
  64. package/src/path-table.js +60 -0
  65. package/src/path.js +14 -0
  66. package/src/patterns/config-define.js +30 -0
  67. package/src/patterns/dependency-call.js +18 -0
  68. package/src/patterns/env-lookup.js +13 -0
  69. package/src/patterns/handler-reference.js +29 -0
  70. package/src/patterns/path-literal.js +35 -0
  71. package/src/patterns/result.js +15 -0
  72. package/src/patterns/router-chain-call.js +50 -0
  73. package/src/patterns/router-construction.js +19 -0
  74. package/src/project/package-reachability.js +782 -0
  75. package/src/project/reachable-graph-builder.js +992 -0
  76. package/src/project/reachable-graph-contract.js +54 -0
  77. package/src/project/reachable-graph-implementation.js +36 -0
  78. package/src/project/router-module-linker.js +710 -0
  79. package/src/project-config-compiler.js +222 -0
  80. package/src/project-target-support.js +500 -0
  81. package/src/provider-toolchain.js +299 -0
  82. package/src/spine/async-surface-normalizer.js +328 -0
  83. package/src/spine/canonical-handler-ir.js +336 -0
  84. package/src/spine/canonical-native-module.js +87 -0
  85. package/src/spine/canonical-native-plan.js +89 -0
  86. package/src/spine/canonical-project.js +76 -0
  87. package/src/spine/canonical-router.js +155 -0
  88. package/src/spine/canonical-source.js +165 -0
  89. package/src/spine/diagnostic-authority.js +290 -0
  90. package/src/spine/equivalence.js +262 -0
  91. package/src/spine/guest-unit-stage.js +87 -0
  92. package/src/spine/handler-ir-emitter.js +455 -0
  93. package/src/spine/handler-ir-managed.js +1598 -0
  94. package/src/spine/handler-ir.js +797 -0
  95. package/src/spine/handler-surface-authority.js +588 -0
  96. package/src/spine/package-operation-seam.js +1015 -0
  97. package/src/spine/pipeline.js +202 -0
  98. package/src/spine/plain-handler-frontend.js +545 -0
  99. package/src/spine/provider-requirement-authority.js +208 -0
  100. package/src/spine/router-control-contract.js +17 -0
  101. package/src/spine/router-handler-frontend.js +514 -0
  102. package/src/spine/router-handler-ir.js +372 -0
  103. package/src/spine/router-topology-frontend.js +638 -0
  104. package/src/stable-id.js +14 -0
@@ -0,0 +1,813 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const ts = require('typescript');
6
+ const { PACKAGE_VERSION, diagnostic, ExtractionError, sourceLoc } = require('./diagnostics.js');
7
+ const { ENV_PROFILE_NAME, SEMANTIC_ENV_KEYS, ALLOWED_ENV_METHODS } = require('./definitions/config-schema.js');
8
+ const { collectDefineConfigCalls, isTopLevelDefineConfigCall } = require('./patterns/config-define.js');
9
+ const { isEnvCall } = require('./patterns/env-lookup.js');
10
+
11
+ function scriptKindForFile(filePath) {
12
+ const ext = path.extname(filePath).toLowerCase();
13
+ if (ext === '.ts' || ext === '.mts' || ext === '.cts') return ts.ScriptKind.TS;
14
+ if (ext === '.tsx') return ts.ScriptKind.TSX;
15
+ if (ext === '.jsx') return ts.ScriptKind.JSX;
16
+ return ts.ScriptKind.JS;
17
+ }
18
+
19
+ function readSourceFile(filePath) {
20
+ const abs = path.resolve(filePath);
21
+ const text = fs.readFileSync(abs, 'utf8');
22
+ return ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, true, scriptKindForFile(abs));
23
+ }
24
+
25
+ function stableFileName(fileName, cwd) {
26
+ const rel = path.relative(cwd || process.cwd(), fileName);
27
+ return rel.startsWith('..') ? fileName : rel || path.basename(fileName);
28
+ }
29
+
30
+ function isStringLiteralLike(node) {
31
+ return ts.isStringLiteral(node) || node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral;
32
+ }
33
+
34
+ function nodeText(sourceFile, node) {
35
+ return node.getText(sourceFile).trim();
36
+ }
37
+
38
+ function locForNode(sourceFile, node) {
39
+ return sourceLoc(sourceFile, node);
40
+ }
41
+
42
+ function configDiagnostic(sourceFile, node, code, message, hint) {
43
+ return diagnostic(sourceFile, node, code, message, hint);
44
+ }
45
+
46
+ function unwrapExpression(node) {
47
+ let current = node;
48
+ while (
49
+ ts.isParenthesizedExpression(current) ||
50
+ ts.isAsExpression(current) ||
51
+ ts.isSatisfiesExpression?.(current) ||
52
+ ts.isNonNullExpression(current) ||
53
+ ts.isTypeAssertionExpression(current)
54
+ ) {
55
+ current = current.expression;
56
+ }
57
+ return current;
58
+ }
59
+
60
+ function valueLiteral(value, sourceFile, node, envRef) {
61
+ return {
62
+ kind: 'literal',
63
+ value,
64
+ envRef,
65
+ loc: node ? locForNode(sourceFile, node) : undefined
66
+ };
67
+ }
68
+
69
+ function valueObject(props, sourceFile, node) {
70
+ return {
71
+ kind: 'object',
72
+ props,
73
+ loc: node ? locForNode(sourceFile, node) : undefined
74
+ };
75
+ }
76
+
77
+ function valueArray(items, sourceFile, node) {
78
+ return {
79
+ kind: 'array',
80
+ items,
81
+ loc: node ? locForNode(sourceFile, node) : undefined
82
+ };
83
+ }
84
+
85
+ function errorValue(sourceFile, node) {
86
+ return valueLiteral(undefined, sourceFile, node);
87
+ }
88
+
89
+ function valueToPlain(node) {
90
+ if (!node) return undefined;
91
+ if (node.kind === 'literal') return node.value;
92
+ if (node.kind === 'array') return node.items.map(valueToPlain).filter((value) => value !== undefined);
93
+ if (node.kind === 'object') {
94
+ const out = {};
95
+ for (const [key, value] of Object.entries(node.props)) {
96
+ const plain = valueToPlain(value);
97
+ if (plain !== undefined) out[key] = plain;
98
+ }
99
+ return out;
100
+ }
101
+ return undefined;
102
+ }
103
+
104
+ function hasOwn(obj, key) {
105
+ return Object.prototype.hasOwnProperty.call(obj, key);
106
+ }
107
+
108
+ function plainObject(value) {
109
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
110
+ }
111
+
112
+ function valueGet(node, key) {
113
+ return node && node.kind === 'object' && hasOwn(node.props, key) ? node.props[key] : undefined;
114
+ }
115
+
116
+ function collectEnvRefs(node, pathSegments = [], out = []) {
117
+ if (!node) return out;
118
+ if (node.kind === 'literal' && node.envRef) {
119
+ out.push({ ...node.envRef, path: pathSegments.slice(), loc: node.loc });
120
+ return out;
121
+ }
122
+ if (node.kind === 'array') {
123
+ node.items.forEach((item, index) => collectEnvRefs(item, [...pathSegments, String(index)], out));
124
+ return out;
125
+ }
126
+ if (node.kind === 'object') {
127
+ for (const [key, value] of Object.entries(node.props)) {
128
+ collectEnvRefs(value, [...pathSegments, key], out);
129
+ }
130
+ }
131
+ return out;
132
+ }
133
+
134
+ function isSecretPath(pathSegments, envName) {
135
+ const joinedPath = pathSegments.join('.').toLowerCase();
136
+ const normalizedEnv = String(envName || '').toLowerCase();
137
+ if (/(secret|password|passwd|token|credential|credentials|private|private[_-]?key|api[_-]?key|access[_-]?key)/.test(joinedPath)) {
138
+ return true;
139
+ }
140
+ if (/(secret|password|passwd|token|credential|credentials|private|private[_-]?key|api[_-]?key|access[_-]?key)/.test(normalizedEnv)) {
141
+ return true;
142
+ }
143
+ if (/\bkey\b/.test(joinedPath) && /(key|secret|credential|token)/.test(normalizedEnv)) {
144
+ return true;
145
+ }
146
+ return false;
147
+ }
148
+
149
+ function envRefForArtifact(ref, pathSegments) {
150
+ const secret = isSecretPath(pathSegments, ref.name);
151
+ const base = {
152
+ $env: ref.name,
153
+ type: ref.type,
154
+ resolved: ref.resolved,
155
+ secret,
156
+ redacted: secret
157
+ };
158
+ if (ref.hasFallback) {
159
+ base.fallback = secret ? '<redacted>' : ref.fallback;
160
+ }
161
+ if (!secret && ref.resolved) {
162
+ base.value = ref.value;
163
+ }
164
+ return base;
165
+ }
166
+
167
+ function valueToArtifact(node, pathSegments = []) {
168
+ if (!node) return undefined;
169
+ if (node.kind === 'literal') {
170
+ if (!node.envRef) return node.value === undefined ? null : node.value;
171
+ const secret = isSecretPath(pathSegments, node.envRef.name);
172
+ if (secret || !node.envRef.resolved) {
173
+ return envRefForArtifact(node.envRef, pathSegments);
174
+ }
175
+ return node.envRef.value;
176
+ }
177
+ if (node.kind === 'array') {
178
+ return node.items.map((item, index) => valueToArtifact(item, [...pathSegments, String(index)]));
179
+ }
180
+ if (node.kind === 'object') {
181
+ const out = {};
182
+ for (const [key, value] of Object.entries(node.props)) {
183
+ out[key] = valueToArtifact(value, [...pathSegments, key]);
184
+ }
185
+ return out;
186
+ }
187
+ return null;
188
+ }
189
+
190
+ function serializeEnvRefs(refs) {
191
+ return refs.map((ref) => {
192
+ const secret = isSecretPath(ref.path, ref.name);
193
+ const out = {
194
+ name: ref.name,
195
+ type: ref.type,
196
+ path: ref.path,
197
+ resolved: ref.resolved,
198
+ secret,
199
+ redacted: secret,
200
+ loc: ref.loc
201
+ };
202
+ if (ref.hasFallback) out.fallback = secret ? '<redacted>' : ref.fallback;
203
+ if (!secret && ref.resolved) out.value = ref.value;
204
+ return out;
205
+ });
206
+ }
207
+
208
+ function normalizeEnvSource(envSource) {
209
+ const values = {};
210
+ if (!envSource) return values;
211
+ if (typeof envSource === 'function') return normalizeEnvSource(envSource());
212
+ if (typeof envSource !== 'object') return values;
213
+ for (const [key, value] of Object.entries(envSource)) {
214
+ if (value === undefined || value === null) continue;
215
+ values[key] = value;
216
+ }
217
+ return values;
218
+ }
219
+
220
+ function parseBoolean(name, value) {
221
+ const normalized = String(value).trim().toLowerCase();
222
+ if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
223
+ if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
224
+ throw new Error(`Pulse env boolean ${name} must be one of true/false/1/0/yes/no/on/off`);
225
+ }
226
+
227
+ function envLookup(envValues, name) {
228
+ if (!hasOwn(envValues, name)) return undefined;
229
+ const value = envValues[name];
230
+ if (value === undefined || value === null) return undefined;
231
+ return String(value);
232
+ }
233
+
234
+ function propertyNameText(sourceFile, nameNode, diagnostics) {
235
+ if (ts.isIdentifier(nameNode) || isStringLiteralLike(nameNode) || ts.isNumericLiteral(nameNode)) {
236
+ return nameNode.text;
237
+ }
238
+ diagnostics.push(configDiagnostic(
239
+ sourceFile,
240
+ nameNode,
241
+ 'PULSEWASM_CONFIG_COMPUTED_KEY',
242
+ 'Computed config keys are not supported in PulseWasm config v1.',
243
+ 'Use static object literal keys only.'
244
+ ));
245
+ return nodeText(sourceFile, nameNode);
246
+ }
247
+
248
+ function literalFallbackToPlain(node) {
249
+ if (!node || node.kind !== 'literal' || node.envRef) return undefined;
250
+ return node.value;
251
+ }
252
+
253
+ function evaluateConfigExpression(sourceFile, node, context, pathSegments = []) {
254
+ const diagnostics = context.diagnostics;
255
+ const expr = unwrapExpression(node);
256
+
257
+ if (isStringLiteralLike(expr)) {
258
+ return valueLiteral(expr.text, sourceFile, expr);
259
+ }
260
+ if (ts.isNumericLiteral(expr)) {
261
+ return valueLiteral(Number(expr.text), sourceFile, expr);
262
+ }
263
+ if (expr.kind === ts.SyntaxKind.TrueKeyword) {
264
+ return valueLiteral(true, sourceFile, expr);
265
+ }
266
+ if (expr.kind === ts.SyntaxKind.FalseKeyword) {
267
+ return valueLiteral(false, sourceFile, expr);
268
+ }
269
+ if (expr.kind === ts.SyntaxKind.NullKeyword) {
270
+ return valueLiteral(null, sourceFile, expr);
271
+ }
272
+ if (ts.isIdentifier(expr) && expr.text === 'undefined') {
273
+ return valueLiteral(undefined, sourceFile, expr);
274
+ }
275
+ if (ts.isPrefixUnaryExpression(expr) && ts.isNumericLiteral(expr.operand)) {
276
+ const value = Number(expr.operand.text);
277
+ if (expr.operator === ts.SyntaxKind.MinusToken) return valueLiteral(-value, sourceFile, expr);
278
+ if (expr.operator === ts.SyntaxKind.PlusToken) return valueLiteral(value, sourceFile, expr);
279
+ }
280
+
281
+ if (ts.isObjectLiteralExpression(expr)) {
282
+ const props = {};
283
+ for (const prop of expr.properties) {
284
+ if (ts.isSpreadAssignment(prop)) {
285
+ diagnostics.push(configDiagnostic(
286
+ sourceFile,
287
+ prop,
288
+ 'PULSEWASM_CONFIG_SPREAD_UNSUPPORTED',
289
+ 'Object spread is not supported in PulseWasm config v1.',
290
+ 'Write the selected profile/config object explicitly.'
291
+ ));
292
+ continue;
293
+ }
294
+ if (ts.isPropertyAssignment(prop)) {
295
+ const key = propertyNameText(sourceFile, prop.name, diagnostics);
296
+ props[key] = evaluateConfigExpression(sourceFile, prop.initializer, context, [...pathSegments, key]);
297
+ continue;
298
+ }
299
+ diagnostics.push(configDiagnostic(
300
+ sourceFile,
301
+ prop,
302
+ 'PULSEWASM_CONFIG_PROPERTY_UNSUPPORTED',
303
+ 'Only plain property assignments are supported in PulseWasm config v1.',
304
+ 'Do not use shorthand, methods, getters, setters, or spreads in defineConfig().'
305
+ ));
306
+ }
307
+ return valueObject(props, sourceFile, expr);
308
+ }
309
+
310
+ if (ts.isArrayLiteralExpression(expr)) {
311
+ const items = [];
312
+ expr.elements.forEach((item, index) => {
313
+ if (ts.isSpreadElement(item)) {
314
+ diagnostics.push(configDiagnostic(
315
+ sourceFile,
316
+ item,
317
+ 'PULSEWASM_CONFIG_SPREAD_UNSUPPORTED',
318
+ 'Array spread is not supported in PulseWasm config v1.',
319
+ 'Write env definitions and arrays explicitly.'
320
+ ));
321
+ return;
322
+ }
323
+ items.push(evaluateConfigExpression(sourceFile, item, context, [...pathSegments, String(index)]));
324
+ });
325
+ return valueArray(items, sourceFile, expr);
326
+ }
327
+
328
+ if (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr) || ts.isFunctionDeclaration(expr)) {
329
+ diagnostics.push(configDiagnostic(
330
+ sourceFile,
331
+ expr,
332
+ 'PULSEWASM_CONFIG_FUNCTION_VALUE',
333
+ 'Function values inside defineConfig() are not supported in PulseWasm config v1.',
334
+ 'Profile engines and runtime functions are host/runtime concerns, not Wasm config truth.'
335
+ ));
336
+ return errorValue(sourceFile, expr);
337
+ }
338
+
339
+ if (ts.isCallExpression(expr)) {
340
+ const envCall = context.envParamName ? isEnvCall(ts, expr.expression, context.envParamName) : null;
341
+ if (!envCall) {
342
+ diagnostics.push(configDiagnostic(
343
+ sourceFile,
344
+ expr,
345
+ 'PULSEWASM_CONFIG_DYNAMIC_CALL',
346
+ 'Only calls to the injected env resolver are supported inside defineConfig().',
347
+ 'Use literals, object/array literals, and env("NAME") / env.number(...) / env.boolean(...) / env.define(...).'
348
+ ));
349
+ return errorValue(sourceFile, expr);
350
+ }
351
+
352
+ const method = envCall.method;
353
+ if (method !== 'string' && !ALLOWED_ENV_METHODS.has(method)) {
354
+ diagnostics.push(configDiagnostic(
355
+ sourceFile,
356
+ envCall.nameNode || expr.expression,
357
+ 'PULSEWASM_CONFIG_ENV_METHOD_UNSUPPORTED',
358
+ `Unsupported env resolver method env.${method}().`,
359
+ 'Allowed methods: env(), env.number(), env.boolean(), and env.define().'
360
+ ));
361
+ return errorValue(sourceFile, expr);
362
+ }
363
+
364
+ if (method === 'define') {
365
+ if (expr.arguments.length !== 1) {
366
+ diagnostics.push(configDiagnostic(
367
+ sourceFile,
368
+ expr,
369
+ 'PULSEWASM_CONFIG_ENV_DEFINE_ARITY',
370
+ 'env.define() expects exactly one array or object literal argument.',
371
+ 'Use env.define(["NAME"]) or env.define({ NAME: { ... } }).'
372
+ ));
373
+ return errorValue(sourceFile, expr);
374
+ }
375
+ const definition = evaluateConfigExpression(sourceFile, expr.arguments[0], context, pathSegments);
376
+ context.envDefinitions.push({ value: valueToPlain(definition), loc: locForNode(sourceFile, expr) });
377
+ return definition;
378
+ }
379
+
380
+ if (expr.arguments.length < 1 || expr.arguments.length > 2) {
381
+ diagnostics.push(configDiagnostic(
382
+ sourceFile,
383
+ expr,
384
+ 'PULSEWASM_CONFIG_ENV_ARITY',
385
+ `${method === 'string' ? 'env' : `env.${method}`}() expects one name and an optional fallback.`,
386
+ 'Use literal env names and literal fallbacks only.'
387
+ ));
388
+ return errorValue(sourceFile, expr);
389
+ }
390
+
391
+ const nameArg = unwrapExpression(expr.arguments[0]);
392
+ if (!isStringLiteralLike(nameArg)) {
393
+ diagnostics.push(configDiagnostic(
394
+ sourceFile,
395
+ nameArg,
396
+ 'PULSEWASM_CONFIG_ENV_NAME',
397
+ 'Env names must be string literals in PulseWasm config v1.',
398
+ 'Use env("NAME") rather than computed env names.'
399
+ ));
400
+ return errorValue(sourceFile, expr);
401
+ }
402
+
403
+ let fallback;
404
+ let hasFallback = false;
405
+ if (expr.arguments.length === 2) {
406
+ const fallbackNode = evaluateConfigExpression(sourceFile, expr.arguments[1], { ...context, envParamName: undefined }, pathSegments);
407
+ fallback = literalFallbackToPlain(fallbackNode);
408
+ hasFallback = true;
409
+ const expectedType = method === 'number' ? 'number' : method === 'boolean' ? 'boolean' : 'string';
410
+ if (fallback === undefined || typeof fallback !== expectedType) {
411
+ diagnostics.push(configDiagnostic(
412
+ sourceFile,
413
+ expr.arguments[1],
414
+ 'PULSEWASM_CONFIG_ENV_FALLBACK',
415
+ `${method === 'string' ? 'env' : `env.${method}`}() fallback must be a ${expectedType} literal.`,
416
+ 'Keep fallback values scalar and literal.'
417
+ ));
418
+ }
419
+ }
420
+
421
+ const name = nameArg.text;
422
+ const raw = envLookup(context.envValues, name);
423
+ let resolved = raw !== undefined;
424
+ let value;
425
+ try {
426
+ if (method === 'number') {
427
+ value = raw === undefined ? fallback : Number(raw);
428
+ if (value !== undefined && !Number.isFinite(value)) {
429
+ throw new Error(`Pulse env number ${name} must be a finite number`);
430
+ }
431
+ } else if (method === 'boolean') {
432
+ value = raw === undefined ? fallback : parseBoolean(name, raw);
433
+ } else {
434
+ value = raw === undefined ? fallback : raw;
435
+ }
436
+ } catch (error) {
437
+ diagnostics.push(configDiagnostic(
438
+ sourceFile,
439
+ expr,
440
+ 'PULSEWASM_CONFIG_ENV_VALUE_INVALID',
441
+ error.message,
442
+ 'Pass a valid env value or use a literal fallback with the correct type.'
443
+ ));
444
+ value = fallback;
445
+ }
446
+ if (!resolved && hasFallback && fallback !== undefined) resolved = true;
447
+
448
+ const envRef = {
449
+ name,
450
+ type: method === 'string' ? 'string' : method,
451
+ value,
452
+ fallback,
453
+ hasFallback,
454
+ resolved,
455
+ loc: locForNode(sourceFile, expr)
456
+ };
457
+ return valueLiteral(value, sourceFile, expr, envRef);
458
+ }
459
+
460
+ diagnostics.push(configDiagnostic(
461
+ sourceFile,
462
+ expr,
463
+ 'PULSEWASM_CONFIG_UNSUPPORTED_EXPRESSION',
464
+ `Unsupported config expression: ${nodeText(sourceFile, expr)}`,
465
+ 'PulseWasm config v1 supports only literals, object/array literals, and bounded env resolver calls.'
466
+ ));
467
+ return errorValue(sourceFile, expr);
468
+ }
469
+
470
+ function findDefineConfigCall(sourceFile, diagnostics) {
471
+ const calls = collectDefineConfigCalls(ts, sourceFile);
472
+
473
+ if (calls.length === 0) {
474
+ diagnostics.push({
475
+ code: 'PULSEWASM_CONFIG_DEFINECONFIG_MISSING',
476
+ message: 'No defineConfig(...) call was found.',
477
+ hint: 'Export or declare a single top-level defineConfig(...) call.',
478
+ loc: { file: sourceFile.fileName }
479
+ });
480
+ return undefined;
481
+ }
482
+ if (calls.length > 1) {
483
+ for (const call of calls) {
484
+ diagnostics.push(configDiagnostic(
485
+ sourceFile,
486
+ call,
487
+ 'PULSEWASM_CONFIG_MULTIPLE_DEFINECONFIG',
488
+ 'Multiple defineConfig(...) calls are not supported in PulseWasm config v1.',
489
+ 'Use one static config declaration per build.'
490
+ ));
491
+ }
492
+ return undefined;
493
+ }
494
+ const call = calls[0];
495
+ if (!isTopLevelDefineConfigCall(ts, sourceFile, call)) {
496
+ diagnostics.push(configDiagnostic(
497
+ sourceFile,
498
+ call,
499
+ 'PULSEWASM_CONFIG_DEFINECONFIG_NOT_TOP_LEVEL',
500
+ 'defineConfig(...) must be a top-level declaration or default export.',
501
+ 'Do not hide config creation inside functions, conditionals, or helper calls.'
502
+ ));
503
+ return undefined;
504
+ }
505
+ return call;
506
+ }
507
+
508
+ function configFactoryBodyExpression(sourceFile, factory, diagnostics) {
509
+ if (factory.modifiers && factory.modifiers.some((mod) => mod.kind === ts.SyntaxKind.AsyncKeyword)) {
510
+ diagnostics.push(configDiagnostic(
511
+ sourceFile,
512
+ factory,
513
+ 'PULSEWASM_CONFIG_ASYNC_FACTORY',
514
+ 'Async config factories are not supported in PulseWasm config v1.',
515
+ 'defineConfig must be synchronously and statically resolvable.'
516
+ ));
517
+ }
518
+ if (factory.parameters.length !== 1 || !ts.isIdentifier(factory.parameters[0].name)) {
519
+ diagnostics.push(configDiagnostic(
520
+ sourceFile,
521
+ factory,
522
+ 'PULSEWASM_CONFIG_FACTORY_SHAPE',
523
+ 'Config factories must accept exactly one identifier parameter.',
524
+ 'Use defineConfig((env) => ({ ... })).'
525
+ ));
526
+ return undefined;
527
+ }
528
+
529
+ if (ts.isArrowFunction(factory) && !ts.isBlock(factory.body)) {
530
+ return { envParamName: factory.parameters[0].name.text, body: factory.body };
531
+ }
532
+
533
+ const body = factory.body;
534
+ if (!body || !ts.isBlock(body)) {
535
+ diagnostics.push(configDiagnostic(
536
+ sourceFile,
537
+ factory,
538
+ 'PULSEWASM_CONFIG_FACTORY_SHAPE',
539
+ 'Config factory bodies must return one object literal.',
540
+ 'Use defineConfig((env) => ({ ... })) or a single return statement.'
541
+ ));
542
+ return undefined;
543
+ }
544
+ if (body.statements.length !== 1 || !ts.isReturnStatement(body.statements[0]) || !body.statements[0].expression) {
545
+ diagnostics.push(configDiagnostic(
546
+ sourceFile,
547
+ body,
548
+ 'PULSEWASM_CONFIG_FACTORY_SHAPE',
549
+ 'Config factory blocks must contain exactly one return statement.',
550
+ 'Do not declare local variables or run arbitrary code in defineConfig().'
551
+ ));
552
+ return undefined;
553
+ }
554
+ return { envParamName: factory.parameters[0].name.text, body: body.statements[0].expression };
555
+ }
556
+
557
+ function evaluateDefineConfigCall(sourceFile, call, context) {
558
+ if (call.arguments.length !== 1) {
559
+ context.diagnostics.push(configDiagnostic(
560
+ sourceFile,
561
+ call,
562
+ 'PULSEWASM_CONFIG_DEFINECONFIG_ARITY',
563
+ 'defineConfig() expects exactly one object literal or config factory.',
564
+ 'Use defineConfig({ ... }) or defineConfig((env) => ({ ... })).'
565
+ ));
566
+ return errorValue(sourceFile, call);
567
+ }
568
+
569
+ const arg = unwrapExpression(call.arguments[0]);
570
+ if (ts.isObjectLiteralExpression(arg)) {
571
+ return evaluateConfigExpression(sourceFile, arg, { ...context, envParamName: undefined }, []);
572
+ }
573
+ if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
574
+ const factory = configFactoryBodyExpression(sourceFile, arg, context.diagnostics);
575
+ if (!factory) return errorValue(sourceFile, arg);
576
+ return evaluateConfigExpression(sourceFile, factory.body, { ...context, envParamName: factory.envParamName }, []);
577
+ }
578
+
579
+ context.diagnostics.push(configDiagnostic(
580
+ sourceFile,
581
+ arg,
582
+ 'PULSEWASM_CONFIG_DEFINECONFIG_INPUT',
583
+ 'defineConfig() only accepts an object literal or a one-argument config factory.',
584
+ 'Do not pass imported objects, computed values, promises, or helper-produced config.'
585
+ ));
586
+ return errorValue(sourceFile, arg);
587
+ }
588
+
589
+ function pathIsUnder(pathSegments, basePath) {
590
+ if (pathSegments.length < basePath.length) return false;
591
+ for (let i = 0; i < basePath.length; i += 1) {
592
+ if (pathSegments[i] !== basePath[i]) return false;
593
+ }
594
+ return true;
595
+ }
596
+
597
+ function pathStartsWithProfile(pathSegments, profileName) {
598
+ return pathSegments.length >= 2 && pathSegments[0] === 'profiles' && pathSegments[1] === profileName;
599
+ }
600
+
601
+ function validateEnvRefBoundaries(sourceFile, refs, activeProfileName, diagnostics) {
602
+ for (const ref of refs) {
603
+ const first = ref.path[0];
604
+ if (first && SEMANTIC_ENV_KEYS.has(first)) {
605
+ diagnostics.push({
606
+ code: 'PULSEWASM_CONFIG_ENV_SEMANTIC_LEAK',
607
+ message: `Env lookup ${ref.name} is not allowed to define route/compiler semantics at ${ref.path.join('.')}.`,
608
+ hint: 'Use literal entry/rootRouter/route/handler semantics. env() may only resolve profile values and PULSE_PROFILE selection.',
609
+ loc: ref.loc
610
+ });
611
+ }
612
+ if (first === 'profile' && ref.name !== ENV_PROFILE_NAME) {
613
+ diagnostics.push({
614
+ code: 'PULSEWASM_CONFIG_PROFILE_ENV_NAME',
615
+ message: `Profile selection must use ${ENV_PROFILE_NAME}, not ${ref.name}.`,
616
+ hint: `Use profile: env("${ENV_PROFILE_NAME}", "local") or pass --profile explicitly.`,
617
+ loc: ref.loc
618
+ });
619
+ }
620
+ if (isSecretPath(ref.path, ref.name) && ref.hasFallback) {
621
+ diagnostics.push({
622
+ code: 'PULSEWASM_CONFIG_SECRET_FALLBACK',
623
+ message: `Secret env lookup ${ref.name} at ${ref.path.join('.')} has a literal fallback.`,
624
+ hint: 'Do not place secret fallback values in source. Resolve secrets through env at runtime/build boundaries only.',
625
+ loc: ref.loc
626
+ });
627
+ }
628
+ if (activeProfileName && pathStartsWithProfile(ref.path, activeProfileName) && !isSecretPath(ref.path, ref.name) && !ref.resolved) {
629
+ diagnostics.push({
630
+ code: 'PULSEWASM_CONFIG_UNRESOLVED_ENV',
631
+ message: `Active profile env lookup ${ref.name} at ${ref.path.join('.')} is unresolved.`,
632
+ hint: 'Provide the env value, add a non-secret literal fallback, or move it into a secret-bearing config key if it is intentionally runtime-resolved.',
633
+ loc: ref.loc
634
+ });
635
+ }
636
+ }
637
+ }
638
+
639
+ function validateNoRouteSemanticKeysInProfiles(rootNode, diagnostics) {
640
+ const profiles = valueGet(rootNode, 'profiles');
641
+ if (!profiles || profiles.kind !== 'object') return;
642
+
643
+ function walk(node, pathSegments) {
644
+ if (!node || node.kind !== 'object') return;
645
+ for (const [key, value] of Object.entries(node.props)) {
646
+ if (SEMANTIC_ENV_KEYS.has(key)) {
647
+ diagnostics.push({
648
+ code: 'PULSEWASM_CONFIG_ROUTE_SEMANTIC_KEY',
649
+ message: `Profile config key ${[...pathSegments, key].join('.')} is reserved for route/compiler semantics.`,
650
+ hint: 'Profiles may configure runtime values, but they may not define routes, routers, handlers, mounts, or middleware.',
651
+ loc: value.loc || node.loc
652
+ });
653
+ }
654
+ walk(value, [...pathSegments, key]);
655
+ }
656
+ }
657
+ walk(profiles, ['profiles']);
658
+ }
659
+
660
+ function validateConfigV2RootBoundary(rootNode, diagnostics) {
661
+ const reservedRootKeys = new Set(['target', 'runtime', 'json', 'backends', 'timeouts', 'assets', 'broadcaster', 'hostCapabilities', 'deployment']);
662
+ if (!rootNode || rootNode.kind !== 'object') return;
663
+ for (const key of Object.keys(rootNode.props || {})) {
664
+ if (!reservedRootKeys.has(key)) continue;
665
+ const value = rootNode.props[key];
666
+ diagnostics.push({
667
+ code: 'PULSEWASM_CONFIG_PROFILE_RUNTIME_KEY_REQUIRED',
668
+ message: `Root config key ${key} belongs under profiles.<name>.runtime in PulseWasm config v2.`,
669
+ hint: 'Root config defines topology only: entry, rootRouter, profiles. Runtime/deployment/payload/capability settings belong to the selected profile runtime object.',
670
+ loc: value.loc || rootNode.loc
671
+ });
672
+ }
673
+ }
674
+
675
+ function selectProfileName(rootNode, rootPlain, options, diagnostics) {
676
+ const profilesPlain = rootPlain && typeof rootPlain === 'object' ? rootPlain.profiles : undefined;
677
+ if (!profilesPlain || typeof profilesPlain !== 'object' || Array.isArray(profilesPlain)) {
678
+ return { selectedProfile: null, profileSource: 'none' };
679
+ }
680
+
681
+ const keys = Object.keys(profilesPlain);
682
+ let selectedProfile;
683
+ let profileSource;
684
+ if (options.profile) {
685
+ selectedProfile = options.profile;
686
+ profileSource = 'cli';
687
+ } else if (options.envValues && options.envValues[ENV_PROFILE_NAME] !== undefined && options.envValues[ENV_PROFILE_NAME] !== null && String(options.envValues[ENV_PROFILE_NAME]) !== '') {
688
+ selectedProfile = String(options.envValues[ENV_PROFILE_NAME]);
689
+ profileSource = 'env';
690
+ } else if (typeof rootPlain.profile === 'string' && rootPlain.profile !== '') {
691
+ selectedProfile = rootPlain.profile;
692
+ profileSource = 'config';
693
+ } else if (hasOwn(profilesPlain, 'local')) {
694
+ selectedProfile = 'local';
695
+ profileSource = 'default-local';
696
+ } else {
697
+ selectedProfile = keys[0] || null;
698
+ profileSource = 'default-first';
699
+ }
700
+
701
+ if (!selectedProfile || !hasOwn(profilesPlain, selectedProfile)) {
702
+ diagnostics.push({
703
+ code: 'PULSEWASM_CONFIG_PROFILE_NOT_FOUND',
704
+ message: `Pulse config profile ${selectedProfile || '<empty>'} is not defined.`,
705
+ hint: `Available profiles: ${keys.join(', ') || '<none>'}`,
706
+ loc: valueGet(rootNode, 'profiles')?.loc || rootNode.loc
707
+ });
708
+ }
709
+ return { selectedProfile, profileSource };
710
+ }
711
+
712
+ function resolveConfigSourceFile(sourceFile, options = {}) {
713
+ const cwd = options.cwd || process.cwd();
714
+ const diagnostics = [];
715
+ const envValues = normalizeEnvSource(options.env || options.envSource);
716
+ const envDefinitions = [];
717
+
718
+ const call = findDefineConfigCall(sourceFile, diagnostics);
719
+ let rootNode = valueObject({}, sourceFile, sourceFile);
720
+ if (call) {
721
+ rootNode = evaluateDefineConfigCall(sourceFile, call, {
722
+ diagnostics,
723
+ envValues,
724
+ envDefinitions,
725
+ envParamName: undefined
726
+ });
727
+ }
728
+
729
+ const allEnvRefs = collectEnvRefs(rootNode);
730
+ const rootPlain = valueToPlain(rootNode) || {};
731
+ const { selectedProfile, profileSource } = selectProfileName(rootNode, rootPlain, { ...options, envValues }, diagnostics);
732
+ validateNoRouteSemanticKeysInProfiles(rootNode, diagnostics);
733
+ validateConfigV2RootBoundary(rootNode, diagnostics);
734
+ validateEnvRefBoundaries(sourceFile, allEnvRefs, selectedProfile, diagnostics);
735
+
736
+ if (diagnostics.length > 0) {
737
+ throw new ExtractionError(diagnostics);
738
+ }
739
+
740
+ const profilePath = selectedProfile ? ['profiles', selectedProfile] : [];
741
+ const activeProfileNode = selectedProfile ? valueGet(valueGet(rootNode, 'profiles'), selectedProfile) : rootNode;
742
+ const activeProfilePlain = plainObject(valueToPlain(activeProfileNode));
743
+ const activeProfileRefs = selectedProfile
744
+ ? allEnvRefs.filter((ref) => pathIsUnder(ref.path, profilePath))
745
+ : allEnvRefs;
746
+ const entry = typeof rootPlain.entry === 'string' ? rootPlain.entry : undefined;
747
+ const rootRouter = typeof rootPlain.rootRouter === 'string' ? rootPlain.rootRouter : undefined;
748
+ const runtime = plainObject(activeProfilePlain.runtime);
749
+ const payload = plainObject(runtime.payload);
750
+ const capabilities = plainObject(runtime.capabilities);
751
+ const json = plainObject(payload.json);
752
+ const body = plainObject(payload.body);
753
+ const backends = plainObject(capabilities.backends || runtime.backends);
754
+ const target = runtime.engine === 'js' ? 'js' : 'wasm';
755
+ const emit = Array.isArray(activeProfilePlain.emit) ? activeProfilePlain.emit : undefined;
756
+ const outDir = typeof activeProfilePlain.outDir === 'string' ? activeProfilePlain.outDir : undefined;
757
+
758
+ const artifact = {
759
+ version: 'pulsewasm.resolved-config.v2',
760
+ generatedBy: PACKAGE_VERSION,
761
+ source: stableFileName(sourceFile.fileName, cwd),
762
+ profile: selectedProfile,
763
+ entry,
764
+ rootRouter,
765
+ runtime: valueToArtifact(valueGet(activeProfileNode, 'runtime'), profilePath.concat('runtime')) || {},
766
+ activeProfile: valueToArtifact(activeProfileNode, profilePath),
767
+ config: valueToArtifact(rootNode, []),
768
+ env: {
769
+ definitions: envDefinitions,
770
+ references: serializeEnvRefs(allEnvRefs),
771
+ activeProfileReferences: serializeEnvRefs(activeProfileRefs)
772
+ },
773
+ secretPolicy: {
774
+ mode: 'redact-secret-like-env-values',
775
+ redacted: serializeEnvRefs(allEnvRefs).filter((ref) => ref.secret).map((ref) => ({
776
+ name: ref.name,
777
+ path: ref.path,
778
+ resolved: ref.resolved
779
+ }))
780
+ }
781
+ };
782
+
783
+ return {
784
+ sourceFile,
785
+ artifact,
786
+ entry,
787
+ rootRouter,
788
+ target,
789
+ runtime,
790
+ json,
791
+ body,
792
+ backends,
793
+ outDir,
794
+ emit,
795
+ selectedProfile,
796
+ profileSource,
797
+ diagnostics: []
798
+ };
799
+ }
800
+
801
+ function resolveConfigFromFile(filePath, options = {}) {
802
+ const sourceFile = readSourceFile(filePath);
803
+ return resolveConfigSourceFile(sourceFile, options);
804
+ }
805
+
806
+ module.exports = {
807
+ ENV_PROFILE_NAME,
808
+ resolveConfigFromFile,
809
+ resolveConfigSourceFile,
810
+ valueToPlain,
811
+ valueToArtifact,
812
+ isSecretPath
813
+ };