feat(nuxt): allow configuring treeshakeable composables (#19383)

This commit is contained in:
Harlan Wilton 2023-03-07 20:30:05 +11:00 committed by GitHub
parent 3a73f42d1c
commit bb61496e98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 119 additions and 19 deletions

View File

@ -16,7 +16,8 @@ import { distDir, pkgDir } from '../dirs'
import { version } from '../../package.json' import { version } from '../../package.json'
import { ImportProtectionPlugin, vueAppPatterns } from './plugins/import-protection' import { ImportProtectionPlugin, vueAppPatterns } from './plugins/import-protection'
import { UnctxTransformPlugin } from './plugins/unctx' 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 { DevOnlyPlugin } from './plugins/dev-only'
import { addModuleTranspiles } from './modules' import { addModuleTranspiles } from './modules'
import { initNitro } from './nitro' import { initNitro } from './nitro'
@ -79,22 +80,31 @@ async function initNuxt (nuxt: Nuxt) {
addVitePlugin(ImportProtectionPlugin.vite(config)) addVitePlugin(ImportProtectionPlugin.vite(config))
addWebpackPlugin(ImportProtectionPlugin.webpack(config)) addWebpackPlugin(ImportProtectionPlugin.webpack(config))
// Add unctx transform
nuxt.hook('modules:done', () => { nuxt.hook('modules:done', () => {
// Add unctx transform
addVitePlugin(UnctxTransformPlugin(nuxt).vite({ sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client })) 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 })) 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) { 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 // DevOnly component tree-shaking - build time only
addVitePlugin(DevOnlyPlugin.vite({ sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client })) addVitePlugin(DevOnlyPlugin.vite({ sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client }))
addWebpackPlugin(DevOnlyPlugin.webpack({ sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client })) addWebpackPlugin(DevOnlyPlugin.webpack({ sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client }))

View File

@ -4,16 +4,22 @@ import { parseQuery, parseURL } from 'ufo'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { createUnplugin } from 'unplugin' import { createUnplugin } from 'unplugin'
interface TreeShakePluginOptions { type ImportPath = string
export interface TreeShakeComposablesPluginOptions {
sourcemap?: boolean sourcemap?: boolean
treeShake: string[] composables: Record<ImportPath, string[]>
} }
export const TreeShakePlugin = createUnplugin((options: TreeShakePluginOptions) => { export const TreeShakeComposablesPlugin = createUnplugin((options: TreeShakeComposablesPluginOptions) => {
const COMPOSABLE_RE = new RegExp(`($\\s+)(${options.treeShake.join('|')})(?=\\()`, 'gm') /**
* @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 { return {
name: 'nuxt:server-treeshake:transform', name: 'nuxt:tree-shake-composables:transform',
enforce: 'post', enforce: 'post',
transformInclude (id) { transformInclude (id) {
const { pathname, search } = parseURL(decodeURIComponent(pathToFileURL(id).href)) const { pathname, search } = parseURL(decodeURIComponent(pathToFileURL(id).href))

View File

@ -51,7 +51,7 @@ export default defineUntypedSchema({
* *
* @example * @example
* ```js * ```js
transpile: [({ isLegacy }) => isLegacy && 'ky'] transpile: [({ isLegacy }) => isLegacy && 'ky']
* ``` * ```
* @type {Array<string | RegExp | ((ctx: { isClient?: boolean; isServer?: boolean; isDev: boolean }) => string | RegExp | false)>} * @type {Array<string | RegExp | ((ctx: { isClient?: boolean; isServer?: boolean; isDev: boolean }) => string | RegExp | false)>}
*/ */
@ -81,7 +81,7 @@ export default defineUntypedSchema({
*/ */
templates: [], templates: [],
/** /**
* Nuxt uses `webpack-bundle-analyzer` to visualize your bundles and how to optimize them. * 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). * 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} * @type {boolean | typeof import('webpack-bundle-analyzer').BundleAnalyzerPlugin.Options | typeof import('rollup-plugin-visualizer').PluginVisualizerOptions}
* *
*/ */
analyze: { analyze: {
$resolve: async (val, get) => { $resolve: async (val, get) => {
if (val !== true) { if (val !== true) {
return val ?? false 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'],
}
)
}
}
},
} }
}) })

View File

@ -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', () => { describe('server tree shaking', () => {
it('should work', async () => { it('should work', async () => {
const html = await $fetch('/client') const html = await $fetch('/client')

View File

@ -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'
}

View File

@ -110,6 +110,10 @@ export default defineNuxtConfig({
const internalParent = pages.find(page => page.path === '/internal-layout') const internalParent = pages.find(page => page.path === '/internal-layout')
internalParent!.children = newPages 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: { vite: {

View File

@ -0,0 +1,13 @@
<script lang="ts" setup>
// server only
useServerOnlyComposable()
// client only
useClientOnlyComposable()
// can only run client side, should be tree shaken from server build
setTitleToPink()
</script>
<template>
<div>
<h1>Tree Shake Example</h1>
</div>
</template>