@astrale-os/adapter-cloudflare 0.5.0-beta.115 → 0.5.0-beta.116

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 (72) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +29 -3
  3. package/dist/adapter.d.ts +3 -4
  4. package/dist/adapter.js +18 -12
  5. package/dist/build/artifact.js +10 -10
  6. package/dist/build/codegen/wrangler.d.ts +0 -2
  7. package/dist/build/codegen/wrangler.js +3 -12
  8. package/dist/build/configuration/worker-name.d.ts +1 -1
  9. package/dist/build/configuration/worker-name.js +2 -2
  10. package/dist/build/direct.d.ts +1 -1
  11. package/dist/build/direct.js +2 -5
  12. package/dist/build/frontend/files.d.ts +3 -1
  13. package/dist/build/frontend/files.js +10 -37
  14. package/dist/build/frontend/readiness.d.ts +10 -0
  15. package/dist/build/frontend/readiness.js +21 -0
  16. package/dist/build/frontend/revision.d.ts +5 -0
  17. package/dist/build/frontend/revision.js +14 -0
  18. package/dist/build/frontend/vite.d.ts +8 -0
  19. package/dist/build/frontend/vite.js +32 -0
  20. package/dist/build/frontend/vite.process.d.ts +7 -0
  21. package/dist/build/frontend/vite.process.js +65 -0
  22. package/dist/build/frontend/vite.runner.d.ts +1 -0
  23. package/dist/build/frontend/vite.runner.js +120 -0
  24. package/dist/build/index.d.ts +1 -0
  25. package/dist/build/index.js +1 -0
  26. package/dist/build/worker/codegen/service.d.ts +1 -0
  27. package/dist/build/worker/codegen/service.js +1 -1
  28. package/dist/build/worker/execution.prepare.d.ts +1 -0
  29. package/dist/build/worker/execution.prepare.js +3 -0
  30. package/dist/deploy/deploy.d.ts +5 -4
  31. package/dist/deploy/deploy.js +23 -6
  32. package/dist/deploy/readiness.d.ts +5 -0
  33. package/dist/deploy/readiness.js +18 -0
  34. package/dist/index.d.ts +8 -7
  35. package/dist/index.js +8 -7
  36. package/dist/params.d.ts +4 -25
  37. package/dist/worker/entry.d.ts +5 -1
  38. package/dist/worker/entry.js +14 -41
  39. package/dist/worker/frontend.d.ts +4 -0
  40. package/dist/worker/frontend.js +46 -0
  41. package/dist/wrangler/process.d.ts +0 -2
  42. package/dist/wrangler/process.js +0 -13
  43. package/dist/wrangler/rollout.d.ts +27 -0
  44. package/dist/wrangler/rollout.js +75 -0
  45. package/dist/wrangler/secrets.d.ts +0 -4
  46. package/dist/wrangler/secrets.js +0 -34
  47. package/dist/wrangler/status.d.ts +6 -2
  48. package/dist/wrangler/status.js +21 -2
  49. package/dist/wrangler/triggers.d.ts +5 -0
  50. package/dist/wrangler/triggers.js +12 -0
  51. package/dist/wrangler/versions.d.ts +13 -0
  52. package/dist/wrangler/versions.js +38 -0
  53. package/package.json +2 -2
  54. package/template/astrale.config.ts +22 -39
  55. package/dist/build/frontend/routes.d.ts +0 -2
  56. package/dist/build/frontend/routes.js +0 -11
  57. package/dist/development/develop.d.ts +0 -15
  58. package/dist/development/develop.js +0 -270
  59. package/dist/development/frontend.d.ts +0 -11
  60. package/dist/development/frontend.js +0 -109
  61. package/dist/development/host.d.ts +0 -2
  62. package/dist/development/host.js +0 -46
  63. package/dist/development/port.d.ts +0 -1
  64. package/dist/development/port.js +0 -12
  65. package/dist/development/readiness.d.ts +0 -14
  66. package/dist/development/readiness.js +0 -71
  67. package/dist/development/resolved-fetch.d.ts +0 -17
  68. package/dist/development/resolved-fetch.js +0 -80
  69. package/dist/development/worker-output.d.ts +0 -2
  70. package/dist/development/worker-output.js +0 -25
  71. package/dist/development/worker.d.ts +0 -17
  72. package/dist/development/worker.js +0 -147
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.0-beta.116](https://github.com/astrale-os/sdk/compare/adapter-cloudflare-v0.5.0-beta.115...adapter-cloudflare-v0.5.0-beta.116) (2026-09-05)
4
+
5
+
6
+ ### Features
7
+
8
+ * add explicit Project environments and remote development ([#415](https://github.com/astrale-os/sdk/issues/415)) ([7357a05](https://github.com/astrale-os/sdk/commit/7357a055a39749238dd9116da0c171bb9a15882d))
9
+
10
+
11
+ ### Dependencies
12
+
13
+ * The following workspace dependencies were updated
14
+ * dependencies
15
+ * @astrale-os/sdk bumped to 0.5.0-beta.113
16
+
3
17
  ## [0.5.0-beta.115](https://github.com/astrale-os/sdk/compare/adapter-cloudflare-v0.5.0-beta.114...adapter-cloudflare-v0.5.0-beta.115) (2026-09-05)
4
18
 
5
19
 
package/README.md CHANGED
@@ -10,9 +10,35 @@ npm install @astrale-os/sdk@beta @astrale-os/adapter-cloudflare@beta
10
10
 
11
11
  Bare installs return to stable releases when the package set is promoted and npm `latest` advances.
12
12
 
13
- `pnpm dev` starts the Worker and optional Vite frontend with hot reload on collision-free allocated
14
- ports. It does not own Kernel installation or public ingress; the managed Astrale adapter composes
15
- those lifecycle responsibilities around this provider-local session.
13
+ One `cloudflare()` value configures one Project Environment. `astrale-domain dev <environment>`
14
+ builds locally and deploys each admitted Worker and frontend update remotely through the same path
15
+ as `deploy`; it starts no local Worker, Vite server, or tunnel. Omitting `route` uses the observed
16
+ stable `*.workers.dev` issuer. Add `installation: { instance: '...' }` to the Project Environment
17
+ when generic tooling should reconcile the ready Release onto an Astrale Kernel.
18
+
19
+ One watcher serializes its own updates. For an existing Worker, it uploads a candidate, reconciles
20
+ configured triggers, and stages the candidate at 0% while the previous version retains 100% of normal
21
+ HTTP traffic. Exact-version probes verify Publication, Bundle, and serving identity before promotion;
22
+ serving the Publication also initializes the Application Runtime. After promotion, normal requests
23
+ must prove that exact version again. A failed candidate remains at 0% and can be replaced on retry.
24
+ Bundled View routes must also serve the prepared browser-file generation before readiness passes.
25
+ This covers frontend-only changes without changing the Schema or reinstalling the Domain. HTML
26
+ and transient asset misses are not cached; ordinary successful asset responses keep their cache policy.
27
+ The first deployment uses ordinary Worker provisioning because no previous version exists to retain.
28
+ An existing traffic-split rollout must be resolved before this single-version workflow can continue.
29
+
30
+ A direct Cloudflare target shared across machines must still have one external writer, normally a
31
+ CI/CD concurrency group. Live deployment checks detect competing changes but are not an atomic
32
+ compare-and-swap or distributed lock. Use separate Worker targets for uncoordinated development.
33
+ Routes, custom domains, schedules, and other non-versioned provider settings are reconciled through
34
+ Wrangler before candidate probing; they can remain changed after a failed attempt. Candidate readiness
35
+ protects the HTTP implementation, not an atomic rollback of external resources or trigger settings.
36
+
37
+ When an Environment also declares `installation`, the SDK excludes another local checkout that
38
+ targets the same resolved `Kernel issuer + Domain origin` (including bookmark aliases), reconciles
39
+ exact installed Release evidence, and opens
40
+ the default View when the Application has a frontend. These are Project-tooling behaviors rather
41
+ than Cloudflare adapter responsibilities.
16
42
 
17
43
  ## Provider-local staging
18
44
 
package/dist/adapter.d.ts CHANGED
@@ -1,15 +1,14 @@
1
+ import type { Deployment } from '@astrale-os/sdk/deployment';
1
2
  import type { Release } from '@astrale-os/sdk/deployment/release';
2
3
  import { verifyReadiness } from '@astrale-os/sdk/deployment/verify';
3
4
  import type { CloudflareParams } from './params.js';
4
5
  import { artifact } from './build/artifact.js';
5
6
  import { type CloudflareDeployDependencies } from './deploy/index.js';
6
- import { developCloudflare } from './development/develop.js';
7
7
  import { signingIdentity } from './identity/source.js';
8
8
  export interface CloudflareDependencies extends CloudflareDeployDependencies {
9
9
  readonly artifact: typeof artifact;
10
- readonly develop: typeof developCloudflare;
11
10
  readonly signingIdentity: typeof signingIdentity;
12
11
  }
13
- export declare function cloudflare(environments: Readonly<Record<string, CloudflareParams>>): import("@astrale-os/sdk/deployment/adapter").Adapter<CloudflareParams>;
14
- export declare function cloudflareWithDependencies(environments: Readonly<Record<string, CloudflareParams>>, dependencies: CloudflareDependencies): import("@astrale-os/sdk/deployment/adapter").Adapter<CloudflareParams>;
12
+ export declare function cloudflare(parameters?: CloudflareParams): Deployment<CloudflareParams>;
13
+ export declare function cloudflareWithDependencies(parameters: CloudflareParams, dependencies: CloudflareDependencies): Deployment<CloudflareParams>;
15
14
  export declare function waitForReadiness(verify: typeof verifyReadiness, release: Release, signal?: AbortSignal): Promise<void>;
package/dist/adapter.js CHANGED
@@ -1,35 +1,41 @@
1
- import { defineAdapter } from '@astrale-os/sdk/deployment/adapter';
1
+ import { defineAdapter, defineDeployment } from '@astrale-os/sdk/deployment/adapter';
2
2
  import { verifyReadiness } from '@astrale-os/sdk/deployment/verify';
3
3
  import { createRequire } from 'node:module';
4
4
  import { artifact, materialize } from './build/artifact.js';
5
+ import { verifyWorkerFrontend } from './build/frontend/readiness.js';
5
6
  import { deployCloudflare } from './deploy/index.js';
6
- import { developCloudflare } from './development/develop.js';
7
+ import { versionReadinessFetch } from './deploy/readiness.js';
7
8
  import { signingIdentity } from './identity/source.js';
8
- import { runWranglerDeploy } from './wrangler/deploy.js';
9
+ import { runWranglerRollout } from './wrangler/rollout.js';
9
10
  import { runWranglerDeploymentStatus } from './wrangler/status.js';
10
11
  const packageVersion = createRequire(import.meta.url)('../package.json')
11
12
  .version;
12
- export function cloudflare(environments) {
13
- return cloudflareWithDependencies(environments, {
13
+ export function cloudflare(parameters = {}) {
14
+ return cloudflareWithDependencies(parameters, {
14
15
  artifact,
15
16
  materialize,
16
- runWranglerDeploy,
17
+ runWranglerRollout,
17
18
  runWranglerDeploymentStatus,
18
- develop: developCloudflare,
19
19
  signingIdentity,
20
- waitForReadiness: (release, signal) => waitForReadiness(verifyReadiness, release, signal),
20
+ waitForReadiness: (release, signal, version, worker, workerName) => waitForReadiness(async (release, options) => {
21
+ const checks = {
22
+ ...options,
23
+ fetch: versionReadinessFetch(release.addressing.issuer, version, globalThis.fetch, workerName),
24
+ };
25
+ await verifyReadiness(release, checks);
26
+ await verifyWorkerFrontend(release, worker, checks);
27
+ }, release, signal),
21
28
  });
22
29
  }
23
- export function cloudflareWithDependencies(environments, dependencies) {
24
- return defineAdapter({
30
+ export function cloudflareWithDependencies(parameters, dependencies) {
31
+ const adapter = defineAdapter({
25
32
  name: 'cloudflare',
26
33
  version: packageVersion,
27
- environments,
28
34
  prepare: dependencies.artifact,
29
- develop: (parameters, context) => dependencies.develop(parameters, context),
30
35
  deploy: (parameters, context) => deployCloudflare(parameters, context, dependencies),
31
36
  secretsFile: (parameters) => parameters.secrets,
32
37
  });
38
+ return defineDeployment(adapter, parameters);
33
39
  }
34
40
  export async function waitForReadiness(verify, release, signal) {
35
41
  const deadline = Date.now() + 90_000;
@@ -6,7 +6,7 @@ import { workerDeployment } from '../release/index.js';
6
6
  import { generateWranglerConfig } from './codegen/wrangler.js';
7
7
  import { deploymentAddressing, resolveRouter, workerName } from './configuration.js';
8
8
  import { frontendFiles } from './frontend/files.js';
9
- import { frontendRoutes } from './frontend/routes.js';
9
+ import { frontendRevision } from './frontend/revision.js';
10
10
  import { encodeWorkerArtifactManifest, inferMediaType, workerContentDigest, WORKER_MANIFEST_PATH, } from './worker/artifact.manifest.js';
11
11
  import { prepareExecution, RELEASE_ENTRY, RELEASE_SERVICE, runtimeVars, } from './worker/execution.prepare.js';
12
12
  import { admitWorkerRuntimeConfiguration } from './worker/runtime.js';
@@ -26,9 +26,16 @@ async function prepare(params, context, direct) {
26
26
  const directory = await mkdtemp(join(stateDirectory, 'prepare-'));
27
27
  try {
28
28
  const router = resolveRouter(params.router);
29
- const name = workerName(params, context.build.schema.compiled.root.origin);
29
+ const name = workerName(params, context.build.schema.compiled.root.origin, context.environment);
30
30
  const revision = context.build.schema.compiled.root.revision;
31
31
  const configuredIssuer = deploymentAddressing(params);
32
+ const frontend = await frontendFiles({
33
+ frontend: context.build.frontend,
34
+ projectDir: context.projectDir,
35
+ output: join(directory, 'frontend'),
36
+ recordSource: context.recordSource,
37
+ signal: context.signal,
38
+ });
32
39
  const modules = await prepareExecution({
33
40
  params,
34
41
  context,
@@ -37,13 +44,8 @@ async function prepare(params, context, direct) {
37
44
  direct,
38
45
  configuredIssuer,
39
46
  router,
47
+ ...(frontend.length === 0 ? {} : { frontendRevision: frontendRevision(frontend) }),
40
48
  });
41
- const frontend = context.mode === 'development'
42
- ? []
43
- : await frontendFiles({
44
- frontend: context.build.frontend,
45
- projectDir: context.projectDir,
46
- });
47
49
  const releaseConfig = generateWranglerConfig({
48
50
  workerName: name,
49
51
  main: `./${RELEASE_ENTRY}`,
@@ -55,8 +57,6 @@ async function prepare(params, context, direct) {
55
57
  ? {
56
58
  frontend: {
57
59
  directory: './frontend',
58
- routes: frontendRoutes(context.build.frontend),
59
- dev: false,
60
60
  },
61
61
  }
62
62
  : {}),
@@ -20,8 +20,6 @@ export interface WranglerCodegenOptions {
20
20
  /** Exact materialized local frontend; absence means no ASSETS binding. */
21
21
  frontend?: {
22
22
  readonly directory: string;
23
- readonly routes: readonly string[];
24
- readonly dev: boolean;
25
23
  };
26
24
  /** Plain (non-secret) vars to bake in. */
27
25
  vars?: Record<string, string>;
@@ -34,18 +34,9 @@ export function generateWranglerConfig(opts) {
34
34
  directory: opts.frontend.directory,
35
35
  binding: 'ASSETS',
36
36
  html_handling: 'none',
37
- run_worker_first: opts.frontend.dev
38
- ? true
39
- : [
40
- '/invoke',
41
- '/install',
42
- '/install/*',
43
- '/health',
44
- '/health/*',
45
- '/.well-known',
46
- '/.well-known/*',
47
- ...opts.frontend.routes,
48
- ],
37
+ // Match the portable Worker artifact: frontend routing and sandbox-safe asset
38
+ // response headers are owned by the Worker for both deployment adapters.
39
+ run_worker_first: true,
49
40
  };
50
41
  }
51
42
  const services = [];
@@ -1,2 +1,2 @@
1
1
  import type { CloudflareParams } from '../../params.js';
2
- export declare function workerName(parameters: CloudflareParams, origin: string): string;
2
+ export declare function workerName(parameters: CloudflareParams, origin: string, environment?: string): string;
@@ -1,6 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  const WORKER_NAME = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u;
3
- export function workerName(parameters, origin) {
3
+ export function workerName(parameters, origin, environment = '') {
4
4
  if (parameters.workerName !== undefined) {
5
5
  if (!WORKER_NAME.test(parameters.workerName) || parameters.workerName.length > 63) {
6
6
  throw new TypeError('Cloudflare workerName is invalid.');
@@ -12,7 +12,7 @@ export function workerName(parameters, origin) {
12
12
  .replace(/[^a-z0-9]+/gu, '-')
13
13
  .slice(0, 12)
14
14
  .replace(/^-+|-+$/gu, '') || 'domain';
15
- const hexadecimal = createHash('sha256').update(origin).digest('hex');
15
+ const hexadecimal = createHash('sha256').update(`${origin}\0${environment}`).digest('hex');
16
16
  const identity = BigInt(`0x${hexadecimal}`).toString(36).padStart(50, '0');
17
17
  return `${readable}-${identity}`;
18
18
  }
@@ -1,4 +1,4 @@
1
1
  import type { PreparedArtifact } from '@astrale-os/sdk/deployment/adapter';
2
2
  import type { CloudflareParams } from '../params.js';
3
3
  import type { WorkerArtifact } from './worker/index.js';
4
- export declare function admitDirectPlacement(artifact: PreparedArtifact, worker: WorkerArtifact, parameters: CloudflareParams): void;
4
+ export declare function admitDirectPlacement(artifact: PreparedArtifact, worker: WorkerArtifact, parameters: CloudflareParams, environment?: string): void;
@@ -1,10 +1,9 @@
1
1
  import { isDeepStrictEqual } from 'node:util';
2
2
  import { generateWranglerConfig } from './codegen/wrangler.js';
3
3
  import { resolveRouter, workerName } from './configuration.js';
4
- import { frontendRoutes } from './frontend/routes.js';
5
4
  import { workerModules } from './worker/modules.js';
6
- export function admitDirectPlacement(artifact, worker, parameters) {
7
- const name = workerName(parameters, worker.origin);
5
+ export function admitDirectPlacement(artifact, worker, parameters, environment = '') {
6
+ const name = workerName(parameters, worker.origin, environment);
8
7
  const release = config(artifact, 'wrangler.jsonc');
9
8
  const router = resolveRouter(parameters.router);
10
9
  const frontend = artifact.build.frontend;
@@ -25,8 +24,6 @@ export function admitDirectPlacement(artifact, worker, parameters) {
25
24
  ? {
26
25
  frontend: {
27
26
  directory: './frontend',
28
- routes: frontendRoutes(frontend),
29
- dev: false,
30
27
  },
31
28
  }
32
29
  : {}),
@@ -3,5 +3,7 @@ import { frontend as frontendBuild } from '@astrale-os/sdk/deployment/build';
3
3
  export declare function frontendFiles(input: {
4
4
  readonly frontend: Build['frontend'];
5
5
  readonly projectDir: string;
6
- readonly onLog?: (line: string) => void;
6
+ readonly output: string;
7
+ readonly recordSource?: (path: string) => void;
8
+ readonly signal: AbortSignal;
7
9
  }): Promise<readonly frontendBuild.FrontendFile[]>;
@@ -1,24 +1,22 @@
1
1
  import { frontend as frontendBuild } from '@astrale-os/sdk/deployment/build';
2
- import { spawn } from 'node:child_process';
3
- import { existsSync } from 'node:fs';
4
2
  import { readFile, readdir, rm } from 'node:fs/promises';
5
3
  import { join } from 'node:path';
4
+ import { buildViteFrontend } from './vite.js';
6
5
  export async function frontendFiles(input) {
7
6
  const frontend = input.frontend;
8
7
  if (frontend === undefined)
9
8
  return [];
10
9
  if (frontend.source.kind === 'external')
11
10
  return frontendBuild.files(frontend, []);
12
- const output = join(input.projectDir, '.astrale', 'frontend-build');
13
- await rm(output, { recursive: true, force: true });
14
- await run(viteCommand(input.projectDir), [
15
- 'build',
16
- '--outDir',
17
- output,
18
- '--emptyOutDir',
19
- ...(frontend.source.config === undefined ? [] : ['--config', frontend.source.config]),
20
- ], input.projectDir, input.onLog);
21
- return frontendBuild.files(frontend, await readFiles(output));
11
+ await rm(input.output, { recursive: true, force: true });
12
+ await buildViteFrontend({
13
+ projectDir: input.projectDir,
14
+ output: input.output,
15
+ ...(frontend.source.config === undefined ? {} : { config: frontend.source.config }),
16
+ recordSource: input.recordSource,
17
+ signal: input.signal,
18
+ });
19
+ return frontendBuild.files(frontend, await readFiles(input.output));
22
20
  }
23
21
  async function readFiles(root, current = '') {
24
22
  const entries = await readdir(join(root, current), { withFileTypes: true });
@@ -33,28 +31,3 @@ async function readFiles(root, current = '') {
33
31
  }
34
32
  return files;
35
33
  }
36
- function viteCommand(projectDir) {
37
- const command = join(projectDir, 'node_modules', '.bin', 'vite');
38
- if (!existsSync(command))
39
- throw new TypeError('Vite frontend requires a project-local vite binary.');
40
- return command;
41
- }
42
- function run(command, args, cwd, onLog) {
43
- return new Promise((resolve, reject) => {
44
- const child = spawn(command, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
45
- let output = '';
46
- const collect = (buffer) => {
47
- const value = buffer.toString();
48
- output += value;
49
- for (const line of value.split('\n'))
50
- if (line.trim() !== '')
51
- onLog?.(line);
52
- };
53
- child.stdout?.on('data', collect);
54
- child.stderr?.on('data', collect);
55
- child.on('error', reject);
56
- child.on('exit', (code) => code === 0
57
- ? resolve()
58
- : reject(new Error(`Vite failed (code ${code ?? 1}):\n${output.slice(-1500)}`)));
59
- });
60
- }
@@ -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.issuer).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,8 @@
1
+ /** Build a Vite frontend in an isolated process and report its authored inputs. */
2
+ export declare function buildViteFrontend(input: {
3
+ readonly projectDir: string;
4
+ readonly output: string;
5
+ readonly config?: string;
6
+ readonly recordSource?: (path: string) => void;
7
+ readonly signal: AbortSignal;
8
+ }): Promise<void>;
@@ -0,0 +1,32 @@
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
+ /** Build a Vite frontend in an isolated process and report its authored inputs. */
7
+ export async function buildViteFrontend(input) {
8
+ const parent = dirname(input.output);
9
+ await mkdir(parent, { recursive: true });
10
+ const scratch = await mkdtemp(join(parent, 'vite-run-'));
11
+ const evidence = join(scratch, 'sources.json');
12
+ try {
13
+ await runViteProcess({
14
+ cwd: input.projectDir,
15
+ arguments: [runnerPath(), input.output, evidence, input.config ?? ''],
16
+ signal: input.signal,
17
+ });
18
+ const sources = JSON.parse(await readFile(evidence, 'utf8'));
19
+ if (!Array.isArray(sources) || sources.some((path) => typeof path !== 'string')) {
20
+ throw new TypeError('Vite preparation source evidence is invalid.');
21
+ }
22
+ for (const path of sources)
23
+ input.recordSource?.(path);
24
+ }
25
+ finally {
26
+ await rm(scratch, { recursive: true, force: true });
27
+ }
28
+ }
29
+ function runnerPath() {
30
+ const built = fileURLToPath(new URL('./vite.runner.js', import.meta.url));
31
+ return existsSync(built) ? built : fileURLToPath(new URL('./vite.runner.ts', import.meta.url));
32
+ }
@@ -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,120 @@
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] = 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
+ let publicDir = false;
23
+ let environment = {
24
+ directory: false,
25
+ mode: 'production',
26
+ };
27
+ const config = configured === '' || configured === undefined ? undefined : configured;
28
+ await vite.build({
29
+ logLevel: 'silent',
30
+ ...(config === undefined
31
+ ? {}
32
+ : { configFile: isAbsolute(config) ? config : resolve(projectDir, config) }),
33
+ build: { outDir: output, emptyOutDir: true },
34
+ plugins: [
35
+ {
36
+ name: 'astrale:preparation-sources',
37
+ configResolved(value) {
38
+ publicDir = value.publicDir;
39
+ environment = { directory: value.envDir, mode: value.mode };
40
+ for (const path of value.configFileDependencies)
41
+ sources.add(path);
42
+ },
43
+ transform(_source, id) {
44
+ const path = physicalModulePath(id);
45
+ if (path !== undefined)
46
+ sources.add(path);
47
+ },
48
+ },
49
+ ],
50
+ });
51
+ for (const path of configCandidates(projectDir, config))
52
+ sources.add(path);
53
+ for (const path of environmentCandidates(environment))
54
+ sources.add(path);
55
+ if (publicDir !== false)
56
+ await collectTree(publicDir, sources);
57
+ await writeFile(evidence, JSON.stringify([...sources]));
58
+ }
59
+ function environmentCandidates(input) {
60
+ const directory = input.directory;
61
+ if (directory === false)
62
+ return [];
63
+ return ['.env', '.env.local', `.env.${input.mode}`, `.env.${input.mode}.local`].map((name) => join(directory, name));
64
+ }
65
+ function physicalModulePath(id) {
66
+ if (id.startsWith('\0'))
67
+ return undefined;
68
+ const path = id.replace(/[?#].*$/u, '');
69
+ if (path.length === 0)
70
+ return undefined;
71
+ if (path.startsWith('file:')) {
72
+ try {
73
+ return fileURLToPath(path);
74
+ }
75
+ catch {
76
+ return undefined;
77
+ }
78
+ }
79
+ return isAbsolute(path) ? path : undefined;
80
+ }
81
+ async function projectVite(projectDir) {
82
+ try {
83
+ const manifest = createRequire(join(projectDir, 'package.json')).resolve('vite/package.json');
84
+ const entry = createRequire(manifest).resolve('vite');
85
+ return (await import(pathToFileURL(entry).href));
86
+ }
87
+ catch (cause) {
88
+ throw new TypeError('Vite frontend requires a project-local Vite package.', { cause });
89
+ }
90
+ }
91
+ function configCandidates(projectDir, configured) {
92
+ if (configured !== undefined) {
93
+ return [isAbsolute(configured) ? configured : resolve(projectDir, configured)];
94
+ }
95
+ return CONFIG_FILES.map((name) => join(projectDir, name));
96
+ }
97
+ async function collectTree(path, output) {
98
+ output.add(path);
99
+ let entries;
100
+ try {
101
+ entries = await readdir(path, { withFileTypes: true });
102
+ }
103
+ catch (cause) {
104
+ if (errorCode(cause) === 'ENOENT')
105
+ return;
106
+ throw cause;
107
+ }
108
+ for (const entry of entries) {
109
+ const child = join(path, entry.name);
110
+ if (entry.isDirectory())
111
+ await collectTree(child, output);
112
+ else if (entry.isFile())
113
+ output.add(child);
114
+ }
115
+ }
116
+ function errorCode(input) {
117
+ return input !== null && typeof input === 'object' && 'code' in input
118
+ ? String(Reflect.get(input, 'code'))
119
+ : undefined;
120
+ }