From bb61496e987571303b0d0bd5e90f61aa7612ca52 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Tue, 7 Mar 2023 20:30:05 +1100 Subject: [PATCH] feat(nuxt): allow configuring treeshakeable composables (#19383) --- packages/nuxt/src/core/nuxt.ts | 32 ++++++++++----- packages/nuxt/src/core/plugins/tree-shake.ts | 16 +++++--- packages/schema/src/config/build.ts | 41 +++++++++++++++++-- test/basic.test.ts | 16 ++++++++ test/fixtures/basic/composables/tree-shake.ts | 16 ++++++++ test/fixtures/basic/nuxt.config.ts | 4 ++ test/fixtures/basic/pages/tree-shake.vue | 13 ++++++ 7 files changed, 119 insertions(+), 19 deletions(-) create mode 100644 test/fixtures/basic/composables/tree-shake.ts create mode 100644 test/fixtures/basic/pages/tree-shake.vue diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index 968d142338..d6e5e6f442 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -16,7 +16,8 @@ import { distDir, pkgDir } from '../dirs' import { version } from '../../package.json' import { ImportProtectionPlugin, vueAppPatterns } from './plugins/import-protection' import { UnctxTransformPlugin } from './plugins/unctx' -import { TreeShakePlugin } from './plugins/tree-shake' +import type { TreeShakeComposablesPluginOptions } from './plugins/tree-shake' +import { TreeShakeComposablesPlugin } from './plugins/tree-shake' import { DevOnlyPlugin } from './plugins/dev-only' import { addModuleTranspiles } from './modules' import { initNitro } from './nitro' @@ -79,22 +80,31 @@ async function initNuxt (nuxt: Nuxt) { addVitePlugin(ImportProtectionPlugin.vite(config)) addWebpackPlugin(ImportProtectionPlugin.webpack(config)) - // Add unctx transform nuxt.hook('modules:done', () => { + // Add unctx transform addVitePlugin(UnctxTransformPlugin(nuxt).vite({ sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client })) addWebpackPlugin(UnctxTransformPlugin(nuxt).webpack({ sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client })) + + // Add composable tree-shaking optimisations + const serverTreeShakeOptions : TreeShakeComposablesPluginOptions = { + sourcemap: nuxt.options.sourcemap.server, + composables: nuxt.options.optimization.treeShake.composables.server + } + if (Object.keys(serverTreeShakeOptions.composables).length) { + addVitePlugin(TreeShakeComposablesPlugin.vite(serverTreeShakeOptions), { client: false }) + addWebpackPlugin(TreeShakeComposablesPlugin.webpack(serverTreeShakeOptions), { client: false }) + } + const clientTreeShakeOptions : TreeShakeComposablesPluginOptions = { + sourcemap: nuxt.options.sourcemap.client, + composables: nuxt.options.optimization.treeShake.composables.client + } + if (Object.keys(clientTreeShakeOptions.composables).length) { + addVitePlugin(TreeShakeComposablesPlugin.vite(clientTreeShakeOptions), { server: false }) + addWebpackPlugin(TreeShakeComposablesPlugin.webpack(clientTreeShakeOptions), { server: false }) + } }) if (!nuxt.options.dev) { - const removeFromServer = ['onBeforeMount', 'onMounted', 'onBeforeUpdate', 'onRenderTracked', 'onRenderTriggered', 'onActivated', 'onDeactivated', 'onBeforeUnmount'] - const removeFromClient = ['onServerPrefetch', 'onRenderTracked', 'onRenderTriggered'] - - // Add tree-shaking optimisations for SSR - build time only - addVitePlugin(TreeShakePlugin.vite({ sourcemap: nuxt.options.sourcemap.server, treeShake: removeFromServer }), { client: false }) - addVitePlugin(TreeShakePlugin.vite({ sourcemap: nuxt.options.sourcemap.client, treeShake: removeFromClient }), { server: false }) - addWebpackPlugin(TreeShakePlugin.webpack({ sourcemap: nuxt.options.sourcemap.server, treeShake: removeFromServer }), { client: false }) - addWebpackPlugin(TreeShakePlugin.webpack({ sourcemap: nuxt.options.sourcemap.client, treeShake: removeFromClient }), { server: false }) - // DevOnly component tree-shaking - build time only addVitePlugin(DevOnlyPlugin.vite({ sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client })) addWebpackPlugin(DevOnlyPlugin.webpack({ sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client })) diff --git a/packages/nuxt/src/core/plugins/tree-shake.ts b/packages/nuxt/src/core/plugins/tree-shake.ts index 5d88a15bf3..2539480c37 100644 --- a/packages/nuxt/src/core/plugins/tree-shake.ts +++ b/packages/nuxt/src/core/plugins/tree-shake.ts @@ -4,16 +4,22 @@ import { parseQuery, parseURL } from 'ufo' import MagicString from 'magic-string' import { createUnplugin } from 'unplugin' -interface TreeShakePluginOptions { +type ImportPath = string + +export interface TreeShakeComposablesPluginOptions { sourcemap?: boolean - treeShake: string[] + composables: Record } -export const TreeShakePlugin = createUnplugin((options: TreeShakePluginOptions) => { - const COMPOSABLE_RE = new RegExp(`($\\s+)(${options.treeShake.join('|')})(?=\\()`, 'gm') +export const TreeShakeComposablesPlugin = createUnplugin((options: TreeShakeComposablesPluginOptions) => { + /** + * @todo Use the options import-path to tree-shake composables in a safer way. + */ + const composableNames = Object.values(options.composables).flat() + const COMPOSABLE_RE = new RegExp(`($\\s+)(${composableNames.join('|')})(?=\\()`, 'gm') return { - name: 'nuxt:server-treeshake:transform', + name: 'nuxt:tree-shake-composables:transform', enforce: 'post', transformInclude (id) { const { pathname, search } = parseURL(decodeURIComponent(pathToFileURL(id).href)) diff --git a/packages/schema/src/config/build.ts b/packages/schema/src/config/build.ts index fa178e7e9f..1517735f7e 100644 --- a/packages/schema/src/config/build.ts +++ b/packages/schema/src/config/build.ts @@ -51,7 +51,7 @@ export default defineUntypedSchema({ * * @example * ```js - transpile: [({ isLegacy }) => isLegacy && 'ky'] + transpile: [({ isLegacy }) => isLegacy && 'ky'] * ``` * @type {Array string | RegExp | false)>} */ @@ -81,7 +81,7 @@ export default defineUntypedSchema({ */ templates: [], - /** + /** * Nuxt uses `webpack-bundle-analyzer` to visualize your bundles and how to optimize them. * * Set to `true` to enable bundle analysis, or pass an object with options: [for webpack](https://github.com/webpack-contrib/webpack-bundle-analyzer#options-for-plugin) or [for vite](https://github.com/btd/rollup-plugin-visualizer#options). @@ -95,7 +95,7 @@ export default defineUntypedSchema({ * @type {boolean | typeof import('webpack-bundle-analyzer').BundleAnalyzerPlugin.Options | typeof import('rollup-plugin-visualizer').PluginVisualizerOptions} * */ - analyze: { + analyze: { $resolve: async (val, get) => { if (val !== true) { return val ?? false @@ -108,5 +108,40 @@ export default defineUntypedSchema({ } } }, + }, + + /** + * Build time optimization configuration. + */ + optimization: { + /** + * Tree shake code from specific builds. + */ + treeShake: { + /** + * Tree shake composables from the server or client builds. + * + * @example + * ```js + * treeShake: { client: { myPackage: ['useServerOnlyComposable'] } } + * ``` + */ + composables: { + server: { + $resolve: async (val, get) => defu(val || {}, + await get('dev') ? {} : { + vue: ['onBeforeMount', 'onMounted', 'onBeforeUpdate', 'onRenderTracked', 'onRenderTriggered', 'onActivated', 'onDeactivated', 'onBeforeUnmount'], + } + ) + }, + client: { + $resolve: async (val, get) => defu(val || {}, + await get('dev') ? {} : { + vue: ['onServerPrefetch', 'onRenderTracked', 'onRenderTriggered'], + } + ) + } + } + }, } }) diff --git a/test/basic.test.ts b/test/basic.test.ts index 32e6b34721..47d2143160 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -560,6 +560,22 @@ describe('reactivity transform', () => { }) }) +describe('composable tree shaking', () => { + it('should work', async () => { + const html = await $fetch('/tree-shake') + + expect(html).toContain('Tree Shake Example') + + const page = await createPage('/tree-shake') + // check page doesn't have any errors or warnings in the console + await page.waitForLoadState('networkidle') + // ensure scoped classes are correctly assigned between client and server + expect(await page.$eval('h1', e => getComputedStyle(e).color)).toBe('rgb(255, 192, 203)') + + await expectNoClientErrors('/tree-shake') + }) +}) + describe('server tree shaking', () => { it('should work', async () => { const html = await $fetch('/client') diff --git a/test/fixtures/basic/composables/tree-shake.ts b/test/fixtures/basic/composables/tree-shake.ts new file mode 100644 index 0000000000..f050457f95 --- /dev/null +++ b/test/fixtures/basic/composables/tree-shake.ts @@ -0,0 +1,16 @@ +export function useServerOnlyComposable () { + if (process.client) { + throw new Error('this should not be called in the browser') + } +} + +export function useClientOnlyComposable () { + // need to do some code that fails in node but not in the browser + if (process.server) { + throw new Error('this should not be called on the server') + } +} + +export function setTitleToPink () { + document.querySelector('h1')!.style.color = 'pink' +} diff --git a/test/fixtures/basic/nuxt.config.ts b/test/fixtures/basic/nuxt.config.ts index f09079b074..9a80ba02c9 100644 --- a/test/fixtures/basic/nuxt.config.ts +++ b/test/fixtures/basic/nuxt.config.ts @@ -110,6 +110,10 @@ export default defineNuxtConfig({ const internalParent = pages.find(page => page.path === '/internal-layout') internalParent!.children = newPages }) + }, + function (_, nuxt) { + nuxt.options.optimization.treeShake.composables.server[nuxt.options.rootDir] = ['useClientOnlyComposable', 'setTitleToPink'] + nuxt.options.optimization.treeShake.composables.client[nuxt.options.rootDir] = ['useServerOnlyComposable'] } ], vite: { diff --git a/test/fixtures/basic/pages/tree-shake.vue b/test/fixtures/basic/pages/tree-shake.vue new file mode 100644 index 0000000000..bfec5c9d59 --- /dev/null +++ b/test/fixtures/basic/pages/tree-shake.vue @@ -0,0 +1,13 @@ + +