@astrale-os/adapter-cloudflare 0.5.0-beta.24 → 0.5.0-beta.26

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.
@@ -5,7 +5,7 @@ import { dirname, join, relative, resolve } from 'node:path';
5
5
  import { pathToFileURL } from 'node:url';
6
6
  import { SIGNING_IDENTITY_BINDING } from '../identity/binding.js';
7
7
  import { releaseNamespace, releaseWorkerName } from '../release/index.js';
8
- import { generateExecutionEntry } from './codegen/execution.js';
8
+ import { generateExecutionEntry, generateExecutionService } from './codegen/execution.js';
9
9
  import { generateGatewayEntry } from './codegen/gateway.js';
10
10
  import { generateWranglerConfig } from './codegen/wrangler.js';
11
11
  import { deploymentAddressing, resolveRouter, workerName } from './configuration.js';
@@ -13,6 +13,9 @@ import { frontendFiles } from './frontend-v1.js';
13
13
  import { qualifyWorkerBundle } from './qualification.js';
14
14
  import { runWranglerBundle } from './wrangler-cli.js';
15
15
  const encoder = new TextEncoder();
16
+ const SERVICE_MODULE = 'service.mjs';
17
+ const RELEASE_ENTRY = 'bundle/index.mjs';
18
+ const RELEASE_SERVICE = `bundle/${SERVICE_MODULE}`;
16
19
  /** Prepare one revision Service and its stateless gateway without provider mutation. */
17
20
  export async function artifact(params, context) {
18
21
  const stateDirectory = join(context.projectDir, '.astrale');
@@ -26,7 +29,7 @@ export async function artifact(params, context) {
26
29
  const namespace = releaseNamespace(name);
27
30
  const target = releaseWorkerName(revision);
28
31
  const configuredIssuer = deploymentAddressing(params);
29
- const source = generateExecutionEntry({
32
+ const serviceSource = generateExecutionService({
30
33
  origin: context.build.schema.compiled.root.origin,
31
34
  runtimeSpecifier,
32
35
  material: material(context.build),
@@ -41,8 +44,40 @@ export async function artifact(params, context) {
41
44
  },
42
45
  }),
43
46
  });
47
+ const serviceSourcePath = join(directory, 'service.gen.ts');
48
+ await writeFile(serviceSourcePath, serviceSource);
49
+ const serviceConfigPath = join(directory, 'wrangler.service.prepare.jsonc');
50
+ await writeFile(serviceConfigPath, generateWranglerConfig({
51
+ workerName: name,
52
+ main: './service.gen.ts',
53
+ vars: {
54
+ ...params.vars,
55
+ ...(configuredIssuer === undefined ? {} : { WORKER_URL: configuredIssuer }),
56
+ },
57
+ secretBindings: [SIGNING_IDENTITY_BINDING],
58
+ ...(router === undefined
59
+ ? {}
60
+ : { router: { binding: router.binding, service: router.service } }),
61
+ ...(params.wrangler === undefined ? {} : { wrangler: params.wrangler }),
62
+ selfBinding: false,
63
+ }));
64
+ const serviceOutput = join(directory, 'dist-service');
65
+ await rm(serviceOutput, { recursive: true, force: true });
66
+ const emittedService = await runWranglerBundle({
67
+ projectDir: context.projectDir,
68
+ configPath: serviceConfigPath,
69
+ outDir: serviceOutput,
70
+ });
71
+ await qualifyWorkerBundle({
72
+ metafilePath: emittedService.metafilePath,
73
+ inputRoot: dirname(serviceConfigPath),
74
+ });
75
+ await admitBundle(emittedService.bundlePath);
76
+ const serviceBundle = await readFile(emittedService.bundlePath);
77
+ await mkdir(dirname(join(directory, RELEASE_SERVICE)), { recursive: true });
78
+ await writeFile(join(directory, RELEASE_SERVICE), serviceBundle);
44
79
  const sourcePath = join(directory, 'worker.gen.ts');
45
- await writeFile(sourcePath, source);
80
+ await writeFile(sourcePath, generateExecutionEntry(`./${RELEASE_SERVICE}`));
46
81
  const configPath = join(directory, 'wrangler.prepare.jsonc');
47
82
  await writeFile(configPath, generateWranglerConfig({
48
83
  workerName: name,
@@ -52,6 +87,7 @@ export async function artifact(params, context) {
52
87
  ...(configuredIssuer === undefined ? {} : { WORKER_URL: configuredIssuer }),
53
88
  },
54
89
  secretBindings: [SIGNING_IDENTITY_BINDING],
90
+ modules: { baseDir: '.', globs: [RELEASE_SERVICE] },
55
91
  ...(router === undefined
56
92
  ? {}
57
93
  : { router: { binding: router.binding, service: router.service } }),
@@ -100,12 +136,13 @@ export async function artifact(params, context) {
100
136
  const gatewayBundle = await readFile(gateway.bundlePath);
101
137
  const releaseConfig = generateWranglerConfig({
102
138
  workerName: target,
103
- main: './bundle/index.mjs',
139
+ main: `./${RELEASE_ENTRY}`,
104
140
  vars: {
105
141
  ...params.vars,
106
142
  ...(configuredIssuer === undefined ? {} : { WORKER_URL: configuredIssuer }),
107
143
  },
108
144
  secretBindings: [SIGNING_IDENTITY_BINDING],
145
+ modules: { baseDir: '.', globs: [RELEASE_SERVICE] },
109
146
  ...(context.build.frontend?.source.kind === 'vite'
110
147
  ? {
111
148
  frontend: {
@@ -131,7 +168,8 @@ export async function artifact(params, context) {
131
168
  const files = [
132
169
  { path: 'wrangler.release.jsonc', content: encoder.encode(releaseConfig) },
133
170
  { path: 'wrangler.gateway.jsonc', content: encoder.encode(gatewayConfig) },
134
- { path: 'bundle/index.mjs', content: new Uint8Array(bundle) },
171
+ { path: RELEASE_ENTRY, content: new Uint8Array(bundle) },
172
+ { path: RELEASE_SERVICE, content: new Uint8Array(serviceBundle) },
135
173
  { path: 'gateway/index.mjs', content: new Uint8Array(gatewayBundle) },
136
174
  ...frontend.map((file) => ({
137
175
  path: `frontend/${file.path}`,
@@ -139,7 +177,7 @@ export async function artifact(params, context) {
139
177
  })),
140
178
  ];
141
179
  return Object.freeze({
142
- entrypoint: 'bundle/index.mjs',
180
+ entrypoint: RELEASE_ENTRY,
143
181
  files: Object.freeze(files),
144
182
  addressing: configuredIssuer === undefined
145
183
  ? Object.freeze({ kind: 'provider-assigned', claim: name })
@@ -10,4 +10,6 @@ export interface ExecutionCodegenOptions {
10
10
  readonly minLabels: number;
11
11
  };
12
12
  }
13
- export declare function generateExecutionEntry(options: ExecutionCodegenOptions): string;
13
+ export declare function generateExecutionService(options: ExecutionCodegenOptions): string;
14
+ /** Generate the isolate-startup entry that realizes the heavy Service on first request. */
15
+ export declare function generateExecutionEntry(serviceSpecifier: string): string;
@@ -1,4 +1,4 @@
1
- export function generateExecutionEntry(options) {
1
+ export function generateExecutionService(options) {
2
2
  const router = options.router;
3
3
  const routerPredicate = router === undefined
4
4
  ? ''
@@ -22,18 +22,18 @@ import type { BuildMaterial } from '@astrale-os/sdk/deployment/build'
22
22
  import type { CloudflareAdapterBindings } from '@astrale-os/adapter-cloudflare/worker'
23
23
 
24
24
  import { cloudflareWorkerEntry } from '@astrale-os/adapter-cloudflare/worker'
25
- import runtime from ${JSON.stringify(options.runtimeSpecifier)}
26
25
  import { decodeSigningIdentity, runtimeBindings } from '@astrale-os/adapter-cloudflare/worker'
27
26
 
28
27
  type WorkerEnv = CloudflareEnv & CloudflareAdapterBindings & {
29
28
  readonly ASTRALE_SIGNING_IDENTITY: string
30
29
  }
31
30
  type RuntimeEnv = Omit<WorkerEnv, 'ASTRALE_SIGNING_IDENTITY'>
32
- const MATERIAL = ${JSON.stringify(options.material)} as const satisfies BuildMaterial
33
31
  ${routerPredicate}
34
32
  export default cloudflareWorkerEntry<WorkerEnv, RuntimeEnv>()({
35
- runtime,
36
- material: MATERIAL,
33
+ load: async () => ({
34
+ runtime: (await import(${JSON.stringify(options.runtimeSpecifier)})).default,
35
+ material: ${JSON.stringify(options.material)} as const satisfies BuildMaterial,
36
+ }),
37
37
  privateKey: (env) => decodeSigningIdentity(env.ASTRALE_SIGNING_IDENTITY),
38
38
  resolveEnvironment: runtimeBindings,
39
39
  resolveUrl: (env, requestOrigin) => env.WORKER_URL ?? env.IDENTITY_ISS ?? requestOrigin,
@@ -42,3 +42,31 @@ export default cloudflareWorkerEntry<WorkerEnv, RuntimeEnv>()({
42
42
  })
43
43
  `;
44
44
  }
45
+ /** Generate the isolate-startup entry that realizes the heavy Service on first request. */
46
+ export function generateExecutionEntry(serviceSpecifier) {
47
+ return `// AUTO-GENERATED by @astrale-os/adapter-cloudflare — do not edit.
48
+ import type { CloudflareWorker } from '@astrale-os/adapter-cloudflare/worker'
49
+
50
+ const SERVICE_SPECIFIER = ${JSON.stringify(serviceSpecifier)}
51
+ let servicePromise: Promise<CloudflareWorker<CloudflareEnv>> | undefined
52
+
53
+ export default {
54
+ async fetch(request: Request, environment: CloudflareEnv, context: ExecutionContext) {
55
+ return (await service()).fetch(request, environment, context)
56
+ },
57
+ }
58
+
59
+ function service(): Promise<CloudflareWorker<CloudflareEnv>> {
60
+ return (servicePromise ??= import(SERVICE_SPECIFIER).then((loaded: unknown) => {
61
+ if (loaded === null || typeof loaded !== 'object') invalid()
62
+ const worker = Reflect.get(loaded, 'default') as unknown
63
+ if (worker === null || typeof worker !== 'object' || typeof Reflect.get(worker, 'fetch') !== 'function') invalid()
64
+ return worker as CloudflareWorker<CloudflareEnv>
65
+ }))
66
+ }
67
+
68
+ function invalid(): never {
69
+ throw new TypeError('Cloudflare execution Service module is invalid.')
70
+ }
71
+ `;
72
+ }
@@ -27,6 +27,11 @@ export interface WranglerCodegenOptions {
27
27
  vars?: Record<string, string>;
28
28
  /** Secret binding names required by generated Worker code. */
29
29
  secretBindings?: readonly string[];
30
+ /** JavaScript modules retained outside the startup entry and loaded on demand. */
31
+ modules?: {
32
+ readonly baseDir: string;
33
+ readonly globs: readonly string[];
34
+ };
30
35
  /** Add the SELF service binding (autobinding). */
31
36
  selfBinding: boolean;
32
37
  /** Bind the stable gateway to its private revision namespace. */
@@ -60,6 +60,17 @@ export function generateWranglerConfig(opts) {
60
60
  if (opts.secretBindings && opts.secretBindings.length > 0) {
61
61
  config.secrets = { required: [...opts.secretBindings] };
62
62
  }
63
+ if (opts.modules !== undefined) {
64
+ config.find_additional_modules = true;
65
+ config.base_dir = opts.modules.baseDir;
66
+ config.rules = [
67
+ {
68
+ type: 'ESModule',
69
+ globs: [...opts.modules.globs],
70
+ fallthrough: true,
71
+ },
72
+ ];
73
+ }
63
74
  if (opts.releases !== undefined) {
64
75
  config.dispatch_namespaces = [opts.releases];
65
76
  }
@@ -75,6 +86,8 @@ const ADAPTER_OWNED_KEYS = [
75
86
  'compatibility_date',
76
87
  'version_metadata',
77
88
  'secrets',
89
+ 'find_additional_modules',
90
+ 'base_dir',
78
91
  ];
79
92
  /**
80
93
  * Deep-merge the dev's raw wrangler overlay over the generated config, after
@@ -84,9 +84,9 @@ export interface WranglerDeploymentStatus {
84
84
  /** Admit Wrangler's current-deployment JSON before it becomes provider evidence. */
85
85
  export declare function admitWranglerDeploymentStatus(output: string): WranglerDeploymentStatus;
86
86
  /**
87
- * Build the single-module worker bundle WITHOUT deploying — `wrangler deploy
87
+ * Build the Worker entry module WITHOUT deploying — `wrangler deploy
88
88
  * --dry-run --outdir` (no Cloudflare auth required). Returns the path of the
89
- * bundled .js module (the managed adapter publishes its bytes).
89
+ * bundled entry .js module (the managed adapter publishes its bytes).
90
90
  */
91
91
  export declare function runWranglerBundle(args: {
92
92
  projectDir: string;
@@ -400,9 +400,9 @@ async function stopChild(child) {
400
400
  });
401
401
  }
402
402
  /**
403
- * Build the single-module worker bundle WITHOUT deploying — `wrangler deploy
403
+ * Build the Worker entry module WITHOUT deploying — `wrangler deploy
404
404
  * --dry-run --outdir` (no Cloudflare auth required). Returns the path of the
405
- * bundled .js module (the managed adapter publishes its bytes).
405
+ * bundled entry .js module (the managed adapter publishes its bytes).
406
406
  */
407
407
  export async function runWranglerBundle(args) {
408
408
  await runWranglerTypes(args);
@@ -17,8 +17,8 @@ export interface CloudflareAdapterBindings {
17
17
  };
18
18
  }
19
19
  export interface CloudflareWorkerConfig<RuntimeValue extends Runtime, Bindings extends CloudflareAdapterBindings, Environment> {
20
- readonly runtime: RuntimeValue;
21
- readonly material: BuildMaterial;
20
+ /** Load the exact Runtime and Build once, after the isolate has accepted a request. */
21
+ readonly load: () => CloudflareWorkerSource<RuntimeValue> | Promise<CloudflareWorkerSource<RuntimeValue>>;
22
22
  readonly privateKey: JsonWebKey | ((bindings: Bindings) => JsonWebKey);
23
23
  readonly resolveEnvironment?: (bindings: Bindings) => Environment;
24
24
  readonly resolveUrl?: (bindings: Bindings, requestOrigin: string) => string;
@@ -27,6 +27,10 @@ export interface CloudflareWorkerConfig<RuntimeValue extends Runtime, Bindings e
27
27
  readonly routeSubrequest?: (url: URL, bindings: Bindings) => Fetcher | null | undefined;
28
28
  readonly invocationTimeoutMs?: number;
29
29
  }
30
+ export interface CloudflareWorkerSource<RuntimeValue extends Runtime> {
31
+ readonly runtime: RuntimeValue;
32
+ readonly material: BuildMaterial;
33
+ }
30
34
  export interface CloudflareWorker<Bindings> {
31
35
  fetch(request: Request, environment: Bindings, executionContext?: ExecutionContext): Response | Promise<Response>;
32
36
  }
@@ -11,43 +11,47 @@ export function cloudflareWorkerEntry() {
11
11
  const realize = () => {
12
12
  if (realized !== undefined)
13
13
  return realized;
14
- const build = restoreBuild(config.material, config.runtime);
15
- const application = serve({
16
- runtime: config.runtime,
17
- load(environment) {
18
- if (currentIssuer !== environment.issuer) {
19
- throw new TypeError('Cloudflare Worker request origin differs from its current Release.');
20
- }
21
- return assemble(build, addressing(environment.issuer));
22
- },
23
- initialize: (environment) => config.resolveEnvironment === undefined
24
- ? environment.bindings
25
- : config.resolveEnvironment(environment.bindings),
26
- identity(environment) {
27
- return createIdentity({
28
- issuer: issuer.accept(environment.issuer),
29
- subject: build.schema.compiled.root.origin,
30
- privateKey: typeof config.privateKey === 'function'
31
- ? config.privateKey(environment.bindings)
32
- : config.privateKey,
33
- fetch: routedFetch(application, environment, config),
34
- ...(new URL(environment.issuer).protocol === 'http:'
35
- ? { allowInsecureHttp: true }
36
- : {}),
37
- });
38
- },
39
- server: {
40
- health: { probe: { check: () => true } },
41
- limits: {
42
- maximumRequestBytes: 1024 * 1024,
43
- maximumPublicationBytes: 1024 * 1024,
44
- maximumBundleBytes: 8 * 1024 * 1024,
45
- maximumDeliveryBytes: 8 * 1024 * 1024,
46
- invocationTimeoutMs: config.invocationTimeoutMs ?? 30_000,
14
+ realized = Promise.resolve()
15
+ .then(config.load)
16
+ .then((source) => {
17
+ const build = restoreBuild(source.material, source.runtime);
18
+ const application = serve({
19
+ runtime: source.runtime,
20
+ load(environment) {
21
+ if (currentIssuer !== environment.issuer) {
22
+ throw new TypeError('Cloudflare Worker request origin differs from its current Release.');
23
+ }
24
+ return assemble(build, addressing(environment.issuer));
25
+ },
26
+ initialize: (environment) => config.resolveEnvironment === undefined
27
+ ? environment.bindings
28
+ : config.resolveEnvironment(environment.bindings),
29
+ identity(environment) {
30
+ return createIdentity({
31
+ issuer: issuer.accept(environment.issuer),
32
+ subject: build.schema.compiled.root.origin,
33
+ privateKey: typeof config.privateKey === 'function'
34
+ ? config.privateKey(environment.bindings)
35
+ : config.privateKey,
36
+ fetch: routedFetch(application, environment, config),
37
+ ...(new URL(environment.issuer).protocol === 'http:'
38
+ ? { allowInsecureHttp: true }
39
+ : {}),
40
+ });
41
+ },
42
+ server: {
43
+ health: { probe: { check: () => true } },
44
+ limits: {
45
+ maximumRequestBytes: 1024 * 1024,
46
+ maximumPublicationBytes: 1024 * 1024,
47
+ maximumBundleBytes: 8 * 1024 * 1024,
48
+ maximumDeliveryBytes: 8 * 1024 * 1024,
49
+ invocationTimeoutMs: config.invocationTimeoutMs ?? 30_000,
50
+ },
47
51
  },
48
- },
52
+ });
53
+ return Object.freeze({ build, application });
49
54
  });
50
- realized = Object.freeze({ build, application });
51
55
  return realized;
52
56
  };
53
57
  return Object.freeze({
@@ -69,7 +73,7 @@ export function cloudflareWorkerEntry() {
69
73
  return new Response('Request origin differs from the current Release.', { status: 421 });
70
74
  }
71
75
  currentIssuer = issuer;
72
- const { application, build } = realize();
76
+ const { application, build } = await realize();
73
77
  const frontend = await frontendAsset(build.frontend, bindings, url, request);
74
78
  if (frontend !== undefined)
75
79
  return frontend;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/adapter-cloudflare",
3
- "version": "0.5.0-beta.24",
3
+ "version": "0.5.0-beta.26",
4
4
  "description": "Deploy an Astrale application through Cloudflare Workers",
5
5
  "keywords": [
6
6
  "adapter",
@@ -45,7 +45,7 @@
45
45
  "hono": "^4.6.20",
46
46
  "jose": "^6.1.3",
47
47
  "wrangler": "4.123.0",
48
- "@astrale-os/sdk": "^0.5.0-beta.21"
48
+ "@astrale-os/sdk": "^0.5.0-beta.23"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@astrale-os/ox": ">=0.1.0 <1.0.0",