@astrale-os/adapter-cloudflare 0.5.0-beta.16 → 0.5.0-beta.161

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 (201) hide show
  1. package/README.md +93 -2
  2. package/dist/adapter.d.ts +14 -0
  3. package/dist/adapter.js +82 -0
  4. package/dist/build/artifact.d.ts +7 -0
  5. package/dist/build/artifact.js +170 -0
  6. package/dist/build/bundle/admit.d.ts +1 -0
  7. package/dist/build/bundle/admit.js +9 -0
  8. package/dist/build/closure.d.ts +2 -0
  9. package/dist/build/closure.js +22 -0
  10. package/dist/build/codegen/execution.d.ts +3 -0
  11. package/dist/build/codegen/execution.js +2 -0
  12. package/dist/build/codegen/merge.d.ts +0 -1
  13. package/dist/build/codegen/merge.js +0 -1
  14. package/dist/build/codegen/wrangler.d.ts +12 -3
  15. package/dist/build/codegen/wrangler.js +26 -18
  16. package/dist/build/configuration/addressing.d.ts +2 -0
  17. package/dist/build/configuration/addressing.js +17 -0
  18. package/dist/build/configuration/invocation-timeout.d.ts +2 -0
  19. package/dist/build/configuration/invocation-timeout.js +9 -0
  20. package/dist/build/configuration/request-limit.d.ts +2 -0
  21. package/dist/build/configuration/request-limit.js +9 -0
  22. package/dist/build/configuration/router.d.ts +7 -0
  23. package/dist/build/configuration/router.js +11 -0
  24. package/dist/build/configuration/worker-name.d.ts +2 -0
  25. package/dist/build/configuration/worker-name.js +18 -0
  26. package/dist/build/configuration.d.ts +5 -0
  27. package/dist/build/configuration.js +5 -0
  28. package/dist/build/direct.d.ts +4 -0
  29. package/dist/build/direct.js +90 -0
  30. package/dist/build/frontend/files.d.ts +19 -0
  31. package/dist/build/frontend/files.js +99 -0
  32. package/dist/build/frontend/readiness.d.ts +10 -0
  33. package/dist/build/frontend/readiness.js +21 -0
  34. package/dist/build/frontend/revision.d.ts +5 -0
  35. package/dist/build/frontend/revision.js +14 -0
  36. package/dist/build/frontend/vite.d.ts +14 -0
  37. package/dist/build/frontend/vite.js +42 -0
  38. package/dist/build/frontend/vite.process.d.ts +7 -0
  39. package/dist/build/frontend/vite.process.js +65 -0
  40. package/dist/build/frontend/vite.runner.d.ts +1 -0
  41. package/dist/build/frontend/vite.runner.js +155 -0
  42. package/dist/build/index.d.ts +10 -16
  43. package/dist/build/index.js +9 -16
  44. package/dist/build/material.d.ts +10 -0
  45. package/dist/build/material.js +25 -0
  46. package/dist/build/materialize.d.ts +4 -0
  47. package/dist/build/materialize.js +10 -0
  48. package/dist/build/metafile.d.ts +5 -0
  49. package/dist/build/metafile.js +14 -0
  50. package/dist/build/qualification.d.ts +5 -0
  51. package/dist/build/qualification.js +7 -0
  52. package/dist/build/roots.d.ts +2 -0
  53. package/dist/build/roots.js +34 -0
  54. package/dist/build/worker/artifact.d.ts +40 -0
  55. package/dist/build/worker/artifact.js +1 -0
  56. package/dist/build/worker/artifact.load.d.ts +3 -0
  57. package/dist/build/worker/artifact.load.js +308 -0
  58. package/dist/build/worker/artifact.manifest.d.ts +48 -0
  59. package/dist/build/worker/artifact.manifest.js +87 -0
  60. package/dist/build/worker/artifact.paths.d.ts +4 -0
  61. package/dist/build/worker/artifact.paths.js +25 -0
  62. package/dist/build/worker/codegen/entry.d.ts +11 -0
  63. package/dist/build/worker/codegen/entry.js +68 -0
  64. package/dist/build/worker/codegen/literal.d.ts +28 -0
  65. package/dist/build/worker/codegen/literal.js +136 -0
  66. package/dist/build/worker/codegen/service.d.ts +21 -0
  67. package/dist/build/worker/codegen/service.js +63 -0
  68. package/dist/build/worker/execution.prepare.d.ts +20 -0
  69. package/dist/build/worker/execution.prepare.js +134 -0
  70. package/dist/build/worker/index.d.ts +2 -0
  71. package/dist/build/worker/index.js +1 -0
  72. package/dist/build/worker/modules.d.ts +11 -0
  73. package/dist/build/worker/modules.js +26 -0
  74. package/dist/build/worker/runtime.d.ts +10 -0
  75. package/dist/build/worker/runtime.js +35 -0
  76. package/dist/build/worker/specifier.d.ts +3 -0
  77. package/dist/build/worker/specifier.js +10 -0
  78. package/dist/cloudflare.d.ts +1 -45
  79. package/dist/cloudflare.js +1 -202
  80. package/dist/deploy/deploy.d.ts +17 -0
  81. package/dist/deploy/deploy.js +229 -0
  82. package/dist/deploy/index.d.ts +1 -0
  83. package/dist/deploy/index.js +1 -0
  84. package/dist/deploy/output.d.ts +2 -0
  85. package/dist/deploy/output.js +10 -0
  86. package/dist/deploy/readiness.d.ts +5 -0
  87. package/dist/deploy/readiness.js +21 -0
  88. package/dist/identity/binding.d.ts +2 -0
  89. package/dist/identity/binding.js +2 -0
  90. package/dist/identity/index.d.ts +3 -0
  91. package/dist/identity/index.js +3 -0
  92. package/dist/identity/material.d.ts +7 -0
  93. package/dist/identity/material.js +42 -0
  94. package/dist/identity/secret.d.ts +3 -0
  95. package/dist/identity/secret.js +7 -0
  96. package/dist/identity/source.d.ts +26 -0
  97. package/dist/identity/source.js +127 -0
  98. package/dist/index.d.ts +10 -9
  99. package/dist/index.js +10 -9
  100. package/dist/params.d.ts +43 -36
  101. package/dist/params.js +0 -1
  102. package/dist/release/deployment.d.ts +44 -0
  103. package/dist/release/deployment.js +141 -0
  104. package/dist/release/index.d.ts +2 -0
  105. package/dist/release/index.js +2 -0
  106. package/dist/release/staged-worker-name.d.ts +3 -0
  107. package/dist/release/staged-worker-name.js +10 -0
  108. package/dist/worker/entry.d.ts +31 -0
  109. package/dist/worker/entry.js +105 -0
  110. package/dist/worker/fetch.d.ts +0 -1
  111. package/dist/worker/fetch.js +0 -1
  112. package/dist/worker/front.d.ts +49 -0
  113. package/dist/worker/front.js +75 -0
  114. package/dist/worker/frontend.d.ts +4 -0
  115. package/dist/worker/frontend.js +83 -0
  116. package/dist/worker/identity.d.ts +7 -0
  117. package/dist/worker/identity.js +23 -0
  118. package/dist/worker/index.d.ts +2 -61
  119. package/dist/worker/index.js +2 -221
  120. package/dist/wrangler/bundle.d.ts +10 -0
  121. package/dist/wrangler/bundle.js +28 -0
  122. package/dist/wrangler/deploy.d.ts +24 -0
  123. package/dist/wrangler/deploy.js +44 -0
  124. package/dist/wrangler/index.d.ts +5 -0
  125. package/dist/wrangler/index.js +5 -0
  126. package/dist/wrangler/process.d.ts +15 -0
  127. package/dist/wrangler/process.js +102 -0
  128. package/dist/wrangler/rollout.d.ts +27 -0
  129. package/dist/wrangler/rollout.js +75 -0
  130. package/dist/wrangler/secrets.d.ts +2 -0
  131. package/dist/wrangler/secrets.js +24 -0
  132. package/dist/wrangler/status.d.ts +18 -0
  133. package/dist/wrangler/status.js +75 -0
  134. package/dist/wrangler/triggers.d.ts +5 -0
  135. package/dist/wrangler/triggers.js +12 -0
  136. package/dist/wrangler/type-generation.d.ts +6 -0
  137. package/dist/wrangler/type-generation.js +19 -0
  138. package/dist/wrangler/versions.d.ts +13 -0
  139. package/dist/wrangler/versions.js +38 -0
  140. package/package.json +39 -19
  141. package/template/astrale.config.ts +30 -31
  142. package/dist/build/codegen/identity.d.ts +0 -10
  143. package/dist/build/codegen/identity.d.ts.map +0 -1
  144. package/dist/build/codegen/identity.js +0 -32
  145. package/dist/build/codegen/identity.js.map +0 -1
  146. package/dist/build/codegen/merge.d.ts.map +0 -1
  147. package/dist/build/codegen/merge.js.map +0 -1
  148. package/dist/build/codegen/worker.d.ts +0 -12
  149. package/dist/build/codegen/worker.d.ts.map +0 -1
  150. package/dist/build/codegen/worker.js +0 -40
  151. package/dist/build/codegen/worker.js.map +0 -1
  152. package/dist/build/codegen/wrangler.d.ts.map +0 -1
  153. package/dist/build/codegen/wrangler.js.map +0 -1
  154. package/dist/build/frontend.d.ts +0 -27
  155. package/dist/build/frontend.d.ts.map +0 -1
  156. package/dist/build/frontend.js +0 -252
  157. package/dist/build/frontend.js.map +0 -1
  158. package/dist/build/index.d.ts.map +0 -1
  159. package/dist/build/index.js.map +0 -1
  160. package/dist/build/parse-output.d.ts +0 -27
  161. package/dist/build/parse-output.d.ts.map +0 -1
  162. package/dist/build/parse-output.js +0 -58
  163. package/dist/build/parse-output.js.map +0 -1
  164. package/dist/build/prepare.d.ts +0 -18
  165. package/dist/build/prepare.d.ts.map +0 -1
  166. package/dist/build/prepare.js +0 -138
  167. package/dist/build/prepare.js.map +0 -1
  168. package/dist/build/worker.d.ts +0 -15
  169. package/dist/build/worker.d.ts.map +0 -1
  170. package/dist/build/worker.js +0 -21
  171. package/dist/build/worker.js.map +0 -1
  172. package/dist/build/wrangler-cli.d.ts +0 -88
  173. package/dist/build/wrangler-cli.d.ts.map +0 -1
  174. package/dist/build/wrangler-cli.js +0 -283
  175. package/dist/build/wrangler-cli.js.map +0 -1
  176. package/dist/cloudflare.d.ts.map +0 -1
  177. package/dist/cloudflare.js.map +0 -1
  178. package/dist/index.d.ts.map +0 -1
  179. package/dist/index.js.map +0 -1
  180. package/dist/params.d.ts.map +0 -1
  181. package/dist/params.js.map +0 -1
  182. package/dist/worker/fetch.d.ts.map +0 -1
  183. package/dist/worker/fetch.js.map +0 -1
  184. package/dist/worker/index.d.ts.map +0 -1
  185. package/dist/worker/index.js.map +0 -1
  186. package/src/build/codegen/identity.ts +0 -40
  187. package/src/build/codegen/merge.ts +0 -92
  188. package/src/build/codegen/worker.ts +0 -53
  189. package/src/build/codegen/wrangler.ts +0 -128
  190. package/src/build/frontend.ts +0 -298
  191. package/src/build/index.ts +0 -29
  192. package/src/build/parse-output.ts +0 -60
  193. package/src/build/prepare.ts +0 -189
  194. package/src/build/worker.ts +0 -37
  195. package/src/build/wrangler-cli.ts +0 -372
  196. package/src/cloudflare.ts +0 -275
  197. package/src/index.ts +0 -21
  198. package/src/params.ts +0 -92
  199. package/src/worker/fetch.ts +0 -46
  200. package/src/worker/index.ts +0 -335
  201. package/src/worker/tsconfig.json +0 -10
@@ -0,0 +1,18 @@
1
+ import { createHash } from 'node:crypto';
2
+ const WORKER_NAME = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u;
3
+ export function workerName(parameters, origin, environment = '') {
4
+ if (parameters.workerName !== undefined) {
5
+ if (!WORKER_NAME.test(parameters.workerName) || parameters.workerName.length > 63) {
6
+ throw new TypeError('Cloudflare workerName is invalid.');
7
+ }
8
+ return parameters.workerName;
9
+ }
10
+ const readable = origin
11
+ .toLowerCase()
12
+ .replace(/[^a-z0-9]+/gu, '-')
13
+ .slice(0, 12)
14
+ .replace(/^-+|-+$/gu, '') || 'domain';
15
+ const hexadecimal = createHash('sha256').update(`${origin}\0${environment}`).digest('hex');
16
+ const identity = BigInt(`0x${hexadecimal}`).toString(36).padStart(50, '0');
17
+ return `${readable}-${identity}`;
18
+ }
@@ -0,0 +1,5 @@
1
+ export { deploymentAddressing } from './configuration/addressing.js';
2
+ export { workerInvocationTimeout } from './configuration/invocation-timeout.js';
3
+ export { workerRequestLimit } from './configuration/request-limit.js';
4
+ export { resolveRouter } from './configuration/router.js';
5
+ export { workerName } from './configuration/worker-name.js';
@@ -0,0 +1,5 @@
1
+ export { deploymentAddressing } from './configuration/addressing.js';
2
+ export { workerInvocationTimeout } from './configuration/invocation-timeout.js';
3
+ export { workerRequestLimit } from './configuration/request-limit.js';
4
+ export { resolveRouter } from './configuration/router.js';
5
+ export { workerName } from './configuration/worker-name.js';
@@ -0,0 +1,4 @@
1
+ import type { PreparedArtifact } from '@astrale-os/sdk/deployment/adapter';
2
+ import type { CloudflareParams } from '../params.js';
3
+ import type { WorkerArtifact } from './worker/index.js';
4
+ export declare function admitDirectPlacement(artifact: PreparedArtifact, worker: WorkerArtifact, parameters: CloudflareParams, environment?: string): void;
@@ -0,0 +1,90 @@
1
+ import { isDeepStrictEqual } from 'node:util';
2
+ import { generateWranglerConfig } from './codegen/wrangler.js';
3
+ import { resolveRouter, workerName } from './configuration.js';
4
+ import { workerModules } from './worker/modules.js';
5
+ export function admitDirectPlacement(artifact, worker, parameters, environment = '') {
6
+ const name = workerName(parameters, worker.origin, environment);
7
+ const release = config(artifact, 'wrangler.jsonc');
8
+ const router = resolveRouter(parameters.router);
9
+ const frontend = artifact.build.frontend;
10
+ const hasViteFrontend = frontend?.source.kind === 'vite';
11
+ equal(worker.assets !== undefined, hasViteFrontend);
12
+ if (worker.assets !== undefined) {
13
+ equal(worker.assets.notFound, 'none');
14
+ equal(worker.assets.runWorkerFirst, true);
15
+ }
16
+ equal(release, generated({
17
+ workerName: name,
18
+ main: `./${worker.release.entrypoint}`,
19
+ ...(parameters.route === undefined ? {} : { route: parameters.route }),
20
+ vars: worker.bindings.vars,
21
+ secretBindings: worker.bindings.requiredSecrets,
22
+ modules: directModules(worker.release),
23
+ ...(hasViteFrontend
24
+ ? {
25
+ frontend: {
26
+ directory: './frontend',
27
+ },
28
+ }
29
+ : {}),
30
+ ...(router === undefined
31
+ ? {}
32
+ : { router: { binding: router.binding, service: router.service } }),
33
+ ...(parameters.wrangler === undefined ? {} : { wrangler: parameters.wrangler }),
34
+ selfBinding: false,
35
+ }));
36
+ equal(release.compatibility_date, worker.runtime.compatibilityDate);
37
+ equalStringCollection(release.compatibility_flags, worker.runtime.compatibilityFlags);
38
+ equal(release.services ?? [], worker.bindings.services.map(serviceBinding));
39
+ equal(record(release.assets).html_handling, worker.assets?.htmlHandling);
40
+ }
41
+ function directModules(worker) {
42
+ try {
43
+ return workerModules(worker);
44
+ }
45
+ catch {
46
+ mismatch();
47
+ }
48
+ }
49
+ function generated(input) {
50
+ return JSON.parse(generateWranglerConfig(input));
51
+ }
52
+ function config(artifact, path) {
53
+ const file = artifact.files.find((candidate) => candidate.path === path);
54
+ if (file === undefined)
55
+ throw new TypeError(`Direct Cloudflare artifact is missing ${path}.`);
56
+ try {
57
+ const value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(file.bytes()));
58
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
59
+ throw new Error();
60
+ return value;
61
+ }
62
+ catch (cause) {
63
+ throw new TypeError(`Direct Cloudflare artifact ${path} is invalid.`, { cause });
64
+ }
65
+ }
66
+ function serviceBinding(binding) {
67
+ return { binding: binding.name, service: binding.service };
68
+ }
69
+ function record(input) {
70
+ return input !== null && typeof input === 'object' && !Array.isArray(input)
71
+ ? input
72
+ : {};
73
+ }
74
+ function equal(actual, expected) {
75
+ if (!isDeepStrictEqual(actual, expected)) {
76
+ throw new TypeError('Direct Cloudflare placement differs from its admitted Worker artifact.', {
77
+ cause: { actual, expected },
78
+ });
79
+ }
80
+ }
81
+ function equalStringCollection(actual, expected) {
82
+ if (!Array.isArray(actual) ||
83
+ actual.some((value) => typeof value !== 'string') ||
84
+ !isDeepStrictEqual([...actual].sort(), [...expected].sort())) {
85
+ mismatch();
86
+ }
87
+ }
88
+ function mismatch() {
89
+ throw new TypeError('Direct Cloudflare placement differs from its admitted Worker artifact.');
90
+ }
@@ -0,0 +1,19 @@
1
+ import type { Build } from '@astrale-os/sdk/deployment/build';
2
+ import { frontend as frontendBuild } from '@astrale-os/sdk/deployment/build';
3
+ /** The served frontend, and which of its files a browser may keep without revalidating. */
4
+ export interface FrontendOutput {
5
+ readonly files: readonly frontendBuild.FrontendFile[];
6
+ /**
7
+ * Served paths the bundler content-addressed, so their bytes can never change under a given name.
8
+ * A frontend Astrale does not build leaves this empty, which is what keeps the absence of evidence
9
+ * indistinguishable from evidence of mutability.
10
+ */
11
+ readonly addressed: readonly string[];
12
+ }
13
+ export declare function frontendFiles(input: {
14
+ readonly frontend: Build['frontend'];
15
+ readonly projectDir: string;
16
+ readonly output: string;
17
+ readonly recordSource?: (path: string) => void;
18
+ readonly signal: AbortSignal;
19
+ }): Promise<FrontendOutput>;
@@ -0,0 +1,99 @@
1
+ import { frontend as frontendBuild } from '@astrale-os/sdk/deployment/build';
2
+ import { readFile, readdir, rm } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { buildViteFrontend } from './vite.js';
5
+ /**
6
+ * Root names that Cloudflare Workers Static Assets reads as configuration rather than serving.
7
+ *
8
+ * Wrangler withholds them from a direct upload and applies them, while the managed upload through
9
+ * Services serves them as ordinary public files, so the same output would behave differently per
10
+ * adapter. The Worker runs first on every request and owns frontend response headers and routing for
11
+ * both, which leaves these files nothing to configure.
12
+ */
13
+ const RESERVED_ROOT_NAMES = new Set([
14
+ '_headers',
15
+ '_redirects',
16
+ '.assetsignore',
17
+ ]);
18
+ /**
19
+ * Name of a Cloudflare Pages worker script, the server-side code of a Pages project.
20
+ *
21
+ * Wrangler refuses to upload a root asset under this name, since that would publish the code, unless
22
+ * an `.assetsignore` exists, and the reserved names above already exclude one. A direct deployment
23
+ * would therefore fail at upload, while the managed upload through Services would publish the code.
24
+ * Neither adapter runs a Pages worker: the Worker is the only server-side code for both.
25
+ */
26
+ const PAGES_WORKER_NAME = '_worker.js';
27
+ export async function frontendFiles(input) {
28
+ const frontend = input.frontend;
29
+ if (frontend === undefined)
30
+ return { files: [], addressed: [] };
31
+ if (frontend.source.kind === 'external') {
32
+ return { files: frontendBuild.files(frontend, []), addressed: [] };
33
+ }
34
+ await rm(input.output, { recursive: true, force: true });
35
+ const built = await buildViteFrontend({
36
+ projectDir: input.projectDir,
37
+ output: input.output,
38
+ ...(frontend.source.config === undefined ? {} : { config: frontend.source.config }),
39
+ recordSource: input.recordSource,
40
+ signal: input.signal,
41
+ });
42
+ const files = frontendBuild.files(frontend, await readFiles(input.output));
43
+ refuseReservedNames(files);
44
+ refusePagesWorker(files);
45
+ // A name the bundler reported but that never reached disk is not a served path; keeping only the
46
+ // intersection means the evidence describes exactly what this deployment will answer with.
47
+ const served = new Set(files.map((file) => file.path));
48
+ return { files, addressed: built.addressed.filter((path) => served.has(path)) };
49
+ }
50
+ /**
51
+ * Refuse output that Cloudflare would read as asset configuration instead of serving it.
52
+ *
53
+ * Only the root is reserved: a nested `docs/_headers` stays an ordinary file. The match ignores case
54
+ * and covers directories, because Wrangler's upload filter withholds any root entry under these names
55
+ * regardless of case, and a case-insensitive filesystem hands it `_Headers` when it reads `_headers`.
56
+ */
57
+ function refuseReservedNames(files) {
58
+ const reserved = new Set(files
59
+ .map((file) => file.path.split('/')[0])
60
+ .filter((name) => RESERVED_ROOT_NAMES.has(name.toLowerCase())));
61
+ if (reserved.size === 0)
62
+ return;
63
+ const names = [...reserved].join(', ');
64
+ throw new TypeError(`Frontend output contains reserved Cloudflare asset configuration name(s) at its root: ${names}. ` +
65
+ 'A direct cloudflare() deployment would apply them as Workers Static Assets configuration, while a managed astrale() deployment would serve them as public files. ' +
66
+ 'The Worker owns frontend response headers and routing for both adapters: remove them from the Vite public directory, or stop the build from emitting them.');
67
+ }
68
+ /**
69
+ * Refuse output that Wrangler would decline to upload as a Cloudflare Pages worker script.
70
+ *
71
+ * Wrangler tests the relative path with a plain `startsWith`, not through its case-insensitive upload
72
+ * filter, so the match here is a case-sensitive prefix of the root name too. It covers a `_worker.js`
73
+ * file or directory and siblings such as `_worker.js.map`, which Wrangler refuses alike, while both
74
+ * adapters serve `_Worker.js` or a nested `docs/_worker.js` as an ordinary file.
75
+ */
76
+ function refusePagesWorker(files) {
77
+ const workers = new Set(files
78
+ .map((file) => file.path.split('/')[0])
79
+ .filter((name) => name.startsWith(PAGES_WORKER_NAME)));
80
+ if (workers.size === 0)
81
+ return;
82
+ const names = [...workers].join(', ');
83
+ throw new TypeError(`Frontend output contains Cloudflare Pages worker name(s) at its root: ${names}. ` +
84
+ `A direct cloudflare() deployment would fail because Wrangler refuses to upload a root name starting with ${PAGES_WORKER_NAME} as possible server-side code, while a managed astrale() deployment would serve them as public files. ` +
85
+ 'The Worker is the only server-side code for both adapters: remove or rename them in the Vite public directory, or stop the build from emitting them.');
86
+ }
87
+ async function readFiles(root, current = '') {
88
+ const entries = await readdir(join(root, current), { withFileTypes: true });
89
+ const files = [];
90
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
91
+ const path = current === '' ? entry.name : `${current}/${entry.name}`;
92
+ if (entry.isDirectory())
93
+ files.push(...(await readFiles(root, path)));
94
+ else if (entry.isFile()) {
95
+ files.push({ path, content: new Uint8Array(await readFile(join(root, path))) });
96
+ }
97
+ }
98
+ return files;
99
+ }
@@ -0,0 +1,10 @@
1
+ import type { Release } from '@astrale-os/sdk/deployment/release';
2
+ import type { verifyReadiness } from '@astrale-os/sdk/deployment/verify';
3
+ import type { WorkerArtifact } from '../worker/artifact.js';
4
+ type ReadinessFetch = NonNullable<NonNullable<Parameters<typeof verifyReadiness>[1]>['fetch']>;
5
+ /** Publication equality alone cannot prove a frontend-only deployment has propagated. */
6
+ export declare function verifyWorkerFrontend(release: Release, worker: Pick<WorkerArtifact, 'assets'>, options?: {
7
+ readonly fetch?: ReadinessFetch;
8
+ readonly signal?: AbortSignal;
9
+ }): Promise<void>;
10
+ export {};
@@ -0,0 +1,21 @@
1
+ import { frontendRevision } from './revision.js';
2
+ /** Publication equality alone cannot prove a frontend-only deployment has propagated. */
3
+ export async function verifyWorkerFrontend(release, worker, options = {}) {
4
+ const frontend = release.build.frontend;
5
+ if (frontend === undefined || frontend.source.kind === 'external')
6
+ return;
7
+ if (worker.assets === undefined)
8
+ throw new Error('Prepared Worker frontend assets are missing.');
9
+ const expected = frontendRevision(worker.assets.files);
10
+ const fetcher = options.fetch ?? globalThis.fetch;
11
+ for (const route of frontend.routes) {
12
+ const response = await fetcher(new URL(route.path, release.addressing.publicUrl).href, {
13
+ redirect: 'error',
14
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
15
+ });
16
+ const matches = response.status === 200 && response.headers.get('X-Astrale-Frontend') === expected;
17
+ await response.body?.cancel();
18
+ if (!matches)
19
+ throw new Error(`Frontend ${route.path} is not serving the prepared browser build yet.`);
20
+ }
21
+ }
@@ -0,0 +1,5 @@
1
+ /** Bind the generated Worker to its exact browser files, independently of Schema revision. */
2
+ export declare function frontendRevision(files: readonly {
3
+ readonly path: string;
4
+ bytes(): Uint8Array;
5
+ }[]): string;
@@ -0,0 +1,14 @@
1
+ import { createHash } from 'node:crypto';
2
+ /** Bind the generated Worker to its exact browser files, independently of Schema revision. */
3
+ export function frontendRevision(files) {
4
+ const hash = createHash('sha256');
5
+ const ordered = files
6
+ .map((file) => ({ path: file.path.replace(/^\//u, ''), bytes: file.bytes }))
7
+ .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
8
+ for (const file of ordered) {
9
+ const bytes = file.bytes();
10
+ hash.update(JSON.stringify([file.path, bytes.byteLength]));
11
+ hash.update(bytes);
12
+ }
13
+ return `sha256:${hash.digest('hex')}`;
14
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Build a Vite frontend in an isolated process and report its authored inputs, along with the
3
+ * outputs the bundler content-addressed. Those are the files a browser may keep forever, and only
4
+ * the build that named them can say which they are.
5
+ */
6
+ export declare function buildViteFrontend(input: {
7
+ readonly projectDir: string;
8
+ readonly output: string;
9
+ readonly config?: string;
10
+ readonly recordSource?: (path: string) => void;
11
+ readonly signal: AbortSignal;
12
+ }): Promise<{
13
+ readonly addressed: readonly string[];
14
+ }>;
@@ -0,0 +1,42 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { runViteProcess } from './vite.process.js';
6
+ /**
7
+ * Build a Vite frontend in an isolated process and report its authored inputs, along with the
8
+ * outputs the bundler content-addressed. Those are the files a browser may keep forever, and only
9
+ * the build that named them can say which they are.
10
+ */
11
+ export async function buildViteFrontend(input) {
12
+ const parent = dirname(input.output);
13
+ await mkdir(parent, { recursive: true });
14
+ const scratch = await mkdtemp(join(parent, 'vite-run-'));
15
+ const evidence = join(scratch, 'sources.json');
16
+ const addressed = join(scratch, 'addressed.json');
17
+ try {
18
+ await runViteProcess({
19
+ cwd: input.projectDir,
20
+ arguments: [runnerPath(), input.output, evidence, input.config ?? '', addressed],
21
+ signal: input.signal,
22
+ });
23
+ const sources = JSON.parse(await readFile(evidence, 'utf8'));
24
+ if (!Array.isArray(sources) || sources.some((path) => typeof path !== 'string')) {
25
+ throw new TypeError('Vite preparation source evidence is invalid.');
26
+ }
27
+ for (const path of sources)
28
+ input.recordSource?.(path);
29
+ const named = JSON.parse(await readFile(addressed, 'utf8'));
30
+ if (!Array.isArray(named) || named.some((path) => typeof path !== 'string')) {
31
+ throw new TypeError('Vite preparation output evidence is invalid.');
32
+ }
33
+ return { addressed: Object.freeze([...named]) };
34
+ }
35
+ finally {
36
+ await rm(scratch, { recursive: true, force: true });
37
+ }
38
+ }
39
+ function runnerPath() {
40
+ const built = fileURLToPath(new URL('./vite.runner.js', import.meta.url));
41
+ return existsSync(built) ? built : fileURLToPath(new URL('./vite.runner.ts', import.meta.url));
42
+ }
@@ -0,0 +1,7 @@
1
+ /** Run one bounded, cancellable Vite subprocess. */
2
+ export declare function runViteProcess(input: {
3
+ readonly cwd: string;
4
+ readonly arguments: readonly string[];
5
+ readonly signal: AbortSignal;
6
+ readonly timeoutMs?: number;
7
+ }): Promise<void>;
@@ -0,0 +1,65 @@
1
+ import { spawn } from 'node:child_process';
2
+ const MAXIMUM_OUTPUT_BYTES = 1024 * 1024;
3
+ const TERMINATION_GRACE_MS = 1_000;
4
+ const VITE_TIMEOUT_MS = 300_000;
5
+ /** Run one bounded, cancellable Vite subprocess. */
6
+ export function runViteProcess(input) {
7
+ if (input.signal.aborted)
8
+ return Promise.reject(abortReason(input.signal));
9
+ return new Promise((resolve, reject) => {
10
+ const child = spawn(process.execPath, input.arguments, {
11
+ cwd: input.cwd,
12
+ stdio: ['ignore', 'pipe', 'pipe'],
13
+ windowsHide: true,
14
+ });
15
+ let output = '';
16
+ let outputBytes = 0;
17
+ let terminating;
18
+ let escalation;
19
+ const timeout = setTimeout(() => terminate(new Error('Vite process timed out.')), input.timeoutMs ?? VITE_TIMEOUT_MS);
20
+ const abort = () => terminate(abortReason(input.signal));
21
+ input.signal.addEventListener('abort', abort, { once: true });
22
+ const cleanup = () => {
23
+ clearTimeout(timeout);
24
+ if (escalation !== undefined)
25
+ clearTimeout(escalation);
26
+ input.signal.removeEventListener('abort', abort);
27
+ };
28
+ function terminate(cause) {
29
+ if (terminating !== undefined)
30
+ return;
31
+ terminating = cause;
32
+ child.kill('SIGTERM');
33
+ escalation = setTimeout(() => {
34
+ if (child.exitCode === null && child.signalCode === null)
35
+ child.kill('SIGKILL');
36
+ }, TERMINATION_GRACE_MS);
37
+ }
38
+ const collect = (buffer) => {
39
+ outputBytes += buffer.byteLength;
40
+ if (outputBytes > MAXIMUM_OUTPUT_BYTES) {
41
+ terminate(new Error('Vite process exceeded its output limit.'));
42
+ return;
43
+ }
44
+ output += buffer.toString();
45
+ };
46
+ child.stdout.on('data', collect);
47
+ child.stderr.on('data', collect);
48
+ child.once('error', (cause) => {
49
+ cleanup();
50
+ reject(terminating ?? cause);
51
+ });
52
+ child.once('close', (code) => {
53
+ cleanup();
54
+ if (terminating !== undefined)
55
+ reject(terminating);
56
+ else if (code === 0)
57
+ resolve();
58
+ else
59
+ reject(new Error(`Vite failed (code ${code ?? 1}):\n${output.slice(-1500)}`));
60
+ });
61
+ });
62
+ }
63
+ function abortReason(signal) {
64
+ return signal.reason instanceof Error ? signal.reason : new Error('Vite process cancelled.');
65
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,155 @@
1
+ import { readdir, writeFile } from 'node:fs/promises';
2
+ import { createRequire } from 'node:module';
3
+ import { isAbsolute, join, resolve } from 'node:path';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
+ const CONFIG_FILES = Object.freeze([
6
+ 'vite.config.js',
7
+ 'vite.config.mjs',
8
+ 'vite.config.ts',
9
+ 'vite.config.cjs',
10
+ 'vite.config.mts',
11
+ 'vite.config.cts',
12
+ ]);
13
+ await main();
14
+ async function main() {
15
+ const [output, evidence, configured, addressed] = process.argv.slice(2);
16
+ if (output === undefined || evidence === undefined) {
17
+ throw new TypeError('Vite preparation runner arguments are invalid.');
18
+ }
19
+ const projectDir = process.cwd();
20
+ const vite = await projectVite(projectDir);
21
+ const sources = new Set();
22
+ const named = new Set();
23
+ let publicDir = false;
24
+ let environment = {
25
+ directory: false,
26
+ mode: 'production',
27
+ };
28
+ const config = configured === '' || configured === undefined ? undefined : configured;
29
+ await vite.build({
30
+ logLevel: 'silent',
31
+ ...(config === undefined
32
+ ? {}
33
+ : { configFile: isAbsolute(config) ? config : resolve(projectDir, config) }),
34
+ build: { outDir: output, emptyOutDir: true },
35
+ plugins: [
36
+ {
37
+ name: 'astrale:preparation-sources',
38
+ configResolved(value) {
39
+ publicDir = value.publicDir;
40
+ environment = { directory: value.envDir, mode: value.mode };
41
+ for (const path of value.configFileDependencies)
42
+ sources.add(path);
43
+ },
44
+ transform(_source, id) {
45
+ const path = physicalModulePath(id);
46
+ if (path !== undefined)
47
+ sources.add(path);
48
+ },
49
+ // The bundler is the only party that knows which outputs it content-addressed: a served path
50
+ // is safe to retain exactly when its name changes with its bytes. Reading that here, from
51
+ // the build that produced them, is what keeps the answer a fact rather than a guess about
52
+ // directory layout or file naming.
53
+ generateBundle(options, bundle) {
54
+ for (const [fileName, entry] of Object.entries(bundle)) {
55
+ if (addressedName(options, entry))
56
+ named.add(fileName);
57
+ }
58
+ },
59
+ },
60
+ ],
61
+ });
62
+ for (const path of configCandidates(projectDir, config))
63
+ sources.add(path);
64
+ for (const path of environmentCandidates(environment))
65
+ sources.add(path);
66
+ if (publicDir !== false)
67
+ await collectTree(publicDir, sources);
68
+ await writeFile(evidence, JSON.stringify([...sources]));
69
+ if (addressed !== undefined)
70
+ await writeFile(addressed, JSON.stringify([...named].sort()));
71
+ }
72
+ /**
73
+ * Whether this output's name carries a content hash.
74
+ *
75
+ * Two facts are required, and each is necessary. The output must carry a `name`, which the bundler
76
+ * sets only for the outputs it named itself: a plugin emitting an explicit `fileName` bypasses the
77
+ * template entirely, and such a file can sit anywhere, including beside the bundle's own output. And
78
+ * the template that was applied must embed `[hash]`, since a project is free to strip it.
79
+ *
80
+ * Anything else leaves the answer unknown, and unknown must read as "not content-addressed": losing
81
+ * the optimization costs a revalidation, while claiming it wrongly serves a stale file for as long
82
+ * as the header says, with no way to recall it.
83
+ */
84
+ function addressedName(options, entry) {
85
+ if (typeof entry.name !== 'string')
86
+ return false;
87
+ const template = entry.type === 'asset'
88
+ ? options.assetFileNames
89
+ : entry.isEntry === true
90
+ ? options.entryFileNames
91
+ : options.chunkFileNames;
92
+ return typeof template === 'string' && template.includes('[hash]');
93
+ }
94
+ function environmentCandidates(input) {
95
+ const directory = input.directory;
96
+ if (directory === false)
97
+ return [];
98
+ return ['.env', '.env.local', `.env.${input.mode}`, `.env.${input.mode}.local`].map((name) => join(directory, name));
99
+ }
100
+ function physicalModulePath(id) {
101
+ if (id.startsWith('\0'))
102
+ return undefined;
103
+ const path = id.replace(/[?#].*$/u, '');
104
+ if (path.length === 0)
105
+ return undefined;
106
+ if (path.startsWith('file:')) {
107
+ try {
108
+ return fileURLToPath(path);
109
+ }
110
+ catch {
111
+ return undefined;
112
+ }
113
+ }
114
+ return isAbsolute(path) ? path : undefined;
115
+ }
116
+ async function projectVite(projectDir) {
117
+ try {
118
+ const manifest = createRequire(join(projectDir, 'package.json')).resolve('vite/package.json');
119
+ const entry = createRequire(manifest).resolve('vite');
120
+ return (await import(pathToFileURL(entry).href));
121
+ }
122
+ catch (cause) {
123
+ throw new TypeError('Vite frontend requires a project-local Vite package.', { cause });
124
+ }
125
+ }
126
+ function configCandidates(projectDir, configured) {
127
+ if (configured !== undefined) {
128
+ return [isAbsolute(configured) ? configured : resolve(projectDir, configured)];
129
+ }
130
+ return CONFIG_FILES.map((name) => join(projectDir, name));
131
+ }
132
+ async function collectTree(path, output) {
133
+ output.add(path);
134
+ let entries;
135
+ try {
136
+ entries = await readdir(path, { withFileTypes: true });
137
+ }
138
+ catch (cause) {
139
+ if (errorCode(cause) === 'ENOENT')
140
+ return;
141
+ throw cause;
142
+ }
143
+ for (const entry of entries) {
144
+ const child = join(path, entry.name);
145
+ if (entry.isDirectory())
146
+ await collectTree(child, output);
147
+ else if (entry.isFile())
148
+ output.add(child);
149
+ }
150
+ }
151
+ function errorCode(input) {
152
+ return input !== null && typeof input === 'object' && 'code' in input
153
+ ? String(Reflect.get(input, 'code'))
154
+ : undefined;
155
+ }
@@ -1,16 +1,10 @@
1
- /**
2
- * `@astrale-os/adapter-cloudflare/build` — the shared Cloudflare-Workers build
3
- * toolkit, exposed for sibling deploy adapters (e.g.
4
- * `@astrale-os/adapter-astrale`).
5
- *
6
- * This is the "compile a `defineDomain` into a Cloudflare Workers bundle + run
7
- * local dev" surface that every Cloudflare-Workers deploy strategy shares,
8
- * regardless of WHERE the bundle ultimately ships. It is NOT part of the public
9
- * adapter API (`@astrale-os/adapter-cloudflare`) — it's the seam two adapters
10
- * build on so the codegen/bundling/dev logic lives in exactly one place.
11
- */
12
- export { buildFrontend, FRONTEND_STAGE_DIR, startFrontendDev, type FrontendDevHandle, type ViteBuild, type ViteBuildInput, } from './frontend.js';
13
- export { logTo, prepare } from './prepare.js';
14
- export { buildCloudflareWorker, type CloudflareWorkerBuild } from './worker.js';
15
- export { bulkPutSecrets, runWranglerBundle, runWranglerDeploy, runWranglerDev, runWranglerTypes, } from './wrangler-cli.js';
16
- //# sourceMappingURL=index.d.ts.map
1
+ export { generateBuildMaterialModule } from './material.js';
2
+ export { artifact, materialize, prepareWorkerArtifact } from './artifact.js';
3
+ export { loadWorkerArtifact } from './worker/index.js';
4
+ export type { WorkerArtifact } from './worker/index.js';
5
+ export { resolveRouter, workerName } from './configuration.js';
6
+ export { frontendFiles } from './frontend/files.js';
7
+ export { verifyWorkerFrontend } from './frontend/readiness.js';
8
+ export { qualifyWorkerBundle } from './qualification.js';
9
+ export { assertSingletonRuntimeRoots } from './roots.js';
10
+ export { runWranglerBundle, runWranglerDeploy, runWranglerDeploymentStatus, runWranglerTypes, } from '../wrangler/index.js';
@@ -1,16 +1,9 @@
1
- /**
2
- * `@astrale-os/adapter-cloudflare/build` — the shared Cloudflare-Workers build
3
- * toolkit, exposed for sibling deploy adapters (e.g.
4
- * `@astrale-os/adapter-astrale`).
5
- *
6
- * This is the "compile a `defineDomain` into a Cloudflare Workers bundle + run
7
- * local dev" surface that every Cloudflare-Workers deploy strategy shares,
8
- * regardless of WHERE the bundle ultimately ships. It is NOT part of the public
9
- * adapter API (`@astrale-os/adapter-cloudflare`) — it's the seam two adapters
10
- * build on so the codegen/bundling/dev logic lives in exactly one place.
11
- */
12
- export { buildFrontend, FRONTEND_STAGE_DIR, startFrontendDev, } from './frontend.js';
13
- export { logTo, prepare } from './prepare.js';
14
- export { buildCloudflareWorker } from './worker.js';
15
- export { bulkPutSecrets, runWranglerBundle, runWranglerDeploy, runWranglerDev, runWranglerTypes, } from './wrangler-cli.js';
16
- //# sourceMappingURL=index.js.map
1
+ export { generateBuildMaterialModule } from './material.js';
2
+ export { artifact, materialize, prepareWorkerArtifact } from './artifact.js';
3
+ export { loadWorkerArtifact } from './worker/index.js';
4
+ export { resolveRouter, workerName } from './configuration.js';
5
+ export { frontendFiles } from './frontend/files.js';
6
+ export { verifyWorkerFrontend } from './frontend/readiness.js';
7
+ export { qualifyWorkerBundle } from './qualification.js';
8
+ export { assertSingletonRuntimeRoots } from './roots.js';
9
+ export { runWranglerBundle, runWranglerDeploy, runWranglerDeploymentStatus, runWranglerTypes, } from '../wrangler/index.js';