From 5bff73061454dbd3984cf813418da8f15af17328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Israel=20Ortu=C3=B1o?= Date: Fri, 7 Jun 2024 17:42:32 +0200 Subject: [PATCH 1/6] feat(nuxt): allow configuring interval for checking app update (#27324) --- .../nuxt/src/app/plugins/check-outdated-build.client.ts | 6 ++++-- packages/nuxt/src/core/nuxt.ts | 4 +++- packages/nuxt/src/core/templates.ts | 1 + packages/schema/src/config/experimental.ts | 8 ++++++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/nuxt/src/app/plugins/check-outdated-build.client.ts b/packages/nuxt/src/app/plugins/check-outdated-build.client.ts index d8187d04d8..874ea667bd 100644 --- a/packages/nuxt/src/app/plugins/check-outdated-build.client.ts +++ b/packages/nuxt/src/app/plugins/check-outdated-build.client.ts @@ -4,6 +4,8 @@ import type { NuxtAppManifestMeta } from '../composables/manifest' import { onNuxtReady } from '../composables/ready' // @ts-expect-error virtual file import { buildAssetsURL } from '#internal/nuxt/paths' +// @ts-expect-error virtual file +import { outdatedBuildInterval } from '#build/nuxt.config.mjs' export default defineNuxtPlugin((nuxtApp) => { if (import.meta.test) { return } @@ -13,7 +15,7 @@ export default defineNuxtPlugin((nuxtApp) => { async function getLatestManifest () { const currentManifest = await getAppManifest() if (timeout) { clearTimeout(timeout) } - timeout = setTimeout(getLatestManifest, 1000 * 60 * 60) + timeout = setTimeout(getLatestManifest, outdatedBuildInterval) try { const meta = await $fetch(buildAssetsURL('builds/latest.json') + `?${Date.now()}`) if (meta.id !== currentManifest.id) { @@ -25,5 +27,5 @@ export default defineNuxtPlugin((nuxtApp) => { } } - onNuxtReady(() => { timeout = setTimeout(getLatestManifest, 1000 * 60 * 60) }) + onNuxtReady(() => { timeout = setTimeout(getLatestManifest, outdatedBuildInterval) }) }) diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index 8b5d389967..e319d53d7c 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -508,7 +508,9 @@ async function initNuxt (nuxt: Nuxt) { global: true, }) - addPlugin(resolve(nuxt.options.appDir, 'plugins/check-outdated-build.client')) + if (nuxt.options.experimental.checkOutdatedBuildInterval !== false) { + addPlugin(resolve(nuxt.options.appDir, 'plugins/check-outdated-build.client')) + } } nuxt.hooks.hook('builder:watch', (event, relativePath) => { diff --git a/packages/nuxt/src/core/templates.ts b/packages/nuxt/src/core/templates.ts index de33767cd4..49a89f8943 100644 --- a/packages/nuxt/src/core/templates.ts +++ b/packages/nuxt/src/core/templates.ts @@ -413,6 +413,7 @@ export const nuxtConfigTemplate: NuxtTemplate = { `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 appId = ${JSON.stringify(ctx.nuxt.options.appId)}`, + `export const outdatedBuildInterval = ${ctx.nuxt.options.experimental.checkOutdatedBuildInterval}`, ].join('\n\n') }, } diff --git a/packages/schema/src/config/experimental.ts b/packages/schema/src/config/experimental.ts index f429a873e8..bf984023fc 100644 --- a/packages/schema/src/config/experimental.ts +++ b/packages/schema/src/config/experimental.ts @@ -319,6 +319,14 @@ export default defineUntypedSchema({ */ appManifest: true, + /** + * Set the time interval (in ms) to check for new builds. Disabled when `experimental.appManifest` is `false`. + * + * Set to `false` to disable. + * @type {number | false} + */ + checkOutdatedBuildInterval: 1000 * 60 * 60, + /** * Set an alternative watcher that will be used as the watching service for Nuxt. * From 601a2620b89220679592230a237f7ef13fdeabf9 Mon Sep 17 00:00:00 2001 From: nopeless <38830903+nopeless@users.noreply.github.com> Date: Sat, 8 Jun 2024 00:55:49 +0900 Subject: [PATCH 2/6] feat(nuxt): warn when accessing private runtimeConfig on client (#26441) --- packages/nuxt/src/app/nuxt.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/nuxt/src/app/nuxt.ts b/packages/nuxt/src/app/nuxt.ts index 8222527514..3339fff931 100644 --- a/packages/nuxt/src/app/nuxt.ts +++ b/packages/nuxt/src/app/nuxt.ts @@ -382,7 +382,7 @@ export function createNuxtApp (options: CreateOptions) { // Expose runtime config const runtimeConfig = import.meta.server ? options.ssrContext!.runtimeConfig : nuxtApp.payload.config! - nuxtApp.provide('config', runtimeConfig) + nuxtApp.provide('config', import.meta.client && import.meta.dev ? wrappedConfig(runtimeConfig) : runtimeConfig) return nuxtApp } @@ -545,3 +545,20 @@ function defineGetter (obj: Record, export function defineAppConfig (config: C): C { return config } + +/** + * Configure error getter on runtime secret property access that doesn't exist on the client side + */ +function wrappedConfig (runtimeConfig: Record) { + if (!import.meta.dev || import.meta.server) { return runtimeConfig } + const keys = Object.keys(runtimeConfig).map(key => `\`${key}\``) + const lastKey = keys.pop() + return new Proxy(runtimeConfig, { + get (target, p: string, receiver) { + if (p !== 'public' && !(p in target) && !p.startsWith('__v') /* vue check for reactivity, e.g. `__v_isRef` */) { + console.warn(`[nuxt] Could not access \`${p}\`. The only available runtime config keys on the client side are ${keys.join(', ')} and ${lastKey}. See \`https://nuxt.com/docs/guide/going-further/runtime-config\` for more information.`) + } + return Reflect.get(target, p, receiver) + }, + }) +} From 27945b492500bf497c9e5d479c54dfe77e6e502b Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 7 Jun 2024 16:57:37 +0100 Subject: [PATCH 3/6] chore: improve types in tests (#27293) --- test/nuxt/composables.test.ts | 8 ++++---- test/nuxt/nuxt-island.test.ts | 6 +++--- test/nuxt/plugin.test.ts | 7 ++++--- test/nuxt/tsconfig.json | 3 +++ tsconfig.json | 3 ++- 5 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 test/nuxt/tsconfig.json diff --git a/test/nuxt/composables.test.ts b/test/nuxt/composables.test.ts index 02e12f7d87..d4bf1eaaa4 100644 --- a/test/nuxt/composables.test.ts +++ b/test/nuxt/composables.test.ts @@ -20,7 +20,6 @@ import { callOnce } from '#app/composables/once' import { useLoadingIndicator } from '#app/composables/loading-indicator' import { useRouteAnnouncer } from '#app/composables/route-announcer' -// @ts-expect-error virtual file import { asyncDataDefaults, nuxtDefaultErrorValue } from '#build/nuxt.config.mjs' registerEndpoint('/api/test', defineEventHandler(event => ({ @@ -38,7 +37,6 @@ describe('app config', () => { `) updateAppConfig({ new: 'value', - // @ts-expect-error property does not exist nuxt: { nested: 42 }, }) expect(appConfig).toMatchInlineSnapshot(` @@ -165,7 +163,7 @@ describe('useAsyncData', () => { // https://github.com/nuxt/nuxt/issues/23411 it('should initialize with error set to null when immediate: false', async () => { - const { error, execute } = useAsyncData(() => ({}), { immediate: false }) + const { error, execute } = useAsyncData(() => Promise.resolve({}), { immediate: false }) expect(error.value).toBe(asyncDataDefaults.errorValue) await execute() expect(error.value).toBe(asyncDataDefaults.errorValue) @@ -217,7 +215,7 @@ describe('useAsyncData', () => { }) it('allows custom access to a cache', async () => { - const { data } = await useAsyncData(() => ({ val: true }), { getCachedData: () => ({ val: false }) }) + const { data } = await useAsyncData(() => Promise.resolve({ val: true }), { getCachedData: () => ({ val: false }) }) expect(data.value).toMatchInlineSnapshot(` { "val": false, @@ -317,6 +315,7 @@ describe('useFetch', () => { it('should timeout', async () => { const { status, error } = await useFetch( + // @ts-expect-error should resolve to a string () => new Promise(resolve => setTimeout(resolve, 5000)), { timeout: 1 }, ) @@ -534,6 +533,7 @@ describe('loading state', () => { describe.skipIf(process.env.TEST_MANIFEST === 'manifest-off')('app manifests', () => { it('getAppManifest', async () => { const manifest = await getAppManifest() + // @ts-expect-error timestamp is not optional delete manifest.timestamp expect(manifest).toMatchInlineSnapshot(` { diff --git a/test/nuxt/nuxt-island.test.ts b/test/nuxt/nuxt-island.test.ts index 68faf38f72..e9cb75ae11 100644 --- a/test/nuxt/nuxt-island.test.ts +++ b/test/nuxt/nuxt-island.test.ts @@ -263,12 +263,12 @@ describe('client components', () => { const componentId = 'ClientWithSlot-12345' vi.doMock(mockPath, () => ({ - default: { + default: defineComponent({ name: 'ClientWithSlot', setup (_, { slots }) { - return () => h('div', { class: 'client-component' }, slots.default()) + return () => h('div', { class: 'client-component' }, slots.default?.()) }, - }, + }), })) const stubFetch = vi.fn(() => { diff --git a/test/nuxt/plugin.test.ts b/test/nuxt/plugin.test.ts index 02d8cef182..e2199a9b04 100644 --- a/test/nuxt/plugin.test.ts +++ b/test/nuxt/plugin.test.ts @@ -11,9 +11,10 @@ vi.mock('#app', async (original) => { } }) -function pluginFactory (name: string, dependsOn?: string[], sequence: string[], parallel = true) { +function pluginFactory (name: string, dependsOn: string[] | undefined, sequence: string[], parallel = true) { return defineNuxtPlugin({ name, + // @ts-expect-error we have a strong type for plugin names dependsOn, async setup () { sequence.push(`start ${name}`) @@ -71,7 +72,7 @@ describe('plugin dependsOn', () => { pluginFactory('A', undefined, sequence), pluginFactory('B', ['A'], sequence), defineNuxtPlugin({ - name, + name: 'some plugin', async setup () { sequence.push('start C') await new Promise(resolve => setTimeout(resolve, 5)) @@ -99,7 +100,7 @@ describe('plugin dependsOn', () => { const plugins = [ pluginFactory('A', undefined, sequence), defineNuxtPlugin({ - name, + name: 'some plugin', async setup () { sequence.push('start C') await new Promise(resolve => setTimeout(resolve, 50)) diff --git a/test/nuxt/tsconfig.json b/test/nuxt/tsconfig.json new file mode 100644 index 0000000000..9e6718eb68 --- /dev/null +++ b/test/nuxt/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../.nuxt/tsconfig.json" +} diff --git a/tsconfig.json b/tsconfig.json index db885122b4..d36558a491 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -44,7 +44,8 @@ "**/examples/**", "**/docs/**", "**/playground/**", + "**/test/nuxt/**", "**/test/fixtures/**", - "test/nuxt/**" + "**/test/fixtures-temp/**" ] } From 2a2847e4bf71a3959f398dcf60c6ae7719cf78a9 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 7 Jun 2024 17:16:21 +0100 Subject: [PATCH 4/6] chore: temporarily disable updates for `jiti` and `@vitejs/plugin-vue` --- renovate.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/renovate.json b/renovate.json index 96895fcd07..232c4eeb9b 100644 --- a/renovate.json +++ b/renovate.json @@ -31,6 +31,8 @@ "main" ], "ignoreDeps": [ + "jiti", + "@vitejs/plugin-vue", "nuxt", "nuxt3", "@nuxt/kit" From c5135efccc58dbf21d55462acd3e2c66533af8d3 Mon Sep 17 00:00:00 2001 From: Luke Nelson Date: Fri, 7 Jun 2024 17:24:50 +0100 Subject: [PATCH 5/6] docs: add note about middleware re-running for error pages (#27481) --- docs/1.getting-started/8.error-handling.md | 4 ++++ docs/2.guide/2.directory-structure/1.middleware.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/docs/1.getting-started/8.error-handling.md b/docs/1.getting-started/8.error-handling.md index e3b944dbc6..c4b44a438d 100644 --- a/docs/1.getting-started/8.error-handling.md +++ b/docs/1.getting-started/8.error-handling.md @@ -125,6 +125,10 @@ When you are ready to remove the error page, you can call the [`clearError`](/do Make sure to check before using anything dependent on Nuxt plugins, such as `$route` or `useRouter`, as if a plugin threw an error, then it won't be re-run until you clear the error. :: +::note +Rendering an error page is an entirely separate page load, meaning any registered middleware will run again. You can use [`useError`](#useerror) in middleware to check if an error is being handled. +:: + ::note If you are running on Node 16 and you set any cookies when rendering your error page, they will [overwrite cookies previously set](https://github.com/nuxt/nuxt/pull/20585). We recommend using a newer version of Node as Node 16 reached end-of-life in September 2023. :: diff --git a/docs/2.guide/2.directory-structure/1.middleware.md b/docs/2.guide/2.directory-structure/1.middleware.md index e4e97484d3..64e66ecf6d 100644 --- a/docs/2.guide/2.directory-structure/1.middleware.md +++ b/docs/2.guide/2.directory-structure/1.middleware.md @@ -134,6 +134,10 @@ export default defineNuxtRouteMiddleware(to => { }) ``` +::note +Rendering an error page is an entirely separate page load, meaning any registered middleware will run again. You can use [`useError`](/docs/getting-started/error-handling#useerror) in middleware to check if an error is being handled. +:: + ## Adding Middleware Dynamically It is possible to add global or named route middleware manually using the [`addRouteMiddleware()`](/docs/api/utils/add-route-middleware) helper function, such as from within a plugin. From 61766702c03eb2b5c62b7e8b428ed61741c0d0e7 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 7 Jun 2024 17:30:14 +0100 Subject: [PATCH 6/6] docs: update link to nitro `error` hook type --- docs/3.api/6.advanced/1.hooks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/3.api/6.advanced/1.hooks.md b/docs/3.api/6.advanced/1.hooks.md index 0980575a37..30204db7e4 100644 --- a/docs/3.api/6.advanced/1.hooks.md +++ b/docs/3.api/6.advanced/1.hooks.md @@ -98,7 +98,7 @@ Hook | Arguments | Description `render:html` | `html, { event }` | Called before constructing the HTML. | [html](https://github.com/nuxt/nuxt/blob/71ef8bd3ff207fd51c2ca18d5a8c7140476780c7/packages/nuxt/src/core/runtime/nitro/renderer.ts#L15), [event](https://github.com/unjs/h3/blob/f6ceb5581043dc4d8b6eab91e9be4531e0c30f8e/src/types.ts#L38) `render:island` | `islandResponse, { event, islandContext }` | Called before constructing the island HTML. | [islandResponse](https://github.com/nuxt/nuxt/blob/e50cabfed1984c341af0d0c056a325a8aec26980/packages/nuxt/src/core/runtime/nitro/renderer.ts#L28), [event](https://github.com/unjs/h3/blob/f6ceb5581043dc4d8b6eab91e9be4531e0c30f8e/src/types.ts#L38), [islandContext](https://github.com/nuxt/nuxt/blob/e50cabfed1984c341af0d0c056a325a8aec26980/packages/nuxt/src/core/runtime/nitro/renderer.ts#L38) `close` | - | Called when Nitro is closed. | - -`error` | `error, { event? }` | Called when an error occurs. | [error](https://github.com/unjs/nitro/blob/main/src/runtime/types.ts#L24), [event](https://github.com/unjs/h3/blob/f6ceb5581043dc4d8b6eab91e9be4531e0c30f8e/src/types.ts#L38) +`error` | `error, { event? }` | Called when an error occurs. | [error](https://github.com/unjs/nitro/blob/d20ffcbd16fc4003b774445e1a01e698c2bb078a/src/types/runtime/nitro.ts#L48), [event](https://github.com/unjs/h3/blob/f6ceb5581043dc4d8b6eab91e9be4531e0c30f8e/src/types.ts#L38) `request` | `event` | Called when a request is received. | [event](https://github.com/unjs/h3/blob/f6ceb5581043dc4d8b6eab91e9be4531e0c30f8e/src/types.ts#L38) `beforeResponse` | `event, { body }` | Called before sending the response. | [event](https://github.com/unjs/h3/blob/f6ceb5581043dc4d8b6eab91e9be4531e0c30f8e/src/types.ts#L38), unknown `afterResponse` | `event, { body }` | Called after sending the response. | [event](https://github.com/unjs/h3/blob/f6ceb5581043dc4d8b6eab91e9be4531e0c30f8e/src/types.ts#L38), unknown