diff --git a/knip.json b/knip.json index dbfd20193..74faba98b 100644 --- a/knip.json +++ b/knip.json @@ -10,6 +10,12 @@ ] }, "playground": { + "entry": [ + "test/**", + "pages/**", + "server/**", + "some-layer/**" + ], "ignoreDependencies": [ "@nuxt/test-utils", "nuxt" diff --git a/packages/nuxi/src/commands/_shared.ts b/packages/nuxi/src/commands/_shared.ts index 4bb46cc3b..673519af9 100644 --- a/packages/nuxi/src/commands/_shared.ts +++ b/packages/nuxi/src/commands/_shared.ts @@ -31,6 +31,14 @@ export const dotEnvArgs = { }, } as const satisfies Record +export const extendsArgs = { + extends: { + type: 'string', + description: 'Extend from a Nuxt layer', + valueHint: 'layer-name', + }, +} as const satisfies Record + export const legacyRootDirArgs = { // cwd falls back to rootDir's default (indirect default) cwd: { diff --git a/packages/nuxi/src/commands/analyze.ts b/packages/nuxi/src/commands/analyze.ts index 3a54dcf27..96c9982f8 100644 --- a/packages/nuxi/src/commands/analyze.ts +++ b/packages/nuxi/src/commands/analyze.ts @@ -13,7 +13,7 @@ import { overrideEnv } from '../utils/env' import { clearDir } from '../utils/fs' import { loadKit } from '../utils/kit' import { logger } from '../utils/logger' -import { cwdArgs, dotEnvArgs, legacyRootDirArgs, logLevelArgs } from './_shared' +import { cwdArgs, dotEnvArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared' export default defineCommand({ meta: { @@ -25,6 +25,7 @@ export default defineCommand({ ...logLevelArgs, ...legacyRootDirArgs, ...dotEnvArgs, + ...extendsArgs, name: { type: 'string', description: 'Name of the analysis', @@ -56,6 +57,7 @@ export default defineCommand({ fileName: ctx.args.dotenv, }, overrides: defu(ctx.data?.overrides, { + ...(ctx.args.extends && { extends: ctx.args.extends }), build: { analyze: { enabled: true, diff --git a/packages/nuxi/src/commands/build.ts b/packages/nuxi/src/commands/build.ts index b6c2469bf..d3fbb1ec2 100644 --- a/packages/nuxi/src/commands/build.ts +++ b/packages/nuxi/src/commands/build.ts @@ -10,7 +10,7 @@ import { overrideEnv } from '../utils/env' import { clearBuildDir } from '../utils/fs' import { loadKit } from '../utils/kit' import { logger } from '../utils/logger' -import { cwdArgs, dotEnvArgs, envNameArgs, legacyRootDirArgs, logLevelArgs } from './_shared' +import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared' export default defineCommand({ meta: { @@ -30,6 +30,7 @@ export default defineCommand({ }, ...dotEnvArgs, ...envNameArgs, + ...extendsArgs, ...legacyRootDirArgs, }, async run(ctx) { @@ -56,6 +57,7 @@ export default defineCommand({ static: ctx.args.prerender, preset: ctx.args.preset || process.env.NITRO_PRESET || process.env.SERVER_PRESET, }, + ...(ctx.args.extends && { extends: ctx.args.extends }), ...ctx.data?.overrides, }, }) diff --git a/packages/nuxi/src/commands/dev.ts b/packages/nuxi/src/commands/dev.ts index b518bb82d..e9d9c7fd7 100644 --- a/packages/nuxi/src/commands/dev.ts +++ b/packages/nuxi/src/commands/dev.ts @@ -25,7 +25,7 @@ import { showVersions } from '../utils/banner' import { overrideEnv } from '../utils/env' import { loadKit } from '../utils/kit' import { logger } from '../utils/logger' -import { cwdArgs, dotEnvArgs, envNameArgs, legacyRootDirArgs, logLevelArgs } from './_shared' +import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared' const startTime: number | undefined = Date.now() const forkSupported = !isTest && (!isBun || isBunForkSupported()) @@ -42,6 +42,7 @@ const command = defineCommand({ ...dotEnvArgs, ...legacyRootDirArgs, ...envNameArgs, + ...extendsArgs, clear: { type: 'boolean', description: 'Clear console on restart', @@ -101,6 +102,7 @@ const command = defineCommand({ overrides: { dev: true, logLevel: ctx.args.logLevel as 'silent' | 'info' | 'verbose', + ...(ctx.args.extends && { extends: ctx.args.extends }), ...ctx.data?.overrides, }, }) @@ -263,7 +265,7 @@ async function createDevProxy(cwd: string, nuxtOptions: NuxtOptions, listenOptio } } -async function startSubprocess(cwd: string, args: { logLevel: string, clear: boolean, dotenv: string, envName: string }, rawArgs: string[], listenOptions: Partial) { +async function startSubprocess(cwd: string, args: { logLevel: string, clear: boolean, dotenv: string, envName: string, extends?: string }, rawArgs: string[], listenOptions: Partial) { let childProc: ChildProcess | undefined let devProxy: DevProxy let ready: Promise | undefined diff --git a/packages/nuxi/src/commands/generate.ts b/packages/nuxi/src/commands/generate.ts index 3af982760..6e2b9015f 100644 --- a/packages/nuxi/src/commands/generate.ts +++ b/packages/nuxi/src/commands/generate.ts @@ -1,6 +1,6 @@ import { defineCommand } from 'citty' -import { cwdArgs, dotEnvArgs, envNameArgs, legacyRootDirArgs, logLevelArgs } from './_shared' +import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared' import buildCommand from './build' export default defineCommand({ @@ -17,6 +17,7 @@ export default defineCommand({ }, ...dotEnvArgs, ...envNameArgs, + ...extendsArgs, ...legacyRootDirArgs, }, async run(ctx) { diff --git a/packages/nuxi/src/commands/prepare.ts b/packages/nuxi/src/commands/prepare.ts index 18239a498..7c9ac9a79 100644 --- a/packages/nuxi/src/commands/prepare.ts +++ b/packages/nuxi/src/commands/prepare.ts @@ -6,7 +6,7 @@ import { relative, resolve } from 'pathe' import { clearBuildDir } from '../utils/fs' import { loadKit } from '../utils/kit' import { logger } from '../utils/logger' -import { cwdArgs, dotEnvArgs, envNameArgs, legacyRootDirArgs, logLevelArgs } from './_shared' +import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared' export default defineCommand({ meta: { @@ -18,6 +18,7 @@ export default defineCommand({ ...cwdArgs, ...logLevelArgs, ...envNameArgs, + ...extendsArgs, ...legacyRootDirArgs, }, async run(ctx) { @@ -36,6 +37,7 @@ export default defineCommand({ overrides: { _prepare: true, logLevel: ctx.args.logLevel as 'silent' | 'info' | 'verbose', + ...(ctx.args.extends && { extends: ctx.args.extends }), ...ctx.data?.overrides, }, }) diff --git a/packages/nuxi/src/commands/preview.ts b/packages/nuxi/src/commands/preview.ts index 452c1ddf8..5bae891af 100644 --- a/packages/nuxi/src/commands/preview.ts +++ b/packages/nuxi/src/commands/preview.ts @@ -12,7 +12,7 @@ import { x } from 'tinyexec' import { loadKit } from '../utils/kit' import { logger } from '../utils/logger' -import { cwdArgs, dotEnvArgs, envNameArgs, legacyRootDirArgs, logLevelArgs } from './_shared' +import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared' const command = defineCommand({ meta: { @@ -23,6 +23,7 @@ const command = defineCommand({ ...cwdArgs, ...logLevelArgs, ...envNameArgs, + ...extendsArgs, ...legacyRootDirArgs, port: getListhenArgs().port, ...dotEnvArgs, @@ -40,6 +41,7 @@ const command = defineCommand({ envName: ctx.args.envName, // c12 will fall back to NODE_ENV ready: true, overrides: { + ...(ctx.args.extends && { extends: ctx.args.extends }), modules: [ function (_, nuxt) { nuxt.hook('nitro:init', (nitro) => { diff --git a/packages/nuxi/src/commands/typecheck.ts b/packages/nuxi/src/commands/typecheck.ts index 0c4c82995..edbc1a059 100644 --- a/packages/nuxi/src/commands/typecheck.ts +++ b/packages/nuxi/src/commands/typecheck.ts @@ -8,7 +8,7 @@ import { isBun } from 'std-env' import { x } from 'tinyexec' import { loadKit } from '../utils/kit' -import { cwdArgs, dotEnvArgs, legacyRootDirArgs, logLevelArgs } from './_shared' +import { cwdArgs, dotEnvArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared' export default defineCommand({ meta: { @@ -19,6 +19,7 @@ export default defineCommand({ ...cwdArgs, ...logLevelArgs, ...dotEnvArgs, + ...extendsArgs, ...legacyRootDirArgs, }, async run(ctx) { @@ -31,7 +32,7 @@ export default defineCommand({ // Prefer local install if possible resolveModulePath('typescript', { try: true }), resolveModulePath('vue-tsc/bin/vue-tsc.js', { try: true }), - writeTypes(cwd, ctx.args.dotenv, ctx.args.logLevel as 'silent' | 'info' | 'verbose'), + writeTypes(cwd, ctx.args.dotenv, ctx.args.logLevel as 'silent' | 'info' | 'verbose', ctx.args.extends), ]) const typeCheckArgs = supportsProjects ? ['-b', '--noEmit'] : ['--noEmit'] @@ -67,7 +68,7 @@ export default defineCommand({ }, }) -async function writeTypes(cwd: string, dotenv?: string, logLevel?: 'silent' | 'info' | 'verbose') { +async function writeTypes(cwd: string, dotenv?: string, logLevel?: 'silent' | 'info' | 'verbose', extendsValue?: string) { const { loadNuxt, buildNuxt, writeTypes } = await loadKit(cwd) const nuxt = await loadNuxt({ cwd, @@ -75,6 +76,7 @@ async function writeTypes(cwd: string, dotenv?: string, logLevel?: 'silent' | 'i overrides: { _prepare: true, logLevel, + ...(extendsValue && { extends: extendsValue }), }, }) diff --git a/packages/nuxi/src/dev/utils.ts b/packages/nuxi/src/dev/utils.ts index 1eb61d98f..edc360cdf 100644 --- a/packages/nuxi/src/dev/utils.ts +++ b/packages/nuxi/src/dev/utils.ts @@ -49,6 +49,7 @@ export interface NuxtDevContext { logLevel: string dotenv: string envName: string + extends?: string } proxy?: { url?: string @@ -232,6 +233,7 @@ export class NuxtDevServer extends EventEmitter { defaults: defu(this.options.defaults, devServerDefaults), overrides: { logLevel: this.options.logLevel as 'silent' | 'info' | 'verbose', + ...(this.options.devContext.args.extends && { extends: this.options.devContext.args.extends }), ...this.options.overrides, vite: { clearScreen: this.options.clear, diff --git a/packages/nuxt-cli/test/e2e/commands.spec.ts b/packages/nuxt-cli/test/e2e/commands.spec.ts index 47bd33bb3..77e0e9184 100644 --- a/packages/nuxt-cli/test/e2e/commands.spec.ts +++ b/packages/nuxt-cli/test/e2e/commands.spec.ts @@ -8,9 +8,10 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { getPort } from 'get-port-please' -import { isCI, isWindows } from 'std-env' +import { isWindows } from 'std-env' import { x } from 'tinyexec' import { describe, expect, it } from 'vitest' +import { fetchWithPolling } from '../utils' const fixtureDir = fileURLToPath(new URL('../../../../playground', import.meta.url)) const nuxi = fileURLToPath(new URL('../../bin/nuxi.mjs', import.meta.url)) @@ -160,21 +161,24 @@ describe('commands', () => { } }) -async function fetchWithPolling(url: string, options: RequestInit = {}, maxAttempts = 10, interval = 100): Promise { - let response: Response | null = null - let attempts = 0 - while (attempts < maxAttempts) { +describe('extends support', () => { + it('works with dev server', { timeout: isWindows ? 200000 : 50000 }, async () => { + const controller = new AbortController() + const port = await getPort({ host: '127.0.0.1', port: 3003 }) + const devProcess = x(nuxi, ['dev', `--host=127.0.0.1`, `--port=${port}`, '--extends=some-layer'], { + nodeOptions: { stdio: 'pipe', cwd: fixtureDir }, + signal: controller.signal, + }) + + // Test that server responds + const response = await fetchWithPolling(`http://127.0.0.1:${port}/extended`, {}, 30, 300) + expect.soft(response?.status).toBe(200) + expect(await response?.text()).toContain('This is an extended page from a layer.') + + controller.abort() try { - response = await fetch(url, options) - if (response.ok) { - return response - } + await devProcess } - catch { - // Ignore errors and retry - } - attempts++ - await new Promise(resolve => setTimeout(resolve, isCI ? interval * 10 : interval)) - } - return response -} + catch {} + }) +}) diff --git a/packages/nuxt-cli/test/utils/index.ts b/packages/nuxt-cli/test/utils/index.ts new file mode 100644 index 000000000..8f3253783 --- /dev/null +++ b/packages/nuxt-cli/test/utils/index.ts @@ -0,0 +1,20 @@ +import { isCI } from 'std-env' + +export async function fetchWithPolling(url: string, options: RequestInit = {}, maxAttempts = 10, interval = 100): Promise { + let response: Response | null = null + let attempts = 0 + while (attempts < maxAttempts) { + try { + response = await fetch(url, options) + if (response.ok) { + return response + } + } + catch { + // Ignore errors and retry + } + attempts++ + await new Promise(resolve => setTimeout(resolve, isCI ? interval * 10 : interval)) + } + return response +} diff --git a/playground/some-layer/nuxt.config.ts b/playground/some-layer/nuxt.config.ts new file mode 100644 index 000000000..268da7f8c --- /dev/null +++ b/playground/some-layer/nuxt.config.ts @@ -0,0 +1 @@ +export default defineNuxtConfig({}) diff --git a/playground/some-layer/pages/extended.vue b/playground/some-layer/pages/extended.vue new file mode 100644 index 000000000..9fda5efa3 --- /dev/null +++ b/playground/some-layer/pages/extended.vue @@ -0,0 +1,5 @@ +