Skip to content

Commit

Permalink
Set the workspace root when doing nft scan (#381)
Browse files Browse the repository at this point in the history
* Debuggin

* Changeset

* More info for debugging

* filter more

* Fix the underlying issue

* fix formatting

* block entire linuxbrew root

* ignore home entirely

* Set the base to the workspace root

* more debuggin

* make it be a URL

* cleanup

* Fix tests

* Apply to Vercel as well

* Fix build

* format code

* Vendor searchRoot for vercel

* formatting

---------

Co-authored-by: Alexander Niebuhr <[email protected]>
  • Loading branch information
matthewp and alexanderniebuhr authored Sep 10, 2024
1 parent 974e2cf commit 46fbb26
Show file tree
Hide file tree
Showing 8 changed files with 313 additions and 126 deletions.
6 changes: 6 additions & 0 deletions .changeset/rich-lies-greet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@astrojs/netlify': patch
'@astrojs/vercel': patch
---

Prevent crawling for dependencies outside of the workspace root
6 changes: 3 additions & 3 deletions packages/netlify/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
"@astrojs/underscore-redirects": "^0.3.4",
"@netlify/functions": "^2.8.0",
"@vercel/nft": "^0.27.4",
"esbuild": "^0.21.5"
"esbuild": "^0.21.5",
"vite": "^5.4.2"
},
"peerDependencies": {
"astro": "^4.2.0"
Expand All @@ -51,8 +52,7 @@
"execa": "^8.0.1",
"fast-glob": "^3.3.2",
"strip-ansi": "^7.1.0",
"typescript": "^5.5.4",
"vite": "^5.4.3"
"typescript": "^5.5.4"
},
"astro": {
"external": true
Expand Down
10 changes: 8 additions & 2 deletions packages/netlify/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,12 @@ export default function netlifyIntegration(
async function writeSSRFunction({
notFoundContent,
logger,
}: { notFoundContent?: string; logger: AstroIntegrationLogger }) {
root,
}: {
notFoundContent?: string;
logger: AstroIntegrationLogger;
root: URL;
}) {
const entry = new URL('./entry.mjs', ssrBuildDir());

const { handler } = await copyDependenciesToFunction(
Expand All @@ -250,6 +255,7 @@ export default function netlifyIntegration(
includeFiles: [],
excludeFiles: [],
logger,
root,
},
TRACE_CACHE
);
Expand Down Expand Up @@ -484,7 +490,7 @@ export default function netlifyIntegration(
try {
notFoundContent = await readFile(new URL('./404.html', dir), 'utf8');
} catch {}
await writeSSRFunction({ notFoundContent, logger });
await writeSSRFunction({ notFoundContent, logger, root: _config.root });
logger.info('Generated SSR Function');
}
if (astroMiddlewareEntryPoint) {
Expand Down
16 changes: 7 additions & 9 deletions packages/netlify/src/lib/nft.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { posix, relative, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
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 'vite';

// Based on the equivalent function in `@astrojs/vercel`
export async function copyDependenciesToFunction(
Expand All @@ -11,24 +13,23 @@ export async function copyDependenciesToFunction(
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 ${relative(fileURLToPath(outDir), entryPath)}`);

// Get root of folder of the system (like C:\ on Windows or / on Linux)
let base = entry;
while (fileURLToPath(base) !== fileURLToPath(new URL('../', base))) {
base = new URL('../', base);
}
// 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
Expand All @@ -37,9 +38,6 @@ export async function copyDependenciesToFunction(
const { nodeFileTrace } = await import('@vercel/nft');
const result = await nodeFileTrace([entryPath], {
base: fileURLToPath(base),
// If you have a route of /dev this appears in source and NFT will try to
// scan your local /dev :8
ignore: ['/dev/**'],
cache,
});

Expand Down
16 changes: 7 additions & 9 deletions packages/vercel/src/lib/nft.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { relative as relativePath } from 'node:path';
import { fileURLToPath } from 'node:url';
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(
{
Expand All @@ -10,24 +12,23 @@ export async function copyDependenciesToFunction(
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)}`);

// Get root of folder of the system (like C:\ on Windows or / on Linux)
let base = entry;
while (fileURLToPath(base) !== fileURLToPath(new URL('../', base))) {
base = new URL('../', base);
}
// 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
Expand All @@ -36,9 +37,6 @@ export async function copyDependenciesToFunction(
const { nodeFileTrace } = await import('@vercel/nft');
const result = await nodeFileTrace([entryPath], {
base: fileURLToPath(base),
// If you have a route of /dev this appears in source and NFT will try to
// scan your local /dev :8
ignore: ['/dev/**'],
cache,
});

Expand Down
101 changes: 101 additions & 0 deletions packages/vercel/src/lib/searchRoot.ts
Original file line number Diff line number Diff line change
@@ -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',
];

export 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
}
}

export 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`
*/
export 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);
}
15 changes: 8 additions & 7 deletions packages/vercel/src/serverless/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ export default function vercelServerless({
? getRouteFuncName(route)
: getFallbackFuncName(entryFile);

await builder.buildServerlessFolder(entryFile, func);
await builder.buildServerlessFolder(entryFile, func, _config.root);

routeDefinitions.push({
src: route.pattern.source,
Expand All @@ -380,22 +380,22 @@ export default function vercelServerless({
const entryFile = new URL(_serverEntry, _buildTempFolder);
if (isr) {
const isrConfig = typeof isr === 'object' ? isr : {};
await builder.buildServerlessFolder(entryFile, NODE_PATH);
await builder.buildServerlessFolder(entryFile, NODE_PATH, _config.root);
if (isrConfig.exclude?.length) {
const dest = _middlewareEntryPoint ? MIDDLEWARE_PATH : NODE_PATH;
for (const route of isrConfig.exclude) {
// vercel interprets src as a regex pattern, so we need to escape it
routeDefinitions.push({ src: escapeRegex(route), dest });
}
}
await builder.buildISRFolder(entryFile, '_isr', isrConfig);
await builder.buildISRFolder(entryFile, '_isr', isrConfig, _config.root);
for (const route of routes) {
const src = route.pattern.source;
const dest = src.startsWith('^\\/_image') ? NODE_PATH : ISR_PATH;
if (!route.prerender) routeDefinitions.push({ src, dest });
}
} else {
await builder.buildServerlessFolder(entryFile, NODE_PATH);
await builder.buildServerlessFolder(entryFile, NODE_PATH, _config.root);
const dest = _middlewareEntryPoint ? MIDDLEWARE_PATH : NODE_PATH;
for (const route of routes) {
if (!route.prerender) routeDefinitions.push({ src: route.pattern.source, dest });
Expand Down Expand Up @@ -485,7 +485,7 @@ class VercelBuilder {
readonly runtime = getRuntime(process, logger)
) {}

async buildServerlessFolder(entry: URL, functionName: string) {
async buildServerlessFolder(entry: URL, functionName: string, root: URL) {
const { config, includeFiles, excludeFiles, logger, NTF_CACHE, runtime, maxDuration } = this;
// .vercel/output/functions/<name>.func/
const functionFolder = new URL(`./functions/${functionName}.func/`, config.outDir);
Expand All @@ -500,6 +500,7 @@ class VercelBuilder {
includeFiles,
excludeFiles,
logger,
root,
},
NTF_CACHE
);
Expand All @@ -519,8 +520,8 @@ class VercelBuilder {
});
}

async buildISRFolder(entry: URL, functionName: string, isr: VercelISRConfig) {
await this.buildServerlessFolder(entry, functionName);
async buildISRFolder(entry: URL, functionName: string, isr: VercelISRConfig, root: URL) {
await this.buildServerlessFolder(entry, functionName, root);
const prerenderConfig = new URL(
`./functions/${functionName}.prerender-config.json`,
this.config.outDir
Expand Down
Loading

0 comments on commit 46fbb26

Please sign in to comment.