Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,55 @@ describe('assemble()', () => {
expect(result.watch).toContain(source);
}, 20_000);

test('rewrites an absolute package link to its staged in-bundle target', async () => {
const root = makeAppRoot();
const { appRel } = writeNextBuild(root);
const standalone = path.join(root, '.next', 'standalone');
const source = path.join(root, 'node_modules', 'pg');
fs.mkdirSync(source, { recursive: true });
fs.writeFileSync(path.join(source, 'index.js'), 'module.exports = "pg";\n');
const linkDir = path.join(standalone, appRel, '.next', 'node_modules');
fs.mkdirSync(linkDir, { recursive: true });
fs.symlinkSync(source, path.join(linkDir, 'pg-traced'), 'dir');

const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'prisma-composer-nextjs-cwd-'));
tmpDirs.push(cwd);
const result = await assemble({
address: 'storefront.web',
cwd,
build: nextjs({ module: moduleUrl(root), appDir: '..' }),
});

const bundle = path.join(cwd, '.prisma-composer', 'artifacts', 'storefront.web', 'bundle');
const bundledTarget = path.join(bundle, 'node_modules', 'pg');
const bundledLink = path.join(bundle, appRel, '.next', 'node_modules', 'pg-traced');
expect(fs.lstatSync(bundledLink).isSymbolicLink()).toBe(true);
expect(path.resolve(path.dirname(bundledLink), fs.readlinkSync(bundledLink))).toBe(
bundledTarget,
);
expect(fs.readFileSync(path.join(bundledTarget, 'index.js'), 'utf8')).toContain('pg');
expect(result.watch).toContain(fs.realpathSync(source));
}, 20_000);

test('rejects an absolute package link outside the declared tracing root', async () => {
const root = makeAppRoot();
const { appRel } = writeNextBuild(root);
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'prisma-composer-nextjs-outside-'));
tmpDirs.push(outside);
fs.writeFileSync(path.join(outside, 'secret.txt'), 'must not ship');
const linkDir = path.join(root, '.next', 'standalone', appRel, '.next', 'node_modules');
fs.mkdirSync(linkDir, { recursive: true });
fs.symlinkSync(outside, path.join(linkDir, 'escaped'), 'dir');

await expect(
assemble({
address: 'storefront.web',
cwd: root,
build: nextjs({ module: moduleUrl(root), appDir: '..' }),
}),
).rejects.toThrow(/assembled bundle contains a symlink whose target escapes the bundle/);
}, 20_000);

test('refuses a manifest whose app location escapes its tracing root', async () => {
const root = makeAppRoot();
writeNextBuild(root);
Expand Down
62 changes: 60 additions & 2 deletions packages/0-framework/2-authoring/nextjs/src/control/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,63 @@ async function collectSymlinks(root: string): Promise<string[]> {
return links;
}

/**
* Windows standalone output can contain absolute package links. An absolute
* build-machine path cannot ship, even when its target belongs to Next's
* declared trace root. Stage that exact target at the corresponding bundle
* path, then preserve the link as a relative in-bundle link.
*
* The link is never dereferenced: its target is copied separately and the
* topology remains a link. Targets outside the declared trace root are left
* untouched for the bundle validator to reject.
*/
async function stageAbsoluteStandaloneLinkTargets(
bundleDir: string,
manifest: ServerFilesManifest,
): Promise<string[]> {
const tracingRoot = manifest.tracingRoot;
if (tracingRoot === undefined || (await lstatIfPresent(tracingRoot)) === undefined) return [];

const tracedRootReal = await fs.promises.realpath(tracingRoot);
const stagedSources = new Set<string>();
let staged = true;
while (staged) {
staged = false;
for (const linkPath of await collectSymlinks(bundleDir)) {
const rawTarget = await fs.promises.readlink(linkPath);
if (!path.isAbsolute(rawTarget)) continue;

let sourceReal: string;
try {
sourceReal = await fs.promises.realpath(linkPath);
} catch {
continue;
}
if (!isWithin(tracedRootReal, sourceReal)) continue;

const target = path.join(bundleDir, path.relative(tracedRootReal, sourceReal));
if (!isWithin(bundleDir, target) || target === linkPath) continue;
if (await hasSymlinkAncestor(bundleDir, target)) continue;
if ((await lstatIfPresent(target)) === undefined) {
Comment thread
AmanVarshney01 marked this conversation as resolved.
Outdated
await fs.promises.mkdir(path.dirname(target), { recursive: true });
await fs.promises.cp(sourceReal, target, { recursive: true, verbatimSymlinks: true });
Comment thread
AmanVarshney01 marked this conversation as resolved.
Outdated
}

const sourceStat = await fs.promises.stat(sourceReal);
const relativeTarget = path.relative(path.dirname(linkPath), target);
await fs.promises.rm(linkPath, { recursive: true, force: true });
await fs.promises.symlink(
relativeTarget,
linkPath,
sourceStat.isDirectory() ? 'dir' : 'file',
);
stagedSources.add(sourceReal);
staged = true;
}
}
return [...stagedSources];
}

/** In-bundle link targets that the standalone tree does not contain — the
* repairs staging has to make. */
async function missingLinkTargets(bundleDir: string): Promise<string[]> {
Expand Down Expand Up @@ -234,7 +291,8 @@ export async function assemble(input: AssembleInput): Promise<Bundle> {
recursive: true,
verbatimSymlinks: true,
});
const stagedLinkTargets = await stageMissingStandaloneLinkTargets(bundleDir, manifest);
const stagedAbsoluteLinkTargets = await stageAbsoluteStandaloneLinkTargets(bundleDir, manifest);
const stagedMissingLinkTargets = await stageMissingStandaloneLinkTargets(bundleDir, manifest);

// The documented copy: Next omits the client assets from standalone; place
// them beside the app's server.js so it serves them (docs: `cp -r public
Expand Down Expand Up @@ -279,7 +337,7 @@ export async function assemble(input: AssembleInput): Promise<Bundle> {
return {
dir: workDir,
entry: path.posix.join('bundle', appRel.split(path.sep).join('/'), 'server.js'),
watch: [standaloneRoot, ...stagedLinkTargets],
watch: [standaloneRoot, ...stagedAbsoluteLinkTargets, ...stagedMissingLinkTargets],
};
}

Expand Down
Loading