@astrale-os/adapter-cloudflare 0.5.0-beta.149 → 0.5.0-beta.150
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.
- package/dist/build/artifact.js +4 -4
- package/dist/build/frontend/files.d.ts +1 -1
- package/dist/build/frontend/files.js +3 -3
- package/dist/build/frontend/vite.d.ts +1 -1
- package/dist/build/frontend/vite.js +3 -3
- package/dist/build/frontend/vite.runner.js +17 -10
- package/dist/build/worker/codegen/service.d.ts +1 -1
- package/dist/build/worker/codegen/service.js +1 -1
- package/dist/build/worker/execution.prepare.d.ts +1 -1
- package/dist/build/worker/execution.prepare.js +2 -2
- package/dist/worker/entry.d.ts +2 -2
- package/dist/worker/entry.js +2 -2
- package/dist/worker/frontend.d.ts +1 -1
- package/dist/worker/frontend.js +19 -12
- package/package.json +2 -2
package/dist/build/artifact.js
CHANGED
|
@@ -29,9 +29,9 @@ async function prepare(params, context, direct) {
|
|
|
29
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
|
-
// `frontend` stays the served file list every branch below already reads; `
|
|
33
|
-
// alongside it into the Worker, where it decides which of those files
|
|
34
|
-
const { files: frontend,
|
|
32
|
+
// `frontend` stays the served file list every branch below already reads; `addressed` rides
|
|
33
|
+
// alongside it into the Worker, where it decides which of those files a browser may retain.
|
|
34
|
+
const { files: frontend, addressed } = await frontendFiles({
|
|
35
35
|
frontend: context.build.frontend,
|
|
36
36
|
projectDir: context.projectDir,
|
|
37
37
|
output: join(directory, 'frontend'),
|
|
@@ -47,7 +47,7 @@ async function prepare(params, context, direct) {
|
|
|
47
47
|
configuredIssuer,
|
|
48
48
|
router,
|
|
49
49
|
...(frontend.length === 0 ? {} : { frontendRevision: frontendRevision(frontend) }),
|
|
50
|
-
...(
|
|
50
|
+
...(addressed.length === 0 ? {} : { frontendAddressed: addressed }),
|
|
51
51
|
});
|
|
52
52
|
const releaseConfig = generateWranglerConfig({
|
|
53
53
|
workerName: name,
|
|
@@ -8,7 +8,7 @@ export interface FrontendOutput {
|
|
|
8
8
|
* A frontend Astrale does not build leaves this empty, which is what keeps the absence of evidence
|
|
9
9
|
* indistinguishable from evidence of mutability.
|
|
10
10
|
*/
|
|
11
|
-
readonly
|
|
11
|
+
readonly addressed: readonly string[];
|
|
12
12
|
}
|
|
13
13
|
export declare function frontendFiles(input: {
|
|
14
14
|
readonly frontend: Build['frontend'];
|
|
@@ -5,9 +5,9 @@ import { buildViteFrontend } from './vite.js';
|
|
|
5
5
|
export async function frontendFiles(input) {
|
|
6
6
|
const frontend = input.frontend;
|
|
7
7
|
if (frontend === undefined)
|
|
8
|
-
return { files: [],
|
|
8
|
+
return { files: [], addressed: [] };
|
|
9
9
|
if (frontend.source.kind === 'external') {
|
|
10
|
-
return { files: frontendBuild.files(frontend, []),
|
|
10
|
+
return { files: frontendBuild.files(frontend, []), addressed: [] };
|
|
11
11
|
}
|
|
12
12
|
await rm(input.output, { recursive: true, force: true });
|
|
13
13
|
const built = await buildViteFrontend({
|
|
@@ -21,7 +21,7 @@ export async function frontendFiles(input) {
|
|
|
21
21
|
// A name the bundler reported but that never reached disk is not a served path; keeping only the
|
|
22
22
|
// intersection means the evidence describes exactly what this deployment will answer with.
|
|
23
23
|
const served = new Set(files.map((file) => file.path));
|
|
24
|
-
return { files,
|
|
24
|
+
return { files, addressed: built.addressed.filter((path) => served.has(path)) };
|
|
25
25
|
}
|
|
26
26
|
async function readFiles(root, current = '') {
|
|
27
27
|
const entries = await readdir(join(root, current), { withFileTypes: true });
|
|
@@ -26,11 +26,11 @@ export async function buildViteFrontend(input) {
|
|
|
26
26
|
}
|
|
27
27
|
for (const path of sources)
|
|
28
28
|
input.recordSource?.(path);
|
|
29
|
-
const
|
|
30
|
-
if (!Array.isArray(
|
|
29
|
+
const named = JSON.parse(await readFile(addressed, 'utf8'));
|
|
30
|
+
if (!Array.isArray(named) || named.some((path) => typeof path !== 'string')) {
|
|
31
31
|
throw new TypeError('Vite preparation output evidence is invalid.');
|
|
32
32
|
}
|
|
33
|
-
return {
|
|
33
|
+
return { addressed: Object.freeze([...named]) };
|
|
34
34
|
}
|
|
35
35
|
finally {
|
|
36
36
|
await rm(scratch, { recursive: true, force: true });
|
|
@@ -19,7 +19,7 @@ async function main() {
|
|
|
19
19
|
const projectDir = process.cwd();
|
|
20
20
|
const vite = await projectVite(projectDir);
|
|
21
21
|
const sources = new Set();
|
|
22
|
-
const
|
|
22
|
+
const named = new Set();
|
|
23
23
|
let publicDir = false;
|
|
24
24
|
let environment = {
|
|
25
25
|
directory: false,
|
|
@@ -47,13 +47,13 @@ async function main() {
|
|
|
47
47
|
sources.add(path);
|
|
48
48
|
},
|
|
49
49
|
// The bundler is the only party that knows which outputs it content-addressed: a served path
|
|
50
|
-
// is safe to
|
|
51
|
-
//
|
|
52
|
-
//
|
|
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
53
|
generateBundle(options, bundle) {
|
|
54
54
|
for (const [fileName, entry] of Object.entries(bundle)) {
|
|
55
55
|
if (addressedName(options, entry))
|
|
56
|
-
|
|
56
|
+
named.add(fileName);
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
59
|
},
|
|
@@ -67,16 +67,23 @@ async function main() {
|
|
|
67
67
|
await collectTree(publicDir, sources);
|
|
68
68
|
await writeFile(evidence, JSON.stringify([...sources]));
|
|
69
69
|
if (addressed !== undefined)
|
|
70
|
-
await writeFile(addressed, JSON.stringify([...
|
|
70
|
+
await writeFile(addressed, JSON.stringify([...named].sort()));
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
73
|
-
* Whether this output's name carries a content hash
|
|
73
|
+
* Whether this output's name carries a content hash.
|
|
74
74
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
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.
|
|
78
83
|
*/
|
|
79
84
|
function addressedName(options, entry) {
|
|
85
|
+
if (typeof entry.name !== 'string')
|
|
86
|
+
return false;
|
|
80
87
|
const template = entry.type === 'asset'
|
|
81
88
|
? options.assetFileNames
|
|
82
89
|
: entry.isEntry === true
|
|
@@ -6,7 +6,7 @@ export interface ExecutionCodegenOptions {
|
|
|
6
6
|
readonly workerSpecifier?: string;
|
|
7
7
|
readonly build: Build;
|
|
8
8
|
readonly frontendRevision?: string;
|
|
9
|
-
readonly
|
|
9
|
+
readonly frontendAddressed?: readonly string[];
|
|
10
10
|
readonly maximumRequestBytes?: number;
|
|
11
11
|
readonly invocationTimeoutMs?: number;
|
|
12
12
|
readonly identityIssuer?: string;
|
|
@@ -36,7 +36,7 @@ type WorkerEnv = CloudflareEnv & CloudflareAdapterBindings & {
|
|
|
36
36
|
type RuntimeEnv = Omit<WorkerEnv, 'ASTRALE_SIGNING_IDENTITY'>
|
|
37
37
|
${routerPredicate}
|
|
38
38
|
export default generatedCloudflareWorkerEntry<WorkerEnv, RuntimeEnv>()({
|
|
39
|
-
${frontend}${options.frontendRevision === undefined ? '' : ` frontendRevision: ${JSON.stringify(options.frontendRevision)},\n`}${options.
|
|
39
|
+
${frontend}${options.frontendRevision === undefined ? '' : ` frontendRevision: ${JSON.stringify(options.frontendRevision)},\n`}${options.frontendAddressed === undefined ? '' : ` frontendAddressed: ${JSON.stringify(options.frontendAddressed)},\n`} load: async () => ({
|
|
40
40
|
runtime: (await import(${JSON.stringify(options.runtimeSpecifier)})).default,
|
|
41
41
|
material: ${JSON.stringify(generated)} as unknown as GeneratedBuildMaterial,
|
|
42
42
|
}),
|
|
@@ -10,7 +10,7 @@ export declare function prepareExecution(input: {
|
|
|
10
10
|
readonly name: string;
|
|
11
11
|
readonly direct: boolean;
|
|
12
12
|
readonly frontendRevision?: string;
|
|
13
|
-
readonly
|
|
13
|
+
readonly frontendAddressed?: readonly string[];
|
|
14
14
|
readonly configuredIssuer?: string;
|
|
15
15
|
readonly router: ReturnType<typeof resolveRouter>;
|
|
16
16
|
}): Promise<{
|
|
@@ -25,9 +25,9 @@ export async function prepareExecution(input) {
|
|
|
25
25
|
workerSpecifier,
|
|
26
26
|
build: input.context.build,
|
|
27
27
|
...(input.frontendRevision === undefined ? {} : { frontendRevision: input.frontendRevision }),
|
|
28
|
-
...(input.
|
|
28
|
+
...(input.frontendAddressed === undefined
|
|
29
29
|
? {}
|
|
30
|
-
: {
|
|
30
|
+
: { frontendAddressed: input.frontendAddressed }),
|
|
31
31
|
...(maximumRequestBytes === undefined ? {} : { maximumRequestBytes }),
|
|
32
32
|
...(invocationTimeoutMs === undefined ? {} : { invocationTimeoutMs }),
|
|
33
33
|
...(input.params.tokenExchange === undefined
|
package/dist/worker/entry.d.ts
CHANGED
|
@@ -20,8 +20,8 @@ export interface CloudflareWorkerConfig<RuntimeValue extends Runtime, Bindings e
|
|
|
20
20
|
readonly frontend?: BuildMaterial['frontend'];
|
|
21
21
|
/** Generated browser-file identity used only for deployment propagation checks. */
|
|
22
22
|
readonly frontendRevision?: string;
|
|
23
|
-
/** Served frontend paths the bundler content-addressed; absent means none may be
|
|
24
|
-
readonly
|
|
23
|
+
/** Served frontend paths the bundler content-addressed; absent means none may be retained. */
|
|
24
|
+
readonly frontendAddressed?: readonly string[];
|
|
25
25
|
/** Load the exact Runtime and Build once, after the isolate has accepted a request. */
|
|
26
26
|
readonly load: () => CloudflareWorkerSource<RuntimeValue, Material> | Promise<CloudflareWorkerSource<RuntimeValue, Material>>;
|
|
27
27
|
readonly privateKey: JsonWebKey | ((bindings: Bindings) => JsonWebKey);
|
package/dist/worker/entry.js
CHANGED
|
@@ -87,13 +87,13 @@ function createCloudflareWorkerEntry(restore) {
|
|
|
87
87
|
}
|
|
88
88
|
currentPublicUrl = publicUrl;
|
|
89
89
|
if (config.frontend !== undefined) {
|
|
90
|
-
const frontend = await frontendAsset(config.frontend, bindings.ASSETS, url, request, config.frontendRevision, config.
|
|
90
|
+
const frontend = await frontendAsset(config.frontend, bindings.ASSETS, url, request, config.frontendRevision, config.frontendAddressed);
|
|
91
91
|
if (frontend !== undefined)
|
|
92
92
|
return withProviderVersion(frontend, bindings);
|
|
93
93
|
}
|
|
94
94
|
const { application, build } = await realize();
|
|
95
95
|
if (config.frontend === undefined) {
|
|
96
|
-
const frontend = await frontendAsset(build.frontend, bindings.ASSETS, url, request, config.frontendRevision, config.
|
|
96
|
+
const frontend = await frontendAsset(build.frontend, bindings.ASSETS, url, request, config.frontendRevision, config.frontendAddressed);
|
|
97
97
|
if (frontend !== undefined)
|
|
98
98
|
return withProviderVersion(frontend, bindings);
|
|
99
99
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { BuildMaterial } from '@astrale-os/sdk/deployment/build/material';
|
|
2
2
|
import type { Fetcher } from './entry.js';
|
|
3
3
|
/** Serve public frontend files without realizing the Application's execution Runtime. */
|
|
4
|
-
export declare function frontendAsset(frontend: BuildMaterial['frontend'], assets: Fetcher | undefined, url: URL, request: Request, revision?: string,
|
|
4
|
+
export declare function frontendAsset(frontend: BuildMaterial['frontend'], assets: Fetcher | undefined, url: URL, request: Request, revision?: string, addressed?: readonly string[]): Promise<Response | undefined>;
|
package/dist/worker/frontend.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Serve public frontend files without realizing the Application's execution Runtime. */
|
|
2
|
-
export async function frontendAsset(frontend, assets, url, request, revision,
|
|
2
|
+
export async function frontendAsset(frontend, assets, url, request, revision, addressed) {
|
|
3
3
|
if (frontend === undefined || frontend.source.kind === 'external')
|
|
4
4
|
return undefined;
|
|
5
5
|
if (reserved(url.pathname) || (request.method !== 'GET' && request.method !== 'HEAD')) {
|
|
@@ -19,13 +19,13 @@ export async function frontendAsset(frontend, assets, url, request, revision, im
|
|
|
19
19
|
if (route === undefined) {
|
|
20
20
|
const response = await assets.fetch(request);
|
|
21
21
|
return assetPath || response.status !== 404
|
|
22
|
-
? publicAsset(response, revision,
|
|
22
|
+
? publicAsset(response, revision, retainable(url, addressed))
|
|
23
23
|
: undefined;
|
|
24
24
|
}
|
|
25
25
|
if (route.path !== url.pathname) {
|
|
26
26
|
const response = await assets.fetch(request);
|
|
27
27
|
if (response.status !== 404)
|
|
28
|
-
return publicAsset(response, revision,
|
|
28
|
+
return publicAsset(response, revision, retainable(url, addressed));
|
|
29
29
|
}
|
|
30
30
|
// A route serves its document, never the requested path, so the document decides: it is named by
|
|
31
31
|
// the route rather than by its bytes, and `publicAsset` already refuses to retain it.
|
|
@@ -38,12 +38,12 @@ export async function frontendAsset(frontend, assets, url, request, revision, im
|
|
|
38
38
|
* path under the bundle's own directory may still have been copied there verbatim from `public/`,
|
|
39
39
|
* and a content-addressed output may sit anywhere the project's configuration puts it.
|
|
40
40
|
*/
|
|
41
|
-
function
|
|
42
|
-
if (
|
|
41
|
+
function retainable(url, addressed) {
|
|
42
|
+
if (addressed === undefined || addressed.length === 0)
|
|
43
43
|
return false;
|
|
44
|
-
return
|
|
44
|
+
return addressed.includes(decodeURIComponent(url.pathname).replace(/^\//u, ''));
|
|
45
45
|
}
|
|
46
|
-
function publicAsset(response, revision,
|
|
46
|
+
function publicAsset(response, revision, addressed) {
|
|
47
47
|
// Plain Views have an opaque sandbox origin. Their modules, CSS and fonts still need
|
|
48
48
|
// public reads; this neither enables credentials nor changes execution endpoint CORS.
|
|
49
49
|
const readable = new Response(response.body, response);
|
|
@@ -51,15 +51,22 @@ function publicAsset(response, revision, immutable) {
|
|
|
51
51
|
readable.headers.set('Access-Control-Allow-Origin', '*');
|
|
52
52
|
if (revision !== undefined)
|
|
53
53
|
readable.headers.set('X-Astrale-Frontend', revision);
|
|
54
|
+
// A 304 states that the copy the browser already holds is current. Answering it with `no-store`
|
|
55
|
+
// would tell that browser to discard what it was just told to keep, so the next visit downloads
|
|
56
|
+
// the file again and every revalidation costs a full transfer: worse than never caching at all.
|
|
57
|
+
const validated = response.status === 304;
|
|
54
58
|
// Documents select hashed assets. Do not retain stale documents or transient asset misses.
|
|
55
|
-
if (!response.ok
|
|
59
|
+
if ((!response.ok && !validated) ||
|
|
60
|
+
response.headers.get('Content-Type')?.split(';')[0] === 'text/html') {
|
|
56
61
|
readable.headers.set('Cache-Control', 'no-store');
|
|
57
62
|
}
|
|
58
|
-
else if (
|
|
63
|
+
else if (addressed) {
|
|
59
64
|
// The bytes behind this name can never change, so a revalidation could only ever confirm what
|
|
60
|
-
// the name already states.
|
|
61
|
-
//
|
|
62
|
-
|
|
65
|
+
// the name already states. `immutable` is deliberately absent: it would also suppress the
|
|
66
|
+
// revalidation a reload performs, which is the one remedy a reader has if this were ever
|
|
67
|
+
// claimed wrongly. Anything not proved content-addressed keeps the platform default, which
|
|
68
|
+
// revalidates and is therefore never wrong, only slower.
|
|
69
|
+
readable.headers.set('Cache-Control', 'public, max-age=31536000');
|
|
63
70
|
}
|
|
64
71
|
return readable;
|
|
65
72
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrale-os/adapter-cloudflare",
|
|
3
|
-
"version": "0.5.0-beta.
|
|
3
|
+
"version": "0.5.0-beta.150",
|
|
4
4
|
"description": "Deploy an Astrale application through Cloudflare Workers",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"adapter",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"registry": "https://registry.npmjs.org/"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@astrale-os/sdk": "0.5.0-beta.
|
|
56
|
+
"@astrale-os/sdk": "0.5.0-beta.147",
|
|
57
57
|
"@noble/hashes": "2.3.0",
|
|
58
58
|
"hono": "^4.6.20",
|
|
59
59
|
"jose": "^6.1.3",
|