@pulse-compute/provider-fastly 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 (69) hide show
  1. package/README.md +53 -1
  2. package/dist/config-schema.json +77 -0
  3. package/dist/index.d.ts +61 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +59 -0
  6. package/dist/index.js.map +1 -0
  7. package/package.json +88 -5
  8. package/src/build/canonical-target.js +436 -0
  9. package/src/build/entities-native.js +564 -0
  10. package/src/build/fixed-memory-runtime.as.ts +148 -0
  11. package/src/build/kv-native.as.ts +229 -0
  12. package/src/build/kv-native.js +20 -0
  13. package/src/build/native-http-effects.js +871 -0
  14. package/src/build/native-http-shell.js +703 -0
  15. package/src/build/native-platform-capabilities.js +2196 -0
  16. package/src/build/s3-native.as.ts +215 -0
  17. package/src/build/s3-native.js +30 -0
  18. package/src/compiler/assets-package-out-parity-proof.js +338 -0
  19. package/src/compiler/assets-provider-proof.js +499 -0
  20. package/src/compiler/config-provider-resolution.js +573 -0
  21. package/src/compiler/fastly-adapter.js +564 -0
  22. package/src/compiler/fastly-command-entry.js +390 -0
  23. package/src/compiler/fastly-config-secret-serve.js +323 -0
  24. package/src/compiler/fastly-hostcall-binding.js +598 -0
  25. package/src/compiler/fastly-readiness.js +477 -0
  26. package/src/compiler/index.js +40 -0
  27. package/src/compiler/lifecycle-parity-contract.js +70 -0
  28. package/src/compiler/lifecycle-parity-proof.js +260 -0
  29. package/src/compiler/route-handler-effect-proof.js +255 -0
  30. package/src/compiler/runtime-provider-kv.js +237 -0
  31. package/src/compiler/runtime-provider-shape.js +109 -0
  32. package/src/config-api.js +63 -0
  33. package/src/config-schema.d.ts +13 -0
  34. package/src/config-schema.js +27 -0
  35. package/src/config-schema.json +77 -0
  36. package/src/index.js +3 -0
  37. package/src/index.ts +118 -0
  38. package/src/javascript/assets-adapter.js +122 -0
  39. package/src/javascript/bindings-adapter.js +237 -0
  40. package/src/javascript/errors.js +26 -0
  41. package/src/javascript/fetch-adapter.js +93 -0
  42. package/src/javascript/grip-broadcast.js +261 -0
  43. package/src/javascript/jwt-verifier.js +137 -0
  44. package/src/javascript/lifecycle.js +55 -0
  45. package/src/javascript/local-loader.js +138 -0
  46. package/src/javascript/logging.js +26 -0
  47. package/src/javascript/package-effects.js +68 -0
  48. package/src/javascript/runtime-host.js +108 -0
  49. package/src/javascript/sha256.js +128 -0
  50. package/src/javascript/source-package.js +776 -0
  51. package/src/javascript/support.js +142 -0
  52. package/src/javascript/target-support-policy.js +264 -0
  53. package/src/javascript/target.js +73 -0
  54. package/src/javascript/test-runtime.js +407 -0
  55. package/src/provider-contract.js +79 -0
  56. package/src/runtime/assets-provider.js +464 -0
  57. package/src/runtime/canonical-api-runtime.js +601 -0
  58. package/src/runtime/index.js +28 -0
  59. package/src/runtime/provider-primitives-contract.js +272 -0
  60. package/src/runtime/provider-primitives.js +82 -0
  61. package/src/runtime/route-handler-effects.js +507 -0
  62. package/src/testing/conditional-kv-host.js +109 -0
  63. package/src/testing/fastly-cli.js +538 -0
  64. package/src/testing/native-http-effects-host.js +390 -0
  65. package/src/testing/native-http-shell-host.js +190 -0
  66. package/src/testing/native-platform-capabilities-host.js +676 -0
  67. package/src/toolchain/config.js +131 -0
  68. package/src/toolchain/index.js +345 -0
  69. package/src/toolchain/s3.js +26 -0
@@ -0,0 +1,776 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const { reportingDescriptor } = require('@pulse-compute/wasm-contracts/logging');
7
+ const { FASTLY_JAVASCRIPT_TARGET_DESCRIPTOR } = require('./target.js');
8
+
9
+ const FASTLY_JAVASCRIPT_SOURCE_PACKAGE_VERSION = 'pulse.fastly-javascript-source-package.v1';
10
+ const FASTLY_JAVASCRIPT_SOURCE_PACKAGE_MANIFEST = 'pulse-fastly-javascript-source-package.json';
11
+ const FASTLY_JAVASCRIPT_DEPLOYMENT_MANIFEST = 'pulse-fastly-javascript-deployment.json';
12
+ const FASTLY_JAVASCRIPT_DEPLOYMENT_CANDIDATE_VERSION = 'pulse.fastly-javascript-deployment-candidate.v1';
13
+ const FASTLY_JAVASCRIPT_DEPLOYMENT_CANDIDATE = 'pulse-fastly-javascript-candidate.json';
14
+ const FASTLY_JS_COMPUTE_VERSION = '3.43.1';
15
+ const ESBUILD_VERSION = '0.28.1';
16
+
17
+ class PulseFastlyJavascriptSourcePackageError extends Error {
18
+ constructor(code, message, detail = {}) {
19
+ super(message);
20
+ this.name = 'PulseFastlyJavascriptSourcePackageError';
21
+ this.code = code;
22
+ this.detail = Object.freeze({ ...detail });
23
+ }
24
+ }
25
+
26
+ function slash(value) {
27
+ return String(value).replace(/\\/g, '/');
28
+ }
29
+
30
+ function stableJson(value) {
31
+ return `${JSON.stringify(value, null, 2)}\n`;
32
+ }
33
+
34
+ function sha256(value) {
35
+ return crypto.createHash('sha256').update(value).digest('hex');
36
+ }
37
+
38
+ function assertInside(root, candidate, label) {
39
+ const relative = path.relative(path.resolve(root), path.resolve(candidate));
40
+ if (relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))) return;
41
+ throw new PulseFastlyJavascriptSourcePackageError(
42
+ 'PULSE_FASTLY_JAVASCRIPT_PACKAGE_PATH_UNSAFE',
43
+ `${label} escapes the Pulse project root.`,
44
+ { root: path.resolve(root), candidate: path.resolve(candidate) }
45
+ );
46
+ }
47
+
48
+ function writeFile(root, relativeFile, content) {
49
+ const file = path.join(root, relativeFile);
50
+ assertInside(root, file, `Output file ${relativeFile}`);
51
+ fs.mkdirSync(path.dirname(file), { recursive: true });
52
+ fs.writeFileSync(file, content);
53
+ const bytes = fs.readFileSync(file);
54
+ return Object.freeze({
55
+ file: slash(relativeFile),
56
+ bytes: bytes.byteLength,
57
+ sha256: sha256(bytes)
58
+ });
59
+ }
60
+
61
+ function packageName(input) {
62
+ const normalized = String(input || 'pulse-app')
63
+ .toLowerCase()
64
+ .replace(/^@/, '')
65
+ .replace(/[^a-z0-9._-]+/g, '-')
66
+ .replace(/^-+|-+$/g, '') || 'pulse-app';
67
+ return `pulse-fastly-${normalized}`;
68
+ }
69
+
70
+ function tomlString(value) {
71
+ return JSON.stringify(String(value));
72
+ }
73
+
74
+ function fastlyToml(providerConfig = {}) {
75
+ const build = providerConfig.build || {};
76
+ const authors = Array.isArray(build.authors) ? build.authors.map(String) : [];
77
+ return [
78
+ 'manifest_version = 3',
79
+ `name = ${tomlString(build.name || 'pulse-app')}`,
80
+ `description = ${tomlString(build.description || 'Pulse application')}`,
81
+ `authors = [${authors.map(tomlString).join(', ')}]`,
82
+ 'language = "javascript"',
83
+ '',
84
+ '[scripts]',
85
+ 'build = "npm run build"',
86
+ ''
87
+ ].join('\n');
88
+ }
89
+
90
+ function downstreamEsbuildConfig() {
91
+ return [
92
+ "import { build } from 'esbuild';",
93
+ '',
94
+ 'await build({',
95
+ " entryPoints: ['src/index.js'],",
96
+ " outfile: 'dist/index.js',",
97
+ ' bundle: true,',
98
+ " format: 'esm',",
99
+ " platform: 'neutral',",
100
+ " target: ['es2022'],",
101
+ " conditions: ['fastly', 'module', 'import', 'default'],",
102
+ " external: ['fastly:*'],",
103
+ " legalComments: 'none',",
104
+ ' sourcemap: false,',
105
+ " logLevel: 'info'",
106
+ '});',
107
+ ''
108
+ ].join('\n');
109
+ }
110
+
111
+ function abortControllerCompatibilitySource() {
112
+ return [
113
+ "if (typeof globalThis.AbortController !== 'function') {",
114
+ ' class PulseFastlyAbortSignal {',
115
+ ' constructor() {',
116
+ ' this.aborted = false;',
117
+ ' this.reason = undefined;',
118
+ ' this.onabort = null;',
119
+ ' this.listeners = [];',
120
+ ' }',
121
+ ' addEventListener(type, listener, options = undefined) {',
122
+ " if (type !== 'abort' || (typeof listener !== 'function' && !(listener && typeof listener.handleEvent === 'function'))) return;",
123
+ ' if (this.listeners.some((entry) => entry.listener === listener)) return;',
124
+ ' this.listeners.push({ listener, once: Boolean(options && options.once) });',
125
+ ' }',
126
+ ' removeEventListener(type, listener) {',
127
+ " if (type !== 'abort') return;",
128
+ ' this.listeners = this.listeners.filter((entry) => entry.listener !== listener);',
129
+ ' }',
130
+ ' dispatchAbort(reason) {',
131
+ ' if (this.aborted) return;',
132
+ ' this.aborted = true;',
133
+ ' this.reason = reason;',
134
+ " const event = Object.freeze({ type: 'abort', target: this, currentTarget: this });",
135
+ ' const snapshot = [...this.listeners];',
136
+ ' for (const entry of snapshot) {',
137
+ ' if (entry.once) this.removeEventListener(\'abort\', entry.listener);',
138
+ ' if (typeof entry.listener === \'function\') entry.listener.call(this, event);',
139
+ ' else entry.listener.handleEvent(event);',
140
+ ' }',
141
+ " if (typeof this.onabort === 'function') this.onabort.call(this, event);",
142
+ ' }',
143
+ ' }',
144
+ ' class PulseFastlyAbortController {',
145
+ ' constructor() { this.signal = new PulseFastlyAbortSignal(); }',
146
+ " abort(reason = new Error('This operation was aborted')) { this.signal.dispatchAbort(reason); }",
147
+ ' }',
148
+ " Object.defineProperty(globalThis, 'AbortController', { configurable: true, writable: true, value: PulseFastlyAbortController });",
149
+ '}',
150
+ ].join('\n');
151
+ }
152
+
153
+ function bootstrapSource(deployment, runtimeConfig) {
154
+ return [
155
+ "import { enforceExplicitBackends } from 'fastly:backend';",
156
+ "import { ConfigStore } from 'fastly:config-store';",
157
+ "import { KVStore } from 'fastly:kv-store';",
158
+ "import { SecretStore } from 'fastly:secret-store';",
159
+ "import application, { createFastlyJavascriptHandler, schemaCodecs } from './application.js';",
160
+ '',
161
+ abortControllerCompatibilitySource(),
162
+ '',
163
+ `export const pulseDeployment = Object.freeze(${JSON.stringify(deployment)});`,
164
+ `export const pulseRuntimeConfig = Object.freeze(${JSON.stringify(runtimeConfig)});`,
165
+ 'if (pulseRuntimeConfig.bindings.dynamicBackends !== true) enforceExplicitBackends();',
166
+ 'export const handler = createFastlyJavascriptHandler(application, {',
167
+ ' ...pulseRuntimeConfig,',
168
+ ' apis: { ConfigStore, SecretStore, KVStore },',
169
+ ' fetchImplementation: globalThis.fetch.bind(globalThis),',
170
+ ' console: globalThis.console,',
171
+ ' schemaCodecs',
172
+ '});',
173
+ "addEventListener('fetch', (event) => event.respondWith(handler(event)));",
174
+ 'export { application, schemaCodecs };',
175
+ 'export default handler;',
176
+ ''
177
+ ].join('\n');
178
+ }
179
+
180
+ function normalizedInputPath(input, projectRoot) {
181
+ const absolute = path.isAbsolute(input) ? input : path.resolve(projectRoot, input);
182
+ const relative = path.relative(projectRoot, absolute);
183
+ return slash(relative || '.');
184
+ }
185
+
186
+ function workspacePatterns(manifest) {
187
+ if (!manifest) return [];
188
+ if (Array.isArray(manifest.workspaces)) return manifest.workspaces;
189
+ if (manifest.workspaces && Array.isArray(manifest.workspaces.packages)) return manifest.workspaces.packages;
190
+ return [];
191
+ }
192
+
193
+ function expandWorkspacePattern(root, pattern) {
194
+ const segments = slash(pattern).split('/').filter(Boolean);
195
+ let candidates = [root];
196
+ for (const segment of segments) {
197
+ const next = [];
198
+ for (const candidate of candidates) {
199
+ if (segment === '*') {
200
+ if (!fs.existsSync(candidate) || !fs.statSync(candidate).isDirectory()) continue;
201
+ for (const entry of fs.readdirSync(candidate, { withFileTypes: true })) {
202
+ if (entry.isDirectory() && entry.name !== 'node_modules' && !entry.name.startsWith('.')) {
203
+ next.push(path.join(candidate, entry.name));
204
+ }
205
+ }
206
+ } else if (!segment.includes('*')) {
207
+ next.push(path.join(candidate, segment));
208
+ }
209
+ }
210
+ candidates = next;
211
+ }
212
+ return candidates;
213
+ }
214
+
215
+ function workspacePackages(projectRoot) {
216
+ let current = path.resolve(projectRoot);
217
+ const filesystemRoot = path.parse(current).root;
218
+ while (true) {
219
+ const manifestFile = path.join(current, 'package.json');
220
+ if (fs.existsSync(manifestFile)) {
221
+ let manifest;
222
+ try { manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8')); }
223
+ catch (_) { manifest = null; }
224
+ const patterns = workspacePatterns(manifest);
225
+ if (patterns.length > 0) {
226
+ const packages = new Map();
227
+ for (const pattern of patterns) {
228
+ for (const root of expandWorkspacePattern(current, pattern)) {
229
+ const packageFile = path.join(root, 'package.json');
230
+ if (!fs.existsSync(packageFile)) continue;
231
+ try {
232
+ const packageManifest = JSON.parse(fs.readFileSync(packageFile, 'utf8'));
233
+ if (packageManifest && typeof packageManifest.name === 'string') {
234
+ packages.set(packageManifest.name, Object.freeze({ root, manifest: packageManifest }));
235
+ }
236
+ } catch (_) {
237
+ // Invalid package manifests are owned by workspace validation.
238
+ }
239
+ }
240
+ }
241
+ return packages;
242
+ }
243
+ }
244
+ if (current === filesystemRoot) break;
245
+ current = path.dirname(current);
246
+ }
247
+ return new Map();
248
+ }
249
+
250
+ function conditionalExportTarget(value) {
251
+ if (typeof value === 'string') return value;
252
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
253
+ for (const condition of ['fastly', 'import', 'module', 'require', 'default']) {
254
+ const target = conditionalExportTarget(value[condition]);
255
+ if (target) return target;
256
+ }
257
+ return null;
258
+ }
259
+
260
+ function workspacePackageAliases(projectRoot) {
261
+ const packages = workspacePackages(projectRoot);
262
+ const aliases = {};
263
+ for (const [name, entry] of packages) {
264
+ const exports = entry.manifest.exports;
265
+ if (exports && typeof exports === 'object' && !Array.isArray(exports)) {
266
+ for (const [subpath, value] of Object.entries(exports)) {
267
+ if (subpath !== '.' && !subpath.startsWith('./')) continue;
268
+ const target = conditionalExportTarget(value);
269
+ if (!target) continue;
270
+ const specifier = subpath === '.' ? name : `${name}/${subpath.slice(2)}`;
271
+ aliases[specifier] = path.resolve(entry.root, target);
272
+ }
273
+ }
274
+ if (!aliases[name]) {
275
+ const target = entry.manifest.module || entry.manifest.main;
276
+ if (target) aliases[name] = path.resolve(entry.root, target);
277
+ }
278
+ }
279
+ return Object.freeze(aliases);
280
+ }
281
+
282
+ function relativeImport(projectRoot, file) {
283
+ const relative = slash(path.relative(projectRoot, file));
284
+ return relative.startsWith('.') ? relative : `./${relative}`;
285
+ }
286
+
287
+ function bundleFastlyJavascriptApplication(plan, projectRoot, schemaBundle, options = {}) {
288
+ const format = options.format === 'cjs' ? 'cjs' : 'esm';
289
+ const external = [
290
+ 'fastly:*',
291
+ ...(options.externalRuntime === true
292
+ ? ['@pulse-compute/runtime', '@pulse-compute/runtime/*']
293
+ : [])
294
+ ];
295
+ let esbuild;
296
+ try { esbuild = require('esbuild'); }
297
+ catch (error) {
298
+ throw new PulseFastlyJavascriptSourcePackageError(
299
+ 'PULSE_FASTLY_JAVASCRIPT_ESBUILD_MISSING',
300
+ `Fastly JavaScript source packaging requires exact esbuild ${ESBUILD_VERSION}.`,
301
+ { package: 'esbuild', version: ESBUILD_VERSION, cause: error && error.message }
302
+ );
303
+ }
304
+ if (esbuild.version !== ESBUILD_VERSION) {
305
+ throw new PulseFastlyJavascriptSourcePackageError(
306
+ 'PULSE_FASTLY_JAVASCRIPT_ESBUILD_VERSION_MISMATCH',
307
+ `Fastly JavaScript source packaging requires exact esbuild ${ESBUILD_VERSION}; received ${String(esbuild.version)}.`,
308
+ { expected: ESBUILD_VERSION, actual: esbuild.version }
309
+ );
310
+ }
311
+ const entryFile = path.resolve(projectRoot, plan.workspace.entry);
312
+ assertInside(projectRoot, entryFile, 'Fastly JavaScript application entry');
313
+ if (!fs.existsSync(entryFile) || !fs.statSync(entryFile).isFile()) {
314
+ throw new PulseFastlyJavascriptSourcePackageError(
315
+ 'PULSE_FASTLY_JAVASCRIPT_ENTRY_MISSING',
316
+ `Fastly JavaScript application entry is missing: ${plan.workspace.entry}.`,
317
+ { entryFile, entry: plan.workspace.entry }
318
+ );
319
+ }
320
+
321
+ const runtimeFile = path.join(__dirname, 'lifecycle.js');
322
+ const entrySource = [
323
+ `import application from ${JSON.stringify(relativeImport(projectRoot, entryFile))};`,
324
+ `import fastlyRuntime from ${JSON.stringify(relativeImport(projectRoot, runtimeFile))};`,
325
+ ...(schemaBundle && schemaBundle.active
326
+ ? [
327
+ 'const schemaCodecs = (() => {',
328
+ ' const module = { exports: {} };',
329
+ schemaBundle.moduleSource,
330
+ ' return module.exports;',
331
+ '})();'
332
+ ]
333
+ : ['const schemaCodecs = null;']),
334
+ 'const { createFastlyJavascriptHandler } = fastlyRuntime;',
335
+ 'export { application, createFastlyJavascriptHandler, schemaCodecs };',
336
+ 'export default application;',
337
+ ''
338
+ ].join('\n');
339
+ const workspaceAliases = workspacePackageAliases(projectRoot);
340
+ const aliases = {
341
+ ...Object.fromEntries(Object.entries(workspaceAliases).filter(([specifier]) =>
342
+ options.externalRuntime !== true
343
+ || (specifier !== '@pulse-compute/runtime' && !specifier.startsWith('@pulse-compute/runtime/'))
344
+ )),
345
+ ...(schemaBundle && schemaBundle.active
346
+ ? { 'node:crypto': path.join(__dirname, 'sha256.js') }
347
+ : {})
348
+ };
349
+
350
+ let result;
351
+ try {
352
+ result = esbuild.buildSync({
353
+ absWorkingDir: projectRoot,
354
+ stdin: {
355
+ contents: entrySource,
356
+ resolveDir: projectRoot,
357
+ sourcefile: 'pulse-fastly-application-entry.js',
358
+ loader: 'js'
359
+ },
360
+ outfile: 'application.js',
361
+ bundle: true,
362
+ write: false,
363
+ metafile: true,
364
+ format,
365
+ platform: 'neutral',
366
+ target: ['es2022'],
367
+ conditions: ['fastly', 'module', 'import', 'default'],
368
+ mainFields: ['module', 'main'],
369
+ packages: 'bundle',
370
+ external,
371
+ legalComments: 'none',
372
+ sourcemap: false,
373
+ charset: 'utf8',
374
+ treeShaking: true,
375
+ logLevel: 'silent',
376
+ alias: aliases
377
+ });
378
+ } catch (error) {
379
+ throw new PulseFastlyJavascriptSourcePackageError(
380
+ 'PULSE_FASTLY_JAVASCRIPT_BUNDLE_FAILED',
381
+ 'Fastly JavaScript source packaging could not bundle the application closure.',
382
+ {
383
+ entry: plan.workspace.entry,
384
+ errors: Array.isArray(error && error.errors)
385
+ ? error.errors.map((entry) => Object.freeze({ text: entry.text, location: entry.location }))
386
+ : [],
387
+ cause: error && error.message
388
+ }
389
+ );
390
+ }
391
+ const output = result.outputFiles && result.outputFiles.find((entry) => entry.path.endsWith('application.js'));
392
+ if (!output) {
393
+ throw new PulseFastlyJavascriptSourcePackageError(
394
+ 'PULSE_FASTLY_JAVASCRIPT_BUNDLE_OUTPUT_MISSING',
395
+ 'Fastly JavaScript bundling did not produce application.js.'
396
+ );
397
+ }
398
+ const text = output.text;
399
+ const nodeImports = [...text.matchAll(/(?:from\s+|import\s*\()\s*["'](node:[^"']+|(?:fs|path|crypto|module|stream|buffer|util|events))["']/g)]
400
+ .map((match) => match[1]);
401
+ if (nodeImports.length > 0) {
402
+ throw new PulseFastlyJavascriptSourcePackageError(
403
+ 'PULSE_FASTLY_JAVASCRIPT_NODE_BUILTIN',
404
+ 'Fastly JavaScript source closure contains a Node-only builtin import.',
405
+ { imports: Object.freeze([...new Set(nodeImports)].sort()) }
406
+ );
407
+ }
408
+ const inputs = Object.freeze(Object.entries(result.metafile.inputs || {})
409
+ .map(([file, metadata]) => Object.freeze({
410
+ file: normalizedInputPath(file, projectRoot),
411
+ bytes: Number(metadata.bytes || 0),
412
+ imports: Object.freeze((metadata.imports || []).map((entry) => Object.freeze({
413
+ path: slash(entry.path),
414
+ kind: entry.kind,
415
+ external: entry.external === true
416
+ })).sort((left, right) => left.path.localeCompare(right.path) || left.kind.localeCompare(right.kind)))
417
+ }))
418
+ .sort((left, right) => left.file.localeCompare(right.file)));
419
+ return Object.freeze({
420
+ format,
421
+ source: text,
422
+ sourceHash: sha256(Buffer.from(text)),
423
+ inputs
424
+ });
425
+ }
426
+
427
+ function assertDeploymentCandidateAgreement(input) {
428
+ const {
429
+ candidate,
430
+ candidateRecord,
431
+ deployment,
432
+ deploymentRecord,
433
+ manifest,
434
+ plan,
435
+ applicationRecord,
436
+ bootstrapRecord,
437
+ packageRecord,
438
+ fastlyRecord
439
+ } = input;
440
+ const mismatches = [];
441
+ const matches = (label, actual, expected) => {
442
+ if (actual !== expected) mismatches.push(Object.freeze({ label, actual, expected }));
443
+ };
444
+ matches('candidate.provider', candidate.provider, manifest.provider);
445
+ matches('candidate.target', candidate.target, manifest.target);
446
+ matches('candidate.targetId', candidate.targetId, manifest.targetId);
447
+ matches('candidate.identity.applicationPlanHash', candidate.identity.applicationPlanHash, plan.planHash);
448
+ matches('candidate.identity.graphHash', candidate.identity.graphHash, plan.graph.graphHash);
449
+ matches('candidate.identity.bundleSha256', candidate.identity.bundleSha256, applicationRecord.sha256);
450
+ matches('candidate.deployment.version', candidate.deployment.version, deployment.version);
451
+ matches('candidate.deployment.manifest', candidate.deployment.manifest, deploymentRecord.file);
452
+ matches('candidate.deployment.sha256', candidate.deployment.sha256, deploymentRecord.sha256);
453
+ matches('candidate.entrypoints.bootstrap', candidate.entrypoints.bootstrap, bootstrapRecord.file);
454
+ matches('candidate.entrypoints.bundledApplication', candidate.entrypoints.bundledApplication, applicationRecord.file);
455
+ matches('candidate.entrypoints.fastlyManifest', candidate.entrypoints.fastlyManifest, fastlyRecord.file);
456
+ matches('candidate.entrypoints.packageManifest', candidate.entrypoints.packageManifest, packageRecord.file);
457
+ matches('manifest.deploymentCandidate.version', manifest.deploymentCandidate.version, candidate.version);
458
+ matches('manifest.deploymentCandidate.status', manifest.deploymentCandidate.status, candidate.status);
459
+ matches('manifest.deploymentCandidate.file', manifest.deploymentCandidate.file, candidateRecord.file);
460
+ matches('manifest.deploymentCandidate.sha256', manifest.deploymentCandidate.sha256, candidateRecord.sha256);
461
+ matches('deployment.candidate.version', deployment.candidate.version, candidate.version);
462
+ matches('deployment.candidate.file', deployment.candidate.file, candidateRecord.file);
463
+ matches('deployment.candidate.status', deployment.candidate.status, candidate.status);
464
+ if (stableJson(manifest.deploymentCandidate.identity) !== stableJson(candidate.identity)) {
465
+ mismatches.push(Object.freeze({ label: 'manifest.deploymentCandidate.identity' }));
466
+ }
467
+ if (stableJson(manifest.deploymentCandidate.commands) !== stableJson(candidate.commands)) {
468
+ mismatches.push(Object.freeze({ label: 'manifest.deploymentCandidate.commands' }));
469
+ }
470
+ if (mismatches.length > 0) {
471
+ throw new PulseFastlyJavascriptSourcePackageError(
472
+ 'PULSE_FASTLY_JAVASCRIPT_DEPLOYMENT_CANDIDATE_MISMATCH',
473
+ 'Fastly JavaScript deployment candidate and source-package manifests disagree.',
474
+ { mismatches: Object.freeze(mismatches) }
475
+ );
476
+ }
477
+ }
478
+
479
+ function writeFastlyJavascriptSourcePackage(options = {}) {
480
+ const plan = options.plan;
481
+ if (!plan || !plan.graph || plan.provider !== 'fastly' || plan.target !== 'javascript' || plan.loadable !== true) {
482
+ throw new PulseFastlyJavascriptSourcePackageError(
483
+ 'PULSE_FASTLY_JAVASCRIPT_PACKAGE_PLAN_UNAVAILABLE',
484
+ 'Fastly JavaScript source packaging requires a loadable Fastly JavaScript application plan.',
485
+ {
486
+ provider: plan && plan.provider,
487
+ target: plan && plan.target,
488
+ blockers: plan && plan.blockers || []
489
+ }
490
+ );
491
+ }
492
+ const projectRoot = path.resolve(options.projectRoot || '.');
493
+ const outDir = path.resolve(options.outDir || path.join(projectRoot, 'dist'));
494
+ assertInside(projectRoot, outDir, 'Fastly JavaScript build output');
495
+ fs.mkdirSync(outDir, { recursive: true });
496
+
497
+ const reporting = reportingDescriptor(options.reporting);
498
+ const descriptor = Object.freeze({
499
+ ...FASTLY_JAVASCRIPT_TARGET_DESCRIPTOR,
500
+ resolvedReporting: reporting
501
+ });
502
+ const schemaBundle = options.schemaBundle;
503
+ const schemaActive = Boolean(schemaBundle && schemaBundle.active);
504
+ const bundle = bundleFastlyJavascriptApplication(plan, projectRoot, schemaBundle);
505
+ const providerConfig = options.providerConfig || {};
506
+ const providerBindings = providerConfig.bindings && typeof providerConfig.bindings === 'object'
507
+ ? providerConfig.bindings
508
+ : {};
509
+ const targetSupport = options.targetSupport || null;
510
+ const deployment = Object.freeze({
511
+ version: 'pulse.fastly-javascript-deployment.v1',
512
+ provider: 'fastly',
513
+ target: 'javascript',
514
+ targetId: descriptor.targetId,
515
+ runtimeClass: 'javascript',
516
+ automaticFallback: false,
517
+ nativeArtifact: false,
518
+ sourcePackage: true,
519
+ executionReady: true,
520
+ providerExecutionReady: true,
521
+ generalAvailable: Boolean(targetSupport?.availability?.generalAvailable),
522
+ deferredTo: Object.freeze([]),
523
+ reporting,
524
+ targetDescriptor: descriptor,
525
+ targetSupport,
526
+ candidate: Object.freeze({
527
+ version: FASTLY_JAVASCRIPT_DEPLOYMENT_CANDIDATE_VERSION,
528
+ file: FASTLY_JAVASCRIPT_DEPLOYMENT_CANDIDATE,
529
+ status: 'structurally-deployable',
530
+ providerRealityValidated: false,
531
+ deployed: false
532
+ }),
533
+ downstreamBuild: Object.freeze({
534
+ owner: '@fastly/js-compute',
535
+ classification: 'javascript-runtime-deployment',
536
+ input: 'dist/index.js',
537
+ output: 'bin/main.wasm',
538
+ pulseNativeLowering: false
539
+ })
540
+ });
541
+ const runtimeConfig = Object.freeze({
542
+ bindings: providerBindings,
543
+ reporting: reporting.name,
544
+ strict: options.strict === true,
545
+ maxStructuredBodyBytes: Number(options.maxStructuredBodyBytes || 65536)
546
+ });
547
+ const packageJson = Object.freeze({
548
+ name: packageName(providerConfig.build && providerConfig.build.name),
549
+ version: '0.0.0',
550
+ private: true,
551
+ type: 'module',
552
+ scripts: Object.freeze({
553
+ prebuild: 'node pulse-esbuild.config.js',
554
+ build: 'js-compute-runtime ./dist/index.js ./bin/main.wasm'
555
+ }),
556
+ dependencies: Object.freeze({
557
+ '@fastly/js-compute': FASTLY_JS_COMPUTE_VERSION
558
+ }),
559
+ devDependencies: Object.freeze({
560
+ esbuild: ESBUILD_VERSION
561
+ })
562
+ });
563
+
564
+ const records = [];
565
+ const record = (relativeFile, content) => {
566
+ const value = writeFile(outDir, relativeFile, content);
567
+ records.push(value);
568
+ return value;
569
+ };
570
+ const applicationRecord = record('src/application.js', bundle.source);
571
+ const bootstrapRecord = record('src/index.js', bootstrapSource(deployment, runtimeConfig));
572
+ const packageRecord = record('package.json', stableJson(packageJson));
573
+ const configRecord = record('pulse-esbuild.config.js', downstreamEsbuildConfig());
574
+ const fastlyRecord = record('fastly.toml', fastlyToml(providerConfig));
575
+ const planRecord = record('pulse-javascript-application-plan.json', stableJson(plan));
576
+ const deploymentRecord = record(FASTLY_JAVASCRIPT_DEPLOYMENT_MANIFEST, stableJson(deployment));
577
+ const schemaRegistryRecord = schemaActive
578
+ ? record('schema-json-registry.json', stableJson(schemaBundle.registry))
579
+ : undefined;
580
+ const schemaCodecsRecord = schemaActive
581
+ ? record('schema-json-codecs.cjs', schemaBundle.moduleSource)
582
+ : undefined;
583
+ const candidate = Object.freeze({
584
+ version: FASTLY_JAVASCRIPT_DEPLOYMENT_CANDIDATE_VERSION,
585
+ status: 'structurally-deployable',
586
+ provider: 'fastly',
587
+ target: 'javascript',
588
+ targetId: descriptor.targetId,
589
+ runtimeClass: 'javascript',
590
+ automaticFallback: false,
591
+ nativeArtifact: false,
592
+ providerRealityValidated: false,
593
+ deployed: false,
594
+ identity: Object.freeze({
595
+ applicationPlanHash: plan.planHash,
596
+ graphHash: plan.graph.graphHash,
597
+ bundleSha256: applicationRecord.sha256,
598
+ targetSupportEvidenceHash: targetSupport && targetSupport.evidenceHash || null,
599
+ schemaSourceHash: schemaActive ? schemaBundle.sourceHash : null
600
+ }),
601
+ sourcePackage: Object.freeze({
602
+ version: FASTLY_JAVASCRIPT_SOURCE_PACKAGE_VERSION,
603
+ manifest: FASTLY_JAVASCRIPT_SOURCE_PACKAGE_MANIFEST
604
+ }),
605
+ deployment: Object.freeze({
606
+ version: deployment.version,
607
+ manifest: deploymentRecord.file,
608
+ sha256: deploymentRecord.sha256
609
+ }),
610
+ entrypoints: Object.freeze({
611
+ bootstrap: bootstrapRecord.file,
612
+ bundledApplication: applicationRecord.file,
613
+ fastlyManifest: fastlyRecord.file,
614
+ packageManifest: packageRecord.file
615
+ }),
616
+ toolchain: Object.freeze({
617
+ '@fastly/js-compute': FASTLY_JS_COMPUTE_VERSION,
618
+ esbuild: ESBUILD_VERSION
619
+ }),
620
+ commands: Object.freeze({
621
+ install: 'npm install',
622
+ build: 'npm run build',
623
+ localReality: 'fastly compute serve'
624
+ }),
625
+ validation: Object.freeze({
626
+ packageClosure: 'bundled',
627
+ loader: 'generated',
628
+ manifestAgreement: 'verified-at-package-write',
629
+ providerEmulation: 'pulse test and pulse dev',
630
+ providerReality: 'not-executed',
631
+ deployment: 'not-executed'
632
+ }),
633
+ files: Object.freeze(records.map((entry) => Object.freeze({
634
+ file: entry.file,
635
+ bytes: entry.bytes,
636
+ sha256: entry.sha256
637
+ })))
638
+ });
639
+ const candidateRecord = record(FASTLY_JAVASCRIPT_DEPLOYMENT_CANDIDATE, stableJson(candidate));
640
+
641
+ const manifest = Object.freeze({
642
+ version: FASTLY_JAVASCRIPT_SOURCE_PACKAGE_VERSION,
643
+ status: 'packaged',
644
+ provider: 'fastly',
645
+ target: 'javascript',
646
+ targetId: descriptor.targetId,
647
+ runtimeClass: 'javascript',
648
+ automaticFallback: false,
649
+ nativeArtifact: false,
650
+ nativeEligibility: plan.nativeEligibility,
651
+ reporting,
652
+ targetDescriptor: descriptor,
653
+ targetSupport,
654
+ capabilityEnvelope: plan.capabilityEnvelope,
655
+ eligibility: Object.freeze({
656
+ applicationPlanLoadable: plan.loadable,
657
+ blockers: plan.blockers,
658
+ providerExecutionReady: true,
659
+ projectStatus: targetSupport && targetSupport.project && targetSupport.project.status || 'eligible',
660
+ restrictions: descriptor.capabilities.restrictions,
661
+ availability: 'evidence-controlled'
662
+ }),
663
+ project: Object.freeze({
664
+ root: '.',
665
+ entry: slash(plan.workspace.entry),
666
+ bundledEntry: applicationRecord.file,
667
+ bootstrap: bootstrapRecord.file
668
+ }),
669
+ plan: Object.freeze({
670
+ version: plan.version,
671
+ planHash: plan.planHash,
672
+ graphVersion: plan.graph.version,
673
+ graphHash: plan.graph.graphHash,
674
+ resolverHash: plan.graph.resolverHash
675
+ }),
676
+ closure: Object.freeze({
677
+ kind: 'esbuild-bundled-source-closure',
678
+ bundle: applicationRecord.file,
679
+ bundleSha256: applicationRecord.sha256,
680
+ inputs: bundle.inputs,
681
+ reachablePackages: plan.packages,
682
+ toolchain: Object.freeze({
683
+ '@fastly/js-compute': FASTLY_JS_COMPUTE_VERSION,
684
+ esbuild: ESBUILD_VERSION
685
+ }),
686
+ workspaceLinks: false
687
+ }),
688
+ package: Object.freeze({
689
+ manifest: packageRecord.file,
690
+ entry: bootstrapRecord.file,
691
+ applicationPlan: planRecord.file,
692
+ deployment: deploymentRecord.file,
693
+ candidate: candidateRecord.file,
694
+ fastlyToml: fastlyRecord.file,
695
+ buildConfig: configRecord.file,
696
+ schemaRegistry: schemaRegistryRecord && schemaRegistryRecord.file,
697
+ schemaCodecs: schemaCodecsRecord && schemaCodecsRecord.file,
698
+ dependencies: packageJson.dependencies,
699
+ devDependencies: packageJson.devDependencies
700
+ }),
701
+ schemas: Object.freeze({
702
+ active: schemaActive,
703
+ registry: schemaRegistryRecord && schemaRegistryRecord.file,
704
+ codecs: schemaCodecsRecord && schemaCodecsRecord.file,
705
+ ids: Object.freeze(schemaActive ? [...schemaBundle.schemaIds] : []),
706
+ responseCaseIds: Object.freeze(schemaActive ? [...schemaBundle.responseCaseIds] : []),
707
+ registryHash: schemaActive ? schemaBundle.registry.registryHash : null,
708
+ codecTableHash: schemaActive ? schemaBundle.codecTableHash : null,
709
+ sourceHash: schemaActive ? schemaBundle.sourceHash : null,
710
+ fullCodecRealization: schemaActive ? schemaBundle.fullCodecRealization === true : false
711
+ }),
712
+ deploymentCandidate: Object.freeze({
713
+ version: candidate.version,
714
+ status: candidate.status,
715
+ file: candidateRecord.file,
716
+ sha256: candidateRecord.sha256,
717
+ providerRealityValidated: false,
718
+ deployed: false,
719
+ identity: candidate.identity,
720
+ commands: candidate.commands
721
+ }),
722
+ downstreamBuild: deployment.downstreamBuild,
723
+ files: Object.freeze([...records]),
724
+ summary: Object.freeze({
725
+ bundledInputs: bundle.inputs.length,
726
+ bundledBytes: applicationRecord.bytes,
727
+ sourcePackageBytes: records.reduce((sum, entry) => sum + entry.bytes, 0),
728
+ wasmFiles: 0,
729
+ nativeFiles: 0
730
+ })
731
+ });
732
+ assertDeploymentCandidateAgreement({
733
+ candidate,
734
+ candidateRecord,
735
+ deployment,
736
+ deploymentRecord,
737
+ manifest,
738
+ plan,
739
+ applicationRecord,
740
+ bootstrapRecord,
741
+ packageRecord,
742
+ fastlyRecord
743
+ });
744
+ const manifestRecord = writeFile(outDir, FASTLY_JAVASCRIPT_SOURCE_PACKAGE_MANIFEST, stableJson(manifest));
745
+
746
+ return Object.freeze({
747
+ version: FASTLY_JAVASCRIPT_SOURCE_PACKAGE_VERSION,
748
+ outDir,
749
+ entryFile: path.join(outDir, bootstrapRecord.file),
750
+ bundledApplicationFile: path.join(outDir, applicationRecord.file),
751
+ packageFile: path.join(outDir, packageRecord.file),
752
+ applicationPlanFile: path.join(outDir, planRecord.file),
753
+ deploymentFile: path.join(outDir, deploymentRecord.file),
754
+ deploymentCandidateFile: path.join(outDir, candidateRecord.file),
755
+ buildConfigFile: path.join(outDir, configRecord.file),
756
+ fastlyTomlFile: path.join(outDir, fastlyRecord.file),
757
+ schemaRegistryFile: schemaRegistryRecord ? path.join(outDir, schemaRegistryRecord.file) : undefined,
758
+ schemaCodecsFile: schemaCodecsRecord ? path.join(outDir, schemaCodecsRecord.file) : undefined,
759
+ manifestFile: path.join(outDir, manifestRecord.file),
760
+ manifest,
761
+ files: Object.freeze([...records, manifestRecord])
762
+ });
763
+ }
764
+
765
+ module.exports = Object.freeze({
766
+ FASTLY_JAVASCRIPT_SOURCE_PACKAGE_VERSION,
767
+ FASTLY_JAVASCRIPT_SOURCE_PACKAGE_MANIFEST,
768
+ FASTLY_JAVASCRIPT_DEPLOYMENT_MANIFEST,
769
+ FASTLY_JAVASCRIPT_DEPLOYMENT_CANDIDATE_VERSION,
770
+ FASTLY_JAVASCRIPT_DEPLOYMENT_CANDIDATE,
771
+ FASTLY_JS_COMPUTE_VERSION,
772
+ ESBUILD_VERSION,
773
+ PulseFastlyJavascriptSourcePackageError,
774
+ bundleFastlyJavascriptApplication,
775
+ writeFastlyJavascriptSourcePackage
776
+ });