From aaf9c98dd9a122fc4cb0d863f629178eb4d437f8 Mon Sep 17 00:00:00 2001 From: remorses Date: Tue, 22 Mar 2022 22:26:03 +0100 Subject: [PATCH 01/11] fix symlink logic with outputStandalone --- packages/next/build/utils.ts | 109 ++++++++++++++---- test/integration/pnpm-support/app/Dockerfile | 17 +++ .../pnpm-support/app/next.config.js | 5 + .../pnpm-support/test/index.test.js | 55 +++++++++ 4 files changed, 166 insertions(+), 20 deletions(-) create mode 100644 test/integration/pnpm-support/app/Dockerfile create mode 100644 test/integration/pnpm-support/app/next.config.js diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index 618a826307de..eb82c75f76b5 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -1135,6 +1135,86 @@ 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: any + copiedSymlinks: any +}) { + if (!copiedFiles.has(dest)) { + copiedFiles.add(dest) + const stat = await fs.lstat(src) + const isDirectory = stat.isDirectory() + if (isDirectory) { + await fs.mkdir(dest, { recursive: true }) + } else { + await fs.mkdir(path.dirname(dest), { recursive: true }) + } + const symlink = await fs.readlink(src).catch(() => null) + + if (!symlink) { + if (isDirectory) { + 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, + }) + } + } else { + await fs.copyFile(src, dest) + } + } else { + const absLink = path.resolve(path.dirname(src), symlink) + // keep the symlink if it is inside the tracingRoot + if (!path.relative(root, absLink).startsWith('..')) { + await copy({ + src: absLink, + dest: path.resolve(path.dirname(dest), symlink), + copiedFiles, + root: root, + copiedSymlinks, + }) + await fs.symlink( + symlink, // symlink is always a relative path + dest + ) + } else { + // copy symlink to previously created, this prevents creating duplicates + if (copiedSymlinks[absLink]) { + await fs.symlink( + path.relative(path.dirname(dest), copiedSymlinks[absLink]), // symlink is always a relative path + dest + ) + } else { + await copy({ + src: absLink, + dest: dest, + copiedFiles, + root: root, + copiedSymlinks, + }) + copiedSymlinks[absLink] = dest + } + } + } + } +} + export async function copyTracedFiles( dir: string, distDir: string, @@ -1145,6 +1225,7 @@ export async function copyTracedFiles( ) { const outputPath = path.join(distDir, 'standalone') const copiedFiles = new Set() + const copiedSymlinks: Record = {} await recursiveDelete(outputPath) async function handleTraceFiles(traceFilePath: string) { @@ -1156,31 +1237,19 @@ 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) - } - } - + await copySema.acquire() + await copy({ + copiedFiles, + copiedSymlinks, + dest: fileOutputPath, + root: tracingRoot, + src: tracedFilePath, + }) await copySema.release() }) ) diff --git a/test/integration/pnpm-support/app/Dockerfile b/test/integration/pnpm-support/app/Dockerfile new file mode 100644 index 000000000000..11665a263bf1 --- /dev/null +++ b/test/integration/pnpm-support/app/Dockerfile @@ -0,0 +1,17 @@ +FROM node:16-alpine AS runner + +WORKDIR /app + +ENV NODE_ENV production + +COPY ./next.config.js ./ +COPY ./package.json ./package.json + +COPY ./.next/standalone ./ +COPY ./.next/static ./.next/static + +EXPOSE 3000 + +ENV PORT 3000 + +CMD ["node", "server.js"] diff --git a/test/integration/pnpm-support/app/next.config.js b/test/integration/pnpm-support/app/next.config.js new file mode 100644 index 000000000000..0568ecc9e540 --- /dev/null +++ b/test/integration/pnpm-support/app/next.config.js @@ -0,0 +1,5 @@ +module.exports = { + experimental: { + outputStandalone: true, + }, +} diff --git a/test/integration/pnpm-support/test/index.test.js b/test/integration/pnpm-support/test/index.test.js index fb50ef49fe7a..c88d2e5ad582 100644 --- a/test/integration/pnpm-support/test/index.test.js +++ b/test/integration/pnpm-support/test/index.test.js @@ -140,6 +140,53 @@ describe('pnpm support', () => { }) }) + it('pnpm works with outputStandalone', async () => { + 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, p).startsWith('..')) { + symlinksOutsideRoot.push([p, symlink]) + } + if (symlink && !fs.existsSync(path.resolve(path.dirname(p), symlink))) { + invalidSymlinks.push([p, symlink]) + } + } + if (symlinksOutsideRoot.length) { + console.log( + symlinksOutsideRoot + .map(([from, to]) => `${path.relative(appDir, from)} -> ${to}`) + .join('\n') + ) + } + expect(symlinksOutsideRoot).toHaveLength( + 0, + 'there are no symlinks pointing outside standalone directory' + ) + if (reactCopies.length) { + console.log(reactCopies.map((p) => path.relative(appDir, p)).join('\n')) + } + expect(reactCopies).toHaveLength(1, 'there is only one copy of react') + if (invalidSymlinks.length) { + console.log( + invalidSymlinks + .map(([from, to]) => `${path.relative(appDir, from)} -> ${to}`) + .join('\n') + ) + } + + expect(invalidSymlinks).toHaveLength(0, 'there are no invalid symlinks') + }) + }) + 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') @@ -174,3 +221,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 + } +} From 119d9ff784e388e89238232a6c8459a739582780 Mon Sep 17 00:00:00 2001 From: remorses Date: Wed, 23 Mar 2022 00:33:15 +0100 Subject: [PATCH 02/11] refactored and commented copy function --- packages/next/build/utils.ts | 97 +++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 45 deletions(-) diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index eb82c75f76b5..2c42d45bae22 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -1156,6 +1156,7 @@ async function copy({ 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 { @@ -1163,55 +1164,58 @@ async function copy({ } const symlink = await fs.readlink(src).catch(() => null) + // normal copy of file or directory if (!symlink) { - if (isDirectory) { - 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, - }) - } - } else { - await fs.copyFile(src, dest) + if (!isDirectory) { + return await fs.copyFile(src, dest) } - } else { - const absLink = path.resolve(path.dirname(src), symlink) - // keep the symlink if it is inside the tracingRoot - if (!path.relative(root, absLink).startsWith('..')) { + const files = await fs.readdir(src) + for (const file of files) { await copy({ - src: absLink, - dest: path.resolve(path.dirname(dest), symlink), + dest: path.join(dest, file), + src: path.join(src, file), copiedFiles, - root: root, copiedSymlinks, + root: root, }) - await fs.symlink( - symlink, // symlink is always a relative path - dest - ) - } else { - // copy symlink to previously created, this prevents creating duplicates - if (copiedSymlinks[absLink]) { - await fs.symlink( - path.relative(path.dirname(dest), copiedSymlinks[absLink]), // symlink is always a relative path - dest - ) - } else { - await copy({ - src: absLink, - dest: dest, - copiedFiles, - root: root, - copiedSymlinks, - }) - copiedSymlinks[absLink] = dest - } } + 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 } } @@ -1238,10 +1242,13 @@ export async function copyTracedFiles( await Promise.all( traceData.files.map(async (relativeFile) => { const tracedFilePath = path.join(traceFileDir, relativeFile) - const fileOutputPath = path.join( - outputPath, - path.relative(tracingRoot, tracedFilePath) - ) + const relative = path.relative(tracingRoot, tracedFilePath) + if (relative.startsWith('..')) { + console.log(`File outside tracingRoot: ${relativeFile}`) + // 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, From b3bc508f386f07165aa53b6dceb2692ee6f3ff28 Mon Sep 17 00:00:00 2001 From: remorses Date: Wed, 23 Mar 2022 00:51:50 +0100 Subject: [PATCH 03/11] faster symlink check --- packages/next/build/utils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index 2c42d45bae22..7b3082036b11 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -1162,7 +1162,10 @@ async function copy({ } else { await fs.mkdir(path.dirname(dest), { recursive: true }) } - const symlink = await fs.readlink(src).catch(() => null) + + const symlink = stat.isSymbolicLink() + ? await fs.readlink(src).catch(() => null) + : null // normal copy of file or directory if (!symlink) { From 22ac196013df4630601d06aa576ce093aed3a573 Mon Sep 17 00:00:00 2001 From: remorses Date: Wed, 23 Mar 2022 01:04:23 +0100 Subject: [PATCH 04/11] removed Dockerfile --- test/integration/pnpm-support/app/Dockerfile | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 test/integration/pnpm-support/app/Dockerfile diff --git a/test/integration/pnpm-support/app/Dockerfile b/test/integration/pnpm-support/app/Dockerfile deleted file mode 100644 index 11665a263bf1..000000000000 --- a/test/integration/pnpm-support/app/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -FROM node:16-alpine AS runner - -WORKDIR /app - -ENV NODE_ENV production - -COPY ./next.config.js ./ -COPY ./package.json ./package.json - -COPY ./.next/standalone ./ -COPY ./.next/static ./.next/static - -EXPOSE 3000 - -ENV PORT 3000 - -CMD ["node", "server.js"] From 06a6759d2c2ac6e154a45f215e9cc01d2fcad5bf Mon Sep 17 00:00:00 2001 From: remorses Date: Wed, 23 Mar 2022 01:06:59 +0100 Subject: [PATCH 05/11] removed console.logs from test --- .../pnpm-support/test/index.test.js | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/test/integration/pnpm-support/test/index.test.js b/test/integration/pnpm-support/test/index.test.js index c88d2e5ad582..c7dea2fc445b 100644 --- a/test/integration/pnpm-support/test/index.test.js +++ b/test/integration/pnpm-support/test/index.test.js @@ -160,30 +160,20 @@ describe('pnpm support', () => { invalidSymlinks.push([p, symlink]) } } - if (symlinksOutsideRoot.length) { - console.log( - symlinksOutsideRoot - .map(([from, to]) => `${path.relative(appDir, from)} -> ${to}`) - .join('\n') - ) - } expect(symlinksOutsideRoot).toHaveLength( 0, - 'there are no symlinks pointing outside standalone directory' + 'there must be no symlinks pointing outside standalone directory' ) - if (reactCopies.length) { - console.log(reactCopies.map((p) => path.relative(appDir, p)).join('\n')) - } - expect(reactCopies).toHaveLength(1, 'there is only one copy of react') - if (invalidSymlinks.length) { - console.log( - invalidSymlinks - .map(([from, to]) => `${path.relative(appDir, from)} -> ${to}`) - .join('\n') - ) - } - expect(invalidSymlinks).toHaveLength(0, 'there are no invalid symlinks') + expect(reactCopies).toHaveLength( + 1, + 'there must be only one copy of react' + ) + + expect(invalidSymlinks).toHaveLength( + 0, + 'all symlinks must point to existing files' + ) }) }) From 7acfea0339ebe4fe6ac29d6f891cb5de1bd5172b Mon Sep 17 00:00:00 2001 From: remorses Date: Wed, 23 Mar 2022 01:10:16 +0100 Subject: [PATCH 06/11] fix symlinksOutsideRoot test --- test/integration/pnpm-support/test/index.test.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/integration/pnpm-support/test/index.test.js b/test/integration/pnpm-support/test/index.test.js index c7dea2fc445b..0c7a382fbe4f 100644 --- a/test/integration/pnpm-support/test/index.test.js +++ b/test/integration/pnpm-support/test/index.test.js @@ -153,7 +153,12 @@ describe('pnpm support', () => { if (p.endsWith('react/cjs/react.production.min.js') && !symlink) { reactCopies.push(p) } - if (symlink && path.relative(standaloneDir, p).startsWith('..')) { + 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))) { From 1de776ae9f0b3de9f9729f7a3e5a06cf15a19c22 Mon Sep 17 00:00:00 2001 From: remorses Date: Wed, 23 Mar 2022 10:47:46 +0100 Subject: [PATCH 07/11] removed a console.log for files out of root --- packages/next/build/utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index 7b3082036b11..748b04593d31 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -1247,7 +1247,6 @@ export async function copyTracedFiles( const tracedFilePath = path.join(traceFileDir, relativeFile) const relative = path.relative(tracingRoot, tracedFilePath) if (relative.startsWith('..')) { - console.log(`File outside tracingRoot: ${relativeFile}`) // do not copy files outside `tracingRoot`, they would be copied out of `standalone` directory and could even pollute user project return } From 8dfa95ae1d796a7e77a9a34c5794777ad833b79f Mon Sep 17 00:00:00 2001 From: remorses Date: Wed, 23 Mar 2022 11:27:34 +0100 Subject: [PATCH 08/11] added missing types to copy --- packages/next/build/utils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index 748b04593d31..dc57973641ca 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -1149,8 +1149,8 @@ async function copy({ src: string dest: string root: string - copiedFiles: any - copiedSymlinks: any + copiedFiles: Set + copiedSymlinks: Record }) { if (!copiedFiles.has(dest)) { copiedFiles.add(dest) @@ -1231,7 +1231,7 @@ export async function copyTracedFiles( middlewareManifest: MiddlewareManifest ) { const outputPath = path.join(distDir, 'standalone') - const copiedFiles = new Set() + const copiedFiles = new Set() const copiedSymlinks: Record = {} await recursiveDelete(outputPath) From fdd1496c5ca6e39887202e78adc4249395142874 Mon Sep 17 00:00:00 2001 From: remorses Date: Wed, 13 Apr 2022 22:14:31 +0200 Subject: [PATCH 09/11] removed custom copy function, fix symlink code --- packages/next/build/utils.ts | 123 ++++++----------------------------- 1 file changed, 21 insertions(+), 102 deletions(-) diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index 333ee07e8684..d9d7a058aaed 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -1135,93 +1135,6 @@ 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 - copiedSymlinks: Record -}) { - 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, @@ -1231,8 +1144,7 @@ export async function copyTracedFiles( middlewareManifest: MiddlewareManifest ) { const outputPath = path.join(distDir, 'standalone') - const copiedFiles = new Set() - const copiedSymlinks: Record = {} + const copiedFiles = new Set() await recursiveDelete(outputPath) async function handleTraceFiles(traceFilePath: string) { @@ -1244,21 +1156,28 @@ export async function copyTracedFiles( await Promise.all( traceData.files.map(async (relativeFile) => { + await copySema.acquire() + const tracedFilePath = path.join(traceFileDir, relativeFile) - 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, + 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(symlink, fileOutputPath) + } else { + await fs.copyFile(tracedFilePath, fileOutputPath) + } } - const fileOutputPath = path.join(outputPath, relative) - await copySema.acquire() - await copy({ - copiedFiles, - copiedSymlinks, - dest: fileOutputPath, - root: tracingRoot, - src: tracedFilePath, - }) + await copySema.release() }) ) From 992866c750f150e48b392d32237ccdf2dd4fa9ef Mon Sep 17 00:00:00 2001 From: remorses Date: Wed, 13 Apr 2022 22:14:58 +0200 Subject: [PATCH 10/11] test outputStandalone and pnpm --- .../pnpm-support/app-multi-page/.npmrc | 2 + .../app-multi-page/next.config.js | 9 ++ .../pnpm-support/test/index.test.js | 125 +++++++++++------- 3 files changed, 86 insertions(+), 50 deletions(-) create mode 100644 test/integration/pnpm-support/app-multi-page/.npmrc create mode 100644 test/integration/pnpm-support/app-multi-page/next.config.js diff --git a/test/integration/pnpm-support/app-multi-page/.npmrc b/test/integration/pnpm-support/app-multi-page/.npmrc new file mode 100644 index 000000000000..84db07921228 --- /dev/null +++ b/test/integration/pnpm-support/app-multi-page/.npmrc @@ -0,0 +1,2 @@ +# put modules outside of the app folder, to simulate what happens in a pnpm workspace +virtual-store-dir=../pnpm \ No newline at end of file diff --git a/test/integration/pnpm-support/app-multi-page/next.config.js b/test/integration/pnpm-support/app-multi-page/next.config.js new file mode 100644 index 000000000000..d6cec9e13193 --- /dev/null +++ b/test/integration/pnpm-support/app-multi-page/next.config.js @@ -0,0 +1,9 @@ +const path = require('path') + +module.exports = { + experimental: { + outputStandalone: true, + // pnpm virtual-store-dir is outside the app directory + outputFileTracingRoot: path.resolve(__dirname, '../'), + }, +} diff --git a/test/integration/pnpm-support/test/index.test.js b/test/integration/pnpm-support/test/index.test.js index 0c7a382fbe4f..6cf1489e8908 100644 --- a/test/integration/pnpm-support/test/index.test.js +++ b/test/integration/pnpm-support/test/index.test.js @@ -13,8 +13,33 @@ const APP_DIRS = { 'app-multi-page': path.join(__dirname, '..', 'app-multi-page'), } +// runs a command showing logs and returning the stdout +const runCommand = (cwd, cmd, args) => { + const proc = execa(cmd, [...args], { + cwd, + stdio: [process.stdin, 'pipe', process.stderr], + }) + + let stdout = '' + proc.stdout.on('data', (data) => { + const s = data.toString() + process.stdout.write(s) + stdout += s + }) + return new Promise((resolve, reject) => { + proc.on('exit', (code) => { + if (code === 0) { + return resolve({ ...proc, stdout }) + } + reject( + new Error(`Command ${cmd} ${args.join(' ')} failed with code ${code}`) + ) + }) + }) +} + const runNpm = (cwd, ...args) => execa('npm', [...args], { cwd }) -const runPnpm = (cwd, ...args) => execa('npx', ['pnpm', ...args], { cwd }) +const runPnpm = (cwd, ...args) => runCommand(cwd, 'npx', ['pnpm', ...args]) async function usingTempDir(fn) { const folder = path.join(os.tmpdir(), Math.random().toString(36).substring(2)) @@ -134,58 +159,66 @@ describe('pnpm support', () => { expect(packageJson.pnpm.overrides[dependency]).toMatch(/^file:/) } - const { stdout, stderr } = await runPnpm(appDir, 'run', 'build') - console.log(stdout, stderr) + const { stdout } = await runPnpm(appDir, 'run', 'build') + expect(stdout).toMatch(/Compiled successfully/) }) }) - it('pnpm works with outputStandalone', async () => { - 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' - ) + it('should execute client-side JS on each page in outputStandalone', async () => { + await usingPnpmCreateNextApp(APP_DIRS['app-multi-page'], async (appDir) => { + const { stdout } = await runPnpm(appDir, 'run', 'build') - expect(reactCopies).toHaveLength( - 1, - 'there must be only one copy of react' - ) + expect(stdout).toMatch(/Compiled successfully/) - expect(invalidSymlinks).toHaveLength( - 0, - 'all symlinks must point to existing files' - ) + let appPort + let appProcess + let browser + try { + appPort = await findPort() + const standaloneDir = path.resolve(appDir, '.next/standalone/app') + + // simulate what happens in a Dockerfile + await fs.remove(path.join(appDir, 'node_modules')) + await fs.copy( + path.resolve(appDir, './.next/static'), + path.resolve(standaloneDir, './.next/static'), + { overwrite: true } + ) + appProcess = execa('node', ['server.js'], { + cwd: standaloneDir, + env: { + PORT: appPort, + }, + stdio: 'inherit', + }) + + await waitFor(1000) + + await renderViaHTTP(appPort, '/') + + browser = await webdriver(appPort, '/', { + waitHydration: false, + }) + expect(await browser.waitForElementByCss('#world').text()).toBe('World') + await browser.close() + + browser = await webdriver(appPort, '/about', { + waitHydration: false, + }) + expect(await browser.waitForElementByCss('#world').text()).toBe('World') + await browser.close() + } finally { + await killProcess(appProcess.pid) + await waitFor(5000) + } }) }) 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') - console.log(stdout, stderr) + const { stdout } = await runPnpm(appDir, 'run', 'build') + expect(stdout).toMatch(/Compiled successfully/) let appPort @@ -216,11 +249,3 @@ 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 - } -} From c474595429a078bd04d15b520886ed912fd0c401 Mon Sep 17 00:00:00 2001 From: remorses Date: Thu, 14 Apr 2022 10:19:13 +0200 Subject: [PATCH 11/11] appProcess is no more a promise --- test/integration/pnpm-support/test/index.test.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/integration/pnpm-support/test/index.test.js b/test/integration/pnpm-support/test/index.test.js index 6cf1489e8908..0bb49f0fabce 100644 --- a/test/integration/pnpm-support/test/index.test.js +++ b/test/integration/pnpm-support/test/index.test.js @@ -226,7 +226,11 @@ describe('pnpm support', () => { let browser try { appPort = await findPort() - appProcess = runPnpm(appDir, 'run', 'start', '--', '--port', appPort) + appProcess = execa('pnpm', ['run', 'start', '--', '--port', appPort], { + cwd: appDir, + stdio: 'inherit', + }) + await waitFor(5000) await renderViaHTTP(appPort, '/')