@astrojs/cloudflare 10.2.5 → 10.3.0

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 CHANGED
@@ -43,9 +43,14 @@ export type Options = {
43
43
  };
44
44
  };
45
45
  /**
46
- * Allow bundling cloudflare worker specific file types
47
- * https://developers.cloudflare.com/workers/wrangler/bundling/
46
+ * Allow bundling cloudflare worker specific file types as importable modules. Defaults to true.
47
+ * When enabled, allows imports of '.wasm', '.bin', and '.txt' file types
48
+ *
49
+ * See https://developers.cloudflare.com/pages/functions/module-support/
50
+ * for reference on how these file types are exported
48
51
  */
52
+ cloudflareModules?: boolean;
53
+ /** @deprecated - use `cloudflareModules`, which defaults to true. You can set `cloudflareModuleLoading: false` to disable */
49
54
  wasmModuleImports?: boolean;
50
55
  };
51
56
  export default function createIntegration(args?: Options): AstroIntegration;
package/dist/index.js CHANGED
@@ -7,14 +7,14 @@ import { AstroError } from 'astro/errors';
7
7
  import { walk } from 'estree-walker';
8
8
  import MagicString from 'magic-string';
9
9
  import { getPlatformProxy } from 'wrangler';
10
+ import { cloudflareModuleLoader, } from './utils/cloudflare-module-loader.js';
10
11
  import { createRoutesFile, getParts } from './utils/generate-routes-json.js';
11
12
  import { setImageConfig } from './utils/image-config.js';
12
13
  import { mutateDynamicPageImportsInPlace, mutatePageMapInPlace } from './utils/index.js';
13
14
  import { NonServerChunkDetector } from './utils/non-server-chunk-detector.js';
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
+ const cloudflareModulePlugin = cloudflareModuleLoader(args?.cloudflareModules ?? args?.wasmModuleImports ?? true);
18
18
  // Initialize the unused chunk analyzer as a shared state between hooks.
19
19
  // The analyzer is used on earlier hooks to collect information about used hooks on a Vite plugin
20
20
  // and then later after the full build to clean up unused chunks, so it has to be shared between them.
@@ -175,6 +175,20 @@ export default function createIntegration(args) {
175
175
  // @ts-expect-error
176
176
  vite.build.rollupOptions.output.banner ||=
177
177
  'globalThis.process ??= {}; globalThis.process.env ??= {};';
178
+ // @ts-expect-error
179
+ vite.build.rollupOptions.output.manualChunks = (id) => {
180
+ if (id.includes('node_modules')) {
181
+ if (id.indexOf('node_modules') !== -1) {
182
+ const basic = id.toString().split('node_modules/')[1];
183
+ const sub1 = basic.split('/')[0];
184
+ if (sub1 !== '.pnpm') {
185
+ return sub1.toString();
186
+ }
187
+ const name2 = basic.split('/')[1];
188
+ return name2.split('@')[name2[0] === '@' ? 1 : 0].toString();
189
+ }
190
+ }
191
+ };
178
192
  vite.build.rollupOptions.external = _config.vite.build?.rollupOptions?.external ?? [];
179
193
  // Cloudflare env is only available per request. This isn't feasible for code that access env vars
180
194
  // in a global way, so we shim their access as `process.env.*`. This is not the recommended way for users to access environment variables. But we'll add this for compatibility for chosen variables. Mainly to support `@astrojs/db`
@@ -191,6 +205,11 @@ export default function createIntegration(args) {
191
205
  vite.resolve ||= {};
192
206
  vite.resolve.conditions ||= [];
193
207
  vite.resolve.conditions = vite.resolve.conditions.filter((c) => c !== 'workerd' && c !== 'worker');
208
+ vite.build ||= {};
209
+ vite.build.rollupOptions ||= {};
210
+ vite.build.rollupOptions.output ||= {};
211
+ // @ts-expect-error
212
+ vite.build.rollupOptions.output.manualChunks = undefined;
194
213
  }
195
214
  },
196
215
  'astro:build:done': async ({ pages, routes, dir, logger }) => {
@@ -0,0 +1,21 @@
1
+ import type { AstroConfig } from 'astro';
2
+ import type { PluginOption } from 'vite';
3
+ export interface CloudflareModulePluginExtra {
4
+ afterBuildCompleted(config: AstroConfig): Promise<void>;
5
+ }
6
+ export type ModuleType = 'CompiledWasm' | 'Text' | 'Data';
7
+ /**
8
+ * Enables support for various non-standard extensions in module imports that cloudflare workers supports.
9
+ *
10
+ * See https://developers.cloudflare.com/pages/functions/module-support/ for reference
11
+ *
12
+ * This adds supports for imports in the following formats:
13
+ * - .wasm
14
+ * - .wasm?module
15
+ * - .bin
16
+ * - .txt
17
+ *
18
+ * @param enabled - if true, will load all cloudflare pages supported types
19
+ * @returns Vite plugin with additional extension method to hook into astro build
20
+ */
21
+ export declare function cloudflareModuleLoader(enabled: boolean): PluginOption & CloudflareModulePluginExtra;
@@ -2,21 +2,32 @@ import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import * as url from 'node:url';
4
4
  /**
5
- * Enables support for wasm modules within cloudflare pages functions
5
+ * Enables support for various non-standard extensions in module imports that cloudflare workers supports.
6
6
  *
7
- * Loads '*.wasm?module' and `*.wasm` imports as WebAssembly modules, which is the only way to load WASM in cloudflare workers.
8
- * Current proposal for WASM modules: https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration
9
- * Cloudflare worker WASM from javascript support: https://developers.cloudflare.com/workers/runtime-apis/webassembly/javascript/
10
- * @param enabled - if true, load '.wasm' imports as Uint8Arrays, otherwise will throw errors when encountered to clarify that it must be enabled
11
- * @returns Vite plugin to load WASM tagged with '?module' as a WASM modules
7
+ * See https://developers.cloudflare.com/pages/functions/module-support/ for reference
8
+ *
9
+ * This adds supports for imports in the following formats:
10
+ * - .wasm
11
+ * - .wasm?module
12
+ * - .bin
13
+ * - .txt
14
+ *
15
+ * @param enabled - if true, will load all cloudflare pages supported types
16
+ * @returns Vite plugin with additional extension method to hook into astro build
12
17
  */
13
18
  export function cloudflareModuleLoader(enabled) {
14
- const enabledAdapters = cloudflareImportAdapters.filter((x) => enabled);
19
+ /**
20
+ * It's likely that eventually cloudflare will add support for custom extensions, like they do in vanilla cloudflare workers,
21
+ * by adding rules to your wrangler.tome
22
+ * https://developers.cloudflare.com/workers/wrangler/bundling/
23
+ */
24
+ const adaptersByExtension = enabled ? { ...defaultAdapters } : {};
25
+ const extensions = Object.keys(adaptersByExtension);
15
26
  let isDev = false;
16
27
  const MAGIC_STRING = '__CLOUDFLARE_ASSET__';
17
28
  const replacements = [];
18
29
  return {
19
- name: 'vite:wasm-module-loader',
30
+ name: 'vite:cf-module-loader',
20
31
  enforce: 'pre',
21
32
  configResolved(config) {
22
33
  isDev = config.command === 'serve';
@@ -24,27 +35,30 @@ export function cloudflareModuleLoader(enabled) {
24
35
  config(_, __) {
25
36
  // let vite know that file format and the magic import string is intentional, and will be handled in this plugin
26
37
  return {
27
- assetsInclude: enabledAdapters.map((x) => `**/*.${x.qualifiedExtension}`),
38
+ assetsInclude: extensions.map((x) => `**/*${x}`),
28
39
  build: {
29
40
  rollupOptions: {
30
41
  // mark the wasm files as external so that they are not bundled and instead are loaded from the files
31
- external: enabledAdapters.map((x) => new RegExp(`^${MAGIC_STRING}.+\\.${x.extension}.mjs$`, 'i')),
42
+ external: extensions.map((x) => new RegExp(`^${MAGIC_STRING}.+${escapeRegExp(x)}.mjs$`, 'i')),
32
43
  },
33
44
  },
34
45
  };
35
46
  },
36
47
  async load(id, _) {
37
- const importAdapter = cloudflareImportAdapters.find((x) => id.endsWith(x.qualifiedExtension));
38
- if (!importAdapter) {
48
+ const maybeExtension = extensions.find((x) => id.endsWith(x));
49
+ const moduleType = (maybeExtension && adaptersByExtension[maybeExtension]) || undefined;
50
+ if (!moduleType || !maybeExtension) {
39
51
  return;
40
52
  }
41
53
  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.`);
54
+ throw new Error(`Cloudflare module loading is experimental. The ${maybeExtension} module cannot be loaded unless you add \`wasmModuleImports: true\` to your astro config.`);
43
55
  }
44
- const filePath = id.replace(/\?module$/, '');
56
+ const moduleLoader = renderers[moduleType];
57
+ const filePath = id.replace(/\?\w+$/, '');
58
+ const extension = maybeExtension.replace(/\?\w+$/, '');
45
59
  const data = await fs.readFile(filePath);
46
60
  const base64 = data.toString('base64');
47
- const inlineModule = importAdapter.asNodeModule(data);
61
+ const inlineModule = moduleLoader(data);
48
62
  if (isDev) {
49
63
  // no need to wire up the assets in dev mode, just rewrite
50
64
  return inlineModule;
@@ -53,7 +67,7 @@ export function cloudflareModuleLoader(enabled) {
53
67
  const hash = hashString(base64);
54
68
  // emit the wasm binary as an asset file, to be picked up later by the esbuild bundle for the worker.
55
69
  // give it a shared deterministic name to make things easy for esbuild to switch on later
56
- const assetName = `${path.basename(filePath).split('.')[0]}.${hash}.${importAdapter.extension}`;
70
+ const assetName = `${path.basename(filePath).split('.')[0]}.${hash}${extension}`;
57
71
  this.emitFile({
58
72
  type: 'asset',
59
73
  // emit the data explicitly as an esset with `fileName` rather than `name` so that
@@ -68,7 +82,7 @@ export function cloudflareModuleLoader(enabled) {
68
82
  fileName: `${assetName}.mjs`,
69
83
  code: inlineModule,
70
84
  });
71
- return `import module from "${MAGIC_STRING}${chunkId}.${importAdapter.extension}.mjs";export default module;`;
85
+ return `import module from "${MAGIC_STRING}${chunkId}${extension}.mjs";export default module;`;
72
86
  },
73
87
  // output original wasm file relative to the chunk now that chunking has been achieved
74
88
  renderChunk(code, chunk, _) {
@@ -79,10 +93,10 @@ export function cloudflareModuleLoader(enabled) {
79
93
  // SSR will need the .mjs suffix removed from the import before this works in cloudflare, but this is done as a final step
80
94
  // so as to support prerendering from nodejs runtime
81
95
  let replaced = code;
82
- for (const loader of enabledAdapters) {
83
- replaced = replaced.replaceAll(
96
+ for (const ext of extensions) {
97
+ const extension = ext.replace(/\?\w+$/, '');
84
98
  // 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) => {
99
+ replaced = replaced.replaceAll(new RegExp(`${MAGIC_STRING}([^\\s]+?)${escapeRegExp(extension)}\\.mjs`, 'g'), (s, assetId) => {
86
100
  const fileName = this.getFileName(assetId);
87
101
  const relativePath = path
88
102
  .relative(path.dirname(chunk.fileName), fileName)
@@ -96,9 +110,6 @@ export function cloudflareModuleLoader(enabled) {
96
110
  return `./${relativePath}`;
97
111
  });
98
112
  }
99
- if (replaced.includes(MAGIC_STRING)) {
100
- console.error('failed to replace', replaced);
101
- }
102
113
  return { code: replaced };
103
114
  },
104
115
  generateBundle(_, bundle) {
@@ -126,8 +137,9 @@ export function cloudflareModuleLoader(enabled) {
126
137
  const baseDir = url.fileURLToPath(config.outDir);
127
138
  const replacementsByFileName = new Map();
128
139
  for (const replacement of replacements) {
129
- if (!replacement.fileName)
140
+ if (!replacement.fileName) {
130
141
  continue;
142
+ }
131
143
  const repls = replacementsByFileName.get(replacement.fileName) || [];
132
144
  if (!repls.length) {
133
145
  replacementsByFileName.set(replacement.fileName, repls);
@@ -139,22 +151,36 @@ export function cloudflareModuleLoader(enabled) {
139
151
  const contents = await fs.readFile(filepath, 'utf-8');
140
152
  let updated = contents;
141
153
  for (const replacement of repls) {
142
- updated = contents.replaceAll(replacement.nodejsImport, replacement.cloudflareImport);
154
+ updated = updated.replaceAll(replacement.nodejsImport, replacement.cloudflareImport);
143
155
  }
144
156
  await fs.writeFile(filepath, updated, 'utf-8');
145
157
  }
146
158
  },
147
159
  };
148
160
  }
149
- const wasmImportAdapter = {
150
- extension: 'wasm',
151
- qualifiedExtension: 'wasm?module',
152
- asNodeModule(fileContents) {
161
+ const renderers = {
162
+ CompiledWasm(fileContents) {
153
163
  const base64 = fileContents.toString('base64');
154
164
  return `const wasmModule = new WebAssembly.Module(Uint8Array.from(atob("${base64}"), c => c.charCodeAt(0)));export default wasmModule;`;
155
165
  },
166
+ Data(fileContents) {
167
+ const base64 = fileContents.toString('base64');
168
+ return `const binModule = Uint8Array.from(atob("${base64}"), c => c.charCodeAt(0)).buffer;export default binModule;`;
169
+ },
170
+ Text(fileContents) {
171
+ const escaped = JSON.stringify(fileContents.toString('utf-8'));
172
+ return `const stringModule = ${escaped};export default stringModule;`;
173
+ },
174
+ };
175
+ const defaultAdapters = {
176
+ // Loads '*.wasm?module' imports as WebAssembly modules, which is the only way to load WASM in cloudflare workers.
177
+ // Current proposal for WASM modules: https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration
178
+ '.wasm?module': 'CompiledWasm',
179
+ // treats the module as a WASM module
180
+ '.wasm': 'CompiledWasm',
181
+ '.bin': 'Data',
182
+ '.txt': 'Text',
156
183
  };
157
- const cloudflareImportAdapters = [wasmImportAdapter];
158
184
  /**
159
185
  * Returns a deterministic 32 bit hash code from a string
160
186
  */
@@ -167,3 +193,6 @@ function hashString(str) {
167
193
  }
168
194
  return new Uint32Array([hash])[0].toString(36);
169
195
  }
196
+ function escapeRegExp(string) {
197
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
198
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@astrojs/cloudflare",
3
3
  "description": "Deploy your site to Cloudflare Workers/Pages",
4
- "version": "10.2.5",
4
+ "version": "10.3.0",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
7
7
  "author": "withastro",
@@ -1,16 +0,0 @@
1
- import type { AstroConfig } from 'astro';
2
- import type { PluginOption } from 'vite';
3
- export interface CloudflareModulePluginExtra {
4
- afterBuildCompleted(config: AstroConfig): Promise<void>;
5
- }
6
- /**
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.
10
- * Current proposal for WASM modules: https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration
11
- * Cloudflare worker WASM from javascript support: https://developers.cloudflare.com/workers/runtime-apis/webassembly/javascript/
12
- * @param enabled - if true, load '.wasm' imports as Uint8Arrays, otherwise will throw errors when encountered to clarify that it must be enabled
13
- * @returns Vite plugin to load WASM tagged with '?module' as a WASM modules
14
- */
15
- export declare function cloudflareModuleLoader(enabled: boolean): PluginOption & CloudflareModulePluginExtra;
16
- export type ImportType = 'wasm';