Skip to content
Closed

Dev #30

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions adapter-cloudflare-polyfill/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { writeFileSync } from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';

import * as esbuild from 'esbuild';


/** @type {import('.').default} */
export default function (options = {}) {
return {
name: '@sveltejs/adapter-cloudflare-polyfill',
async adapt(builder) {
const files = fileURLToPath(new URL('./src', import.meta.url).href);
const dest = builder.getBuildDirectory('cloudflare');
const tmp = builder.getBuildDirectory('cloudflare-tmp');

builder.rimraf(dest);
builder.rimraf(tmp);
builder.mkdirp(tmp);

// generate 404.html first which can then be overridden by prerendering, if the user defined such a page
await builder.generateFallback(path.join(dest, '404.html'));

const dest_dir = `${dest}${builder.config.kit.paths.base}`;
const written_files = builder.writeClient(dest_dir);
builder.writePrerendered(dest_dir);

const relativePath = path.posix.relative(tmp, builder.getServerDirectory());

writeFileSync(
`${tmp}/manifest.js`,
`export const manifest = ${builder.generateManifest({ relativePath })};\n\n` +
`export const prerendered = new Set(${JSON.stringify(builder.prerendered.paths)});\n`
);

writeFileSync(
`${dest}/_routes.json`,
JSON.stringify(get_routes_json(builder, written_files, options.routes ?? {}), null, '\t')
);

writeFileSync(`${dest}/_headers`, generate_headers(builder.getAppPath()), { flag: 'a' });

builder.copy(`${files}/worker.js`, `${tmp}/_worker.js`, {
replace: {
SERVER: `${relativePath}/index.js`,
MANIFEST: './manifest.js'
}
});

await esbuild.build({
platform: 'node',
conditions: ['worker', 'workerd', 'browser'],
sourcemap: 'linked',
target: 'es2022',
entryPoints: [`${tmp}/_worker.js`],
outfile: `${dest}/_worker.js`,
allowOverwrite: true,
format: 'esm',
bundle: true,
loader: {
'.wasm': 'copy'
},
external: [
'cloudflare:*',
'node:events',
'node:buffer',
'node:stream'
]
});
}
};
}

/**
* @param {import('@sveltejs/kit').Builder} builder
* @param {string[]} assets
* @param {import('./index').AdapterOptions['routes']} routes
* @returns {import('.').RoutesJSONSpec}
*/
function get_routes_json(builder, assets, { include = ['/*'], exclude = ['<all>'] }) {
if (!Array.isArray(include) || !Array.isArray(exclude)) {
throw new Error('routes.include and routes.exclude must be arrays');
}

if (include.length === 0) {
throw new Error('routes.include must contain at least one route');
}

if (include.length > 100) {
throw new Error('routes.include must contain 100 or fewer routes');
}

exclude = exclude
.flatMap((rule) => (rule === '<all>' ? ['<build>', '<files>', '<prerendered>'] : rule))
.flatMap((rule) => {
if (rule === '<build>') {
return `/${builder.getAppPath()}/*`;
}

if (rule === '<files>') {
return assets
.filter(
(file) =>
!(
file.startsWith(`${builder.config.kit.appDir}/`) ||
file === '_headers' ||
file === '_redirects'
)
)
.map((file) => `/${file}`);
}

if (rule === '<prerendered>') {
const prerendered = [];
for (const path of builder.prerendered.paths) {
if (!builder.prerendered.redirects.has(path)) {
prerendered.push(path);
}
}

return prerendered;
}

return rule;
});

const excess = include.length + exclude.length - 100;
if (excess > 0) {
const message = `Function includes/excludes exceeds _routes.json limits (see https://developers.cloudflare.com/pages/platform/functions/routing/#limits). Dropping ${excess} exclude rules — this will cause unnecessary function invocations.`;
builder.log.warn(message);

exclude.length -= excess;
}

return {
version: 1,
description: 'Generated by @sveltejs/adapter-cloudflare-polyfill',
include,
exclude
};
}

/** @param {string} app_dir */
function generate_headers(app_dir) {
return `
# === START AUTOGENERATED SVELTE IMMUTABLE HEADERS ===
/${app_dir}/*
X-Robots-Tag: noindex
Cache-Control: no-cache
/${app_dir}/immutable/*
! Cache-Control
Cache-Control: public, immutable, max-age=31536000
# === END AUTOGENERATED SVELTE IMMUTABLE HEADERS ===
`.trimEnd();
}
53 changes: 53 additions & 0 deletions adapter-cloudflare-polyfill/nodePolyfill.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as path from 'node:path';

import { build } from 'esbuild';
import { nodeModulesPolyfillPlugin } from 'esbuild-plugins-node-modules-polyfill';
import { glob } from 'glob';

/**
* @param {import('@sveltejs/kit').Adapter} adapter
*/
export function nodeCompat(adapter) {
return {
name: `${adapter.name} & node compat`,
/**
* @param {import('@sveltejs/kit').Builder} builder
*/
async adapt(builder) {
const build_dir = builder.getBuildDirectory('node-compat');

builder.rimraf(build_dir);

const server_dir = builder.getServerDirectory();
console.log(server_dir);
const sources = await glob(path.join(server_dir, '**/*.js'), {
nodir: true
});

await build({
platform: 'browser',
conditions: ['worker', 'workerd', 'browser'],
entryPoints: sources,
outdir: build_dir,
format: 'esm',
bundle: true,
loader: {
'.wasm': 'copy'
},
external: ['cloudflare:*'],
plugins: [
nodeModulesPolyfillPlugin({
globals: {
process: true,
Buffer: true
}
})
]
});

builder.getServerDirectory = () => build_dir;

adapter.adapt(builder);
}
};
}
65 changes: 65 additions & 0 deletions adapter-cloudflare-polyfill/src/worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { manifest, prerendered } from 'MANIFEST';
import { Server } from 'SERVER';
import * as Cache from 'worktop/cache';

const server = new Server(manifest);

/** @type {import('worktop/cfw').Module.Worker<{ ASSETS: import('worktop/cfw.durable').Durable.Object }>} */
const worker = {
async fetch(req, env, context) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await server.init({ env });
// skip cache if "cache-control: no-cache" in request
let pragma = req.headers.get('cache-control') || '';
let res = !pragma.includes('no-cache') && (await Cache.lookup(req));
if (res) return res;

let { pathname } = new URL(req.url);
try {
pathname = decodeURIComponent(pathname);
} catch {
// ignore invalid URI
}

const stripped_pathname = pathname.replace(/\/$/, '');

// prerendered pages and /static files
let is_static_asset = false;
const filename = stripped_pathname.substring(1);
if (filename) {
is_static_asset =
manifest.assets.has(filename) || manifest.assets.has(filename + '/index.html');
}

const location = pathname.at(-1) === '/' ? stripped_pathname : pathname + '/';

if (is_static_asset || prerendered.has(pathname)) {
res = await env.ASSETS.fetch(req);
} else if (location && prerendered.has(location)) {
res = new Response('', {
status: 308,
headers: {
location
}
});
} else {
// dynamically-generated pages
res = await server.respond(req, {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
platform: { env, context, caches, cf: req.cf },
getClientAddress() {
return req.headers.get('cf-connecting-ip');
}
});
}

// write to `Cache` only if response is not an error,
// let `Cache.save` handle the Cache-Control and Vary headers
pragma = res.headers.get('cache-control') || '';
return pragma && res.status < 400 ? Cache.save(req, res, context) : res;
}
};

export default worker;
13 changes: 10 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@
"preview": "vite preview",
"test": "npm run test:integration && npm run test:unit",
"test:integration": "playwright test",
"test:unit": "vitest"
"test:unit": "vitest",
"pages:dev": "wrangler pages dev --compatibility-date=2023-10-25 --proxy 5173 -- pnpm run dev"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.428.0",
"@aws-sdk/lib-storage": "^3.428.0",
"nodemailer": "^6.9.7"
"@aws-sdk/lib-storage": "^3.428.0"
},
"devDependencies": {
"@floating-ui/dom": "^1.5.3",
Expand All @@ -31,20 +31,25 @@
"@typescript-eslint/eslint-plugin": "^6.7.5",
"@typescript-eslint/parser": "^6.7.5",
"autoprefixer": "^10.4.16",
"esbuild": "^0.19.5",
"esbuild-plugins-node-modules-polyfill": "^1.6.1",
"eslint": "^8.51.0",
"eslint-config-prettier": "^9.0.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-svelte": "^2.34.0",
"eslint-plugin-tailwindcss": "^3.13.0",
"glob": "^10.3.10",
"lodash.set": "^4.3.2",
"nodemailer": "^6.9.7",
"postcss": "^8.4.31",
"postgres": "^3.4.0",
"prettier": "^3.0.3",
"prettier-plugin-svelte": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.6",
"svelte": "^4.2.1",
"svelte-check": "^3.5.2",
"svelte-node-compat": "^0.0.7",
"sveltekit-superforms": "^1.8.0",
"tailwind-merge": "^1.14.0",
"tailwindcss": "^3.3.3",
Expand All @@ -53,6 +58,8 @@
"valibot": "^0.19.0",
"vite": "^4.4.11",
"vitest": "^0.34.6",
"worktop": "^0.7.3",
"wrangler": "^3.15.0",
"zod": "^3.22.4"
}
}
Loading