[remix] Create ensured dependency symlink at the start directory, instead of root of repo (#10224)

When the `ensureResolvable()` function runs in a monorepo, the symlink should be created in the `node_modules` directory within the app subdirectory, rather than the root of the repository. Fixes the following issue:

![image](https://github.com/vercel/vercel/assets/71256/c0171533-881a-425e-85b9-82318ef19032)
This commit is contained in:
Nathan Rajlich
2023-07-18 11:27:19 -07:00
committed by GitHub
parent c670e51712
commit 5439d7c0c9
23 changed files with 1875 additions and 95 deletions

View File

@@ -16,7 +16,6 @@ import {
runNpmInstall,
runPackageJsonScript,
scanParentDirs,
walkParentDirs,
} from '@vercel/build-utils';
import { getConfig } from '@vercel/static-config';
import { nodeFileTrace } from '@vercel/nft';
@@ -43,6 +42,7 @@ import {
findEntry,
chdirAndReadConfig,
addDependency,
ensureResolvable,
} from './utils';
import semver from 'semver';
@@ -659,99 +659,6 @@ async function createRenderEdgeFunction(
return fn;
}
async function ensureResolvable(
start: string,
base: string,
pkgName: string
): Promise<string> {
try {
const resolvedPkgPath = _require.resolve(`${pkgName}/package.json`, {
paths: [start],
});
const resolvedPath = dirname(resolvedPkgPath);
if (!relative(base, resolvedPath).startsWith(`..${sep}`)) {
// Resolved path is within the root of the project, so all good
debug(`"${pkgName}" resolved to '${resolvedPath}'`);
return resolvedPath;
}
} catch (err: any) {
if (err.code !== 'MODULE_NOT_FOUND') {
throw err;
}
}
// If we got to here then `pkgName` was not resolvable up to the root
// of the project. Try a couple symlink tricks, otherwise we'll bail.
// Attempt to find the package in `node_modules/.pnpm` (pnpm)
const pnpmDir = await walkParentDirs({
base,
start,
filename: 'node_modules/.pnpm',
});
if (pnpmDir) {
const prefix = `${pkgName.replace('/', '+')}@`;
const packages = await fs.readdir(pnpmDir);
const match = packages.find(p => p.startsWith(prefix));
if (match) {
const pkgDir = join(pnpmDir, match, 'node_modules', pkgName);
await ensureSymlink(pkgDir, join(pnpmDir, '..'), pkgName);
return pkgDir;
}
}
// Attempt to find the package in `node_modules/.store` (npm 9+ linked mode)
const npmDir = await walkParentDirs({
base,
start,
filename: 'node_modules/.store',
});
if (npmDir) {
const prefix = `${basename(pkgName)}@`;
const prefixDir = join(npmDir, dirname(pkgName));
const packages = await fs.readdir(prefixDir);
const match = packages.find(p => p.startsWith(prefix));
if (match) {
const pkgDir = join(prefixDir, match, 'node_modules', pkgName);
await ensureSymlink(pkgDir, join(npmDir, '..'), pkgName);
return pkgDir;
}
}
throw new Error(
`Failed to resolve "${pkgName}". To fix this error, add "${pkgName}" to "dependencies" in your \`package.json\` file.`
);
}
async function ensureSymlink(
target: string,
nodeModulesDir: string,
pkgName: string
) {
const symlinkPath = join(nodeModulesDir, pkgName);
const symlinkDir = dirname(symlinkPath);
const relativeTarget = relative(symlinkDir, target);
try {
const existingTarget = await fs.readlink(symlinkPath);
if (existingTarget === relativeTarget) {
// Symlink is already the expected value, so do nothing
return;
} else {
// If a symlink already exists then delete it if the target doesn't match
await fs.unlink(symlinkPath);
}
} catch (err: any) {
// Ignore when path does not exist or is not a symlink
if (err.code !== 'ENOENT' && err.code !== 'EINVAL') {
throw err;
}
}
await fs.symlink(relativeTarget, symlinkPath);
debug(`Created symlink for "${pkgName}"`);
}
async function writeEntrypointFile(
path: string,
data: string,

View File

@@ -1,8 +1,9 @@
import { existsSync, promises as fs } from 'fs';
import { join, relative, resolve } from 'path';
import { basename, dirname, join, relative, resolve, sep } from 'path';
import { pathToRegexp, Key } from 'path-to-regexp';
import { debug, spawnAsync } from '@vercel/build-utils';
import { readConfig } from '@remix-run/dev/dist/config';
import { walkParentDirs } from '@vercel/build-utils';
import type {
ConfigRoute,
RouteManifest,
@@ -42,6 +43,8 @@ export interface ResolvedRoutePaths {
rePath: string;
}
const _require: typeof require = eval('require');
const SPLAT_PATH = '/:params*';
const entryExts = ['.js', '.jsx', '.ts', '.tsx'];
@@ -273,3 +276,96 @@ export function addDependency(
}
return spawnAsync(cliType, args.concat(names), opts);
}
export async function ensureResolvable(
start: string,
base: string,
pkgName: string
): Promise<string> {
try {
const resolvedPkgPath = _require.resolve(`${pkgName}/package.json`, {
paths: [start],
});
const resolvedPath = dirname(resolvedPkgPath);
if (!relative(base, resolvedPath).startsWith(`..${sep}`)) {
// Resolved path is within the root of the project, so all good
debug(`"${pkgName}" resolved to '${resolvedPath}'`);
return resolvedPath;
}
} catch (err: any) {
if (err.code !== 'MODULE_NOT_FOUND') {
throw err;
}
}
// If we got to here then `pkgName` was not resolvable up to the root
// of the project. Try a couple symlink tricks, otherwise we'll bail.
// Attempt to find the package in `node_modules/.pnpm` (pnpm)
const pnpmDir = await walkParentDirs({
base,
start,
filename: 'node_modules/.pnpm',
});
if (pnpmDir) {
const prefix = `${pkgName.replace('/', '+')}@`;
const packages = await fs.readdir(pnpmDir);
const match = packages.find(p => p.startsWith(prefix));
if (match) {
const pkgDir = join(pnpmDir, match, 'node_modules', pkgName);
await ensureSymlink(pkgDir, join(start, 'node_modules'), pkgName);
return pkgDir;
}
}
// Attempt to find the package in `node_modules/.store` (npm 9+ linked mode)
const npmDir = await walkParentDirs({
base,
start,
filename: 'node_modules/.store',
});
if (npmDir) {
const prefix = `${basename(pkgName)}@`;
const prefixDir = join(npmDir, dirname(pkgName));
const packages = await fs.readdir(prefixDir);
const match = packages.find(p => p.startsWith(prefix));
if (match) {
const pkgDir = join(prefixDir, match, 'node_modules', pkgName);
await ensureSymlink(pkgDir, join(start, 'node_modules'), pkgName);
return pkgDir;
}
}
throw new Error(
`Failed to resolve "${pkgName}". To fix this error, add "${pkgName}" to "dependencies" in your \`package.json\` file.`
);
}
async function ensureSymlink(
target: string,
nodeModulesDir: string,
pkgName: string
) {
const symlinkPath = join(nodeModulesDir, pkgName);
const symlinkDir = dirname(symlinkPath);
const relativeTarget = relative(symlinkDir, target);
try {
const existingTarget = await fs.readlink(symlinkPath);
if (existingTarget === relativeTarget) {
// Symlink is already the expected value, so do nothing
return;
} else {
// If a symlink already exists then delete it if the target doesn't match
await fs.unlink(symlinkPath);
}
} catch (err: any) {
// Ignore when path does not exist or is not a symlink
if (err.code !== 'ENOENT' && err.code !== 'EINVAL') {
throw err;
}
}
await fs.symlink(relativeTarget, symlinkPath);
debug(`Created symlink for "${pkgName}"`);
}