@astrojs/cloudflare 10.2.4 → 10.2.5
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/index.d.ts +4 -1
- package/dist/index.js +4 -4
- package/dist/utils/wasm-module-loader.d.ts +10 -7
- package/dist/utils/wasm-module-loader.js +97 -40
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -42,7 +42,10 @@ export type Options = {
|
|
|
42
42
|
path: string;
|
|
43
43
|
};
|
|
44
44
|
};
|
|
45
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Allow bundling cloudflare worker specific file types
|
|
47
|
+
* https://developers.cloudflare.com/workers/wrangler/bundling/
|
|
48
|
+
*/
|
|
46
49
|
wasmModuleImports?: boolean;
|
|
47
50
|
};
|
|
48
51
|
export default function createIntegration(args?: Options): AstroIntegration;
|
package/dist/index.js
CHANGED
|
@@ -11,9 +11,10 @@ import { createRoutesFile, getParts } from './utils/generate-routes-json.js';
|
|
|
11
11
|
import { setImageConfig } from './utils/image-config.js';
|
|
12
12
|
import { mutateDynamicPageImportsInPlace, mutatePageMapInPlace } from './utils/index.js';
|
|
13
13
|
import { NonServerChunkDetector } from './utils/non-server-chunk-detector.js';
|
|
14
|
-
import {
|
|
14
|
+
import { cloudflareModuleLoader } from './utils/wasm-module-loader.js';
|
|
15
15
|
export default function createIntegration(args) {
|
|
16
16
|
let _config;
|
|
17
|
+
const cloudflareModulePlugin = cloudflareModuleLoader(args?.wasmModuleImports ?? false);
|
|
17
18
|
// Initialize the unused chunk analyzer as a shared state between hooks.
|
|
18
19
|
// The analyzer is used on earlier hooks to collect information about used hooks on a Vite plugin
|
|
19
20
|
// and then later after the full build to clean up unused chunks, so it has to be shared between them.
|
|
@@ -32,9 +33,7 @@ export default function createIntegration(args) {
|
|
|
32
33
|
vite: {
|
|
33
34
|
// load .wasm files as WebAssembly modules
|
|
34
35
|
plugins: [
|
|
35
|
-
|
|
36
|
-
disabled: !args?.wasmModuleImports,
|
|
37
|
-
}),
|
|
36
|
+
cloudflareModulePlugin,
|
|
38
37
|
chunkAnalyzer.getPlugin(),
|
|
39
38
|
{
|
|
40
39
|
name: 'dynamic-imports-analyzer',
|
|
@@ -195,6 +194,7 @@ export default function createIntegration(args) {
|
|
|
195
194
|
}
|
|
196
195
|
},
|
|
197
196
|
'astro:build:done': async ({ pages, routes, dir, logger }) => {
|
|
197
|
+
await cloudflareModulePlugin.afterBuildCompleted(_config);
|
|
198
198
|
const PLATFORM_FILES = ['_headers', '_redirects', '_routes.json'];
|
|
199
199
|
if (_config.base !== '/') {
|
|
200
200
|
for (const file of PLATFORM_FILES) {
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { AstroConfig } from 'astro';
|
|
2
|
+
import type { PluginOption } from 'vite';
|
|
3
|
+
export interface CloudflareModulePluginExtra {
|
|
4
|
+
afterBuildCompleted(config: AstroConfig): Promise<void>;
|
|
5
|
+
}
|
|
2
6
|
/**
|
|
3
|
-
*
|
|
7
|
+
* Enables support for wasm modules within cloudflare pages functions
|
|
8
|
+
*
|
|
9
|
+
* Loads '*.wasm?module' and `*.wasm` imports as WebAssembly modules, which is the only way to load WASM in cloudflare workers.
|
|
4
10
|
* Current proposal for WASM modules: https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration
|
|
5
11
|
* Cloudflare worker WASM from javascript support: https://developers.cloudflare.com/workers/runtime-apis/webassembly/javascript/
|
|
6
|
-
* @param
|
|
7
|
-
* otherwise it will error obscurely in the esbuild and vite builds
|
|
8
|
-
* @param assetsDirectory - the folder name for the assets directory in the build directory. Usually '_astro'
|
|
12
|
+
* @param enabled - if true, load '.wasm' imports as Uint8Arrays, otherwise will throw errors when encountered to clarify that it must be enabled
|
|
9
13
|
* @returns Vite plugin to load WASM tagged with '?module' as a WASM modules
|
|
10
14
|
*/
|
|
11
|
-
export declare function
|
|
12
|
-
|
|
13
|
-
}): NonNullable<AstroConfig['vite']['plugins']>[number];
|
|
15
|
+
export declare function cloudflareModuleLoader(enabled: boolean): PluginOption & CloudflareModulePluginExtra;
|
|
16
|
+
export type ImportType = 'wasm';
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import * as fs from 'node:fs';
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
+
import * as url from 'node:url';
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
+
* Enables support for wasm modules within cloudflare pages functions
|
|
6
|
+
*
|
|
7
|
+
* Loads '*.wasm?module' and `*.wasm` imports as WebAssembly modules, which is the only way to load WASM in cloudflare workers.
|
|
5
8
|
* Current proposal for WASM modules: https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration
|
|
6
9
|
* Cloudflare worker WASM from javascript support: https://developers.cloudflare.com/workers/runtime-apis/webassembly/javascript/
|
|
7
|
-
* @param
|
|
8
|
-
* otherwise it will error obscurely in the esbuild and vite builds
|
|
9
|
-
* @param assetsDirectory - the folder name for the assets directory in the build directory. Usually '_astro'
|
|
10
|
+
* @param enabled - if true, load '.wasm' imports as Uint8Arrays, otherwise will throw errors when encountered to clarify that it must be enabled
|
|
10
11
|
* @returns Vite plugin to load WASM tagged with '?module' as a WASM modules
|
|
11
12
|
*/
|
|
12
|
-
export function
|
|
13
|
-
const
|
|
13
|
+
export function cloudflareModuleLoader(enabled) {
|
|
14
|
+
const enabledAdapters = cloudflareImportAdapters.filter((x) => enabled);
|
|
14
15
|
let isDev = false;
|
|
16
|
+
const MAGIC_STRING = '__CLOUDFLARE_ASSET__';
|
|
17
|
+
const replacements = [];
|
|
15
18
|
return {
|
|
16
19
|
name: 'vite:wasm-module-loader',
|
|
17
20
|
enforce: 'pre',
|
|
@@ -21,39 +24,40 @@ export function wasmModuleLoader({ disabled, }) {
|
|
|
21
24
|
config(_, __) {
|
|
22
25
|
// let vite know that file format and the magic import string is intentional, and will be handled in this plugin
|
|
23
26
|
return {
|
|
24
|
-
assetsInclude:
|
|
27
|
+
assetsInclude: enabledAdapters.map((x) => `**/*.${x.qualifiedExtension}`),
|
|
25
28
|
build: {
|
|
26
29
|
rollupOptions: {
|
|
27
30
|
// mark the wasm files as external so that they are not bundled and instead are loaded from the files
|
|
28
|
-
external:
|
|
31
|
+
external: enabledAdapters.map((x) => new RegExp(`^${MAGIC_STRING}.+\\.${x.extension}.mjs$`, 'i')),
|
|
29
32
|
},
|
|
30
33
|
},
|
|
31
34
|
};
|
|
32
35
|
},
|
|
33
|
-
load(id, _) {
|
|
34
|
-
|
|
36
|
+
async load(id, _) {
|
|
37
|
+
const importAdapter = cloudflareImportAdapters.find((x) => id.endsWith(x.qualifiedExtension));
|
|
38
|
+
if (!importAdapter) {
|
|
35
39
|
return;
|
|
36
40
|
}
|
|
37
|
-
if (
|
|
38
|
-
throw new Error(`
|
|
41
|
+
if (!enabled) {
|
|
42
|
+
throw new Error(`Cloudflare module loading is experimental. The ${importAdapter.qualifiedExtension} module cannot be loaded unless you add \`wasmModuleImports: true\` to your astro config.`);
|
|
39
43
|
}
|
|
40
|
-
const filePath = id.
|
|
41
|
-
const data = fs.
|
|
44
|
+
const filePath = id.replace(/\?module$/, '');
|
|
45
|
+
const data = await fs.readFile(filePath);
|
|
42
46
|
const base64 = data.toString('base64');
|
|
43
|
-
const
|
|
47
|
+
const inlineModule = importAdapter.asNodeModule(data);
|
|
44
48
|
if (isDev) {
|
|
45
49
|
// no need to wire up the assets in dev mode, just rewrite
|
|
46
|
-
return
|
|
50
|
+
return inlineModule;
|
|
47
51
|
}
|
|
48
52
|
// just some shared ID
|
|
49
53
|
const hash = hashString(base64);
|
|
50
54
|
// emit the wasm binary as an asset file, to be picked up later by the esbuild bundle for the worker.
|
|
51
55
|
// give it a shared deterministic name to make things easy for esbuild to switch on later
|
|
52
|
-
const assetName = `${path.basename(filePath).split('.')[0]}.${hash}.
|
|
56
|
+
const assetName = `${path.basename(filePath).split('.')[0]}.${hash}.${importAdapter.extension}`;
|
|
53
57
|
this.emitFile({
|
|
54
58
|
type: 'asset',
|
|
55
|
-
//
|
|
56
|
-
// vite doesn't give it a random id in its name
|
|
59
|
+
// emit the data explicitly as an esset with `fileName` rather than `name` so that
|
|
60
|
+
// vite doesn't give it a random hash-id in its name--We need to be able to easily rewrite from
|
|
57
61
|
// the .mjs loader and the actual wasm asset later in the ESbuild for the worker
|
|
58
62
|
fileName: assetName,
|
|
59
63
|
source: data,
|
|
@@ -62,42 +66,95 @@ export function wasmModuleLoader({ disabled, }) {
|
|
|
62
66
|
const chunkId = this.emitFile({
|
|
63
67
|
type: 'prebuilt-chunk',
|
|
64
68
|
fileName: `${assetName}.mjs`,
|
|
65
|
-
code:
|
|
69
|
+
code: inlineModule,
|
|
66
70
|
});
|
|
67
|
-
return `import
|
|
71
|
+
return `import module from "${MAGIC_STRING}${chunkId}.${importAdapter.extension}.mjs";export default module;`;
|
|
68
72
|
},
|
|
69
|
-
// output original wasm file relative to the chunk
|
|
73
|
+
// output original wasm file relative to the chunk now that chunking has been achieved
|
|
70
74
|
renderChunk(code, chunk, _) {
|
|
71
75
|
if (isDev)
|
|
72
76
|
return;
|
|
73
|
-
if (
|
|
77
|
+
if (!code.includes(MAGIC_STRING))
|
|
74
78
|
return;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
.relative(path.dirname(chunk.fileName), fileName)
|
|
83
|
-
.replaceAll('\\', '/'); // fix windows paths for import
|
|
84
|
-
return `./${relativePath}`;
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
// SSG
|
|
88
|
-
if (isPrerendered) {
|
|
89
|
-
final = code.replaceAll(/__WASM_ASSET__([A-Za-z\d]+).wasm.mjs/g, (s, assetId) => {
|
|
79
|
+
// SSR will need the .mjs suffix removed from the import before this works in cloudflare, but this is done as a final step
|
|
80
|
+
// so as to support prerendering from nodejs runtime
|
|
81
|
+
let replaced = code;
|
|
82
|
+
for (const loader of enabledAdapters) {
|
|
83
|
+
replaced = replaced.replaceAll(
|
|
84
|
+
// chunk id can be many things, (alpha numeric, dollars, or underscores, maybe more)
|
|
85
|
+
new RegExp(`${MAGIC_STRING}([^\\s]+?)\\.${loader.extension}\\.mjs`, 'g'), (s, assetId) => {
|
|
90
86
|
const fileName = this.getFileName(assetId);
|
|
91
87
|
const relativePath = path
|
|
92
88
|
.relative(path.dirname(chunk.fileName), fileName)
|
|
93
89
|
.replaceAll('\\', '/'); // fix windows paths for import
|
|
90
|
+
// record this replacement for later, to adjust it to import the unbundled asset
|
|
91
|
+
replacements.push({
|
|
92
|
+
chunkName: chunk.name,
|
|
93
|
+
cloudflareImport: relativePath.replace(/\.mjs$/, ''),
|
|
94
|
+
nodejsImport: relativePath,
|
|
95
|
+
});
|
|
94
96
|
return `./${relativePath}`;
|
|
95
97
|
});
|
|
96
98
|
}
|
|
97
|
-
|
|
99
|
+
if (replaced.includes(MAGIC_STRING)) {
|
|
100
|
+
console.error('failed to replace', replaced);
|
|
101
|
+
}
|
|
102
|
+
return { code: replaced };
|
|
103
|
+
},
|
|
104
|
+
generateBundle(_, bundle) {
|
|
105
|
+
// associate the chunk name to the final file name. After the prerendering is done, we can use this to replace the imports in the _worker.js
|
|
106
|
+
// in a targetted way
|
|
107
|
+
const replacementsByChunkName = new Map();
|
|
108
|
+
for (const replacement of replacements) {
|
|
109
|
+
const repls = replacementsByChunkName.get(replacement.chunkName) || [];
|
|
110
|
+
if (!repls.length) {
|
|
111
|
+
replacementsByChunkName.set(replacement.chunkName, repls);
|
|
112
|
+
}
|
|
113
|
+
repls.push(replacement);
|
|
114
|
+
}
|
|
115
|
+
for (const chunk of Object.values(bundle)) {
|
|
116
|
+
const repls = chunk.name && replacementsByChunkName.get(chunk.name);
|
|
117
|
+
for (const replacement of repls || []) {
|
|
118
|
+
replacement.fileName = chunk.fileName;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
/**
|
|
123
|
+
* Once prerendering is complete, restore the imports in the _worker.js to cloudflare compatible ones, removing the .mjs suffix.
|
|
124
|
+
*/
|
|
125
|
+
async afterBuildCompleted(config) {
|
|
126
|
+
const baseDir = url.fileURLToPath(config.outDir);
|
|
127
|
+
const replacementsByFileName = new Map();
|
|
128
|
+
for (const replacement of replacements) {
|
|
129
|
+
if (!replacement.fileName)
|
|
130
|
+
continue;
|
|
131
|
+
const repls = replacementsByFileName.get(replacement.fileName) || [];
|
|
132
|
+
if (!repls.length) {
|
|
133
|
+
replacementsByFileName.set(replacement.fileName, repls);
|
|
134
|
+
}
|
|
135
|
+
repls.push(replacement);
|
|
136
|
+
}
|
|
137
|
+
for (const [fileName, repls] of replacementsByFileName.entries()) {
|
|
138
|
+
const filepath = path.join(baseDir, '_worker.js', fileName);
|
|
139
|
+
const contents = await fs.readFile(filepath, 'utf-8');
|
|
140
|
+
let updated = contents;
|
|
141
|
+
for (const replacement of repls) {
|
|
142
|
+
updated = contents.replaceAll(replacement.nodejsImport, replacement.cloudflareImport);
|
|
143
|
+
}
|
|
144
|
+
await fs.writeFile(filepath, updated, 'utf-8');
|
|
145
|
+
}
|
|
98
146
|
},
|
|
99
147
|
};
|
|
100
148
|
}
|
|
149
|
+
const wasmImportAdapter = {
|
|
150
|
+
extension: 'wasm',
|
|
151
|
+
qualifiedExtension: 'wasm?module',
|
|
152
|
+
asNodeModule(fileContents) {
|
|
153
|
+
const base64 = fileContents.toString('base64');
|
|
154
|
+
return `const wasmModule = new WebAssembly.Module(Uint8Array.from(atob("${base64}"), c => c.charCodeAt(0)));export default wasmModule;`;
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
const cloudflareImportAdapters = [wasmImportAdapter];
|
|
101
158
|
/**
|
|
102
159
|
* Returns a deterministic 32 bit hash code from a string
|
|
103
160
|
*/
|