Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nuxt): allows the support of multiple shared runtime context #27068

Merged
merged 14 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/3.api/2.composables/use-nuxt-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ const nuxtApp = useNuxtApp()

If runtime context is unavailable in your scope, `useNuxtApp` will throw an exception when called. You can use [`tryUseNuxtApp`](#tryusenuxtapp) instead for composables that do not require `nuxtApp`, or to simply check if context is available or not without an exception.

<!--
note
By default, the shared runtime context of Nuxt is namespaced under the [`buildId`](/docs/api/nuxt-config#buildid) option. It allows the support of multiple runtime contexts.

## Params

- `appName`: an optional application name. If you do not provide it, the Nuxt `buildId` option is used. Otherwise, it must match with an existing `buildId`. -->

## Methods

### `provide (name, value)`
Expand Down Expand Up @@ -278,3 +286,7 @@ export function useStandType() {
}
}
```

<!-- ### Params

- `appName`: an optional application name. If you do not provide it, the Nuxt `buildId` option is used. Otherwise, it must match with an existing `buildId`. -->
26 changes: 19 additions & 7 deletions packages/nuxt/src/app/nuxt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,14 @@ import type { ViewTransition } from './plugins/view-transitions.client'

import type { NuxtAppLiterals } from '#app'

const nuxtAppCtx = /* @__PURE__ */ getContext<NuxtApp>('nuxt-app', {
asyncContext: !!__NUXT_ASYNC_CONTEXT__ && import.meta.server,
})
// @ts-expect-error virtual import
import { buildId } from '#build/nuxt.config.mjs'

function getNuxtAppCtx (appName?: string) {
return getContext<NuxtApp>(appName || buildId || 'nuxt-app', {
asyncContext: !!__NUXT_ASYNC_CONTEXT__ && import.meta.server,
})
}

type HookResult = Promise<void> | void

Expand Down Expand Up @@ -93,6 +98,8 @@ export interface NuxtPayload {
}

interface _NuxtApp {
/** @internal */
_name: string
vueApp: App<Element>
globalName: string
versions: Record<string, string>
Expand Down Expand Up @@ -237,6 +244,7 @@ export interface CreateOptions {
export function createNuxtApp (options: CreateOptions) {
let hydratingCount = 0
const nuxtApp: NuxtApp = {
name: buildId,
danielroe marked this conversation as resolved.
Show resolved Hide resolved
_scope: effectScope(),
provide: undefined,
globalName: 'nuxt',
Expand Down Expand Up @@ -447,6 +455,7 @@ export function isNuxtPlugin (plugin: unknown) {
*/
export function callWithNuxt<T extends (...args: any[]) => any> (nuxt: NuxtApp | _NuxtApp, setup: T, args?: Parameters<T>) {
const fn: () => ReturnType<T> = () => args ? setup(...args as Parameters<T>) : setup()
const nuxtAppCtx = getNuxtAppCtx(nuxt._name)
if (import.meta.server) {
return nuxt.vueApp.runWithContext(() => nuxtAppCtx.callAsync(nuxt as NuxtApp, fn))
} else {
Expand All @@ -463,13 +472,14 @@ export function callWithNuxt<T extends (...args: any[]) => any> (nuxt: NuxtApp |
* Returns `null` if Nuxt instance is unavailable.
* @since 3.10.0
*/
export function tryUseNuxtApp (): NuxtApp | null {
export function tryUseNuxtApp (): NuxtApp | null
export function tryUseNuxtApp (appName?: string): NuxtApp | null {
let nuxtAppInstance
if (hasInjectionContext()) {
nuxtAppInstance = getCurrentInstance()?.appContext.app.$nuxt
}

nuxtAppInstance = nuxtAppInstance || nuxtAppCtx.tryUse()
nuxtAppInstance = nuxtAppInstance || getNuxtAppCtx(appName).tryUse()

return nuxtAppInstance || null
}
Expand All @@ -481,8 +491,10 @@ export function tryUseNuxtApp (): NuxtApp | null {
* Throws an error if Nuxt instance is unavailable.
* @since 3.0.0
*/
export function useNuxtApp (): NuxtApp {
const nuxtAppInstance = tryUseNuxtApp()
export function useNuxtApp (): NuxtApp
export function useNuxtApp (appName?: string): NuxtApp {
danielroe marked this conversation as resolved.
Show resolved Hide resolved
// @ts-expect-error internal usage of appName
const nuxtAppInstance = tryUseNuxtApp(appName)

if (!nuxtAppInstance) {
if (import.meta.dev) {
Expand Down
3 changes: 1 addition & 2 deletions packages/nuxt/src/core/nitro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { existsSync, promises as fsp, readFileSync } from 'node:fs'
import { cpus } from 'node:os'
import { join, relative, resolve } from 'pathe'
import { createRouter as createRadixRouter, exportMatcher, toRouteMatcher } from 'radix3'
import { randomUUID } from 'uncrypto'
import { joinURL, withTrailingSlash } from 'ufo'
import { build, copyPublicAssets, createDevServer, createNitro, prepare, prerender, scanHandlers, writeTypes } from 'nitropack'
import type { Nitro, NitroConfig, NitroOptions } from 'nitropack'
Expand Down Expand Up @@ -237,7 +236,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
if (nuxt.options.experimental.appManifest) {
// @ts-expect-error untyped nuxt property
const buildId = nuxt.options.appConfig.nuxt!.buildId ||=
(nuxt.options.dev ? 'dev' : nuxt.options.test ? 'test' : randomUUID())
(nuxt.options.dev ? 'dev' : nuxt.options.test ? 'test' : nuxt.options.buildId)
const buildTimestamp = Date.now()

const manifestPrefix = joinURL(nuxt.options.app.buildAssetsDir, 'builds')
Expand Down
1 change: 1 addition & 0 deletions packages/nuxt/src/core/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ export const nuxtConfigTemplate: NuxtTemplate = {
`export const fetchDefaults = ${JSON.stringify(fetchDefaults)}`,
`export const vueAppRootContainer = ${ctx.nuxt.options.app.rootId ? `'#${ctx.nuxt.options.app.rootId}'` : `'body > ${ctx.nuxt.options.app.rootTag}'`}`,
`export const viewTransition = ${ctx.nuxt.options.experimental.viewTransition}`,
`export const buildId = ${JSON.stringify(ctx.nuxt.options.buildId)}`,
].join('\n\n')
},
}
1 change: 1 addition & 0 deletions packages/schema/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"std-env": "^3.7.0",
"ufo": "^1.5.3",
"unimport": "^3.7.1",
"uncrypto": "^0.1.3",
"untyped": "^1.4.2"
},
"engines": {
Expand Down
8 changes: 8 additions & 0 deletions packages/schema/src/config/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { join, relative, resolve } from 'pathe'
import { isDebug, isDevelopment, isTest } from 'std-env'
import { defu } from 'defu'
import { findWorkspaceDir } from 'pkg-types'
import { randomUUID } from 'uncrypto'
import type { RuntimeConfig } from '../types/config'

export default defineUntypedSchema({
Expand Down Expand Up @@ -153,6 +154,13 @@ export default defineUntypedSchema({
$resolve: async (val: string | undefined, get): Promise<string> => resolve(await get('rootDir') as string, val || '.nuxt'),
},

/**
* A unique identifier matching the build. This may contain the hash of the current state of the project.
*/
buildId: {
$resolve: (val: string) => val ?? randomUUID(),
},

/**
* Used to set the modules directories for path resolving (for example, webpack's
* `resolveLoading`, `nodeExternals` and `postcss`).
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 39 additions & 0 deletions test/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2642,3 +2642,42 @@ describe('defineNuxtComponent watch duplicate', () => {
expect(await page.getByTestId('define-nuxt-component-state').first().innerText()).toBe('2')
})
})

describe('namespace access to useNuxtApp', () => {
it('should return the nuxt instance when used with correct buildId', async () => {
const { page, pageErrors } = await renderPage('/namespace-nuxt-app')

expect(pageErrors).toEqual([])

await page.waitForFunction(() => window.useNuxtApp?.() && !window.useNuxtApp?.().isHydrating)

// Defaulting to buildId
await page.evaluate(() => window.useNuxtApp?.())
// Using correct configured buildId
// @ts-expect-error not public API yet
await page.evaluate(() => window.useNuxtApp?.('nuxt-app-basic'))

await page.close()
})

it('should throw an error when used with wrong buildId', async () => {
const { page, pageErrors } = await renderPage('/namespace-nuxt-app')

expect(pageErrors).toEqual([])

await page.waitForFunction(() => window.useNuxtApp?.() && !window.useNuxtApp?.().isHydrating)

let error: unknown
try {
// Using wrong/unknown buildId
// @ts-expect-error not public API yet
await page.evaluate(() => window.useNuxtApp?.('nuxt-app-unknown'))
} catch (err) {
error = err
}

expect(error).toBeTruthy()

await page.close()
})
})
1 change: 1 addition & 0 deletions test/fixtures/basic/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default defineNuxtConfig({
},
buildDir: process.env.NITRO_BUILD_DIR,
builder: process.env.TEST_BUILDER as 'webpack' | 'vite' ?? 'vite',
buildId: 'nuxt-app-basic',
build: {
transpile: [
(ctx) => {
Expand Down
10 changes: 10 additions & 0 deletions test/fixtures/basic/pages/namespace-nuxt-app.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script setup lang="ts">
// Defaulting to buildId
useNuxtApp()
// Using correct configured buildId
useNuxtApp('nuxt-app-basic')
</script>

<template>
<div>Nuxt instance available</div>
</template>
1 change: 1 addition & 0 deletions vitest.nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default defineVitestConfig({
environmentOptions: {
nuxt: {
overrides: {
buildId: 'nuxt-app',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: before release we need to update @nuxt/test-utils to set buildId to nuxt-app in the vitest environment...

experimental: {
appManifest: process.env.TEST_MANIFEST !== 'manifest-off',
},
Expand Down
Loading