diff --git a/packages/nuxt/src/core/app.ts b/packages/nuxt/src/core/app.ts index 7dbe752b69..1ae3d3b8e0 100644 --- a/packages/nuxt/src/core/app.ts +++ b/packages/nuxt/src/core/app.ts @@ -79,7 +79,8 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?: await nuxt.callHook('app:templatesGenerated', app, filteredTemplates, options) } -async function resolveApp (nuxt: Nuxt, app: NuxtApp) { +/** @internal */ +export async function resolveApp (nuxt: Nuxt, app: NuxtApp) { // Resolve main (app.vue) if (!app.mainComponent) { app.mainComponent = await findPath( diff --git a/packages/nuxt/test/app.test.ts b/packages/nuxt/test/app.test.ts new file mode 100644 index 0000000000..e8f3e65644 --- /dev/null +++ b/packages/nuxt/test/app.test.ts @@ -0,0 +1,221 @@ +import { fileURLToPath } from 'node:url' +import { mkdir, rm, writeFile } from 'node:fs/promises' +import { randomUUID } from 'node:crypto' +import { afterAll, describe, expect, it } from 'vitest' +import { dirname, join, normalize, resolve } from 'pathe' +import { withoutTrailingSlash } from 'ufo' +import { createApp, resolveApp } from '../src/core/app' +import { loadNuxt } from '../src' + +const repoRoot = withoutTrailingSlash(normalize(fileURLToPath(new URL('../../../', import.meta.url)))) + +describe('resolveApp', () => { + afterAll(async () => { + await rm(resolve(repoRoot, '.fixture'), { recursive: true, force: true }) + }) + it('resolves app with default configuration', async () => { + const app = await getResolvedApp([]) + expect(app).toMatchInlineSnapshot(` + { + "components": [], + "configs": [], + "dir": "", + "errorComponent": "/packages/nuxt/src/app/components/nuxt-error-page.vue", + "extensions": [ + ".js", + ".jsx", + ".mjs", + ".ts", + ".tsx", + ".vue", + ], + "layouts": {}, + "mainComponent": "@nuxt/ui-templates/dist/templates/welcome.vue", + "middleware": [ + { + "global": true, + "name": "manifest-route-rule", + "path": "/packages/nuxt/src/app/middleware/manifest-route-rule.ts", + }, + ], + "plugins": [ + { + "mode": "client", + "src": "/packages/nuxt/src/app/plugins/payload.client.ts", + }, + { + "mode": "server", + "src": "/packages/nuxt/src/app/plugins/revive-payload.server.ts", + }, + { + "mode": "client", + "src": "/packages/nuxt/src/app/plugins/revive-payload.client.ts", + }, + { + "filename": "components.plugin.mjs", + "getContents": [Function], + "mode": "all", + "src": "/.nuxt/components.plugin.mjs", + }, + { + "mode": "all", + "src": "/packages/nuxt/src/head/runtime/plugins/unhead.ts", + }, + { + "mode": "all", + "src": "/packages/nuxt/src/app/plugins/router.ts", + }, + { + "mode": "client", + "src": "/packages/nuxt/src/app/plugins/chunk-reload.client.ts", + }, + { + "mode": "client", + "src": "/packages/nuxt/src/app/plugins/check-outdated-build.client.ts", + }, + ], + "rootComponent": "/packages/nuxt/src/app/components/nuxt-root.vue", + "templates": [], + } + `) + }) + + it('resolves layer plugins in correct order', async () => { + const app = await getResolvedApp([ + // layer 1 + 'layer1/plugins/02.plugin.ts', + 'layer1/plugins/object-named.ts', + 'layer1/plugins/override-test.ts', + 'layer1/nuxt.config.ts', + // layer 2 + 'layer2/plugins/01.plugin.ts', + 'layer2/plugins/object-named.ts', + 'layer2/plugins/override-test.ts', + 'layer2/nuxt.config.ts', + // final (user) layer + 'plugins/00.plugin.ts', + 'plugins/object-named.ts', + { + name: 'nuxt.config.ts', + contents: 'export default defineNuxtConfig({ extends: [\'./layer2\', \'./layer1\'] })' + } + ]) + const fixturePlugins = app.plugins.filter(p => !('getContents' in p) && p.src.includes('')).map(p => p.src) + // TODO: support overriding named plugins + expect(fixturePlugins).toMatchInlineSnapshot(` + [ + "/layer1/plugins/02.plugin.ts", + "/layer1/plugins/object-named.ts", + "/layer1/plugins/override-test.ts", + "/layer2/plugins/01.plugin.ts", + "/layer2/plugins/object-named.ts", + "/layer2/plugins/override-test.ts", + "/plugins/00.plugin.ts", + "/plugins/object-named.ts", + ] + `) + }) + + it('resolves layer middleware in correct order', async () => { + const app = await getResolvedApp([ + // layer 1 + 'layer1/middleware/global.global.ts', + 'layer1/middleware/named-from-layer.ts', + 'layer1/middleware/named-override.ts', + 'layer1/nuxt.config.ts', + // layer 2 + 'layer2/middleware/global.global.ts', + 'layer2/middleware/named-from-layer.ts', + 'layer2/middleware/named-override.ts', + 'layer2/plugins/override-test.ts', + 'layer2/nuxt.config.ts', + // final (user) layer + 'middleware/named-override.ts', + 'middleware/named.ts', + { + name: 'nuxt.config.ts', + contents: 'export default defineNuxtConfig({ extends: [\'./layer2\', \'./layer1\'] })' + } + ]) + const fixtureMiddleware = app.middleware.filter(p => p.path.includes('')).map(p => p.path) + // TODO: fix this + expect(fixtureMiddleware).toMatchInlineSnapshot(` + [ + "/layer1/middleware/global.global.ts", + "/layer1/middleware/named-from-layer.ts", + "/layer1/middleware/named-override.ts", + "/middleware/named.ts", + ] + `) + }) + + it('resolves layer layouts correctly', async () => { + const app = await getResolvedApp([ + // layer 1 + 'layer1/layouts/default.vue', + 'layer1/layouts/layer.vue', + 'layer1/nuxt.config.ts', + // layer 2 + 'layer2/layouts/default.vue', + 'layer2/layouts/layer.vue', + 'layer2/nuxt.config.ts', + // final (user) layer + 'layouts/default.vue', + { + name: 'nuxt.config.ts', + contents: 'export default defineNuxtConfig({ extends: [\'./layer2\', \'./layer1\'] })' + } + ]) + expect(app.layouts).toMatchInlineSnapshot(` + { + "default": { + "file": "/layouts/default.vue", + "name": "default", + }, + "layer": { + "file": "/layer2/layouts/layer.vue", + "name": "layer", + }, + } + `) + }) +}) + +async function getResolvedApp (files: Array) { + const rootDir = resolve(repoRoot, 'node_modules/.fixture', randomUUID()) + await mkdir(rootDir, { recursive: true }) + for (const file of files) { + const filename = typeof file === 'string' ? join(rootDir, file) : join(rootDir, file.name) + await mkdir(dirname(filename), { recursive: true }) + await writeFile(filename, typeof file === 'string' ? '' : file.contents || '') + } + + const nuxt = await loadNuxt({ cwd: rootDir }) + const app = createApp(nuxt) + await resolveApp(nuxt, app) + + const normaliseToRepo = (id?: string | null) => + id?.replace(rootDir, '').replace(repoRoot, '').replace(/.*node_modules\//, '') + + app.dir = normaliseToRepo(app.dir)! + + const componentKeys = ['rootComponent', 'errorComponent', 'mainComponent'] as const + for (const _key of componentKeys) { + const key = _key as typeof componentKeys[number] + app[key] = normaliseToRepo(app[key]) + } + for (const plugin of app.plugins) { + plugin.src = normaliseToRepo(plugin.src)! + } + for (const mw of app.middleware) { + mw.path = normaliseToRepo(mw.path)! + } + + for (const layout in app.layouts) { + app.layouts[layout].file = normaliseToRepo(app.layouts[layout].file)! + } + + await nuxt.close() + + return app +}