aboutsummaryrefslogtreecommitdiff
path: root/packages/integrations/vercel/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'packages/integrations/vercel/src/lib')
-rw-r--r--packages/integrations/vercel/src/lib/nft.ts83
-rw-r--r--packages/integrations/vercel/src/lib/redirects.ts135
-rw-r--r--packages/integrations/vercel/src/lib/searchRoot.ts101
-rw-r--r--packages/integrations/vercel/src/lib/web-analytics.ts30
4 files changed, 349 insertions, 0 deletions
diff --git a/packages/integrations/vercel/src/lib/nft.ts b/packages/integrations/vercel/src/lib/nft.ts
new file mode 100644
index 000000000..4381943bb
--- /dev/null
+++ b/packages/integrations/vercel/src/lib/nft.ts
@@ -0,0 +1,83 @@
+import { relative as relativePath } from 'node:path';
+import { fileURLToPath, pathToFileURL } from 'node:url';
+import { copyFilesToFolder } from '@astrojs/internal-helpers/fs';
+import { appendForwardSlash } from '@astrojs/internal-helpers/path';
+import type { AstroIntegrationLogger } from 'astro';
+import { searchForWorkspaceRoot } from './searchRoot.js';
+
+export async function copyDependenciesToFunction(
+ {
+ entry,
+ outDir,
+ includeFiles,
+ excludeFiles,
+ logger,
+ root,
+ }: {
+ entry: URL;
+ outDir: URL;
+ includeFiles: URL[];
+ excludeFiles: URL[];
+ logger: AstroIntegrationLogger;
+ root: URL;
+ },
+ // we want to pass the caching by reference, and not by value
+ cache: object,
+): Promise<{ handler: string }> {
+ const entryPath = fileURLToPath(entry);
+ logger.info(`Bundling function ${relativePath(fileURLToPath(outDir), entryPath)}`);
+
+ // Set the base to the workspace root
+ const base = pathToFileURL(appendForwardSlash(searchForWorkspaceRoot(fileURLToPath(root))));
+
+ // The Vite bundle includes an import to `@vercel/nft` for some reason,
+ // and that trips up `@vercel/nft` itself during the adapter build. Using a
+ // dynamic import helps prevent the issue.
+ // TODO: investigate why
+ const { nodeFileTrace } = await import('@vercel/nft');
+ const result = await nodeFileTrace([entryPath], {
+ base: fileURLToPath(base),
+ cache,
+ });
+
+ for (const error of result.warnings) {
+ if (error.message.startsWith('Failed to resolve dependency')) {
+ const [, module, file] = /Cannot find module '(.+?)' loaded from (.+)/.exec(error.message)!;
+
+ // The import(astroRemark) sometimes fails to resolve, but it's not a problem
+ if (module === '@astrojs/') continue;
+
+ // Sharp is always external and won't be able to be resolved, but that's also not a problem
+ if (module === 'sharp') continue;
+
+ if (entryPath === file) {
+ logger.debug(
+ `[@astrojs/vercel] The module "${module}" couldn't be resolved. This may not be a problem, but it's worth checking.`,
+ );
+ } else {
+ logger.debug(
+ `[@astrojs/vercel] The module "${module}" inside the file "${file}" couldn't be resolved. This may not be a problem, but it's worth checking.`,
+ );
+ }
+ }
+ // parse errors are likely not js and can safely be ignored,
+ // such as this html file in "main" meant for nw instead of node:
+ // https://github.com/vercel/nft/issues/311
+ else if (error.message.startsWith('Failed to parse')) {
+ continue;
+ } else {
+ throw error;
+ }
+ }
+
+ const commonAncestor = await copyFilesToFolder(
+ [...result.fileList].map((file) => new URL(file, base)).concat(includeFiles),
+ outDir,
+ excludeFiles,
+ );
+
+ return {
+ // serverEntry location inside the outDir
+ handler: relativePath(commonAncestor, entryPath),
+ };
+}
diff --git a/packages/integrations/vercel/src/lib/redirects.ts b/packages/integrations/vercel/src/lib/redirects.ts
new file mode 100644
index 000000000..612c8e917
--- /dev/null
+++ b/packages/integrations/vercel/src/lib/redirects.ts
@@ -0,0 +1,135 @@
+import nodePath from 'node:path';
+import { isRemotePath, removeLeadingForwardSlash } from '@astrojs/internal-helpers/path';
+import type { AstroConfig, IntegrationResolvedRoute, RoutePart } from 'astro';
+
+import type { Redirect } from '@vercel/routing-utils';
+
+const pathJoin = nodePath.posix.join;
+
+// Copied from astro/packages/astro/src/core/routing/manifest/create.ts
+// Disable eslint as we're not sure how to improve this regex yet
+// eslint-disable-next-line regexp/no-super-linear-backtracking
+const ROUTE_DYNAMIC_SPLIT = /\[(.+?\(.+?\)|.+?)\]/;
+const ROUTE_SPREAD = /^\.{3}.+$/;
+function getParts(part: string, file: string) {
+ const result: RoutePart[] = [];
+ part.split(ROUTE_DYNAMIC_SPLIT).map((str, i) => {
+ if (!str) return;
+ const dynamic = i % 2 === 1;
+
+ const [, content] = dynamic ? /([^(]+)$/.exec(str) || [null, null] : [null, str];
+
+ if (!content || (dynamic && !/^(?:\.\.\.)?[\w$]+$/.test(content))) {
+ throw new Error(`Invalid route ${file} — parameter name must match /^[a-zA-Z0-9_$]+$/`);
+ }
+
+ result.push({
+ content,
+ dynamic,
+ spread: dynamic && ROUTE_SPREAD.test(content),
+ });
+ });
+
+ return result;
+}
+/**
+ * Convert Astro routes into Vercel path-to-regexp syntax, which are the input for getTransformedRoutes
+ */
+function getMatchPattern(segments: RoutePart[][]) {
+ return segments
+ .map((segment) => {
+ return segment
+ .map((part) => {
+ if (part.spread) {
+ // Extract parameter name from spread syntax (e.g., "...slug" -> "slug")
+ const paramName = part.content.startsWith('...') ? part.content.slice(3) : part.content;
+ return `:${paramName}*`;
+ }
+ if (part.dynamic) {
+ return `:${part.content}`;
+ }
+ return part.content;
+ })
+ .join('');
+ })
+ .join('/');
+}
+
+// Copied from /home/juanm04/dev/misc/astro/packages/astro/src/core/routing/manifest/create.ts
+// 2022-04-26
+function getMatchRegex(segments: RoutePart[][]) {
+ return segments
+ .map((segment, segmentIndex) => {
+ return segment.length === 1 && segment[0].spread
+ ? '(?:\\/(.*?))?'
+ : // Omit leading slash if segment is a spread.
+ // This is handled using a regex in Astro core.
+ // To avoid complex data massaging, we handle in-place here.
+ (segmentIndex === 0 ? '' : '/') +
+ segment
+ .map((part) => {
+ if (part)
+ return part.spread
+ ? '(.*?)'
+ : part.dynamic
+ ? '([^/]+?)'
+ : part.content
+ .normalize()
+ .replace(/\?/g, '%3F')
+ .replace(/#/g, '%23')
+ .replace(/%5B/g, '[')
+ .replace(/%5D/g, ']')
+ .replace(/[*+?^${}()|[\]\\]/g, '\\$&');
+ })
+ .join('');
+ })
+ .join('');
+}
+
+function getRedirectLocation(route: IntegrationResolvedRoute, config: AstroConfig): string {
+ if (route.redirectRoute) {
+ const pattern = getMatchPattern(route.redirectRoute.segments);
+ return pathJoin(config.base, pattern);
+ }
+
+ const destination =
+ typeof route.redirect === 'object' ? route.redirect.destination : (route.redirect ?? '');
+
+ if (isRemotePath(destination)) {
+ return destination;
+ }
+
+ return pathJoin(config.base, destination);
+}
+
+function getRedirectStatus(route: IntegrationResolvedRoute): number {
+ if (typeof route.redirect === 'object') {
+ return route.redirect.status;
+ }
+ return 301;
+}
+
+export function escapeRegex(content: string) {
+ const segments = removeLeadingForwardSlash(content)
+ .split(nodePath.posix.sep)
+ .filter(Boolean)
+ .map((s: string) => {
+ return getParts(s, content);
+ });
+ return `^/${getMatchRegex(segments)}$`;
+}
+
+export function getRedirects(routes: IntegrationResolvedRoute[], config: AstroConfig): Redirect[] {
+ const redirects: Redirect[] = [];
+
+ for (const route of routes) {
+ if (route.type === 'redirect') {
+ redirects.push({
+ source: config.base + getMatchPattern(route.segments),
+ destination: getRedirectLocation(route, config),
+ statusCode: getRedirectStatus(route),
+ });
+ }
+ }
+ return redirects;
+}
diff --git a/packages/integrations/vercel/src/lib/searchRoot.ts b/packages/integrations/vercel/src/lib/searchRoot.ts
new file mode 100644
index 000000000..c770fddb4
--- /dev/null
+++ b/packages/integrations/vercel/src/lib/searchRoot.ts
@@ -0,0 +1,101 @@
+// Taken from: https://github.com/vitejs/vite/blob/1a76300cd16827f0640924fdc21747ce140c35fb/packages/vite/src/node/server/searchRoot.ts
+// MIT license
+// See https://github.com/vitejs/vite/blob/1a76300cd16827f0640924fdc21747ce140c35fb/LICENSE
+import fs from 'node:fs';
+import { dirname, join } from 'node:path';
+
+// https://github.com/vitejs/vite/issues/2820#issuecomment-812495079
+const ROOT_FILES = [
+ // '.git',
+
+ // https://pnpm.io/workspaces/
+ 'pnpm-workspace.yaml',
+
+ // https://rushjs.io/pages/advanced/config_files/
+ // 'rush.json',
+
+ // https://nx.dev/latest/react/getting-started/nx-setup
+ // 'workspace.json',
+ // 'nx.json',
+
+ // https://github.com/lerna/lerna#lernajson
+ 'lerna.json',
+];
+
+function tryStatSync(file: string): fs.Stats | undefined {
+ try {
+ // The "throwIfNoEntry" is a performance optimization for cases where the file does not exist
+ return fs.statSync(file, { throwIfNoEntry: false });
+ } catch {
+ // Ignore errors
+ }
+}
+
+function isFileReadable(filename: string): boolean {
+ if (!tryStatSync(filename)) {
+ return false;
+ }
+
+ try {
+ // Check if current process has read permission to the file
+ fs.accessSync(filename, fs.constants.R_OK);
+
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+// npm: https://docs.npmjs.com/cli/v7/using-npm/workspaces#installing-workspaces
+// yarn: https://classic.yarnpkg.com/en/docs/workspaces/#toc-how-to-use-it
+function hasWorkspacePackageJSON(root: string): boolean {
+ const path = join(root, 'package.json');
+ if (!isFileReadable(path)) {
+ return false;
+ }
+ try {
+ const content = JSON.parse(fs.readFileSync(path, 'utf-8')) || {};
+ return !!content.workspaces;
+ } catch {
+ return false;
+ }
+}
+
+function hasRootFile(root: string): boolean {
+ return ROOT_FILES.some((file) => fs.existsSync(join(root, file)));
+}
+
+function hasPackageJSON(root: string) {
+ const path = join(root, 'package.json');
+ return fs.existsSync(path);
+}
+
+/**
+ * Search up for the nearest `package.json`
+ */
+function searchForPackageRoot(current: string, root = current): string {
+ if (hasPackageJSON(current)) return current;
+
+ const dir = dirname(current);
+ // reach the fs root
+ if (!dir || dir === current) return root;
+
+ return searchForPackageRoot(dir, root);
+}
+
+/**
+ * Search up for the nearest workspace root
+ */
+export function searchForWorkspaceRoot(
+ current: string,
+ root = searchForPackageRoot(current),
+): string {
+ if (hasRootFile(current)) return current;
+ if (hasWorkspacePackageJSON(current)) return current;
+
+ const dir = dirname(current);
+ // reach the fs root
+ if (!dir || dir === current) return root;
+
+ return searchForWorkspaceRoot(dir, root);
+}
diff --git a/packages/integrations/vercel/src/lib/web-analytics.ts b/packages/integrations/vercel/src/lib/web-analytics.ts
new file mode 100644
index 000000000..d6ee4d78d
--- /dev/null
+++ b/packages/integrations/vercel/src/lib/web-analytics.ts
@@ -0,0 +1,30 @@
+export type VercelWebAnalyticsConfig = {
+ enabled: boolean;
+};
+
+export async function getInjectableWebAnalyticsContent({
+ mode,
+}: {
+ mode: 'development' | 'production';
+}) {
+ const base = `window.va = window.va || function () { (window.vaq = window.vaq || []).push(arguments); };`;
+
+ if (mode === 'development') {
+ return `
+ ${base}
+ var script = document.createElement('script');
+ script.defer = true;
+ script.src = 'https://cdn.vercel-insights.com/v1/script.debug.js';
+ var head = document.querySelector('head');
+ head.appendChild(script);
+ `;
+ }
+
+ return `${base}
+ var script = document.createElement('script');
+ script.defer = true;
+ script.src = '/_vercel/insights/script.js';
+ var head = document.querySelector('head');
+ head.appendChild(script);
+ `;
+}