Skip to content
This repository was archived by the owner on Feb 10, 2025. It is now read-only.

Commit 46fbb26

Browse files
Set the workspace root when doing nft scan (#381)
* 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 <alexander@nbhr.io>
1 parent 974e2cf commit 46fbb26

File tree

8 files changed

+313
-126
lines changed

8 files changed

+313
-126
lines changed

.changeset/rich-lies-greet.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@astrojs/netlify': patch
3+
'@astrojs/vercel': patch
4+
---
5+
6+
Prevent crawling for dependencies outside of the workspace root

packages/netlify/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
"@astrojs/underscore-redirects": "^0.3.4",
3636
"@netlify/functions": "^2.8.0",
3737
"@vercel/nft": "^0.27.4",
38-
"esbuild": "^0.21.5"
38+
"esbuild": "^0.21.5",
39+
"vite": "^5.4.2"
3940
},
4041
"peerDependencies": {
4142
"astro": "^4.2.0"
@@ -51,8 +52,7 @@
5152
"execa": "^8.0.1",
5253
"fast-glob": "^3.3.2",
5354
"strip-ansi": "^7.1.0",
54-
"typescript": "^5.5.4",
55-
"vite": "^5.4.3"
55+
"typescript": "^5.5.4"
5656
},
5757
"astro": {
5858
"external": true

packages/netlify/src/index.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,12 @@ export default function netlifyIntegration(
240240
async function writeSSRFunction({
241241
notFoundContent,
242242
logger,
243-
}: { notFoundContent?: string; logger: AstroIntegrationLogger }) {
243+
root,
244+
}: {
245+
notFoundContent?: string;
246+
logger: AstroIntegrationLogger;
247+
root: URL;
248+
}) {
244249
const entry = new URL('./entry.mjs', ssrBuildDir());
245250

246251
const { handler } = await copyDependenciesToFunction(
@@ -250,6 +255,7 @@ export default function netlifyIntegration(
250255
includeFiles: [],
251256
excludeFiles: [],
252257
logger,
258+
root,
253259
},
254260
TRACE_CACHE
255261
);
@@ -484,7 +490,7 @@ export default function netlifyIntegration(
484490
try {
485491
notFoundContent = await readFile(new URL('./404.html', dir), 'utf8');
486492
} catch {}
487-
await writeSSRFunction({ notFoundContent, logger });
493+
await writeSSRFunction({ notFoundContent, logger, root: _config.root });
488494
logger.info('Generated SSR Function');
489495
}
490496
if (astroMiddlewareEntryPoint) {

packages/netlify/src/lib/nft.ts

+7-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { posix, relative, sep } from 'node:path';
2-
import { fileURLToPath } from 'node:url';
2+
import { fileURLToPath, pathToFileURL } from 'node:url';
33
import { copyFilesToFolder } from '@astrojs/internal-helpers/fs';
4+
import { appendForwardSlash } from '@astrojs/internal-helpers/path';
45
import type { AstroIntegrationLogger } from 'astro';
6+
import { searchForWorkspaceRoot } from 'vite';
57

68
// Based on the equivalent function in `@astrojs/vercel`
79
export async function copyDependenciesToFunction(
@@ -11,24 +13,23 @@ export async function copyDependenciesToFunction(
1113
includeFiles,
1214
excludeFiles,
1315
logger,
16+
root,
1417
}: {
1518
entry: URL;
1619
outDir: URL;
1720
includeFiles: URL[];
1821
excludeFiles: URL[];
1922
logger: AstroIntegrationLogger;
23+
root: URL;
2024
},
2125
// we want to pass the caching by reference, and not by value
2226
cache: object
2327
): Promise<{ handler: string }> {
2428
const entryPath = fileURLToPath(entry);
2529
logger.info(`Bundling function ${relative(fileURLToPath(outDir), entryPath)}`);
2630

27-
// Get root of folder of the system (like C:\ on Windows or / on Linux)
28-
let base = entry;
29-
while (fileURLToPath(base) !== fileURLToPath(new URL('../', base))) {
30-
base = new URL('../', base);
31-
}
31+
// Set the base to the workspace root
32+
const base = pathToFileURL(appendForwardSlash(searchForWorkspaceRoot(fileURLToPath(root))));
3233

3334
// The Vite bundle includes an import to `@vercel/nft` for some reason,
3435
// and that trips up `@vercel/nft` itself during the adapter build. Using a
@@ -37,9 +38,6 @@ export async function copyDependenciesToFunction(
3738
const { nodeFileTrace } = await import('@vercel/nft');
3839
const result = await nodeFileTrace([entryPath], {
3940
base: fileURLToPath(base),
40-
// If you have a route of /dev this appears in source and NFT will try to
41-
// scan your local /dev :8
42-
ignore: ['/dev/**'],
4341
cache,
4442
});
4543

packages/vercel/src/lib/nft.ts

+7-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { relative as relativePath } from 'node:path';
2-
import { fileURLToPath } from 'node:url';
2+
import { fileURLToPath, pathToFileURL } from 'node:url';
33
import { copyFilesToFolder } from '@astrojs/internal-helpers/fs';
4+
import { appendForwardSlash } from '@astrojs/internal-helpers/path';
45
import type { AstroIntegrationLogger } from 'astro';
6+
import { searchForWorkspaceRoot } from './searchRoot.js';
57

68
export async function copyDependenciesToFunction(
79
{
@@ -10,24 +12,23 @@ export async function copyDependenciesToFunction(
1012
includeFiles,
1113
excludeFiles,
1214
logger,
15+
root,
1316
}: {
1417
entry: URL;
1518
outDir: URL;
1619
includeFiles: URL[];
1720
excludeFiles: URL[];
1821
logger: AstroIntegrationLogger;
22+
root: URL;
1923
},
2024
// we want to pass the caching by reference, and not by value
2125
cache: object
2226
): Promise<{ handler: string }> {
2327
const entryPath = fileURLToPath(entry);
2428
logger.info(`Bundling function ${relativePath(fileURLToPath(outDir), entryPath)}`);
2529

26-
// Get root of folder of the system (like C:\ on Windows or / on Linux)
27-
let base = entry;
28-
while (fileURLToPath(base) !== fileURLToPath(new URL('../', base))) {
29-
base = new URL('../', base);
30-
}
30+
// Set the base to the workspace root
31+
const base = pathToFileURL(appendForwardSlash(searchForWorkspaceRoot(fileURLToPath(root))));
3132

3233
// The Vite bundle includes an import to `@vercel/nft` for some reason,
3334
// and that trips up `@vercel/nft` itself during the adapter build. Using a
@@ -36,9 +37,6 @@ export async function copyDependenciesToFunction(
3637
const { nodeFileTrace } = await import('@vercel/nft');
3738
const result = await nodeFileTrace([entryPath], {
3839
base: fileURLToPath(base),
39-
// If you have a route of /dev this appears in source and NFT will try to
40-
// scan your local /dev :8
41-
ignore: ['/dev/**'],
4240
cache,
4341
});
4442

packages/vercel/src/lib/searchRoot.ts

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Taken from: https://github.com/vitejs/vite/blob/1a76300cd16827f0640924fdc21747ce140c35fb/packages/vite/src/node/server/searchRoot.ts
2+
// MIT license
3+
// See https://github.com/vitejs/vite/blob/1a76300cd16827f0640924fdc21747ce140c35fb/LICENSE
4+
import fs from 'node:fs';
5+
import { dirname, join } from 'node:path';
6+
7+
// https://github.com/vitejs/vite/issues/2820#issuecomment-812495079
8+
const ROOT_FILES = [
9+
// '.git',
10+
11+
// https://pnpm.io/workspaces/
12+
'pnpm-workspace.yaml',
13+
14+
// https://rushjs.io/pages/advanced/config_files/
15+
// 'rush.json',
16+
17+
// https://nx.dev/latest/react/getting-started/nx-setup
18+
// 'workspace.json',
19+
// 'nx.json',
20+
21+
// https://github.com/lerna/lerna#lernajson
22+
'lerna.json',
23+
];
24+
25+
export function tryStatSync(file: string): fs.Stats | undefined {
26+
try {
27+
// The "throwIfNoEntry" is a performance optimization for cases where the file does not exist
28+
return fs.statSync(file, { throwIfNoEntry: false });
29+
} catch {
30+
// Ignore errors
31+
}
32+
}
33+
34+
export function isFileReadable(filename: string): boolean {
35+
if (!tryStatSync(filename)) {
36+
return false;
37+
}
38+
39+
try {
40+
// Check if current process has read permission to the file
41+
fs.accessSync(filename, fs.constants.R_OK);
42+
43+
return true;
44+
} catch {
45+
return false;
46+
}
47+
}
48+
49+
// npm: https://docs.npmjs.com/cli/v7/using-npm/workspaces#installing-workspaces
50+
// yarn: https://classic.yarnpkg.com/en/docs/workspaces/#toc-how-to-use-it
51+
function hasWorkspacePackageJSON(root: string): boolean {
52+
const path = join(root, 'package.json');
53+
if (!isFileReadable(path)) {
54+
return false;
55+
}
56+
try {
57+
const content = JSON.parse(fs.readFileSync(path, 'utf-8')) || {};
58+
return !!content.workspaces;
59+
} catch {
60+
return false;
61+
}
62+
}
63+
64+
function hasRootFile(root: string): boolean {
65+
return ROOT_FILES.some((file) => fs.existsSync(join(root, file)));
66+
}
67+
68+
function hasPackageJSON(root: string) {
69+
const path = join(root, 'package.json');
70+
return fs.existsSync(path);
71+
}
72+
73+
/**
74+
* Search up for the nearest `package.json`
75+
*/
76+
export function searchForPackageRoot(current: string, root = current): string {
77+
if (hasPackageJSON(current)) return current;
78+
79+
const dir = dirname(current);
80+
// reach the fs root
81+
if (!dir || dir === current) return root;
82+
83+
return searchForPackageRoot(dir, root);
84+
}
85+
86+
/**
87+
* Search up for the nearest workspace root
88+
*/
89+
export function searchForWorkspaceRoot(
90+
current: string,
91+
root = searchForPackageRoot(current)
92+
): string {
93+
if (hasRootFile(current)) return current;
94+
if (hasWorkspacePackageJSON(current)) return current;
95+
96+
const dir = dirname(current);
97+
// reach the fs root
98+
if (!dir || dir === current) return root;
99+
100+
return searchForWorkspaceRoot(dir, root);
101+
}

packages/vercel/src/serverless/adapter.ts

+8-7
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ export default function vercelServerless({
369369
? getRouteFuncName(route)
370370
: getFallbackFuncName(entryFile);
371371

372-
await builder.buildServerlessFolder(entryFile, func);
372+
await builder.buildServerlessFolder(entryFile, func, _config.root);
373373

374374
routeDefinitions.push({
375375
src: route.pattern.source,
@@ -380,22 +380,22 @@ export default function vercelServerless({
380380
const entryFile = new URL(_serverEntry, _buildTempFolder);
381381
if (isr) {
382382
const isrConfig = typeof isr === 'object' ? isr : {};
383-
await builder.buildServerlessFolder(entryFile, NODE_PATH);
383+
await builder.buildServerlessFolder(entryFile, NODE_PATH, _config.root);
384384
if (isrConfig.exclude?.length) {
385385
const dest = _middlewareEntryPoint ? MIDDLEWARE_PATH : NODE_PATH;
386386
for (const route of isrConfig.exclude) {
387387
// vercel interprets src as a regex pattern, so we need to escape it
388388
routeDefinitions.push({ src: escapeRegex(route), dest });
389389
}
390390
}
391-
await builder.buildISRFolder(entryFile, '_isr', isrConfig);
391+
await builder.buildISRFolder(entryFile, '_isr', isrConfig, _config.root);
392392
for (const route of routes) {
393393
const src = route.pattern.source;
394394
const dest = src.startsWith('^\\/_image') ? NODE_PATH : ISR_PATH;
395395
if (!route.prerender) routeDefinitions.push({ src, dest });
396396
}
397397
} else {
398-
await builder.buildServerlessFolder(entryFile, NODE_PATH);
398+
await builder.buildServerlessFolder(entryFile, NODE_PATH, _config.root);
399399
const dest = _middlewareEntryPoint ? MIDDLEWARE_PATH : NODE_PATH;
400400
for (const route of routes) {
401401
if (!route.prerender) routeDefinitions.push({ src: route.pattern.source, dest });
@@ -485,7 +485,7 @@ class VercelBuilder {
485485
readonly runtime = getRuntime(process, logger)
486486
) {}
487487

488-
async buildServerlessFolder(entry: URL, functionName: string) {
488+
async buildServerlessFolder(entry: URL, functionName: string, root: URL) {
489489
const { config, includeFiles, excludeFiles, logger, NTF_CACHE, runtime, maxDuration } = this;
490490
// .vercel/output/functions/<name>.func/
491491
const functionFolder = new URL(`./functions/${functionName}.func/`, config.outDir);
@@ -500,6 +500,7 @@ class VercelBuilder {
500500
includeFiles,
501501
excludeFiles,
502502
logger,
503+
root,
503504
},
504505
NTF_CACHE
505506
);
@@ -519,8 +520,8 @@ class VercelBuilder {
519520
});
520521
}
521522

522-
async buildISRFolder(entry: URL, functionName: string, isr: VercelISRConfig) {
523-
await this.buildServerlessFolder(entry, functionName);
523+
async buildISRFolder(entry: URL, functionName: string, isr: VercelISRConfig, root: URL) {
524+
await this.buildServerlessFolder(entry, functionName, root);
524525
const prerenderConfig = new URL(
525526
`./functions/${functionName}.prerender-config.json`,
526527
this.config.outDir

0 commit comments

Comments
 (0)