@astrojs/cloudflare 13.6.1 → 13.7.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.
@@ -88,7 +88,7 @@ function serverStart({
88
88
  host,
89
89
  base
90
90
  }) {
91
- const version = "13.6.1";
91
+ const version = "13.7.0";
92
92
  const localPrefix = `${colors.dim("\u2503")} Local `;
93
93
  const networkPrefix = `${colors.dim("\u2503")} Network `;
94
94
  const emptyPrefix = " ".repeat(11);
package/dist/fetch.js CHANGED
@@ -10,9 +10,15 @@ import {
10
10
  createLocals,
11
11
  getClientAddress
12
12
  } from "./utils/cf.js";
13
- setGetEnv(createGetEnv(globalEnv));
14
- const app = createApp();
13
+ let app;
14
+ function ensureInitialized() {
15
+ if (!app) {
16
+ setGetEnv(createGetEnv(globalEnv));
17
+ app = createApp();
18
+ }
19
+ }
15
20
  async function cf(state, env, ctx) {
21
+ ensureInitialized();
16
22
  injectSessionBinding(app.manifest, env);
17
23
  const staticAsset = matchStaticAsset(app.manifest, state.request.url, env);
18
24
  if (staticAsset) return staticAsset;
package/dist/index.js CHANGED
@@ -1,14 +1,18 @@
1
1
  import { createReadStream, existsSync, readFileSync } from "node:fs";
2
- import { appendFile, readFile, rename, stat, writeFile } from "node:fs/promises";
2
+ import { appendFile, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
3
3
  import { relative } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { normalizePath } from "vite";
6
6
  import { createInterface } from "node:readline/promises";
7
- import { removeLeadingForwardSlash } from "@astrojs/internal-helpers/path";
7
+ import {
8
+ removeLeadingForwardSlash,
9
+ removeTrailingForwardSlash
10
+ } from "@astrojs/internal-helpers/path";
8
11
  import { createRedirectsFromAstroRoutes, printAsRedirects } from "@astrojs/underscore-redirects";
9
12
  import { cloudflare as cfVitePlugin } from "@cloudflare/vite-plugin";
10
13
  import { astroFrontmatterScanPlugin } from "./esbuild-plugin-astro-frontmatter.js";
11
14
  import { getParts } from "./utils/generate-routes-json.js";
15
+ import { buildAssetsHeadersContent } from "./utils/headers.js";
12
16
  import {
13
17
  normalizeImageServiceConfig,
14
18
  setImageConfig
@@ -378,6 +382,39 @@ function createIntegration({
378
382
  } catch {
379
383
  }
380
384
  }
385
+ if (_config.build.assetsPrefix) {
386
+ logger.debug(
387
+ "Skipping Cache-Control injection for assets \u2014 `build.assetsPrefix` is set, so assets are served from a different origin."
388
+ );
389
+ } else {
390
+ const headersPath = new URL("./_headers", _originalClientDir);
391
+ const result = await buildAssetsHeadersContent(
392
+ {
393
+ assetsDir: _config.build.assets,
394
+ basePrefix: removeTrailingForwardSlash(_config.base),
395
+ headersPath
396
+ },
397
+ (path) => readFile(path, "utf-8")
398
+ );
399
+ if (result === null) {
400
+ logger.debug(
401
+ `Skipping Cache-Control injection \u2014 _headers already sets Cache-Control on a matching rule.`
402
+ );
403
+ } else {
404
+ const tempPath = new URL("./_headers.tmp", _originalClientDir);
405
+ try {
406
+ await writeFile(tempPath, result.content);
407
+ await rename(tempPath, headersPath);
408
+ } catch (err) {
409
+ await unlink(tempPath).catch(() => {
410
+ });
411
+ throw err;
412
+ }
413
+ logger.info(
414
+ `Injected immutable Cache-Control for ${result.assetsPattern} into _headers.`
415
+ );
416
+ }
417
+ }
381
418
  let redirectsExists = false;
382
419
  try {
383
420
  const redirectsStat = await stat(new URL("./_redirects", _originalClientDir));
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Returns true if the given `_headers` content already declares (or detaches)
3
+ * a `Cache-Control` directive on any rule whose URL pattern matches `path`.
4
+ *
5
+ * Used to avoid emitting a second `Cache-Control` rule for hashed assets when
6
+ * the user already has one — Cloudflare merges duplicate header values across
7
+ * matching rules with a comma, which produces contradictory cache directives.
8
+ */
9
+ export declare function headersFileHasCacheControlForPath(content: string, path: string): boolean;
10
+ /**
11
+ * Computes the content to write to `_headers` to inject an immutable
12
+ * Cache-Control rule for the hashed assets directory.
13
+ *
14
+ * Returns `null` when injection should be skipped because the existing
15
+ * `_headers` already declares `Cache-Control` on a rule matching the assets
16
+ * path — Cloudflare merges duplicate header values with a comma, which would
17
+ * produce contradictory directives.
18
+ */
19
+ export declare function buildAssetsHeadersContent(opts: {
20
+ assetsDir: string;
21
+ basePrefix: string;
22
+ headersPath: URL;
23
+ }, readFile: (path: URL) => Promise<string>): Promise<{
24
+ content: string;
25
+ assetsPattern: string;
26
+ } | null>;
@@ -0,0 +1,63 @@
1
+ function cfHeadersPatternToRegex(pattern) {
2
+ let regexStr = "";
3
+ let i = 0;
4
+ while (i < pattern.length) {
5
+ const ch = pattern[i];
6
+ if (ch === "*") {
7
+ regexStr += ".*";
8
+ i++;
9
+ } else if (ch === ":" && /[A-Za-z]/.test(pattern[i + 1] ?? "")) {
10
+ i++;
11
+ while (i < pattern.length && /\w/.test(pattern[i])) i++;
12
+ regexStr += "[^/]+";
13
+ } else {
14
+ regexStr += ch.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
15
+ i++;
16
+ }
17
+ }
18
+ return new RegExp(`^${regexStr}$`);
19
+ }
20
+ function headersFileHasCacheControlForPath(content, path) {
21
+ let matchesCurrentSection = false;
22
+ for (const rawLine of content.split("\n")) {
23
+ const trimmed = rawLine.trim();
24
+ if (!trimmed || trimmed.startsWith("#")) continue;
25
+ const isSectionHeader = !/^\s/.test(rawLine);
26
+ if (isSectionHeader) {
27
+ const pathOnly = trimmed.replace(/^https?:\/\/[^/]+/, "");
28
+ try {
29
+ matchesCurrentSection = cfHeadersPatternToRegex(pathOnly).test(path);
30
+ } catch {
31
+ matchesCurrentSection = false;
32
+ }
33
+ } else if (matchesCurrentSection && // Either `Cache-Control: value` (set) or `! Cache-Control` (detach).
34
+ /^\s+(?:!\s+cache-control\s*$|cache-control\s*:)/i.test(rawLine)) {
35
+ return true;
36
+ }
37
+ }
38
+ return false;
39
+ }
40
+ async function buildAssetsHeadersContent(opts, readFile) {
41
+ const { assetsDir, basePrefix, headersPath } = opts;
42
+ const assetsPattern = `${basePrefix}/${assetsDir}/*`;
43
+ const probePath = `${basePrefix}/${assetsDir}/probe`;
44
+ let existingHeaders = "";
45
+ try {
46
+ existingHeaders = await readFile(headersPath);
47
+ } catch {
48
+ }
49
+ if (headersFileHasCacheControlForPath(existingHeaders, probePath)) {
50
+ return null;
51
+ }
52
+ const cacheBlock = `${assetsPattern}
53
+ Cache-Control: public, max-age=31536000, immutable
54
+ `;
55
+ const normalizedExisting = existingHeaders && !existingHeaders.endsWith("\n") ? existingHeaders + "\n" : existingHeaders;
56
+ const content = normalizedExisting ? `${cacheBlock}
57
+ ${normalizedExisting}` : cacheBlock;
58
+ return { content, assetsPattern };
59
+ }
60
+ export {
61
+ buildAssetsHeadersContent,
62
+ headersFileHasCacheControlForPath
63
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@astrojs/cloudflare",
3
3
  "description": "Deploy your site to Cloudflare Workers",
4
- "version": "13.6.1",
4
+ "version": "13.7.0",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
7
7
  "author": "withastro",
@@ -55,7 +55,7 @@
55
55
  "cheerio": "1.2.0",
56
56
  "devalue": "^5.8.1",
57
57
  "prismjs": "^1.30.0",
58
- "astro": "6.4.3",
58
+ "astro": "6.4.5",
59
59
  "astro-scripts": "0.0.14"
60
60
  },
61
61
  "publishConfig": {