diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5b227224b4..d534abb877d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,15 +12,26 @@ on: branches: - main -jobs: - build: - runs-on: ${{ matrix.os }} +# https://github.com/vitejs/vite/blob/main/.github/workflows/ci.yml +env: + # 7 GiB by default on GitHub, setting to 6 GiB + # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources + NODE_OPTIONS: --max-old-space-size=6144 + # install playwright binary manually (because pnpm only runs install script once) + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1" - strategy: - matrix: - os: [ubuntu-latest] - node: [16] +# Remove default permissions of GITHUB_TOKEN for security +# https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.sha }} + cancel-in-progress: ${{ github.event_name != 'push' }} + +jobs: + build: + runs-on: ubuntu-latest timeout-minutes: 10 steps: @@ -28,7 +39,7 @@ jobs: - run: corepack enable - uses: actions/setup-node@v3 with: - node-version: ${{ matrix.node }} + node-version: 18 cache: "pnpm" - name: Install dependencies @@ -37,20 +48,18 @@ jobs: - name: Build run: pnpm build + - name: Test (types) + run: pnpm test:types + - name: Cache dist - uses: actions/cache@v3 + uses: actions/upload-artifact@v3 with: + retention-days: 5 + name: dist path: packages/*/dist - key: ${{ matrix.os }}-node-v${{ matrix.node }}-${{ github.sha }} lint: - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [ubuntu-latest] - node: [16] - + runs-on: ubuntu-latest timeout-minutes: 10 steps: @@ -58,7 +67,7 @@ jobs: - run: corepack enable - uses: actions/setup-node@v3 with: - node-version: ${{ matrix.node }} + node-version: 18 cache: "pnpm" - name: Install dependencies @@ -67,40 +76,19 @@ jobs: - name: Lint run: pnpm lint - typecheck: - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [ubuntu-latest] - node: [16] - - timeout-minutes: 10 - - steps: - - uses: actions/checkout@v3 - - run: corepack enable - - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node }} - cache: "pnpm" - - - name: Install dependencies - run: pnpm install - - - name: Build (stub) - run: pnpm build:stub - - - name: Typecheck - run: pnpm typecheck - test-fixtures: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ubuntu-latest, windows-latest] + env: ['dev', 'built'] + builder: ['vite', 'webpack'] node: [16] + exclude: + - env: 'dev' + builder: 'webpack' timeout-minutes: 10 @@ -115,22 +103,28 @@ jobs: - name: Install dependencies run: pnpm install - # https://github.com/vitejs/vite/blob/main/.github/workflows/ci.yml#L62 # Install playwright's binary under custom directory to cache - - name: Set Playwright path + - name: (non-windows) Set Playwright path and Get playwright version if: runner.os != 'Windows' - run: echo "PLAYWRIGHT_BROWSERS_PATH=$HOME/.cache/playwright-bin" >> $GITHUB_ENV - - name: Set Playwright path (windows) + run: | + echo "PLAYWRIGHT_BROWSERS_PATH=$HOME/.cache/playwright-bin" >> $GITHUB_ENV + PLAYWRIGHT_VERSION="$(pnpm ls --depth 0 --json -w playwright | jq --raw-output '.[0].unsavedDependencies["playwright"].version')" + echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV + + - name: (windows) Set Playwright path and Get playwright version if: runner.os == 'Windows' - run: echo "PLAYWRIGHT_BROWSERS_PATH=$HOME\.cache\playwright-bin" >> $env:GITHUB_ENV + run: | + echo "PLAYWRIGHT_BROWSERS_PATH=$HOME\.cache\playwright-bin" >> $env:GITHUB_ENV + $env:PLAYWRIGHT_VERSION="$(pnpm ls --depth 0 --json -w playwright | jq --raw-output '.[0].unsavedDependencies[\"playwright\"].version')" + echo "PLAYWRIGHT_VERSION=$env:PLAYWRIGHT_VERSION" >> $env:GITHUB_ENV - name: Cache Playwright's binary uses: actions/cache@v3 with: - # Playwright removes unused browsers automatically - # So does not need to add playwright version to key - key: ${{ runner.os }}-playwright-bin-v1 + key: ${{ runner.os }}-playwright-bin-v1-${{ env.PLAYWRIGHT_VERSION }} path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} + restore-keys: | + ${{ runner.os }}-playwright-bin-v1- - name: Install Playwright # does not need to explicitly set chromium after https://github.com/microsoft/playwright/issues/14862 is solved @@ -139,96 +133,23 @@ jobs: - name: Build (stub) run: pnpm build:stub + - name: Typecheck + run: pnpm typecheck + env: + TEST_ENV: ${{ matrix.env }} + TEST_BUILDER: ${{ matrix.builder }} + - name: Test (unit) run: pnpm test:unit + env: + TEST_ENV: ${{ matrix.env }} + TEST_BUILDER: ${{ matrix.builder }} - name: Test (fixtures) run: pnpm test:fixtures - - - name: Test (fixtures with dev) - run: pnpm test:fixtures:dev env: - NODE_OPTIONS: --max-old-space-size=8192 - - test-fixtures-webpack: - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [ubuntu-latest, windows-latest] - node: [16] - - timeout-minutes: 10 - - steps: - - uses: actions/checkout@v3 - - run: corepack enable - - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node }} - cache: "pnpm" - - - name: Install dependencies - run: pnpm install - - # https://github.com/vitejs/vite/blob/main/.github/workflows/ci.yml#L62 - # Install playwright's binary under custom directory to cache - - name: Set Playwright path (non-windows) - if: runner.os != 'Windows' - run: echo "PLAYWRIGHT_BROWSERS_PATH=$HOME/.cache/playwright-bin" >> $GITHUB_ENV - - name: Set Playwright path (windows) - if: runner.os == 'Windows' - run: echo "PLAYWRIGHT_BROWSERS_PATH=$HOME\.cache\playwright-bin" >> $env:GITHUB_ENV - - - name: Cache Playwright's binary - uses: actions/cache@v3 - with: - # Playwright removes unused browsers automatically - # So does not need to add playwright version to key - key: ${{ runner.os }}-playwright-bin-v1 - path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} - - - name: Install Playwright - # does not need to explicitly set chromium after https://github.com/microsoft/playwright/issues/14862 is solved - run: pnpm playwright install chromium - - - name: Build (stub) - run: pnpm build:stub - - - name: Test (fixtures) - run: pnpm test:fixtures:webpack - - test-types: - needs: - - build - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [ubuntu-latest] - node: [16] - - timeout-minutes: 10 - - steps: - - uses: actions/checkout@v3 - - run: corepack enable - - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node }} - cache: "pnpm" - - - name: Install dependencies - run: pnpm install - - - name: Restore dist cache - uses: actions/cache@v3 - with: - path: packages/*/dist - key: ${{ matrix.os }}-node-v${{ matrix.node }}-${{ github.sha }} - - - name: Test (types) - run: pnpm test:types + TEST_ENV: ${{ matrix.env }} + TEST_BUILDER: ${{ matrix.builder }} build-release: if: | @@ -240,8 +161,6 @@ jobs: - lint - build - test-fixtures - - test-fixtures-webpack - - test-types runs-on: ${{ matrix.os }} strategy: @@ -263,10 +182,10 @@ jobs: run: pnpm install - name: Restore dist cache - uses: actions/cache@v3 + uses: actions/download-artifact@v3 with: - path: packages/*/dist - key: ${{ matrix.os }}-node-v${{ matrix.node }}-${{ github.sha }} + name: dist + path: packages - name: Release Edge run: ./scripts/release-edge.sh diff --git a/.nuxtrc b/.nuxtrc new file mode 100644 index 00000000000..f54e61d4ffa --- /dev/null +++ b/.nuxtrc @@ -0,0 +1 @@ +telemetry.enabled=false diff --git a/package.json b/package.json index b47bd6038c8..a9a5ce021e6 100644 --- a/package.json +++ b/package.json @@ -14,14 +14,14 @@ "lint": "eslint --ext .vue,.ts,.js,.mjs .", "lint:docs": "markdownlint ./docs/content/1.docs && case-police 'docs/content/1.docs/**/*.md'", "lint:docs:fix": "markdownlint ./docs/content/1.docs --fix && case-police 'docs/content/1.docs/**/*.md' --fix", - "nuxi": "NUXT_TELEMETRY_DISABLED=1 JITI_ESM_RESOLVE=1 nuxi", - "nuxt": "NUXT_TELEMETRY_DISABLED=1 JITI_ESM_RESOLVE=1 nuxi", + "nuxi": "JITI_ESM_RESOLVE=1 nuxi", + "nuxt": "JITI_ESM_RESOLVE=1 nuxi", "play": "pnpm nuxi dev playground", "play:build": "pnpm nuxi build playground", "play:preview": "pnpm nuxi preview playground", - "test:fixtures": "NUXT_TELEMETRY_DISABLED=1 pnpm nuxi prepare test/fixtures/basic && JITI_ESM_RESOLVE=1 vitest run --dir test", - "test:fixtures:dev": "NUXT_TELEMETRY_DISABLED=1 NUXT_TEST_DEV=true pnpm test:fixtures", - "test:fixtures:webpack": "NUXT_TELEMETRY_DISABLED=1 TEST_WITH_WEBPACK=1 pnpm test:fixtures", + "test:fixtures": "pnpm nuxi prepare test/fixtures/basic && JITI_ESM_RESOLVE=1 vitest run --dir test", + "test:fixtures:dev": "TEST_ENV=dev pnpm test:fixtures", + "test:fixtures:webpack": "TEST_BUILDER=webpack pnpm test:fixtures", "test:types": "pnpm nuxi prepare test/fixtures/basic && cd test/fixtures/basic && npx vue-tsc --noEmit", "test:unit": "JITI_ESM_RESOLVE=1 vitest run --dir packages", "typecheck": "tsc --noEmit" diff --git a/test/basic.test.ts b/test/basic.test.ts index a49028e5f38..775fb61f3ae 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -1,21 +1,27 @@ import { fileURLToPath } from 'node:url' -import { promises as fsp } from 'node:fs' import { describe, expect, it } from 'vitest' import { joinURL, withQuery } from 'ufo' import { isWindows } from 'std-env' -import { join, normalize } from 'pathe' +import { normalize } from 'pathe' // eslint-disable-next-line import/order import { setup, fetch, $fetch, startServer, isDev, createPage, url } from '@nuxt/test-utils' import type { NuxtIslandResponse } from '../packages/nuxt/src/core/runtime/nitro/renderer' -import { expectNoClientErrors, fixturesDir, expectWithPolling, renderPage, withLogs } from './utils' +import { expectNoClientErrors, expectWithPolling, renderPage, withLogs } from './utils' + +const isWebpack = process.env.TEST_BUILDER === 'webpack' -const fixturePath = join(fixturesDir, 'basic') await setup({ - rootDir: fixturePath, + rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)), + dev: process.env.TEST_ENV === 'dev', server: true, browser: true, - setupTimeout: (isWindows ? 240 : 120) * 1000 + setupTimeout: (isWindows ? 240 : 120) * 1000, + nuxtConfig: { + builder: isWebpack ? 'webpack' : 'vite', + buildDir: process.env.NITRO_BUILD_DIR, + nitro: { output: { dir: process.env.NITRO_OUTPUT_DIR } } + } }) describe('server api', () => { @@ -543,7 +549,7 @@ describe('deferred app suspense resolve', () => { await page.waitForLoadState('networkidle') // Wait for all pending micro ticks to be cleared in case hydration haven't finished yet. - await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 0))) + await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10))) const hydrationLogs = logs.filter(log => log.includes('isHydrating')) expect(hydrationLogs.length).toBe(3) @@ -571,7 +577,7 @@ describe('page key', () => { // Wait for all pending micro ticks to be cleared, // so we are not resolved too early when there are repeated page loading - await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 0))) + await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10))) expect(logs.filter(l => l.includes('Child Setup')).length).toBe(1) }) @@ -590,7 +596,7 @@ describe('page key', () => { // Wait for all pending micro ticks to be cleared, // so we are not resolved too early when there are repeated page loading - await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 0))) + await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10))) expect(logs.filter(l => l.includes('Child Setup')).length).toBe(2) }) @@ -611,7 +617,7 @@ describe('layout change not load page twice', () => { // Wait for all pending micro ticks to be cleared, // so we are not resolved too early when there are repeated page loading - await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 0))) + await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10))) expect(logs.filter(l => l.includes('Layout2 Page Setup')).length).toBe(1) }) @@ -633,7 +639,7 @@ describe('automatically keyed composables', () => { }) }) -describe.skipIf(process.env.NUXT_TEST_DEV || process.env.TEST_WITH_WEBPACK)('inlining component styles', () => { +describe.skipIf(isDev() || isWebpack)('inlining component styles', () => { it('should inline styles', async () => { const html = await $fetch('/styles') for (const style of [ @@ -680,28 +686,29 @@ describe('prefetching', () => { }) }) -describe.runIf(process.env.NUXT_TEST_DEV)('detecting invalid root nodes', () => { - it('should detect invalid root nodes in pages', async () => { - for (const path of ['1', '2', '3', '4']) { - const { consoleLogs } = await renderPage(joinURL('/invalid-root', path)) - const consoleLogsWarns = consoleLogs.filter(i => i.type === 'warning').map(w => w.text).join('\n') - expect(consoleLogsWarns).toContain('does not have a single root node and will cause errors when navigating between routes') - } +describe.runIf(isDev())('detecting invalid root nodes', () => { + it.each(['1', '2', '3', '4'])('should detect invalid root nodes in pages (\'/invalid-root/%s\')', async (path) => { + const { consoleLogs, page } = await renderPage(joinURL('/invalid-root', path)) + await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10))) + await expectWithPolling( + () => consoleLogs + .map(w => w.text).join('\n') + .includes('does not have a single root node and will cause errors when navigating between routes'), + true + ) }) - it('should not complain if there is no transition', async () => { - for (const path of ['fine']) { - const { consoleLogs } = await renderPage(joinURL('/invalid-root', path)) - - const consoleLogsWarns = consoleLogs.filter(i => i.type === 'warning') + it.each(['fine'])('should not complain if there is no transition (%s)', async (path) => { + const { consoleLogs, page } = await renderPage(joinURL('/invalid-root', path)) + await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10))) - expect(consoleLogsWarns.length).toEqual(0) - } + const consoleLogsWarns = consoleLogs.filter(i => i.type === 'warning') + expect(consoleLogsWarns.length).toEqual(0) }) }) // TODO: dynamic paths in dev -describe.skipIf(process.env.NUXT_TEST_DEV)('dynamic paths', () => { +describe.skipIf(isDev())('dynamic paths', () => { it('should work with no overrides', async () => { const html: string = await $fetch('/assets') for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*?)\)/g)) { @@ -711,7 +718,7 @@ describe.skipIf(process.env.NUXT_TEST_DEV)('dynamic paths', () => { }) // webpack injects CSS differently - it.skipIf(process.env.TEST_WITH_WEBPACK)('adds relative paths to CSS', async () => { + it.skipIf(isWebpack)('adds relative paths to CSS', async () => { const html: string = await $fetch('/assets') const urls = Array.from(html.matchAll(/(href|src)="(.*?)"|url\(([^)]*?)\)/g)).map(m => m[2] || m[3]) const cssURL = urls.find(u => /_nuxt\/assets.*\.css$/.test(u)) @@ -740,7 +747,7 @@ describe.skipIf(process.env.NUXT_TEST_DEV)('dynamic paths', () => { url.startsWith('/foo/_other/') || url === '/foo/public.svg' || // TODO: webpack does not yet support dynamic static paths - (process.env.TEST_WITH_WEBPACK && url === '/public.svg') + (isWebpack && url === '/public.svg') ).toBeTruthy() } }) @@ -757,7 +764,7 @@ describe.skipIf(process.env.NUXT_TEST_DEV)('dynamic paths', () => { url.startsWith('./_nuxt/') || url === './public.svg' || // TODO: webpack does not yet support dynamic static paths - (process.env.TEST_WITH_WEBPACK && url === '/public.svg') + (isWebpack && url === '/public.svg') ).toBeTruthy() expect(url.startsWith('./_nuxt/_nuxt')).toBeFalsy() } @@ -785,7 +792,7 @@ describe.skipIf(process.env.NUXT_TEST_DEV)('dynamic paths', () => { url.startsWith('https://example.com/_cdn/') || url === 'https://example.com/public.svg' || // TODO: webpack does not yet support dynamic static paths - (process.env.TEST_WITH_WEBPACK && url === '/public.svg') + (isWebpack && url === '/public.svg') ).toBeTruthy() } }) @@ -819,7 +826,7 @@ describe('component islands', () => { it('renders components with route', async () => { const result: NuxtIslandResponse = await $fetch('/__nuxt_island/RouteComponent?url=/foo') - if (process.env.NUXT_TEST_DEV) { + if (isDev()) { result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates')) } @@ -846,7 +853,7 @@ describe('component islands', () => { }) })) - if (process.env.NUXT_TEST_DEV) { + if (isDev()) { result.head.link = result.head.link.filter(l => !l.href.includes('@nuxt+ui-templates')) const fixtureDir = normalize(fileURLToPath(new URL('./fixtures/basic', import.meta.url))) for (const link of result.head.link) { @@ -860,7 +867,8 @@ describe('component islands', () => { key: s.key.replace(/-[a-zA-Z0-9]+$/, '') })) - if (!(process.env.NUXT_TEST_DEV || process.env.TEST_WITH_WEBPACK)) { + // TODO: fix rendering of styles in webpack + if (!isDev() && !isWebpack) { expect(result.head).toMatchInlineSnapshot(` { "link": [], @@ -872,7 +880,7 @@ describe('component islands', () => { ], } `) - } else if (process.env.NUXT_TEST_DEV) { + } else if (isDev() && !isWebpack) { expect(result.head).toMatchInlineSnapshot(` { "link": [ @@ -908,7 +916,7 @@ describe('component islands', () => { }) }) -describe.runIf(process.env.NUXT_TEST_DEV && !process.env.TEST_WITH_WEBPACK)('vite plugins', () => { +describe.runIf(isDev() && !isWebpack)('vite plugins', () => { it('does not override vite plugins', async () => { expect(await $fetch('/vite-plugin-without-path')).toBe('vite-plugin without path') expect(await $fetch('/__nuxt-test')).toBe('vite-plugin with __nuxt prefix') @@ -918,7 +926,7 @@ describe.runIf(process.env.NUXT_TEST_DEV && !process.env.TEST_WITH_WEBPACK)('vit }) }) -describe.skipIf(process.env.NUXT_TEST_DEV || isWindows)('payload rendering', () => { +describe.skipIf(isDev() || isWindows)('payload rendering', () => { it('renders a payload', async () => { const payload = await $fetch('/random/a/_payload.js', { responseType: 'text' }) expect(payload).toMatch( @@ -937,7 +945,7 @@ describe.skipIf(process.env.NUXT_TEST_DEV || isWindows)('payload rendering', () await page.goto(url('/random/a')) await page.waitForLoadState('networkidle') - const importSuffix = process.env.NUXT_TEST_DEV && !process.env.TEST_WITH_WEBPACK ? '?import' : '' + const importSuffix = isDev() && !isWebpack ? '?import' : '' // We are manually prefetching other payloads expect(requests).toContain('/random/c/_payload.js') @@ -970,7 +978,7 @@ describe.skipIf(process.env.NUXT_TEST_DEV || isWindows)('payload rendering', () // We are not refetching payloads we've already prefetched // Note: we refetch on dev as urls differ between '' and '?import' - // expect(requests.filter(p => p.includes('_payload')).length).toBe(process.env.NUXT_TEST_DEV ? 1 : 0) + // expect(requests.filter(p => p.includes('_payload')).length).toBe(isDev() ? 1 : 0) }) }) @@ -995,64 +1003,3 @@ describe.skipIf(isWindows)('useAsyncData', () => { await expectNoClientErrors('/useAsyncData/promise-all') }) }) - -// HMR should be at the last -// TODO: fix HMR on Windows -if (isDev() && !isWindows) { - describe('hmr', () => { - it('should work', async () => { - const { page, pageErrors, consoleLogs } = await renderPage('/') - - expect(await page.title()).toBe('Basic fixture') - expect((await page.$('.sugar-counter').then(r => r!.textContent()))!.trim()) - .toEqual('Sugar Counter 12 x 2 = 24 Inc') - - // reactive - await page.$('.sugar-counter button').then(r => r!.click()) - expect((await page.$('.sugar-counter').then(r => r!.textContent()))!.trim()) - .toEqual('Sugar Counter 13 x 2 = 26 Inc') - - // modify file - let indexVue = await fsp.readFile(join(fixturePath, 'pages/index.vue'), 'utf8') - indexVue = indexVue - .replace('