@astrojs/cloudflare 9.2.1 → 10.0.1

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.
@@ -0,0 +1,205 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { writeFile } from 'node:fs/promises';
3
+ import { posix } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { prependForwardSlash, removeLeadingForwardSlash, removeTrailingForwardSlash, } from '@astrojs/internal-helpers/path';
6
+ import glob from 'tiny-glob';
7
+ // Copied from https://github.com/withastro/astro/blob/3776ecf0aa9e08a992d3ae76e90682fd04093721/packages/astro/src/core/routing/manifest/create.ts#L45-L70
8
+ // We're not sure how to improve this regex yet
9
+ const ROUTE_DYNAMIC_SPLIT = /\[(.+?\(.+?\)|.+?)\]/;
10
+ const ROUTE_SPREAD = /^\.{3}.+$/;
11
+ export function getParts(part) {
12
+ const result = [];
13
+ part.split(ROUTE_DYNAMIC_SPLIT).map((str, i) => {
14
+ if (!str)
15
+ return;
16
+ const dynamic = i % 2 === 1;
17
+ const [, content] = dynamic ? /([^(]+)$/.exec(str) || [null, null] : [null, str];
18
+ if (!content || (dynamic && !/^(?:\.\.\.)?[\w$]+$/.test(content))) {
19
+ throw new Error('Parameter name must match /^[a-zA-Z0-9_$]+$/');
20
+ }
21
+ result.push({
22
+ content,
23
+ dynamic,
24
+ spread: dynamic && ROUTE_SPREAD.test(content),
25
+ });
26
+ });
27
+ return result;
28
+ }
29
+ async function writeRoutesFileToOutDir(_config, logger, include, exclude) {
30
+ try {
31
+ await writeFile(new URL('./_routes.json', _config.outDir), JSON.stringify({
32
+ version: 1,
33
+ include: include,
34
+ exclude: exclude,
35
+ }, null, 2), 'utf-8');
36
+ }
37
+ catch (error) {
38
+ logger.error("There was an error writing the '_routes.json' file to the output directory.");
39
+ }
40
+ }
41
+ function segmentsToCfSyntax(segments, _config) {
42
+ const pathSegments = [];
43
+ if (removeLeadingForwardSlash(removeTrailingForwardSlash(_config.base)).length > 0) {
44
+ pathSegments.push(removeLeadingForwardSlash(removeTrailingForwardSlash(_config.base)));
45
+ }
46
+ for (const segment of segments.flat()) {
47
+ if (segment.dynamic)
48
+ pathSegments.push('*');
49
+ else
50
+ pathSegments.push(segment.content);
51
+ }
52
+ return pathSegments;
53
+ }
54
+ class TrieNode {
55
+ children = new Map();
56
+ isEndOfPath = false;
57
+ hasWildcardChild = false;
58
+ }
59
+ class PathTrie {
60
+ root;
61
+ constructor() {
62
+ this.root = new TrieNode();
63
+ }
64
+ insert(path) {
65
+ let node = this.root;
66
+ for (const segment of path) {
67
+ if (segment === '*') {
68
+ node.hasWildcardChild = true;
69
+ break;
70
+ }
71
+ if (!node.children.has(segment)) {
72
+ node.children.set(segment, new TrieNode());
73
+ }
74
+ // biome-ignore lint/style/noNonNullAssertion: The `if` condition above ensures that the segment exists inside the map
75
+ node = node.children.get(segment);
76
+ }
77
+ node.isEndOfPath = true;
78
+ }
79
+ /**
80
+ * Depth-first search (dfs), traverses the "graph" segment by segment until the end or wildcard (*).
81
+ * It makes sure that all necessary paths are returned, but not paths with an existing wildcard prefix.
82
+ * e.g. if we have a path like /foo/* and /foo/bar, we only want to return /foo/*
83
+ */
84
+ dfs(node, path, allPaths) {
85
+ if (node.hasWildcardChild) {
86
+ allPaths.push([...path, '*']);
87
+ return;
88
+ }
89
+ if (node.isEndOfPath) {
90
+ allPaths.push([...path]);
91
+ }
92
+ for (const [segment, childNode] of node.children) {
93
+ this.dfs(childNode, [...path, segment], allPaths);
94
+ }
95
+ }
96
+ getAllPaths() {
97
+ const allPaths = [];
98
+ this.dfs(this.root, [], allPaths);
99
+ return allPaths;
100
+ }
101
+ }
102
+ export async function createRoutesFile(_config, logger, routes, pages, redirects, includeExtends, excludeExtends) {
103
+ const includePaths = [];
104
+ const excludePaths = [];
105
+ let hasPrerendered404 = false;
106
+ for (const route of routes) {
107
+ const convertedPath = segmentsToCfSyntax(route.segments, _config);
108
+ if (route.pathname === '/404' && route.prerender === true)
109
+ hasPrerendered404 = true;
110
+ switch (route.type) {
111
+ case 'page':
112
+ if (route.prerender === false)
113
+ includePaths.push(convertedPath);
114
+ break;
115
+ case 'endpoint':
116
+ if (route.prerender === false)
117
+ includePaths.push(convertedPath);
118
+ else
119
+ excludePaths.push(convertedPath);
120
+ break;
121
+ case 'redirect':
122
+ excludePaths.push(convertedPath);
123
+ break;
124
+ default:
125
+ /**
126
+ * We don't know the type, so we are conservative!
127
+ * Invoking the function on these is a safe-bet because
128
+ * the function will fallback to static asset fetching
129
+ */
130
+ includePaths.push(convertedPath);
131
+ break;
132
+ }
133
+ }
134
+ for (const page of pages) {
135
+ const pageSegments = removeLeadingForwardSlash(page.pathname)
136
+ .split(posix.sep)
137
+ .filter(Boolean)
138
+ .map((s) => {
139
+ return getParts(s);
140
+ });
141
+ excludePaths.push(segmentsToCfSyntax(pageSegments, _config));
142
+ }
143
+ if (existsSync(fileURLToPath(_config.publicDir))) {
144
+ const staticFiles = await glob(`${fileURLToPath(_config.publicDir)}/**/*`, {
145
+ cwd: fileURLToPath(_config.publicDir),
146
+ filesOnly: true,
147
+ dot: true,
148
+ });
149
+ for (const staticFile of staticFiles) {
150
+ if (['_headers', '_redirects', '_routes.json'].includes(staticFile))
151
+ continue;
152
+ const staticPath = staticFile;
153
+ const segments = removeLeadingForwardSlash(staticPath)
154
+ .split(posix.sep)
155
+ .filter(Boolean)
156
+ .map((s) => {
157
+ return getParts(s);
158
+ });
159
+ excludePaths.push(segmentsToCfSyntax(segments, _config));
160
+ }
161
+ }
162
+ /**
163
+ * All files in the `_config.build.assets` path, e.g. `_astro`
164
+ * are considered static assets and should not be handled by the function
165
+ * therefore we exclude a wildcard for that, e.g. `/_astro/*`
166
+ */
167
+ const assetsPath = segmentsToCfSyntax([
168
+ [{ content: _config.build.assets, dynamic: false, spread: false }],
169
+ [{ content: '', dynamic: true, spread: false }],
170
+ ], _config);
171
+ excludePaths.push(assetsPath);
172
+ for (const redirect of redirects) {
173
+ excludePaths.push(segmentsToCfSyntax(redirect, _config));
174
+ }
175
+ const includeTrie = new PathTrie();
176
+ for (const includePath of includePaths) {
177
+ includeTrie.insert(includePath);
178
+ }
179
+ const deduplicatedIncludePaths = includeTrie.getAllPaths();
180
+ const excludeTrie = new PathTrie();
181
+ for (const excludePath of excludePaths) {
182
+ excludeTrie.insert(excludePath);
183
+ }
184
+ const deduplicatedExcludePaths = excludeTrie.getAllPaths();
185
+ /**
186
+ * Cloudflare allows no more than 100 include/exclude rules combined
187
+ * https://developers.cloudflare.com/pages/functions/routing/#limits
188
+ */
189
+ const CLOUDFLARE_COMBINED_LIMIT = 100;
190
+ if (!hasPrerendered404 ||
191
+ deduplicatedIncludePaths.length + (includeExtends?.length ?? 0) > CLOUDFLARE_COMBINED_LIMIT ||
192
+ deduplicatedExcludePaths.length + (excludeExtends?.length ?? 0) > CLOUDFLARE_COMBINED_LIMIT) {
193
+ await writeRoutesFileToOutDir(_config, logger, ['/*'].concat(includeExtends?.map((entry) => entry.pattern) ?? []), deduplicatedExcludePaths
194
+ .map((path) => `${prependForwardSlash(path.join('/'))}`)
195
+ .concat(excludeExtends?.map((entry) => entry.pattern) ?? [])
196
+ .slice(0, 99));
197
+ }
198
+ else {
199
+ await writeRoutesFileToOutDir(_config, logger, deduplicatedIncludePaths
200
+ .map((path) => `${prependForwardSlash(path.join('/'))}`)
201
+ .concat(includeExtends?.map((entry) => entry.pattern) ?? []), deduplicatedExcludePaths
202
+ .map((path) => `${prependForwardSlash(path.join('/'))}`)
203
+ .concat(excludeExtends?.map((entry) => entry.pattern) ?? []));
204
+ }
205
+ }
@@ -1,5 +1,5 @@
1
1
  import type { AstroConfig, AstroIntegrationLogger } from 'astro';
2
- export declare function prepareImageConfig(service: string, config: AstroConfig['image'], command: 'dev' | 'build' | 'preview', logger: AstroIntegrationLogger): {
2
+ export declare function setImageConfig(service: string, config: AstroConfig['image'], command: 'dev' | 'build' | 'preview', logger: AstroIntegrationLogger): {
3
3
  service: import("astro").ImageServiceConfig<Record<string, any>>;
4
4
  domains: string[];
5
5
  remotePatterns: {
@@ -1,5 +1,5 @@
1
1
  import { passthroughImageService, sharpImageService } from 'astro/config';
2
- export function prepareImageConfig(service, config, command, logger) {
2
+ export function setImageConfig(service, config, command, logger) {
3
3
  switch (service) {
4
4
  case 'passthrough':
5
5
  return { ...config, service: passthroughImageService() };
@@ -8,7 +8,6 @@ import type { AstroConfig } from 'astro';
8
8
  * @param assetsDirectory - the folder name for the assets directory in the build directory. Usually '_astro'
9
9
  * @returns Vite plugin to load WASM tagged with '?module' as a WASM modules
10
10
  */
11
- export declare function wasmModuleLoader({ disabled, assetsDirectory, }: {
11
+ export declare function wasmModuleLoader({ disabled, }: {
12
12
  disabled: boolean;
13
- assetsDirectory: string;
14
13
  }): NonNullable<AstroConfig['vite']['plugins']>[number];
@@ -9,7 +9,7 @@ import * as path from 'node:path';
9
9
  * @param assetsDirectory - the folder name for the assets directory in the build directory. Usually '_astro'
10
10
  * @returns Vite plugin to load WASM tagged with '?module' as a WASM modules
11
11
  */
12
- export function wasmModuleLoader({ disabled, assetsDirectory, }) {
12
+ export function wasmModuleLoader({ disabled, }) {
13
13
  const postfix = '.wasm?module';
14
14
  let isDev = false;
15
15
  return {
@@ -22,7 +22,12 @@ export function wasmModuleLoader({ disabled, assetsDirectory, }) {
22
22
  // let vite know that file format and the magic import string is intentional, and will be handled in this plugin
23
23
  return {
24
24
  assetsInclude: ['**/*.wasm?module'],
25
- build: { rollupOptions: { external: /^__WASM_ASSET__.+\.wasm\.mjs$/i } },
25
+ build: {
26
+ rollupOptions: {
27
+ // mark the wasm files as external so that they are not bundled and instead are loaded from the files
28
+ external: [/^__WASM_ASSET__.+\.wasm$/i, /^__WASM_ASSET__.+\.wasm.mjs$/i],
29
+ },
30
+ },
26
31
  };
27
32
  },
28
33
  load(id, _) {
@@ -35,10 +40,7 @@ export function wasmModuleLoader({ disabled, assetsDirectory, }) {
35
40
  const filePath = id.slice(0, -1 * '?module'.length);
36
41
  const data = fs.readFileSync(filePath);
37
42
  const base64 = data.toString('base64');
38
- const base64Module = `
39
- const wasmModule = new WebAssembly.Module(Uint8Array.from(atob("${base64}"), c => c.charCodeAt(0)));
40
- export default wasmModule
41
- `;
43
+ const base64Module = `const wasmModule = new WebAssembly.Module(Uint8Array.from(atob("${base64}"), c => c.charCodeAt(0)));export default wasmModule;`;
42
44
  if (isDev) {
43
45
  // no need to wire up the assets in dev mode, just rewrite
44
46
  return base64Module;
@@ -53,8 +55,8 @@ export default wasmModule
53
55
  // put it explicitly in the _astro assets directory with `fileName` rather than `name` so that
54
56
  // vite doesn't give it a random id in its name. We need to be able to easily rewrite from
55
57
  // the .mjs loader and the actual wasm asset later in the ESbuild for the worker
56
- fileName: path.join(assetsDirectory, assetName),
57
- source: fs.readFileSync(filePath),
58
+ fileName: assetName,
59
+ source: data,
58
60
  });
59
61
  // however, by default, the SSG generator cannot import the .wasm as a module, so embed as a base64 string
60
62
  const chunkId = this.emitFile({
@@ -62,10 +64,7 @@ export default wasmModule
62
64
  fileName: `${assetName}.mjs`,
63
65
  code: base64Module,
64
66
  });
65
- return `
66
- import wasmModule from "__WASM_ASSET__${chunkId}.wasm.mjs";
67
- export default wasmModule;
68
- `;
67
+ return `import wasmModule from "__WASM_ASSET__${chunkId}.wasm.mjs";export default wasmModule;`;
69
68
  },
70
69
  // output original wasm file relative to the chunk
71
70
  renderChunk(code, chunk, _) {
@@ -73,13 +72,28 @@ export default wasmModule;
73
72
  return;
74
73
  if (!/__WASM_ASSET__/g.test(code))
75
74
  return;
76
- const final = code.replaceAll(/__WASM_ASSET__([A-Za-z\d]+).wasm.mjs/g, (s, assetId) => {
77
- const fileName = this.getFileName(assetId);
78
- const relativePath = path
79
- .relative(path.dirname(chunk.fileName), fileName)
80
- .replaceAll('\\', '/'); // fix windows paths for import
81
- return `./${relativePath}`;
82
- });
75
+ const isPrerendered = Object.keys(chunk.modules).some((moduleId) => this.getModuleInfo(moduleId)?.meta?.astro?.pageOptions?.prerender === true);
76
+ let final = code;
77
+ // SSR
78
+ if (!isPrerendered) {
79
+ final = code.replaceAll(/__WASM_ASSET__([A-Za-z\d]+).wasm.mjs/g, (s, assetId) => {
80
+ const fileName = this.getFileName(assetId).replace(/\.mjs$/, '');
81
+ const relativePath = path
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) => {
90
+ const fileName = this.getFileName(assetId);
91
+ const relativePath = path
92
+ .relative(path.dirname(chunk.fileName), fileName)
93
+ .replaceAll('\\', '/'); // fix windows paths for import
94
+ return `./${relativePath}`;
95
+ });
96
+ }
83
97
  return { code: final };
84
98
  },
85
99
  };
package/package.json CHANGED
@@ -1,8 +1,7 @@
1
1
  {
2
- "//comment": "test changeset-bot",
3
2
  "name": "@astrojs/cloudflare",
4
3
  "description": "Deploy your site to Cloudflare Workers/Pages",
5
- "version": "9.2.1",
4
+ "version": "10.0.1",
6
5
  "type": "module",
7
6
  "types": "./dist/index.d.ts",
8
7
  "author": "withastro",
@@ -31,27 +30,24 @@
31
30
  ],
32
31
  "dependencies": {
33
32
  "@astrojs/underscore-redirects": "^0.3.3",
34
- "@cloudflare/workers-types": "^4.20231025.0",
35
- "miniflare": "3.20231030.2",
36
- "@iarna/toml": "^2.2.5",
37
- "dotenv": "^16.3.1",
33
+ "@astrojs/internal-helpers": "0.3.0",
34
+ "@cloudflare/workers-types": "^4.20240320.1",
35
+ "miniflare": "^3.20240320.0",
38
36
  "esbuild": "^0.19.5",
39
- "find-up": "^6.3.0",
40
- "tiny-glob": "^0.2.9"
37
+ "tiny-glob": "^0.2.9",
38
+ "wrangler": "^3.39.0"
41
39
  },
42
40
  "peerDependencies": {
43
41
  "astro": "^4.2.0"
44
42
  },
45
43
  "devDependencies": {
46
44
  "execa": "^8.0.1",
47
- "fast-glob": "^3.3.1",
48
- "@types/iarna__toml": "^2.0.2",
45
+ "fast-glob": "^3.3.2",
49
46
  "strip-ansi": "^7.1.0",
50
- "astro": "^4.3.5",
47
+ "astro": "^4.5.8",
51
48
  "cheerio": "1.0.0-rc.12",
52
- "wrangler": "^3.15.0",
53
- "@astrojs/test-utils": "0.0.1",
54
- "astro-scripts": "0.0.14"
49
+ "astro-scripts": "0.0.14",
50
+ "@astrojs/test-utils": "0.0.1"
55
51
  },
56
52
  "publishConfig": {
57
53
  "provenance": true
@@ -1,14 +0,0 @@
1
- import type { Request as CFRequest, CacheStorage, EventContext } from '@cloudflare/workers-types';
2
- import type { SSRManifest } from 'astro';
3
- export interface DirectoryRuntime<T extends object = object> {
4
- runtime: {
5
- waitUntil: (promise: Promise<any>) => void;
6
- env: EventContext<unknown, string, unknown>['env'] & T;
7
- cf: CFRequest['cf'];
8
- caches: CacheStorage;
9
- };
10
- }
11
- export declare function createExports(manifest: SSRManifest): {
12
- onRequest: (context: EventContext<unknown, string, unknown>) => Promise<Response | import("@cloudflare/workers-types").Response>;
13
- manifest: SSRManifest;
14
- };
@@ -1,40 +0,0 @@
1
- import { App } from 'astro/app';
2
- import { getProcessEnvProxy, isNode } from '../util.js';
3
- if (!isNode) {
4
- process.env = getProcessEnvProxy();
5
- }
6
- export function createExports(manifest) {
7
- const app = new App(manifest);
8
- const onRequest = async (context) => {
9
- const request = context.request;
10
- const { env } = context;
11
- // TODO: remove this any cast in the future
12
- // REF: the type cast to any is needed because the Cloudflare Env Type is not assignable to type 'ProcessEnv'
13
- process.env = env;
14
- const { pathname } = new URL(request.url);
15
- // static assets fallback, in case default _routes.json is not used
16
- if (manifest.assets.has(pathname)) {
17
- return env.ASSETS.fetch(request);
18
- }
19
- const routeData = app.match(request);
20
- Reflect.set(request, Symbol.for('astro.clientAddress'), request.headers.get('cf-connecting-ip'));
21
- const locals = {
22
- runtime: {
23
- waitUntil: (promise) => {
24
- context.waitUntil(promise);
25
- },
26
- env: context.env,
27
- cf: request.cf,
28
- caches: caches,
29
- },
30
- };
31
- const response = await app.render(request, { routeData, locals });
32
- if (app.setCookieHeaders) {
33
- for (const setCookieHeader of app.setCookieHeaders(response)) {
34
- response.headers.append('Set-Cookie', setCookieHeader);
35
- }
36
- }
37
- return response;
38
- };
39
- return { onRequest, manifest };
40
- }
@@ -1,5 +0,0 @@
1
- import type { AstroAdapter } from 'astro';
2
- export declare function getAdapter({ isModeDirectory, functionPerRoute, }: {
3
- isModeDirectory: boolean;
4
- functionPerRoute: boolean;
5
- }): AstroAdapter;
@@ -1,30 +0,0 @@
1
- export function getAdapter({ isModeDirectory, functionPerRoute, }) {
2
- const astroFeatures = {
3
- hybridOutput: 'stable',
4
- staticOutput: 'unsupported',
5
- serverOutput: 'stable',
6
- assets: {
7
- supportKind: 'stable',
8
- isSharpCompatible: false,
9
- isSquooshCompatible: false,
10
- },
11
- };
12
- if (isModeDirectory) {
13
- return {
14
- name: '@astrojs/cloudflare',
15
- serverEntrypoint: '@astrojs/cloudflare/entrypoints/server.directory.js',
16
- exports: ['onRequest', 'manifest'],
17
- adapterFeatures: {
18
- functionPerRoute,
19
- edgeMiddleware: false,
20
- },
21
- supportedAstroFeatures: astroFeatures,
22
- };
23
- }
24
- return {
25
- name: '@astrojs/cloudflare',
26
- serverEntrypoint: '@astrojs/cloudflare/entrypoints/server.advanced.js',
27
- exports: ['default'],
28
- supportedAstroFeatures: astroFeatures,
29
- };
30
- }
package/dist/util.d.ts DELETED
@@ -1,2 +0,0 @@
1
- export declare const isNode: boolean;
2
- export declare function getProcessEnvProxy(): {};
package/dist/util.js DELETED
@@ -1,10 +0,0 @@
1
- export const isNode = typeof process === 'object' && Object.prototype.toString.call(process) === '[object process]';
2
- export function getProcessEnvProxy() {
3
- return new Proxy({}, {
4
- get: (target, prop) => {
5
- console.warn(
6
- // NOTE: \0 prevents Vite replacement
7
- `Unable to access \`import.meta\0.env.${prop.toString()}\` on initialization as the Cloudflare platform only provides the environment variables per request. Please move the environment variable access inside a function that's only called after a request has been received.`);
8
- },
9
- });
10
- }
@@ -1,8 +0,0 @@
1
- /**
2
- * Remove duplicates and redundant patterns from an `include` or `exclude` list.
3
- * Otherwise Cloudflare will throw an error on deployment. Plus, it saves more entries.
4
- * E.g. `['/foo/*', '/foo/*', '/foo/bar'] => ['/foo/*']`
5
- * @param patterns a list of `include` or `exclude` patterns
6
- * @returns a deduplicated list of patterns
7
- */
8
- export declare function deduplicatePatterns(patterns: string[]): string[];
@@ -1,25 +0,0 @@
1
- /**
2
- * Remove duplicates and redundant patterns from an `include` or `exclude` list.
3
- * Otherwise Cloudflare will throw an error on deployment. Plus, it saves more entries.
4
- * E.g. `['/foo/*', '/foo/*', '/foo/bar'] => ['/foo/*']`
5
- * @param patterns a list of `include` or `exclude` patterns
6
- * @returns a deduplicated list of patterns
7
- */
8
- export function deduplicatePatterns(patterns) {
9
- const openPatterns = [];
10
- // A value in the set may only occur once; it is unique in the set's collection.
11
- // ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set
12
- const uniquePatterns = [...new Set(patterns)];
13
- for (const pattern of uniquePatterns) {
14
- if (pattern.endsWith('*')) {
15
- openPatterns.push(new RegExp(`^${pattern.replace(/(\*\/)*\*$/g, '(?=.{2,}).+[^*\n]$')}`));
16
- }
17
- }
18
- return uniquePatterns
19
- .sort((a, b) => a.length - b.length)
20
- .filter((pattern) => {
21
- if (openPatterns.some((p) => p.test(pattern)))
22
- return false;
23
- return true;
24
- });
25
- }