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

add unenv plugin v2 #105

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 3 additions & 1 deletion packages/vercel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@
"esbuild": "^0.23.0",
"fast-glob": "^3.3.2",
"magicast": "^0.3.4",
"zod": "^3.23.8"
"zod": "^3.23.8",
"unenv-nightly": "2.0.0-1721914978.1a79944",
"resolve-from": "^5.0.0"
}
}
41 changes: 20 additions & 21 deletions packages/vercel/src/build.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ResolvedConfig } from 'vite';
import glob from 'fast-glob';
import { builtinModules } from 'module'
import path, { basename } from 'path';
import { getOutput, getRoot, pathRelativeTo } from './utils';
import { build, BuildOptions, type Plugin } from 'esbuild';
Expand All @@ -14,6 +13,7 @@ import { generateCode, loadFile, type ASTNode } from 'magicast';
import { getNodeVersion } from '@vercel/build-utils';
import { nodeFileTrace } from '@vercel/nft';
import { findRoot } from '@manypkg/find-root';
import { unenvPlugin } from './esbuild/unenvPlugin';

export function getAdditionalEndpoints(resolvedConfig: ResolvedConfig) {
return (resolvedConfig.vercel?.additionalEndpoints ?? []).map((e) => ({
Expand Down Expand Up @@ -92,29 +92,29 @@ const vercelOgPlugin = (ctx: { found: boolean; index: string }): Plugin => {
};
};

const standardBuildOptions: BuildOptions = {
bundle: true,
target: 'es2022',
format: 'esm',
platform: 'node',
logLevel: 'info',
logOverride: {
'ignored-bare-import': 'verbose',
'require-resolve-not-external': 'verbose',
},
minify: false,
plugins: [],
define: {
'process.env.NODE_ENV': '"production"',
'import.meta.env.NODE_ENV': '"production"',
},
};

export async function buildFn(
resolvedConfig: ResolvedConfig,
entry: ViteVercelApiEntry,
buildOptions?: BuildOptions,
) {
const standardBuildOptions: BuildOptions = {
bundle: true,
target: 'es2022',
format: 'esm',
platform: 'node',
logLevel: 'info',
logOverride: {
'ignored-bare-import': 'verbose',
'require-resolve-not-external': 'verbose',
},
minify: false,
plugins: [],
define: {
'process.env.NODE_ENV': '"production"',
'import.meta.env.NODE_ENV': '"production"',
},
};

assert(
entry.destination.length > 0,
`Endpoint ${
Expand Down Expand Up @@ -156,7 +156,6 @@ export async function buildFn(

if (entry.edge) {
delete options.platform;
options.external = [...builtinModules, ...builtinModules.map((m) => `node:${m}`)]
options.conditions = [
'edge-light',
'worker',
Expand All @@ -165,7 +164,7 @@ export async function buildFn(
'import',
'require',
];
options.plugins?.push(edgeWasmPlugin);
options.plugins?.push(edgeWasmPlugin, unenvPlugin());
options.format = 'esm';
} else if (options.format === 'esm') {
options.banner = {
Expand Down
215 changes: 215 additions & 0 deletions packages/vercel/src/esbuild/unenvPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
// credits:
// https://github.com/cloudflare/workers-sdk/blob/e24939c53475228e12a3c5228aa652c6473a889f/packages/wrangler/src/deployment-bundle/esbuild-plugins/hybrid-nodejs-compat.ts

import type { Plugin, PluginBuild } from 'esbuild';
import { builtinModules, createRequire } from 'node:module';
import path from 'node:path';
import resolveFrom from 'resolve-from';
import { env, nodeless, vercel } from 'unenv-nightly';
import { packagePath } from '../utils';

const require_ = createRequire(import.meta.url);

const NODE_REQUIRE_NAMESPACE = 'node-require';
const UNENV_GLOBALS_RE = /_virtual_unenv_inject-([^.]+)\.js$/;

const UNENV_REGEX = /\bunenv\b/g;
const NODEJS_MODULES_RE = new RegExp(`^(node:)?(${builtinModules.join('|')})$`);

function replaceUnenv<T>(value: T): T {
if (typeof value === 'string') {
return value.replace(UNENV_REGEX, 'unenv-nightly') as T;
}

if (Array.isArray(value)) {
return value.map(replaceUnenv) as T;
}

if (typeof value === 'object' && value !== null) {
return Object.fromEntries(
Object.entries(value).map(([k, v]) => [k, replaceUnenv(v)]),
) as T;
}

return value;
}

export function unenvPlugin(): Plugin {
const { alias, inject, external, polyfill } = replaceUnenv(
env(nodeless, vercel),
);

// already included in polyfill
delete inject.global;
delete inject.process;
delete inject.Buffer;
delete inject.performance;

return {
name: 'unenv',
setup(build: PluginBuild) {
handlePolyfills(build, polyfill);
handleRequireCallsToNodeJSBuiltins(build);
handleAliasedNodeJSPackages(build, alias, external);
handleNodeJSGlobals(build, inject);
},
};
}

function handlePolyfills(build: PluginBuild, polyfill: string[]): void {
if (polyfill.length === 0) return;

build.initialOptions.inject = [
...(build.initialOptions.inject ?? []),
...polyfill.map((id) =>
resolveFrom(packagePath, id).replace(/\.cjs$/, '.mjs'),
),
];
}

function handleRequireCallsToNodeJSBuiltins(build: PluginBuild): void {
build.onResolve({ filter: NODEJS_MODULES_RE }, (args) => {
if (args.kind === 'require-call') {
return {
path: args.path,
namespace: NODE_REQUIRE_NAMESPACE,
};
}
});

build.onLoad(
{ filter: /.*/, namespace: NODE_REQUIRE_NAMESPACE },
({ path: modulePath }) => ({
contents: `
import libDefault from '${modulePath}';
export default libDefault;
`,
loader: 'js',
}),
);
}

function handleAliasedNodeJSPackages(
build: PluginBuild,
alias: Record<string, string>,
external: string[],
): void {
// esbuild expects alias paths to be absolute
const aliasAbsolute = Object.fromEntries(
Object.entries(alias)
.map(([key, value]) => {
let resolvedAliasPath;
try {
resolvedAliasPath = require_.resolve(value);
} catch (e) {
// this is an alias for package that is not installed in the current app => ignore
resolvedAliasPath = '';
}

return [key, resolvedAliasPath.replace(/\.cjs$/, '.mjs')];
})
.filter((entry) => entry[1] !== ''),
);
const UNENV_ALIAS_RE = new RegExp(
`^(${Object.keys(aliasAbsolute).join('|')})$`,
);

build.onResolve({ filter: UNENV_ALIAS_RE }, (args) => {
// Resolve the alias to its absolute path and potentially mark it as external
return {
path: aliasAbsolute[args.path],
external: external.includes(alias[args.path]),
};
});
}

function handleNodeJSGlobals(
build: PluginBuild,
inject: Record<string, string | string[]>,
): void {
build.initialOptions.inject = [
...(build.initialOptions.inject ?? []),
...Object.keys(inject).map((globalName) =>
path.resolve(
packagePath,
`_virtual_unenv_inject-${encodeToLowerCase(globalName)}.js`,
),
),
];

build.onResolve({ filter: UNENV_GLOBALS_RE }, ({ path: filePath }) => ({
path: filePath,
}));

build.onLoad({ filter: UNENV_GLOBALS_RE }, ({ path: filePath }) => {
const match = filePath.match(UNENV_GLOBALS_RE);
if (!match) {
throw new Error(`Invalid global polyfill path: ${filePath}`);
}

const globalName = decodeFromLowerCase(match[1]);
const globalMapping = inject[globalName];

if (typeof globalMapping === 'string') {
return handleStringGlobalMapping(globalName, globalMapping);
} else if (Array.isArray(globalMapping)) {
return handleArrayGlobalMapping(globalName, globalMapping);
} else {
throw new Error(`Invalid global mapping for ${globalName}`);
}
});
}

function handleStringGlobalMapping(globalName: string, globalMapping: string) {
// workaround for wrongly published unenv
const possiblePaths = [globalMapping, `${globalMapping}/index`];
// the absolute path of the file
let found = '';
for (const p of possiblePaths) {
try {
// mjs to support tree-shaking
found ||= resolveFrom(packagePath, p).replace(/\.cjs$/, '.mjs');
if (found) {
break;
}
} catch (error) {
// ignore
}
}

if (!found) {
throw new Error(`Could not resolve global mapping for ${globalName}`);
}

return {
contents: `
import globalVar from "${found}";
const exportable = /* @__PURE__ */ (() => globalThis.${globalName} = globalVar)();
export { exportable as '${globalName}', exportable as 'globalThis.${globalName}' };
`,
};
}

function handleArrayGlobalMapping(
globalName: string,
globalMapping: string[],
): { contents: string } {
const [moduleName, exportName] = globalMapping;
return {
contents: `
import { ${exportName} } from "${moduleName}";
const exportable = /* @__PURE__ */ (() => globalThis.${globalName} = ${exportName})();
export { exportable as '${globalName}', exportable as 'global.${globalName}', exportable as 'globalThis.${globalName}' };
`,
};
}

export function encodeToLowerCase(str: string): string {
return str
.replace(/\$/g, '$$')
.replace(/[A-Z]/g, (letter) => `$${letter.toLowerCase()}`);
}

export function decodeFromLowerCase(str: string): string {
return str.replace(/\$(.)/g, (_, letter) => letter.toUpperCase());
}
16 changes: 16 additions & 0 deletions packages/vercel/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { normalizePath, ResolvedConfig, UserConfig } from 'vite';
import path from 'path';
import { createRequire } from 'module';

export function getRoot(config: UserConfig | ResolvedConfig): string {
return normalizePath(config.root || process.cwd());
Expand Down Expand Up @@ -30,3 +31,18 @@ export function pathRelativeTo(
path.relative(normalizePath(path.join(root, rel)), filePath),
);
}

function getOwnPackagePath() {
const require_ = createRequire(import.meta.url);
// vercel/dist/index.cjs
const resolved = require_.resolve('vite-plugin-vercel');
return path.resolve(
resolved,
// vercel/dist
'..',
// vercel
'..',
);
}

export const packagePath = getOwnPackagePath();
1 change: 1 addition & 0 deletions packages/vercel/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ export default defineConfig([
dts: {
entry: './src/index.ts',
},
shims: true,
},
]);
Loading
Loading