Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: ISR support for adapter-vercel #9063

Merged
merged 9 commits into from
Feb 17, 2023
Merged
Show file tree
Hide file tree
Changes from 6 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
5 changes: 5 additions & 0 deletions .changeset/pretty-kids-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/adapter-vercel': patch
---

fix: get ISR working on Vercel
8 changes: 2 additions & 6 deletions documentation/docs/25-build-and-deploy/90-adapter-vercel.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,18 +88,14 @@ export const config = {
// Setting the value to `false` means it will never expire.
expiration: 60,

// Option group number of the asset. Assets with the same group number will all be re-validated at the same time.
group: 1,

// Random token that can be provided in the URL to bypass the cached version of the asset, by requesting the asset
// with a __prerender_bypass=<token> cookie.
//
// Making a `GET` or `HEAD` request with `x-prerender-revalidate: <token>` will force the asset to be re-validated.
bypassToken: BYPASS_TOKEN,

// List of query string parameter names that will be cached independently.
// If an empty array, query values are not considered for caching.
// If `undefined` each unique query value is cached independently
// List of valid query parameters. Other parameters (such as utm tracking codes) will be ignored,
// ensuring that they do not result in content being regenerated unnecessarily
allowQuery: ['search']
}
};
Expand Down
14 changes: 14 additions & 0 deletions packages/adapter-vercel/files/serverless.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,25 @@ await server.init({
env: /** @type {Record<string, string>} */ (process.env)
});

const DATA_SUFFIX = '/__data.json';

/**
* @param {import('http').IncomingMessage} req
* @param {import('http').ServerResponse} res
*/
export default async (req, res) => {
if (req.url) {
const [path, search] = req.url.split('?');

const params = new URLSearchParams(search);
const pathname = params.get('__pathname');

if (pathname) {
params.delete('__pathname');
req.url = `${pathname}${path.endsWith(DATA_SUFFIX) ? DATA_SUFFIX : ''}?${params}`;
}
}

/** @type {Request} */
let request;

Expand Down
4 changes: 0 additions & 4 deletions packages/adapter-vercel/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@ export interface ServerlessConfig {
* Expiration time (in seconds) before the cached asset will be re-generated by invoking the Serverless Function. Setting the value to `false` means it will never expire.
*/
expiration: number | false;
/**
* Option group number of the asset. Assets with the same group number will all be re-validated at the same time.
*/
group?: number;
/**
* Random token that can be provided in the URL to bypass the cached version of the asset, by requesting the asset
* with a __prerender_bypass=<token> cookie.
Expand Down
103 changes: 67 additions & 36 deletions packages/adapter-vercel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,22 +76,6 @@ const plugin = function (defaults = {}) {
`${dirs.functions}/${name}.func`,
config
);

if (config.isr) {
write(
`${dirs.functions}/${name}.prerender-config.json`,
JSON.stringify(
{
expiration: config.isr.expiration,
group: config.isr.group,
bypassToken: config.isr.bypassToken,
allowQuery: config.isr.allowQuery
},
null,
'\t'
)
);
}
}

/**
Expand Down Expand Up @@ -158,6 +142,9 @@ const plugin = function (defaults = {}) {
/** @type {Map<string, string>} */
const functions = new Map();

/** @type {Map<import('@sveltejs/kit').RouteDefinition<import('.').Config>, { expiration: number | false, bypassToken: string | undefined, allowQuery: string[], group: number, passQuery: true }>} */
const isr_config = new Map();

// group routes by config
for (const route of builder.routes) {
if (route.prerender === true) continue;
Expand All @@ -175,6 +162,20 @@ const plugin = function (defaults = {}) {

const config = { runtime, ...defaults, ...route.config };

if (config.isr) {
if (config.isr.allowQuery?.includes('__pathname')) {
throw new Error('__pathname is a reserved query parameter for isr.allowQuery');
}

isr_config.set(route, {
expiration: config.isr.expiration,
bypassToken: config.isr.bypassToken,
allowQuery: ['__pathname', ...(config.isr.allowQuery ?? [])],
group: isr_config.size + 1,
passQuery: true
});
}

const hash = hash_config(config);

// first, check there are no routes with incompatible configs that will be merged
Expand Down Expand Up @@ -238,12 +239,47 @@ const plugin = function (defaults = {}) {
src = '^/?';
}

src += '(?:/__data.json)?$';

const name = functions.get(pattern);
if (name) {
static_config.routes.push({ src, dest: `/${name}` });
functions.delete(pattern);
const isr = isr_config.get(route);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't have any route config, we'll never get into here because functions is empty (because of line 216)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch. fixed

if (isr) {
const base = `${dirs.functions}/${route.id.slice(1)}`;
builder.mkdirp(base);

const target = `${dirs.functions}/${name}.func`;
const relative = path.relative(path.dirname(base), target);

// create a symlink to the actual function, but use the
// route name so that we can derive the correct URL
fs.symlinkSync(relative, `${base}.func`);
fs.symlinkSync(`../${relative}`, `${base}/__data.json.func`);

let i = 1;
const pathname = route.segments
.map((segment) => {
return segment.dynamic ? `$${i++}` : segment.content;
})
.join('/');

const json = JSON.stringify(isr, null, '\t');

write(`${base}.prerender-config.json`, json);
write(`${base}/__data.json.prerender-config.json`, json);

const q = `?__pathname=/${pathname}`;

static_config.routes.push({
src: src + '$',
dest: `${route.id}${q}`
});

static_config.routes.push({
src: src + '/__data.json$',
dest: `${route.id}/__data.json${q}`
});
} else {
static_config.routes.push({ src: src + '(?:/__data.json)?$', dest: `/${name}` });
}
}
}

Expand All @@ -266,11 +302,7 @@ function hash_config(config) {
config.external ?? '',
config.regions ?? '',
config.memory ?? '',
config.maxDuration ?? '',
config.isr?.expiration ?? '',
config.isr?.group ?? '',
config.isr?.bypassToken ?? '',
config.isr?.allowQuery ?? ''
config.maxDuration ?? ''
].join('/');
}

Expand Down Expand Up @@ -400,22 +432,21 @@ async function create_function_bundle(builder, entry, dir, config) {
}
}

const files = Array.from(traced.fileList);

// find common ancestor directory
/** @type {string[]} */
let common_parts = [];
let common_parts = files[0]?.split(path.sep) ?? [];

for (const file of traced.fileList) {
if (common_parts) {
const parts = file.split(path.sep);
for (let i = 1; i < files.length; i += 1) {
const file = files[i];
const parts = file.split(path.sep);

for (let i = 0; i < common_parts.length; i += 1) {
if (parts[i] !== common_parts[i]) {
common_parts = common_parts.slice(0, i);
break;
}
for (let i = 0; i < common_parts.length; i += 1) {
if (parts[i] !== common_parts[i]) {
common_parts = common_parts.slice(0, i);
break;
Rich-Harris marked this conversation as resolved.
Show resolved Hide resolved
}
} else {
common_parts = path.dirname(file).split(path.sep);
}
}

Expand Down