@recranet/astro-workers-for-platforms-adapter 1.0.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/README.md ADDED
@@ -0,0 +1 @@
1
+ # astro-workers-for-platforms-adapter
package/adapter.mjs ADDED
@@ -0,0 +1,58 @@
1
+ // Custom Astro adapter for Cloudflare Workers for Platforms.
2
+ //
3
+ // Architecture:
4
+ // Request → Dispatch Worker → User Worker (worker-entry.mjs) → env.ASSETS.fetch()
5
+ //
6
+ // This adapter ensures Astro always emits a worker entry, even for fully static sites.
7
+ // While static assets could be uploaded to R2 and served by the dispatch worker,
8
+ // Workers for Platforms supports a native Static Assets binding (env.ASSETS) that
9
+ // serves files from the edge with per-tenant isolation — no R2 needed.
10
+ //
11
+ // The official @astrojs/cloudflare adapter cannot be used here because:
12
+ // - It requires wrangler as a peer dependency (we deploy via the CF API directly)
13
+ // - As of Astro 6 (PR #15478), it deletes the _worker.js output for fully static
14
+ // sites, which is the opposite of what Workers for Platforms needs
15
+ // - Its worker entrypoint is designed for Pages/Workers, not the dispatch namespace
16
+ // API with env.ASSETS binding and run_worker_first config
17
+ //
18
+ // Build flow:
19
+ // `astro build` triggers this adapter, which sets `ssr.noExternal: true` so Vite
20
+ // bundles all dependencies (including hono/utils/mime used in worker-entry.mjs)
21
+ // into a single dist/server/entry.mjs with no external imports. Static assets
22
+ // are prerendered into dist/client/.
23
+ //
24
+ // The build output (entry.mjs + static assets) is deployed by the astro-sandbox
25
+ // worker in studio via deploy.ts, which uses the Workers for Platforms 3-step asset
26
+ // upload API (create session → upload buckets → deploy worker with completion token).
27
+ export default function cloudflareWorkerScriptAdapter() {
28
+ return {
29
+ name: 'cloudflare-worker-script',
30
+ hooks: {
31
+ 'astro:config:setup': ({ updateConfig }) => {
32
+ updateConfig({
33
+ vite: {
34
+ ssr: {
35
+ // Bundle all dependencies into the server entry (no external imports)
36
+ noExternal: true,
37
+ },
38
+ },
39
+ });
40
+ },
41
+ 'astro:config:done': ({ setAdapter }) => {
42
+ setAdapter({
43
+ name: 'cloudflare-worker-script',
44
+ serverEntrypoint: new URL('./worker-entry.mjs', import.meta.url),
45
+ previewEntrypoint: new URL('./preview-entry.mjs', import.meta.url).pathname,
46
+ entrypointResolution: 'auto',
47
+ supportedAstroFeatures: {
48
+ staticOutput: 'stable',
49
+ hybridOutput: { support: 'unsupported', message: '', suppress: 'all' },
50
+ serverOutput: { support: 'unsupported', message: '', suppress: 'all' },
51
+ sharpImageService: { support: 'unsupported', message: '', suppress: 'all' },
52
+ envGetSecret: 'stable',
53
+ },
54
+ });
55
+ },
56
+ },
57
+ };
58
+ }
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@recranet/astro-workers-for-platforms-adapter",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "exports": "./adapter.mjs",
6
+ "files": [
7
+ "adapter.mjs",
8
+ "worker-entry.mjs",
9
+ "preview-entry.mjs",
10
+ "vite-plugin-astro-preview.mjs"
11
+ ],
12
+ "peerDependencies": {
13
+ "astro": "^6.0.0"
14
+ },
15
+ "dependencies": {
16
+ "hono": "^4.12.7"
17
+ },
18
+ "scripts": {
19
+ "publish": "npm publish . --access public"
20
+ }
21
+ }
@@ -0,0 +1,30 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { preview } from 'vite';
3
+ import { vitePluginAstroPreview } from './vite-plugin-astro-preview.mjs';
4
+
5
+ export default async function createPreviewServer({ client, host, port, headers, logger }) {
6
+ const clientDir = fileURLToPath(client);
7
+
8
+ const previewServer = await preview({
9
+ configFile: false,
10
+ appType: 'mpa',
11
+ build: { outDir: clientDir },
12
+ preview: { host, port, headers },
13
+ plugins: [vitePluginAstroPreview(clientDir)],
14
+ });
15
+
16
+ const address = previewServer.resolvedUrls?.local?.[0] ?? `http://${host ?? 'localhost'}:${port}/`;
17
+ logger.info(`Preview server listening on ${address}`);
18
+
19
+ return {
20
+ host: host ?? 'localhost',
21
+ port,
22
+ closed: () =>
23
+ new Promise((resolve, reject) => {
24
+ previewServer.httpServer.addListener('close', resolve);
25
+ previewServer.httpServer.addListener('error', reject);
26
+ }),
27
+ server: previewServer.httpServer,
28
+ stop: previewServer.close.bind(previewServer),
29
+ };
30
+ }
@@ -0,0 +1,53 @@
1
+ // Adapted from astro/dist/core/preview/vite-plugin-astro-preview.js
2
+ // Handles clean URL routing for static builds in Vite's MPA preview server.
3
+ import fs from 'node:fs';
4
+ import { join } from 'node:path';
5
+
6
+ export function vitePluginAstroPreview(outDir) {
7
+ return {
8
+ name: 'astro:preview',
9
+ apply: 'serve',
10
+ configurePreviewServer(server) {
11
+ // Replace Vite's default 404 handler with one that serves 404.html if it exists
12
+ server.middlewares.use((req, res, next) => {
13
+ for (const middleware of server.middlewares.stack) {
14
+ if (middleware.handle.name === 'vite404Middleware') {
15
+ middleware.handle = (_req, _res) => {
16
+ const errorPagePath = join(outDir, '404.html');
17
+ if (fs.existsSync(errorPagePath)) {
18
+ _res.statusCode = 404;
19
+ _res.setHeader('Content-Type', 'text/html');
20
+ _res.end(fs.readFileSync(errorPagePath));
21
+ } else {
22
+ _res.statusCode = 404;
23
+ _res.end('Not Found');
24
+ }
25
+ };
26
+ }
27
+ }
28
+ next();
29
+ });
30
+
31
+ // Post-middleware: rewrite clean URLs to their HTML files
32
+ return () => {
33
+ server.middlewares.use((req, _res, next) => {
34
+ const pathname = req.url?.split('?')[0];
35
+ if (pathname?.endsWith('/')) {
36
+ const htmlPath = join(outDir, pathname.slice(0, -1) + '.html');
37
+ if (fs.existsSync(htmlPath)) {
38
+ req.url = pathname.slice(0, -1) + '.html';
39
+ return next();
40
+ }
41
+ } else if (pathname && !pathname.includes('.')) {
42
+ const indexPath = join(outDir, pathname, 'index.html');
43
+ if (fs.existsSync(indexPath)) {
44
+ req.url = pathname + '/index.html';
45
+ return next();
46
+ }
47
+ }
48
+ next();
49
+ });
50
+ };
51
+ },
52
+ };
53
+ }
@@ -0,0 +1,26 @@
1
+ // Worker entry point for Cloudflare Workers for Platforms.
2
+ // Serves static assets via the env.ASSETS binding provided by the dispatch namespace,
3
+ // with fixes for missing content-type headers and cache-control.
4
+ import { getMimeType } from 'hono/utils/mime';
5
+
6
+ export default {
7
+ async fetch(request, env) {
8
+ const asset = await env.ASSETS.fetch(request);
9
+
10
+ // Clone into a mutable response so we can set headers.
11
+ const response = new Response(asset.body, asset);
12
+
13
+ // Prevent edge/browser caching — themes are redeployed in-place and must
14
+ // always reflect the latest version.
15
+ response.headers.set('cache-control', 'no-store');
16
+
17
+ // Workers for Platforms sometimes omits content-type on asset responses.
18
+ // Derive it from the file extension so browsers handle CSS, JS, etc. correctly.
19
+ const mimeType = getMimeType(new URL(request.url).pathname);
20
+ if (!response.headers.get('content-type') && mimeType) {
21
+ response.headers.set('content-type', mimeType);
22
+ }
23
+
24
+ return response;
25
+ },
26
+ };