From 12ab7a786a37ec18da6df37739859d68818e2fdd Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 6 Jul 2022 14:48:05 +0100 Subject: [PATCH 1/5] feat(nuxt): add plugin to tree-shake composables --- packages/nuxt/package.json | 1 + packages/nuxt/src/core/nuxt.ts | 28 ++++++++++++ packages/nuxt/src/core/plugins/tree-shake.ts | 48 ++++++++++++++++++++ yarn.lock | 1 + 4 files changed, 78 insertions(+) create mode 100644 packages/nuxt/src/core/plugins/tree-shake.ts diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index b80e7d7b863..63eff52a780 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -59,6 +59,7 @@ "pathe": "^0.3.2", "perfect-debounce": "^0.1.3", "scule": "^0.2.1", + "strip-literal": "^0.4.0", "ufo": "^0.8.4", "unctx": "^1.1.4", "unenv": "^0.5.2", diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index a8b4dffb298..cbe7f2b15a3 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -14,6 +14,7 @@ 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 { addModuleTranspiles } from './modules' import { initNitro } from './nitro' @@ -67,6 +68,33 @@ async function initNuxt (nuxt: Nuxt) { addVitePlugin(UnctxTransformPlugin(nuxt).vite({ sourcemap: nuxt.options.sourcemap })) addWebpackPlugin(UnctxTransformPlugin(nuxt).webpack({ sourcemap: nuxt.options.sourcemap })) + const getTreeshakeOptions = (isServer: boolean) => ({ + sourcemap: nuxt.options.sourcemap, + treeShake: { + onBeforeMount: isServer, + onMounted: isServer, + onBeforeUpdate: isServer, + onRenderTracked: isServer /* || !nuxt.options.dev */, + onRenderTriggered: isServer /* || !nuxt.options.dev */, + onActivated: isServer, + onDeactivated: isServer, + onBeforeUnmount: isServer, + onServerPrefetch: !isServer + } + }) + + if (!nuxt.options.dev) { + // Add tree-shaking optimisations for SSR - build time only + nuxt.hook('vite:extendConfig', (config, { isServer }) => { + config.plugins.push(TreeShakePlugin.vite(getTreeshakeOptions(isServer))) + }) + nuxt.hook('webpack:config', (configs) => { + for (const config of configs) { + config.plugins.push(TreeShakePlugin.webpack(getTreeshakeOptions(config.name === 'server'))) + } + }) + } + // Transpile layers within node_modules nuxt.options.build.transpile.push( ...nuxt.options._layers.filter(i => i.cwd && i.cwd.includes('node_modules')).map(i => i.cwd) diff --git a/packages/nuxt/src/core/plugins/tree-shake.ts b/packages/nuxt/src/core/plugins/tree-shake.ts new file mode 100644 index 00000000000..f90d2d84517 --- /dev/null +++ b/packages/nuxt/src/core/plugins/tree-shake.ts @@ -0,0 +1,48 @@ +import { pathToFileURL } from 'node:url' +import { stripLiteral } from 'strip-literal' +import { parseQuery, parseURL } from 'ufo' +import MagicString from 'magic-string' +import { createUnplugin } from 'unplugin' + +interface TreeShakePluginOptions { + sourcemap?: boolean + treeShake: Record +} + +export const TreeShakePlugin = createUnplugin((options: TreeShakePluginOptions) => { + const fnsToShake = Object.entries(options.treeShake).filter(([, value]) => value).map(([key]) => key) + const COMPOSABLE_RE = new RegExp(`($|\\s*)(${fnsToShake.join('|')})(?=\\()`, 'g') + + return { + name: 'nuxt:server-treeshake:transfrom', + enforce: 'post', + transformInclude (id) { + const { pathname, search } = parseURL(decodeURIComponent(pathToFileURL(id).href)) + const { type, macro } = parseQuery(search) + + // vue files + if (pathname.endsWith('.vue') && (type === 'script' || macro || !search)) { + return true + } + + // js files + if (pathname.match(/\.((c|m)?j|t)sx?$/g)) { + return true + } + }, + transform (code, id) { + const s = new MagicString(code) + const strippedCode = stripLiteral(code) + for (const match of strippedCode.matchAll(COMPOSABLE_RE) || []) { + s.overwrite(match.index, match.index + match[0].length, `/*#__PURE__*/ false && ${match[0]}`) + } + + if (s.hasChanged()) { + return { + code: s.toString(), + map: options.sourcemap && s.generateMap({ source: id, includeContent: true }) + } + } + } + } +}) diff --git a/yarn.lock b/yarn.lock index 7f6ac914ee0..ffffd504d50 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9881,6 +9881,7 @@ __metadata: pathe: ^0.3.2 perfect-debounce: ^0.1.3 scule: ^0.2.1 + strip-literal: ^0.4.0 ufo: ^0.8.4 unbuild: latest unctx: ^1.1.4 From fa303f1c3b91a8344517ae3fc64f8351e90d687b Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 6 Jul 2022 14:53:58 +0100 Subject: [PATCH 2/5] test: add test suite --- test/basic.test.ts | 8 ++++++++ test/fixtures/basic/components/BreaksServer.ts | 5 +++++ test/fixtures/basic/pages/client.vue | 16 ++++++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 test/fixtures/basic/components/BreaksServer.ts create mode 100644 test/fixtures/basic/pages/client.vue diff --git a/test/basic.test.ts b/test/basic.test.ts index ea4ce3a0ca0..24213c4c75b 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -244,6 +244,14 @@ describe('reactivity transform', () => { }) }) +describe('server tree shaking', () => { + it('should work', async () => { + const html = await $fetch('/client') + + expect(html).toContain('This page should not crash when rendered') + }) +}) + describe('extends support', () => { describe('layouts & pages', () => { it('extends foo/layouts/default & foo/pages/index', async () => { diff --git a/test/fixtures/basic/components/BreaksServer.ts b/test/fixtures/basic/components/BreaksServer.ts new file mode 100644 index 00000000000..f98006dcc3c --- /dev/null +++ b/test/fixtures/basic/components/BreaksServer.ts @@ -0,0 +1,5 @@ +window.test = true + +export default () => ({ + render: () => 'hi' +}) diff --git a/test/fixtures/basic/pages/client.vue b/test/fixtures/basic/pages/client.vue new file mode 100644 index 00000000000..457e6c0e7d3 --- /dev/null +++ b/test/fixtures/basic/pages/client.vue @@ -0,0 +1,16 @@ + + + From e62fb98e1e5625cb34b62bd4be6beb4def8903d8 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 6 Jul 2022 14:59:40 +0100 Subject: [PATCH 3/5] test: ignore type error --- test/fixtures/basic/components/BreaksServer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/fixtures/basic/components/BreaksServer.ts b/test/fixtures/basic/components/BreaksServer.ts index f98006dcc3c..b3c50e01d64 100644 --- a/test/fixtures/basic/components/BreaksServer.ts +++ b/test/fixtures/basic/components/BreaksServer.ts @@ -1,3 +1,4 @@ +// @ts-ignore window.test = true export default () => ({ From 0182613a4c19127c855ad5bc1a8a1f6e71f11bed Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 6 Jul 2022 16:26:36 +0100 Subject: [PATCH 4/5] refactor: use `addVitePlugin` and `addWebpackPlugin` --- packages/nuxt/src/core/nuxt.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index cbe7f2b15a3..a88eed0bbfc 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -85,14 +85,10 @@ async function initNuxt (nuxt: Nuxt) { if (!nuxt.options.dev) { // Add tree-shaking optimisations for SSR - build time only - nuxt.hook('vite:extendConfig', (config, { isServer }) => { - config.plugins.push(TreeShakePlugin.vite(getTreeshakeOptions(isServer))) - }) - nuxt.hook('webpack:config', (configs) => { - for (const config of configs) { - config.plugins.push(TreeShakePlugin.webpack(getTreeshakeOptions(config.name === 'server'))) - } - }) + addVitePlugin(TreeShakePlugin.vite(getTreeshakeOptions(true)), { client: false }) + addVitePlugin(TreeShakePlugin.vite(getTreeshakeOptions(false)), { server: false }) + addWebpackPlugin(TreeShakePlugin.webpack(getTreeshakeOptions(true)), { client: false }) + addWebpackPlugin(TreeShakePlugin.webpack(getTreeshakeOptions(false)), { server: false }) } // Transpile layers within node_modules From 37d6823fd6740007a1797457f8ca3d882cddb120 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 7 Jul 2022 12:04:05 +0100 Subject: [PATCH 5/5] fix: return no-op, perf improvements, and preset list --- packages/nuxt/src/core/nuxt.ts | 26 ++++++-------------- packages/nuxt/src/core/plugins/tree-shake.ts | 9 ++++--- 2 files changed, 12 insertions(+), 23 deletions(-) diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index a88eed0bbfc..55b308230c7 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -68,27 +68,15 @@ async function initNuxt (nuxt: Nuxt) { addVitePlugin(UnctxTransformPlugin(nuxt).vite({ sourcemap: nuxt.options.sourcemap })) addWebpackPlugin(UnctxTransformPlugin(nuxt).webpack({ sourcemap: nuxt.options.sourcemap })) - const getTreeshakeOptions = (isServer: boolean) => ({ - sourcemap: nuxt.options.sourcemap, - treeShake: { - onBeforeMount: isServer, - onMounted: isServer, - onBeforeUpdate: isServer, - onRenderTracked: isServer /* || !nuxt.options.dev */, - onRenderTriggered: isServer /* || !nuxt.options.dev */, - onActivated: isServer, - onDeactivated: isServer, - onBeforeUnmount: isServer, - onServerPrefetch: !isServer - } - }) - 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(getTreeshakeOptions(true)), { client: false }) - addVitePlugin(TreeShakePlugin.vite(getTreeshakeOptions(false)), { server: false }) - addWebpackPlugin(TreeShakePlugin.webpack(getTreeshakeOptions(true)), { client: false }) - addWebpackPlugin(TreeShakePlugin.webpack(getTreeshakeOptions(false)), { server: false }) + addVitePlugin(TreeShakePlugin.vite({ sourcemap: nuxt.options.sourcemap, treeShake: removeFromServer }), { client: false }) + addVitePlugin(TreeShakePlugin.vite({ sourcemap: nuxt.options.sourcemap, treeShake: removeFromClient }), { server: false }) + addWebpackPlugin(TreeShakePlugin.webpack({ sourcemap: nuxt.options.sourcemap, treeShake: removeFromServer }), { client: false }) + addWebpackPlugin(TreeShakePlugin.webpack({ sourcemap: nuxt.options.sourcemap, treeShake: removeFromClient }), { server: false }) } // Transpile layers within node_modules diff --git a/packages/nuxt/src/core/plugins/tree-shake.ts b/packages/nuxt/src/core/plugins/tree-shake.ts index f90d2d84517..88080d3246e 100644 --- a/packages/nuxt/src/core/plugins/tree-shake.ts +++ b/packages/nuxt/src/core/plugins/tree-shake.ts @@ -6,12 +6,11 @@ import { createUnplugin } from 'unplugin' interface TreeShakePluginOptions { sourcemap?: boolean - treeShake: Record + treeShake: string[] } export const TreeShakePlugin = createUnplugin((options: TreeShakePluginOptions) => { - const fnsToShake = Object.entries(options.treeShake).filter(([, value]) => value).map(([key]) => key) - const COMPOSABLE_RE = new RegExp(`($|\\s*)(${fnsToShake.join('|')})(?=\\()`, 'g') + const COMPOSABLE_RE = new RegExp(`($|\\s*)(${options.treeShake.join('|')})(?=\\()`, 'g') return { name: 'nuxt:server-treeshake:transfrom', @@ -31,10 +30,12 @@ export const TreeShakePlugin = createUnplugin((options: TreeShakePluginOptions) } }, transform (code, id) { + if (!code.match(COMPOSABLE_RE)) { return } + const s = new MagicString(code) const strippedCode = stripLiteral(code) for (const match of strippedCode.matchAll(COMPOSABLE_RE) || []) { - s.overwrite(match.index, match.index + match[0].length, `/*#__PURE__*/ false && ${match[0]}`) + s.overwrite(match.index, match.index + match[0].length, `(() => {}) || /*#__PURE__*/ false && ${match[0]}`) } if (s.hasChanged()) {