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

Fix symlink and copy logic to standalone directory when using outputStandalone #35535

Merged
merged 17 commits into from Apr 15, 2022
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
126 changes: 102 additions & 24 deletions packages/next/build/utils.ts
Expand Up @@ -1135,6 +1135,93 @@ export function getUnresolvedModuleFromError(
return builtinModules.find((item: string) => item === moduleName)
}

// copies files or directories from `src` to `dest`,
// dereferences (copies) symlinks if they are outside of `root`
// symlinks are dereferenced only once, then they are linked again from their new location inside `root` to prevent duplicate dependencies
// symlinks inside `root` are kept as symlinks
async function copy({
src,
dest,
copiedFiles,
root,
copiedSymlinks,
}: {
src: string
dest: string
root: string
copiedFiles: Set<string>
copiedSymlinks: Record<string, string>
}) {
if (!copiedFiles.has(dest)) {
copiedFiles.add(dest)
const stat = await fs.lstat(src)
const isDirectory = stat.isDirectory()
// create parent directories
if (isDirectory) {
await fs.mkdir(dest, { recursive: true })
} else {
await fs.mkdir(path.dirname(dest), { recursive: true })
}

const symlink = stat.isSymbolicLink()
? await fs.readlink(src).catch(() => null)
: null

// normal copy of file or directory
if (!symlink) {
if (!isDirectory) {
return await fs.copyFile(src, dest)
}
const files = await fs.readdir(src)
for (const file of files) {
await copy({
dest: path.join(dest, file),
src: path.join(src, file),
copiedFiles,
copiedSymlinks,
root: root,
})
}
return
}

const absLink = path.resolve(path.dirname(src), symlink)
const outsideRoot = path.relative(root, absLink).startsWith('..')

if (!outsideRoot) {
// copy symlinked file inside root
await copy({
src: absLink,
dest: path.resolve(path.dirname(dest), symlink),
copiedFiles,
root: root,
copiedSymlinks,
})
// keep the symlink if it is inside the tracingRoot
return await fs.symlink(
symlink, // symlink is always a relative path
dest
)
}
// create symlink to previously copied one, this prevents creating duplicates
if (copiedSymlinks[absLink]) {
return await fs.symlink(
path.relative(path.dirname(dest), copiedSymlinks[absLink]),
dest
)
}
// copy the symlinks that are outside root
await copy({
src: absLink,
dest: dest,
copiedFiles,
root: root,
copiedSymlinks,
})
copiedSymlinks[absLink] = dest
}
}

export async function copyTracedFiles(
dir: string,
distDir: string,
Expand All @@ -1144,7 +1231,8 @@ export async function copyTracedFiles(
middlewareManifest: MiddlewareManifest
) {
const outputPath = path.join(distDir, 'standalone')
const copiedFiles = new Set()
const copiedFiles = new Set<string>()
const copiedSymlinks: Record<string, string> = {}
await recursiveDelete(outputPath)

async function handleTraceFiles(traceFilePath: string) {
Expand All @@ -1156,31 +1244,21 @@ export async function copyTracedFiles(

await Promise.all(
traceData.files.map(async (relativeFile) => {
await copySema.acquire()

const tracedFilePath = path.join(traceFileDir, relativeFile)
const fileOutputPath = path.join(
outputPath,
path.relative(tracingRoot, tracedFilePath)
)

if (!copiedFiles.has(fileOutputPath)) {
copiedFiles.add(fileOutputPath)

await fs.mkdir(path.dirname(fileOutputPath), { recursive: true })
const symlink = await fs.readlink(tracedFilePath).catch(() => null)

if (symlink) {
console.log('symlink', path.relative(tracingRoot, symlink))
await fs.symlink(
path.relative(tracingRoot, symlink),
fileOutputPath
)
} else {
await fs.copyFile(tracedFilePath, fileOutputPath)
}
const relative = path.relative(tracingRoot, tracedFilePath)
if (relative.startsWith('..')) {
// do not copy files outside `tracingRoot`, they would be copied out of `standalone` directory and could even pollute user project
return
}

const fileOutputPath = path.join(outputPath, relative)
await copySema.acquire()
await copy({
copiedFiles,
copiedSymlinks,
dest: fileOutputPath,
root: tracingRoot,
src: tracedFilePath,
})
await copySema.release()
})
)
Expand Down
5 changes: 5 additions & 0 deletions test/integration/pnpm-support/app/next.config.js
@@ -0,0 +1,5 @@
module.exports = {
experimental: {
outputStandalone: true,
},
}
50 changes: 50 additions & 0 deletions test/integration/pnpm-support/test/index.test.js
Expand Up @@ -140,6 +140,48 @@ describe('pnpm support', () => {
})
})

it('pnpm works with outputStandalone', async () => {
remorses marked this conversation as resolved.
Show resolved Hide resolved
await usingPnpmCreateNextApp(APP_DIRS['app'], async (appDir) => {
await runPnpm(appDir, 'run', 'build')

const symlinksOutsideRoot = []
const invalidSymlinks = []
const reactCopies = []
const standaloneDir = path.resolve(appDir, '.next/standalone')
for await (const p of walk(standaloneDir)) {
const symlink = await fs.readlink(p).catch(() => null)
if (p.endsWith('react/cjs/react.production.min.js') && !symlink) {
reactCopies.push(p)
}
if (
symlink &&
path
.relative(standaloneDir, path.resolve(path.dirname(p), symlink))
.startsWith('..')
) {
symlinksOutsideRoot.push([p, symlink])
}
if (symlink && !fs.existsSync(path.resolve(path.dirname(p), symlink))) {
invalidSymlinks.push([p, symlink])
}
}
expect(symlinksOutsideRoot).toHaveLength(
0,
'there must be no symlinks pointing outside standalone directory'
)

expect(reactCopies).toHaveLength(
1,
'there must be only one copy of react'
)

expect(invalidSymlinks).toHaveLength(
0,
'all symlinks must point to existing files'
)
})
})

it('should execute client-side JS on each page', async () => {
await usingPnpmCreateNextApp(APP_DIRS['app-multi-page'], async (appDir) => {
const { stdout, stderr } = await runPnpm(appDir, 'run', 'build')
Expand Down Expand Up @@ -174,3 +216,11 @@ describe('pnpm support', () => {
})
})
})

async function* walk(dir) {
for await (const d of await fs.promises.opendir(dir)) {
const entry = path.join(dir, d.name)
if (d.isDirectory()) yield* walk(entry)
else if (d.isFile() || d.isSymbolicLink()) yield entry
}
}