From ade3378a00c7c748310768d0ae4e0578c2fcb071 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 1 Apr 2022 14:22:22 +0100 Subject: [PATCH] refactor(bridge): align bridge with vite and inline systemjs polyfill in entry (#4005) --- packages/bridge/src/vite/client.ts | 21 +++--- packages/bridge/src/vite/manifest.ts | 11 ++- packages/bridge/src/vite/module.ts | 9 ++- packages/bridge/src/vite/server.ts | 15 +++- packages/bridge/src/vite/vite.ts | 85 ++++++++++++++++------- packages/vite/src/client.ts | 3 +- packages/vite/src/plugins/dynamic-base.ts | 13 +++- packages/vite/src/server.ts | 2 - packages/vite/src/vite.ts | 5 +- packages/webpack/src/webpack.ts | 2 - test/bridge.test.ts | 58 +++++++++++++++- test/fixtures/bridge/assets/logo.svg | 18 +++++ test/fixtures/bridge/pages/assets.vue | 20 ++++++ test/fixtures/bridge/static/public.svg | 18 +++++ 14 files changed, 227 insertions(+), 53 deletions(-) create mode 100644 test/fixtures/bridge/assets/logo.svg create mode 100644 test/fixtures/bridge/pages/assets.vue create mode 100644 test/fixtures/bridge/static/public.svg diff --git a/packages/bridge/src/vite/client.ts b/packages/bridge/src/vite/client.ts index 4e2d377c9f..1fe6b0014b 100644 --- a/packages/bridge/src/vite/client.ts +++ b/packages/bridge/src/vite/client.ts @@ -5,11 +5,14 @@ import PluginLegacy from '@vitejs/plugin-legacy' import { logger } from '@nuxt/kit' import { joinURL } from 'ufo' import { devStyleSSRPlugin } from '../../../vite/src/plugins/dev-ssr-css' +import { RelativeAssetPlugin } from '../../../vite/src/plugins/dynamic-base' import { jsxPlugin } from './plugins/jsx' import { ViteBuildContext, ViteOptions } from './types' export async function buildClient (ctx: ViteBuildContext) { - const alias = {} + const alias = { + '#_config': resolve(ctx.nuxt.options.buildDir, 'config.client.mjs') + } for (const p of ctx.builder.plugins) { alias[p.name] = p.mode === 'server' ? `defaultexport:${resolve(ctx.nuxt.options.buildDir, 'empty.js')}` @@ -18,27 +21,27 @@ export async function buildClient (ctx: ViteBuildContext) { const clientConfig: vite.InlineConfig = vite.mergeConfig(ctx.config, { define: { - 'process.client': 'true', - 'process.server': 'false', - 'process.static': 'false', - 'module.hot': 'false' + 'process.client': true, + 'process.server': false, + 'process.static': false, + 'module.hot': false }, cacheDir: resolve(ctx.nuxt.options.rootDir, 'node_modules/.cache/vite/client'), resolve: { alias }, build: { - outDir: resolve(ctx.nuxt.options.buildDir, 'dist/client'), rollupOptions: { input: resolve(ctx.nuxt.options.buildDir, 'client.js') }, manifest: true, - ssrManifest: true + outDir: resolve(ctx.nuxt.options.buildDir, 'dist/client') }, plugins: [ jsxPlugin(), createVuePlugin(ctx.config.vue), PluginLegacy(), + RelativeAssetPlugin(), devStyleSSRPlugin({ rootDir: ctx.nuxt.options.rootDir, buildAssetsURL: joinURL(ctx.nuxt.options.app.baseURL, ctx.nuxt.options.app.buildAssetsDir) @@ -67,10 +70,6 @@ export async function buildClient (ctx: ViteBuildContext) { const viteMiddleware = (req, res, next) => { // Workaround: vite devmiddleware modifies req.url const originalURL = req.url - req.url = req.url.replace('/_nuxt/', '/.nuxt/') - if (req.url.includes('@vite/client')) { - req.url = '/@vite/client' - } viteServer.middlewares.handle(req, res, (err) => { req.url = originalURL next(err) diff --git a/packages/bridge/src/vite/manifest.ts b/packages/bridge/src/vite/manifest.ts index 4ea604ffce..e7eaef1b36 100644 --- a/packages/bridge/src/vite/manifest.ts +++ b/packages/bridge/src/vite/manifest.ts @@ -56,22 +56,27 @@ export async function generateBuildManifest (ctx: ViteBuildContext) { // Search for polyfill file, we don't include it in the client entry const polyfillName = initialEntries.find(id => id.startsWith('polyfills-legacy.')) + const polyfill = await fse.readFile(rDist('client/' + polyfillName), 'utf-8') // @vitejs/plugin-legacy uses SystemJS which need to call `System.import` to load modules const clientImports = initialJs.filter(id => id !== polyfillName) - const clientEntryCode = `var imports = ${JSON.stringify(clientImports)}\nimports.reduce((p, id) => p.then(() => System.import(id)), Promise.resolve())` + const clientEntryCode = [ + polyfill, + 'var appConfig = window?.__NUXT__?.config.app || {}', + 'var publicBase = appConfig.cdnURL || ("." + appConfig.baseURL)', + `var imports = ${JSON.stringify(clientImports)};`, + 'imports.reduce((p, id) => p.then(() => System.import(publicBase + appConfig.buildAssetsDir.slice(1) + id)), Promise.resolve())' + ].join('\n') const clientEntryName = 'entry-legacy.' + hash(clientEntryCode) + '.js' const clientManifest = { // This publicPath will be ignored by Nitro and computed dynamically publicPath: ctx.nuxt.options.app.buildAssetsDir, all: uniq([ - polyfillName, clientEntryName, ...clientEntries.flatMap(getModuleIds) ]).filter(Boolean), initial: [ - polyfillName, clientEntryName, ...initialAssets ].filter(Boolean), diff --git a/packages/bridge/src/vite/module.ts b/packages/bridge/src/vite/module.ts index 30134d9f4d..4c347da90f 100644 --- a/packages/bridge/src/vite/module.ts +++ b/packages/bridge/src/vite/module.ts @@ -1,4 +1,5 @@ -import { logger, addPluginTemplate, defineNuxtModule } from '@nuxt/kit' +import { logger, addPluginTemplate, defineNuxtModule, addTemplate } from '@nuxt/kit' +import { publicPathTemplate, clientConfigTemplate } from '../../../nuxt3/src/core/templates' import { version } from '../../package.json' import { middlewareTemplate, storeTemplate } from './templates' import type { ViteOptions } from './types' @@ -43,6 +44,12 @@ export default defineNuxtModule({ } addPluginTemplate(middlewareTemplate) + addTemplate(clientConfigTemplate) + addTemplate({ + ...publicPathTemplate, + options: { nuxt } + }) + nuxt.hook('builder:prepared', async (builder) => { if (nuxt.options._prepare) { return } builder.bundleBuilder.close() diff --git a/packages/bridge/src/vite/server.ts b/packages/bridge/src/vite/server.ts index 4017069f1b..32fb40a3ae 100644 --- a/packages/bridge/src/vite/server.ts +++ b/packages/bridge/src/vite/server.ts @@ -45,7 +45,11 @@ export async function buildServer (ctx: ViteBuildContext) { 'axios' ], noExternal: [ + // TODO: Use externality for production (rollup) build + /\/esm\/.*\.js$/, /\.(es|esm|esm-browser|esm-bundler).js$/, + '#app', + /@nuxt\/nitro\/(dist|src)/, ...ctx.nuxt.options.build.transpile.filter(i => typeof i === 'string') ] }, @@ -54,11 +58,14 @@ export async function buildServer (ctx: ViteBuildContext) { ssr: ctx.nuxt.options.ssr ?? true, ssrManifest: true, rollupOptions: { + // Private nitro alias: packages/nitro/src/rollup/config.ts#L234 + external: ['#_config'], input: resolve(ctx.nuxt.options.buildDir, 'server.js'), output: { - format: 'esm', entryFileNames: 'server.mjs', - chunkFileNames: 'chunks/[name].mjs' + chunkFileNames: 'chunks/[name].mjs', + preferConst: true, + format: 'module' }, onwarn (warning, rollupWarn) { if (!['UNUSED_EXTERNAL_IMPORT'].includes(warning.code)) { @@ -67,6 +74,10 @@ export async function buildServer (ctx: ViteBuildContext) { } } }, + server: { + // https://github.com/vitest-dev/vitest/issues/229#issuecomment-1002685027 + preTransformRequests: false + }, plugins: [ jsxPlugin(), vuePlugin diff --git a/packages/bridge/src/vite/vite.ts b/packages/bridge/src/vite/vite.ts index 1ec626dc30..464fdbeefb 100644 --- a/packages/bridge/src/vite/vite.ts +++ b/packages/bridge/src/vite/vite.ts @@ -1,9 +1,12 @@ import { resolve } from 'pathe' import * as vite from 'vite' -import { logger } from '@nuxt/kit' -import { withoutLeadingSlash } from 'ufo' +import { isIgnored, logger } from '@nuxt/kit' +import { sanitizeFilePath } from 'mlly' +import { getPort } from 'get-port-please' +import { joinURL, withoutLeadingSlash } from 'ufo' import { distDir } from '../dirs' import { warmupViteServer } from '../../../vite/src/utils/warmup' +import { DynamicBasePlugin } from '../../../vite/src/plugins/dynamic-base' import { buildClient } from './client' import { buildServer } from './server' import { defaultExportPlugin } from './plugins/default-export' @@ -18,20 +21,43 @@ async function bundle (nuxt: Nuxt, builder: any) { p.src = nuxt.resolver.resolvePath(resolve(nuxt.options.buildDir, p.src)) } + const hmrPortDefault = 24678 // Vite's default HMR port + const hmrPort = await getPort({ + port: hmrPortDefault, + ports: Array.from({ length: 20 }, (_, i) => hmrPortDefault + 1 + i) + }) + const ctx: ViteBuildContext = { nuxt, builder, config: vite.mergeConfig( { - root: nuxt.options.rootDir, + // defaults from packages/schema/src/config/vite + root: nuxt.options.srcDir, mode: nuxt.options.dev ? 'development' : 'production', logLevel: 'warn', + base: nuxt.options.dev + ? joinURL(nuxt.options.app.baseURL, nuxt.options.app.buildAssetsDir) + : '/__NUXT_BASE__/', + publicDir: resolve(nuxt.options.rootDir, nuxt.options.srcDir, nuxt.options.dir.static), + vue: { + isProduction: !nuxt.options.dev, + template: { + compilerOptions: nuxt.options.vue.compilerOptions + } + }, + esbuild: { + jsxFactory: 'h', + jsxFragment: 'Fragment', + tsconfigRaw: '{}' + }, + clearScreen: false, define: { + 'process.dev': nuxt.options.dev, 'process.static': nuxt.options.target === 'static', 'process.env.NODE_ENV': JSON.stringify(nuxt.options.dev ? 'development' : 'production'), 'process.mode': JSON.stringify(nuxt.options.dev ? 'development' : 'production'), - 'process.target': JSON.stringify(nuxt.options.target), - 'process.dev': nuxt.options.dev + 'process.target': JSON.stringify(nuxt.options.target) }, resolve: { extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'], @@ -39,22 +65,17 @@ async function bundle (nuxt: Nuxt, builder: any) { ...nuxt.options.alias, '#build': nuxt.options.buildDir, '.nuxt': nuxt.options.buildDir, - '/.nuxt/entry.mjs': resolve(nuxt.options.buildDir, 'client.js'), + '/entry.mjs': resolve(nuxt.options.buildDir, 'client.js'), 'web-streams-polyfill/ponyfill/es2018': resolve(distDir, 'runtime/vite/mock/web-streams-polyfill.mjs'), 'whatwg-url': resolve(distDir, 'runtime/vite/mock/whatwg-url.mjs'), // Cannot destructure property 'AbortController' of .. 'abort-controller': resolve(distDir, 'runtime/vite/mock/abort-controller.mjs') } }, - vue: {}, - server: { - fs: { - strict: false - } - }, - css: resolveCSSOptions(nuxt), optimizeDeps: { exclude: [ + ...nuxt.options.build.transpile.filter(i => typeof i === 'string'), + 'vue-demi', 'ufo', 'date-fns', 'nanoid', @@ -64,26 +85,42 @@ async function bundle (nuxt: Nuxt, builder: any) { // 'vue-demi' ] }, - esbuild: { - jsxFactory: 'h', - jsxFragment: 'Fragment', - tsconfigRaw: '{}' - }, - publicDir: resolve(nuxt.options.srcDir, nuxt.options.dir.static), - clearScreen: false, + css: resolveCSSOptions(nuxt), build: { - assetsDir: withoutLeadingSlash(nuxt.options.app.buildAssetsDir), - emptyOutDir: false + assetsDir: nuxt.options.dev ? withoutLeadingSlash(nuxt.options.app.buildAssetsDir) : '.', + emptyOutDir: false, + rollupOptions: { + output: { sanitizeFileName: sanitizeFilePath } + } }, plugins: [ replace({ __webpack_public_path__: 'globalThis.__webpack_public_path__' }), jsxPlugin(), + DynamicBasePlugin.vite(), defaultExportPlugin() - ] + ], + server: { + watch: { + ignored: isIgnored + }, + hmr: { + clientPort: hmrPort, + port: hmrPort + }, + fs: { + strict: false, + allow: [ + nuxt.options.buildDir, + nuxt.options.srcDir, + nuxt.options.rootDir, + ...nuxt.options.modulesDir + ] + } + } } as ViteOptions, - nuxt.options.vite || {} + nuxt.options.vite ) } diff --git a/packages/vite/src/client.ts b/packages/vite/src/client.ts index 62ef9ccbf7..f756868601 100644 --- a/packages/vite/src/client.ts +++ b/packages/vite/src/client.ts @@ -11,7 +11,7 @@ import { wpfs } from './utils/wpfs' import type { ViteBuildContext, ViteOptions } from './vite' import { writeManifest } from './manifest' import { devStyleSSRPlugin } from './plugins/dev-ssr-css' -import { DynamicBasePlugin, RelativeAssetPlugin } from './plugins/dynamic-base' +import { RelativeAssetPlugin } from './plugins/dynamic-base' import { viteNodePlugin } from './vite-node' export async function buildClient (ctx: ViteBuildContext) { @@ -41,7 +41,6 @@ export async function buildClient (ctx: ViteBuildContext) { cacheDirPlugin(ctx.nuxt.options.rootDir, 'client'), vuePlugin(ctx.config.vue), viteJsxPlugin(), - DynamicBasePlugin.vite({ env: 'client', devAppConfig: ctx.nuxt.options.app }), RelativeAssetPlugin(), devStyleSSRPlugin({ rootDir: ctx.nuxt.options.rootDir, diff --git a/packages/vite/src/plugins/dynamic-base.ts b/packages/vite/src/plugins/dynamic-base.ts index 80b4ab0271..1d6008ad02 100644 --- a/packages/vite/src/plugins/dynamic-base.ts +++ b/packages/vite/src/plugins/dynamic-base.ts @@ -4,8 +4,6 @@ import type { Plugin } from 'vite' import MagicString from 'magic-string' interface DynamicBasePluginOptions { - env: 'dev' | 'server' | 'client' - devAppConfig?: Record globalPublicPath?: string } @@ -18,6 +16,15 @@ export const RelativeAssetPlugin = function (): Plugin { for (const file in bundle) { const asset = bundle[file] + if (asset.fileName.includes('legacy') && asset.type === 'chunk' && asset.code.includes('innerHTML')) { + for (const delimiter of ['`', '"', "'"]) { + asset.code = asset.code.replace( + new RegExp(`(?<=innerHTML=)${delimiter}([^${delimiter}]*)\\/__NUXT_BASE__\\/([^${delimiter}]*)${delimiter}`, 'g'), + /* eslint-disable-next-line no-template-curly-in-string */ + '`$1${(window?.__NUXT__?.config.app.cdnURL || window?.__NUXT__?.config.app.baseURL) + window?.__NUXT__?.config.app.buildAssetsDir.slice(1)}$2`' + ) + } + } if (asset.type === 'asset' && typeof asset.source === 'string' && asset.fileName.endsWith('.css')) { const depth = file.split('/').length - 1 const assetBase = depth === 0 ? '.' : Array.from({ length: depth }).map(() => '..').join('/') @@ -33,7 +40,7 @@ export const RelativeAssetPlugin = function (): Plugin { const VITE_ASSET_RE = /^export default ["'](__VITE_ASSET.*)["']$/ -export const DynamicBasePlugin = createUnplugin(function (options: DynamicBasePluginOptions) { +export const DynamicBasePlugin = createUnplugin(function (options: DynamicBasePluginOptions = {}) { return { name: 'nuxt:dynamic-base-path', resolveId (id) { diff --git a/packages/vite/src/server.ts b/packages/vite/src/server.ts index 539e1b6c13..e606c74bfd 100644 --- a/packages/vite/src/server.ts +++ b/packages/vite/src/server.ts @@ -10,7 +10,6 @@ import { ViteBuildContext, ViteOptions } from './vite' import { wpfs } from './utils/wpfs' import { cacheDirPlugin } from './plugins/cache-dir' import { prepareDevServerEntry } from './vite-node' -import { DynamicBasePlugin } from './plugins/dynamic-base' import { isCSS, isDirectory, readDirRecursively } from './utils' import { bundleRequest } from './dev-bundler' import { writeManifest } from './manifest' @@ -76,7 +75,6 @@ export async function buildServer (ctx: ViteBuildContext) { plugins: [ cacheDirPlugin(ctx.nuxt.options.rootDir, 'server'), vuePlugin(ctx.config.vue), - DynamicBasePlugin.vite({ env: ctx.nuxt.options.dev ? 'dev' : 'server', devAppConfig: ctx.nuxt.options.app }), viteJsxPlugin() ] } as ViteOptions) diff --git a/packages/vite/src/vite.ts b/packages/vite/src/vite.ts index 3c89ddedfc..08ae5f8849 100644 --- a/packages/vite/src/vite.ts +++ b/packages/vite/src/vite.ts @@ -9,6 +9,7 @@ import { getPort } from 'get-port-please' import { buildClient } from './client' import { buildServer } from './server' import virtual from './plugins/virtual' +import { DynamicBasePlugin } from './plugins/dynamic-base' import { warmupViteServer } from './utils/warmup' import { resolveCSSOptions } from './css' @@ -25,7 +26,6 @@ export interface ViteBuildContext { } export async function bundle (nuxt: Nuxt) { - // TODO: After nitropack refactor, try if we can resuse the same server port as Nuxt const hmrPortDefault = 24678 // Vite's default HMR port const hmrPort = await getPort({ port: hmrPortDefault, @@ -64,7 +64,8 @@ export async function bundle (nuxt: Nuxt) { } }, plugins: [ - virtual(nuxt.vfs) + virtual(nuxt.vfs), + DynamicBasePlugin.vite() ], vue: { reactivityTransform: nuxt.options.experimental.reactivityTransform diff --git a/packages/webpack/src/webpack.ts b/packages/webpack/src/webpack.ts index 01de8b5259..cf25c4f329 100644 --- a/packages/webpack/src/webpack.ts +++ b/packages/webpack/src/webpack.ts @@ -34,8 +34,6 @@ export async function bundle (nuxt: Nuxt) { // Configure compilers const compilers = webpackConfigs.map((config) => { config.plugins.push(DynamicBasePlugin.webpack({ - env: nuxt.options.dev ? 'dev' : config.name as 'client', - devAppConfig: nuxt.options.app, globalPublicPath: '__webpack_public_path__' })) diff --git a/test/bridge.test.ts b/test/bridge.test.ts index b911aa9f4c..995a4b845e 100644 --- a/test/bridge.test.ts +++ b/test/bridge.test.ts @@ -1,6 +1,6 @@ import { fileURLToPath } from 'url' import { describe, expect, it } from 'vitest' -import { setup, $fetch } from '@nuxt/test-utils' +import { setup, $fetch, startServer } from '@nuxt/test-utils' describe('fixtures:bridge', async () => { await setup({ @@ -23,4 +23,60 @@ describe('fixtures:bridge', async () => { expect(html).toContain('Hello Vue 2!') }) }) + + describe('dynamic paths', () => { + if (process.env.TEST_WITH_WEBPACK) { + // TODO: + it.todo('work with webpack') + return + } + it('should work with no overrides', async () => { + const html = await $fetch('/assets') + for (const match of html.matchAll(/(href|src)="(.*?)"/g)) { + const url = match[2] + expect(url.startsWith('/_nuxt/') || url === '/public.svg').toBeTruthy() + } + }) + + it('adds relative paths to CSS', async () => { + const html = await $fetch('/assets') + const urls = Array.from(html.matchAll(/(href|src)="(.*?)"/g)).map(m => m[2]) + const cssURL = urls.find(u => /_nuxt\/assets.*\.css$/.test(u)) + const css = await $fetch(cssURL) + const imageUrls = Array.from(css.matchAll(/url\(([^)]*)\)/g)).map(m => m[1].replace(/[-.][\w]{8}\./g, '.')) + expect(imageUrls).toMatchInlineSnapshot(` + [ + "./logo.svg", + "../public.svg", + ] + `) + }) + + it('should allow setting base URL and build assets directory', async () => { + process.env.NUXT_APP_BUILD_ASSETS_DIR = '/_other/' + process.env.NUXT_APP_BASE_URL = '/foo/' + await startServer() + + const html = await $fetch('/assets') + for (const match of html.matchAll(/(href|src)="(.*?)"/g)) { + const url = match[2] + // TODO: should be /foo/public.svg + expect(url.startsWith('/foo/_other/') || url === '/public.svg').toBeTruthy() + } + }) + + it('should allow setting CDN URL', async () => { + process.env.NUXT_APP_BASE_URL = '/foo/' + process.env.NUXT_APP_CDN_URL = 'https://example.com/' + process.env.NUXT_APP_BUILD_ASSETS_DIR = '/_cdn/' + await startServer() + + const html = await $fetch('/assets') + for (const match of html.matchAll(/(href|src)="(.*?)"/g)) { + const url = match[2] + // TODO: should be https://example.com/public.svg + expect(url.startsWith('https://example.com/_cdn/') || url === '/public.svg').toBeTruthy() + } + }) + }) }) diff --git a/test/fixtures/bridge/assets/logo.svg b/test/fixtures/bridge/assets/logo.svg new file mode 100644 index 0000000000..c31de56afc --- /dev/null +++ b/test/fixtures/bridge/assets/logo.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/test/fixtures/bridge/pages/assets.vue b/test/fixtures/bridge/pages/assets.vue new file mode 100644 index 0000000000..40648319ca --- /dev/null +++ b/test/fixtures/bridge/pages/assets.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/test/fixtures/bridge/static/public.svg b/test/fixtures/bridge/static/public.svg new file mode 100644 index 0000000000..c31de56afc --- /dev/null +++ b/test/fixtures/bridge/static/public.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + +