From 6be02c993ddb11dd85f5cdb69d325926f56074c1 Mon Sep 17 00:00:00 2001 From: sanjayy-gowdaa Date: Wed, 2 Jul 2025 01:37:00 +0530 Subject: [PATCH 1/6] feat: add --extends option --- packages/nuxi/src/commands/_shared.ts | 8 ++++++++ packages/nuxi/src/commands/analyze.ts | 4 +++- packages/nuxi/src/commands/build.ts | 4 +++- packages/nuxi/src/commands/dev.ts | 4 +++- packages/nuxi/src/commands/generate.ts | 3 ++- packages/nuxi/src/commands/prepare.ts | 4 +++- packages/nuxi/src/commands/preview.ts | 4 +++- packages/nuxi/src/commands/typecheck.ts | 8 +++++--- 8 files changed, 30 insertions(+), 9 deletions(-) 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 3dfed828e..5ae2f1945 100644 --- a/packages/nuxi/src/commands/dev.ts +++ b/packages/nuxi/src/commands/dev.ts @@ -24,7 +24,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()) @@ -41,6 +41,7 @@ const command = defineCommand({ ...dotEnvArgs, ...legacyRootDirArgs, ...envNameArgs, + ...extendsArgs, clear: { type: 'boolean', description: 'Clear console on restart', @@ -100,6 +101,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, }, }) 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..b277c6ac4 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, envNameArgs, 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 }), }, }) From ec30d9ba9f051cd9e5d6fad2e8cca49d79fafb18 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 1 Jul 2025 20:27:32 +0000 Subject: [PATCH 2/6] [autofix.ci] apply automated fixes --- packages/nuxi/src/commands/typecheck.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxi/src/commands/typecheck.ts b/packages/nuxi/src/commands/typecheck.ts index b277c6ac4..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, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared' +import { cwdArgs, dotEnvArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared' export default defineCommand({ meta: { From 744570085055cca28aa8f1e340928573598e24d9 Mon Sep 17 00:00:00 2001 From: sanjayy-gowdaa Date: Wed, 2 Jul 2025 04:08:23 +0530 Subject: [PATCH 3/6] Fixes issue where --extends CLI option was not being passed down to the dev server --- packages/nuxi/src/commands/dev.ts | 2 +- packages/nuxi/src/dev/utils.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/nuxi/src/commands/dev.ts b/packages/nuxi/src/commands/dev.ts index 5ae2f1945..19d3ead41 100644 --- a/packages/nuxi/src/commands/dev.ts +++ b/packages/nuxi/src/commands/dev.ts @@ -274,7 +274,7 @@ async function createDevProxy(nuxtOptions: NuxtOptions, 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/dev/utils.ts b/packages/nuxi/src/dev/utils.ts index 861673446..95fd94e77 100644 --- a/packages/nuxi/src/dev/utils.ts +++ b/packages/nuxi/src/dev/utils.ts @@ -47,6 +47,7 @@ export interface NuxtDevContext { logLevel: string dotenv: string envName: string + extends?: string } proxy?: { url?: string @@ -238,6 +239,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, From ceaa78ebb9e0ff7972624355ee4135d9c5186ef3 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 7 Jul 2025 15:11:27 +0100 Subject: [PATCH 4/6] test: add test case for dev server extends --- packages/nuxt-cli/test/e2e/commands.spec.ts | 22 ++------------- packages/nuxt-cli/test/e2e/extends.spec.ts | 31 +++++++++++++++++++++ packages/nuxt-cli/test/utils/index.ts | 20 +++++++++++++ playground/some-layer/nuxt.config.ts | 1 + playground/some-layer/pages/extended.vue | 5 ++++ 5 files changed, 59 insertions(+), 20 deletions(-) create mode 100644 packages/nuxt-cli/test/e2e/extends.spec.ts create mode 100644 packages/nuxt-cli/test/utils/index.ts create mode 100644 playground/some-layer/nuxt.config.ts create mode 100644 playground/some-layer/pages/extended.vue diff --git a/packages/nuxt-cli/test/e2e/commands.spec.ts b/packages/nuxt-cli/test/e2e/commands.spec.ts index 47bd33bb3..f9b03a003 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)) @@ -159,22 +160,3 @@ 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) { - 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/packages/nuxt-cli/test/e2e/extends.spec.ts b/packages/nuxt-cli/test/e2e/extends.spec.ts new file mode 100644 index 000000000..7dd428fbd --- /dev/null +++ b/packages/nuxt-cli/test/e2e/extends.spec.ts @@ -0,0 +1,31 @@ +import { fileURLToPath } from 'node:url' +import { getPort } from 'get-port-please' +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)) + +describe('extends support', () => { + it('works with dev server', 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 { + await devProcess + } + 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 @@ + From 858df12b5e499ce25bd7d66712fd8efed4714111 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 7 Jul 2025 15:17:45 +0100 Subject: [PATCH 5/6] test: move into single file --- packages/nuxt-cli/test/e2e/commands.spec.ts | 22 +++++++++++++++ packages/nuxt-cli/test/e2e/extends.spec.ts | 31 --------------------- 2 files changed, 22 insertions(+), 31 deletions(-) delete mode 100644 packages/nuxt-cli/test/e2e/extends.spec.ts diff --git a/packages/nuxt-cli/test/e2e/commands.spec.ts b/packages/nuxt-cli/test/e2e/commands.spec.ts index f9b03a003..77e0e9184 100644 --- a/packages/nuxt-cli/test/e2e/commands.spec.ts +++ b/packages/nuxt-cli/test/e2e/commands.spec.ts @@ -160,3 +160,25 @@ describe('commands', () => { } } }) + +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 { + await devProcess + } + catch {} + }) +}) diff --git a/packages/nuxt-cli/test/e2e/extends.spec.ts b/packages/nuxt-cli/test/e2e/extends.spec.ts deleted file mode 100644 index 7dd428fbd..000000000 --- a/packages/nuxt-cli/test/e2e/extends.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { fileURLToPath } from 'node:url' -import { getPort } from 'get-port-please' -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)) - -describe('extends support', () => { - it('works with dev server', 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 { - await devProcess - } - catch {} - }) -}) From 11a55a7051a9eca5e3da9cd67a5c95dbfa632b25 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 7 Jul 2025 15:21:43 +0100 Subject: [PATCH 6/6] chore: ignore layer deps --- knip.json | 6 ++++++ 1 file changed, 6 insertions(+) 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"