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

Set the workspace root when doing nft scan #381

Merged
merged 19 commits into from
Sep 10, 2024
Merged
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"
bluwy marked this conversation as resolved.
Show resolved Hide resolved
},
"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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

today I learned


// 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have copied the function for vercel but not for netlify?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason simply having a import 'vite' in the Vercel adapter causes tests to timeout. I spent too much time and couldn't figure out why.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could move it into our shared utils, like we already did with some other vite stuff

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