diff --git a/.github/workflows/build_test_deploy.yml b/.github/workflows/build_test_deploy.yml index d74d4da95102b14..0d6149ba67b810c 100644 --- a/.github/workflows/build_test_deploy.yml +++ b/.github/workflows/build_test_deploy.yml @@ -347,35 +347,6 @@ jobs: - run: xvfb-run node run-tests.js test/integration/with-electron/test/index.test.js if: ${{needs.build.outputs.docsChange != 'docs only change'}} - testYarnPnP: - runs-on: ubuntu-latest - needs: [build, build-native-dev] - env: - NODE_OPTIONS: '--unhandled-rejections=strict' - YARN_COMPRESSION_LEVEL: '0' - steps: - - name: Setup node - uses: actions/setup-node@v2 - if: ${{ steps.docs-change.outputs.docsChange != 'docs only change' }} - with: - node-version: 14 - - - uses: actions/cache@v2 - if: ${{needs.build.outputs.docsChange != 'docs only change'}} - id: restore-build - with: - path: ./* - key: ${{ github.sha }}-${{ github.run_number }}-${{ github.run_attempt }} - - - uses: actions/download-artifact@v2 - if: ${{needs.build.outputs.docsChange != 'docs only change'}} - with: - name: next-swc-dev-binary - path: packages/next-swc/native - - - run: bash ./scripts/test-pnp.sh - if: ${{needs.build.outputs.docsChange != 'docs only change'}} - testsPass: name: thank you, next runs-on: ubuntu-latest @@ -387,7 +358,6 @@ jobs: checkPrecompiled, testIntegration, testUnit, - testYarnPnP, testDev, testProd, ] diff --git a/packages/next/build/webpack/loaders/next-swc-loader.js b/packages/next/build/webpack/loaders/next-swc-loader.js index eeecab0f236e036..70e596102ed7e30 100644 --- a/packages/next/build/webpack/loaders/next-swc-loader.js +++ b/packages/next/build/webpack/loaders/next-swc-loader.js @@ -101,6 +101,8 @@ export function pitch() { ;(async () => { let loaderOptions = this.getOptions() || {} if ( + // TODO: investigate swc file reading in PnP mode? + !process.versions.pnp && loaderOptions.fileReading && !EXCLUDED_PATHS.test(this.resourcePath) && this.loaders.length - 1 === this.loaderIndex && diff --git a/run-tests.js b/run-tests.js index ea6a69be59de260..abffb908030372c 100644 --- a/run-tests.js +++ b/run-tests.js @@ -36,6 +36,7 @@ const cleanUpAndExit = async (code) => { if (process.env.NEXT_TEST_STARTER) { await fs.remove(process.env.NEXT_TEST_STARTER) } + console.log(`exiting with code ${code}`) process.exit(code) } diff --git a/scripts/test-pnp.sh b/scripts/test-pnp.sh deleted file mode 100755 index 1189b61916ad66e..000000000000000 --- a/scripts/test-pnp.sh +++ /dev/null @@ -1,43 +0,0 @@ -declare -a testCases=( - # Tests the webpack require hook - "progressive-web-app" - "with-eslint" - "with-typescript" - "with-next-sass" - # Tests @next/mdx - "with-mdx" - # Tests babel config - "with-styled-components" -) - -set -e -set -x - -# Speeds up testing locally -export CI=1 - -rm -rf ./e2e-tests - -initialDir=$(pwd) - -for testCase in "${testCases[@]}" -do - cd $initialDir - - echo "--- Testing $testCase ---" - mkdir -p "./e2e-tests/$testCase" - cp -r "./examples/$testCase/." "./e2e-tests/$testCase" - cd "./e2e-tests/$testCase" - - # TODO: remove after able to load bindings with YarnPnP - echo '{"presets": ["next/babel"]}' > .babelrc - - touch yarn.lock - yarn set version berry - - yarn config set pnpFallbackMode none - yarn config set enableGlobalCache true - yarn link --all --private -r ../.. - - yarn build --no-lint -done diff --git a/test/e2e/yarn-pnp/test/pwa-example.test.ts b/test/e2e/yarn-pnp/test/pwa-example.test.ts new file mode 100644 index 000000000000000..03c6e38de047380 --- /dev/null +++ b/test/e2e/yarn-pnp/test/pwa-example.test.ts @@ -0,0 +1,5 @@ +import { runTests } from './utils' + +describe('yarn PnP', () => { + runTests('progressive-web-app') +}) diff --git a/test/e2e/yarn-pnp/test/utils.ts b/test/e2e/yarn-pnp/test/utils.ts new file mode 100644 index 000000000000000..d7b3f89d810b8f5 --- /dev/null +++ b/test/e2e/yarn-pnp/test/utils.ts @@ -0,0 +1,49 @@ +import fs from 'fs-extra' +import { join } from 'path' +import { fetchViaHTTP } from 'next-test-utils' +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' + +jest.setTimeout(2 * 60 * 1000) + +export function runTests(example = '') { + let next: NextInstance + + beforeAll(async () => { + const srcDir = join(__dirname, '../../../../examples', example) + const srcFiles = await fs.readdir(srcDir) + + const packageJson = await fs.readJson(join(srcDir, 'package.json')) + + next = await createNext({ + files: srcFiles.reduce((prev, file) => { + if (file !== 'package.json') { + prev[file] = new FileRef(join(srcDir, file)) + } + return prev + }, {} as { [key: string]: FileRef }), + dependencies: { + ...packageJson.dependencies, + ...packageJson.devDependencies, + }, + installCommand: ({ dependencies }) => { + const pkgs = Object.keys(dependencies).reduce((prev, cur) => { + prev.push(`${cur}@${dependencies[cur]}`) + return prev + }, [] as string[]) + return `yarn set version berry && yarn config set enableGlobalCache true && yarn config set compressionLevel 0 && yarn add ${pkgs.join( + ' ' + )}` + }, + buildCommand: `yarn next build --no-lint`, + startCommand: (global as any).isNextDev ? `yarn next` : `yarn next start`, + }) + }) + afterAll(() => next?.destroy()) + + it(`should compile and serve the index page correctly ${example}`, async () => { + const res = await fetchViaHTTP(next.url, '/') + expect(res.status).toBe(200) + expect(await res.text()).toContain(' { + runTests('with-eslint') +}) diff --git a/test/e2e/yarn-pnp/test/with-mdx.test.ts b/test/e2e/yarn-pnp/test/with-mdx.test.ts new file mode 100644 index 000000000000000..e25d3400037b07a --- /dev/null +++ b/test/e2e/yarn-pnp/test/with-mdx.test.ts @@ -0,0 +1,5 @@ +import { runTests } from './utils' + +describe('yarn PnP', () => { + runTests('with-mdx') +}) diff --git a/test/e2e/yarn-pnp/test/with-next-sass.test.ts b/test/e2e/yarn-pnp/test/with-next-sass.test.ts new file mode 100644 index 000000000000000..d1e5319da157949 --- /dev/null +++ b/test/e2e/yarn-pnp/test/with-next-sass.test.ts @@ -0,0 +1,5 @@ +import { runTests } from './utils' + +describe('yarn PnP', () => { + runTests('with-next-sass') +}) diff --git a/test/e2e/yarn-pnp/test/with-styled-components.test.ts b/test/e2e/yarn-pnp/test/with-styled-components.test.ts new file mode 100644 index 000000000000000..f8ee921afe98855 --- /dev/null +++ b/test/e2e/yarn-pnp/test/with-styled-components.test.ts @@ -0,0 +1,5 @@ +import { runTests } from './utils' + +describe('yarn PnP', () => { + runTests('with-styled-components') +}) diff --git a/test/e2e/yarn-pnp/test/with-typescript.test.ts b/test/e2e/yarn-pnp/test/with-typescript.test.ts new file mode 100644 index 000000000000000..07a11a31cea7a86 --- /dev/null +++ b/test/e2e/yarn-pnp/test/with-typescript.test.ts @@ -0,0 +1,5 @@ +import { runTests } from './utils' + +describe('yarn PnP', () => { + runTests('with-typescript') +}) diff --git a/test/lib/create-next-install.js b/test/lib/create-next-install.js index f3f5df619791fab..824bf1b29cbf5bd 100644 --- a/test/lib/create-next-install.js +++ b/test/lib/create-next-install.js @@ -2,10 +2,11 @@ const os = require('os') const path = require('path') const execa = require('execa') const fs = require('fs-extra') +const childProcess = require('child_process') const { linkPackages } = require('../../.github/actions/next-stats-action/src/prepare/repo-setup')() -async function createNextInstall(dependencies) { +async function createNextInstall(dependencies, installCommand) { const tmpDir = await fs.realpath(process.env.NEXT_TEST_DIR || os.tmpdir()) const origRepoDir = path.join(__dirname, '../../') const installDir = path.join(tmpDir, `next-install-${Date.now()}`) @@ -45,30 +46,46 @@ async function createNextInstall(dependencies) { } const pkgPaths = await linkPackages(tmpRepoDir) + const combinedDependencies = { + ...dependencies, + next: pkgPaths.get('next'), + } await fs.ensureDir(installDir) await fs.writeFile( path.join(installDir, 'package.json'), JSON.stringify( { - dependencies: { - ...dependencies, - next: pkgPaths.get('next'), - }, + dependencies: combinedDependencies, private: true, }, null, 2 ) ) - await execa('yarn', ['install'], { - cwd: installDir, - stdio: ['ignore', 'inherit', 'inherit'], - env: { - ...process.env, - YARN_CACHE_FOLDER: path.join(installDir, '.yarn-cache'), - }, - }) + + if (installCommand) { + const installString = + typeof installCommand === 'function' + ? installCommand({ dependencies: combinedDependencies }) + : installCommand + + console.log('running install command', installString) + + childProcess.execSync(installString, { + cwd: installDir, + stdio: ['ignore', 'inherit', 'inherit'], + }) + } else { + await execa('yarn', ['install'], { + cwd: installDir, + stdio: ['ignore', 'inherit', 'inherit'], + env: { + ...process.env, + YARN_CACHE_FOLDER: path.join(installDir, '.yarn-cache'), + }, + }) + } await fs.remove(tmpRepoDir) return installDir diff --git a/test/lib/e2e-utils.ts b/test/lib/e2e-utils.ts index 736af74930d0db7..dcfc054dac4bf2b 100644 --- a/test/lib/e2e-utils.ts +++ b/test/lib/e2e-utils.ts @@ -1,20 +1,34 @@ import path from 'path' import assert from 'assert' import { NextConfig } from 'next' -import { NextInstance } from './next-modes/base' +import { InstallCommand, NextInstance } from './next-modes/base' import { NextDevInstance } from './next-modes/next-dev' import { NextStartInstance } from './next-modes/next-start' -const testFile = module.parent.filename const testsFolder = path.join(__dirname, '..') +let testFile +const testFileRegex = /\.test\.(js|tsx?)/ + +const visitedModules = new Set() +const checkParent = (mod) => { + if (!mod?.parent || visitedModules.has(mod)) return + testFile = mod.parent.filename || '' + visitedModules.add(mod) + + if (!testFileRegex.test(testFile)) { + checkParent(mod.parent) + } +} +checkParent(module) + process.env.TEST_FILE_PATH = testFile let testMode = process.env.NEXT_TEST_MODE -if (!testFile.match(/\.test\.(js|tsx?)/)) { +if (!testFileRegex.test(testFile)) { throw new Error( - 'e2e-utils imported from non-test file (must end with .test.(js,ts,tsx)' + `e2e-utils imported from non-test file ${testFile} (must end with .test.(js,ts,tsx)` ) } @@ -97,6 +111,9 @@ export async function createNext(opts: { } nextConfig?: NextConfig skipStart?: boolean + installCommand?: InstallCommand + buildCommand?: string + startCommand?: string }): Promise { try { if (nextInstance) { diff --git a/test/lib/next-modes/base.ts b/test/lib/next-modes/base.ts index cb1c1e9a0e389c1..08f0689d296083a 100644 --- a/test/lib/next-modes/base.ts +++ b/test/lib/next-modes/base.ts @@ -8,12 +8,18 @@ import { ChildProcess } from 'child_process' import { createNextInstall } from '../create-next-install' type Event = 'stdout' | 'stderr' | 'error' | 'destroy' +export type InstallCommand = + | string + | ((ctx: { dependencies: { [key: string]: string } }) => string) export class NextInstance { protected files: { [filename: string]: string | FileRef } protected nextConfig?: NextConfig + protected installCommand?: InstallCommand + protected buildCommand?: string + protected startCommand?: string protected dependencies?: { [name: string]: string } protected events: { [eventName: string]: Set } public testDir: string @@ -27,6 +33,9 @@ export class NextInstance { files, dependencies, nextConfig, + installCommand, + buildCommand, + startCommand, }: { files: { [filename: string]: string | FileRef @@ -35,10 +44,16 @@ export class NextInstance { [name: string]: string } nextConfig?: NextConfig + installCommand?: InstallCommand + buildCommand?: string + startCommand?: string }) { this.files = files this.dependencies = dependencies this.nextConfig = nextConfig + this.installCommand = installCommand + this.buildCommand = buildCommand + this.startCommand = startCommand this.events = {} this.isDestroyed = false this.isStopping = false @@ -59,15 +74,23 @@ export class NextInstance { `next-test-${Date.now()}-${(Math.random() * 1000) | 0}` ) - if (process.env.NEXT_TEST_STARTER && !this.dependencies) { + if ( + process.env.NEXT_TEST_STARTER && + !this.dependencies && + !this.installCommand + ) { await fs.copy(process.env.NEXT_TEST_STARTER, this.testDir) } else if (!skipIsolatedNext) { - this.testDir = await createNextInstall({ - react: 'latest', - 'react-dom': 'latest', - ...this.dependencies, - }) + this.testDir = await createNextInstall( + { + react: 'latest', + 'react-dom': 'latest', + ...this.dependencies, + }, + this.installCommand + ) } + console.log('created next.js install, writing test files') for (const filename of Object.keys(this.files)) { const item = this.files[filename] @@ -182,6 +205,7 @@ export class NextInstance { if (!process.env.NEXT_TEST_SKIP_CLEANUP) { await fs.remove(this.testDir) } + console.log(`destroyed next instance`) } public get url() { diff --git a/test/lib/next-modes/next-dev.ts b/test/lib/next-modes/next-dev.ts index 165eb32335859a9..0a27917038f9fbe 100644 --- a/test/lib/next-modes/next-dev.ts +++ b/test/lib/next-modes/next-dev.ts @@ -1,5 +1,3 @@ -import path from 'path' -import resolveFrom from 'resolve-from' import { spawn } from 'child_process' import { NextInstance } from './base' @@ -22,44 +20,47 @@ export class NextDevInstance extends NextInstance { if (this.childProcess) { throw new Error('next already started') } - // we don't use yarn next here as yarn detaches itself from the - // child process making it harder to kill all processes - const nextDir = path.dirname(resolveFrom(this.testDir, 'next/package.json')) + let startArgs = ['yarn', 'next'] - this.childProcess = spawn('node', [path.join(nextDir, '/dist/bin/next')], { - cwd: this.testDir, - stdio: ['ignore', 'pipe', 'pipe'], - shell: false, - env: { - ...process.env, - NODE_ENV: '' as any, - __NEXT_TEST_MODE: '1', - __NEXT_RAND_PORT: '1', - __NEXT_TEST_WITH_DEVTOOL: '1', - }, - }) + if (this.startCommand) { + startArgs = this.startCommand.split(' ') + } - this.childProcess.stdout.on('data', (chunk) => { - const msg = chunk.toString() - process.stdout.write(chunk) - this._cliOutput += msg - this.emit('stdout', [msg]) - }) - this.childProcess.stderr.on('data', (chunk) => { - const msg = chunk.toString() - process.stderr.write(chunk) - this._cliOutput += msg - this.emit('stderr', [msg]) - }) + await new Promise((resolve) => { + this.childProcess = spawn(startArgs[0], startArgs.slice(1), { + cwd: this.testDir, + stdio: ['ignore', 'pipe', 'pipe'], + shell: false, + env: { + ...process.env, + NODE_ENV: '' as any, + __NEXT_TEST_MODE: '1', + __NEXT_RAND_PORT: '1', + __NEXT_TEST_WITH_DEVTOOL: '1', + }, + }) - this.childProcess.on('close', (code) => { - if (this.isStopping) return - if (code) { - throw new Error(`next dev exited unexpectedly with code ${code}`) - } - }) + this.childProcess.stdout.on('data', (chunk) => { + const msg = chunk.toString() + process.stdout.write(chunk) + this._cliOutput += msg + this.emit('stdout', [msg]) + }) + this.childProcess.stderr.on('data', (chunk) => { + const msg = chunk.toString() + process.stderr.write(chunk) + this._cliOutput += msg + this.emit('stderr', [msg]) + }) - await new Promise((resolve) => { + this.childProcess.on('close', (code, signal) => { + if (this.isStopping) return + if (code || signal) { + throw new Error( + `next dev exited unexpectedly with code/signal ${code || signal}` + ) + } + }) const readyCb = (msg) => { if (msg.includes('started server on') && msg.includes('url:')) { this._url = msg.split('url: ').pop().trim() diff --git a/test/lib/next-modes/next-start.ts b/test/lib/next-modes/next-start.ts index 5ede3212a491f2a..6ab484c9e88efa0 100644 --- a/test/lib/next-modes/next-start.ts +++ b/test/lib/next-modes/next-start.ts @@ -1,8 +1,7 @@ import path from 'path' import fs from 'fs-extra' -import resolveFrom from 'resolve-from' -import { spawn, SpawnOptions } from 'child_process' import { NextInstance } from './base' +import { spawn, SpawnOptions } from 'child_process' export class NextStartInstance extends NextInstance { private _buildId: string @@ -49,21 +48,29 @@ export class NextStartInstance extends NextInstance { this.emit('stderr', [msg]) }) } - const nextDir = path.dirname(resolveFrom(this.testDir, 'next/package.json')) + let buildArgs = ['yarn', 'next', 'build'] + let startArgs = ['yarn', 'next', 'start'] - this.childProcess = spawn( - 'node', - [path.join(nextDir, '/dist/bin/next'), 'build'], - spawnOpts - ) - handleStdio() + if (this.buildCommand) { + buildArgs = this.buildCommand.split(' ') + } + if (this.startCommand) { + startArgs = this.startCommand.split(' ') + } await new Promise((resolve, reject) => { - this.childProcess.on('exit', (code) => { - if (code) reject(new Error(`next build failed with code ${code}`)) + console.log('running', buildArgs.join(' ')) + this.childProcess = spawn(buildArgs[0], buildArgs.slice(1), spawnOpts) + handleStdio() + this.childProcess.on('exit', (code, signal) => { + if (code || signal) + reject( + new Error(`next build failed with code/signal ${code || signal}`) + ) else resolve() }) }) + this._buildId = ( await fs.readFile( path.join( @@ -74,23 +81,22 @@ export class NextStartInstance extends NextInstance { 'utf8' ) ).trim() - // we don't use yarn next here as yarn detaches itself from the - // child process making it harder to kill all processes - this.childProcess = spawn( - 'node', - [path.join(nextDir, '/dist/bin/next'), 'start'], - spawnOpts - ) - handleStdio() - this.childProcess.on('close', (code) => { - if (this.isStopping) return - if (code) { - throw new Error(`next start exited unexpectedly with code ${code}`) - } - }) + console.log('running', startArgs.join(' ')) await new Promise((resolve) => { + this.childProcess = spawn(startArgs[0], startArgs.slice(1), spawnOpts) + handleStdio() + + this.childProcess.on('close', (code, signal) => { + if (this.isStopping) return + if (code || signal) { + throw new Error( + `next start exited unexpectedly with code/signal ${code || signal}` + ) + } + }) + const readyCb = (msg) => { if (msg.includes('started server on') && msg.includes('url:')) { this._url = msg.split('url: ').pop().trim()