From 9681a8937d0c973bc685b757faa686023a4536fc Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 22 Dec 2020 17:15:59 +0000 Subject: [PATCH] feat(config, vue-app, vue-renderer): support dynamic `base` and `publicPath` (#8520) Co-authored-by: pooya parsa --- packages/config/src/options.js | 20 ++++-- .../test/__snapshots__/options.test.js.snap | 13 +++- packages/generator/src/generator.js | 4 +- packages/vue-app/template/client.js | 8 ++- packages/vue-app/template/index.js | 2 +- packages/vue-app/template/router.js | 5 +- packages/vue-app/template/server.js | 3 +- packages/vue-app/template/utils.js | 2 +- packages/vue-renderer/src/renderer.js | 7 +- test/dev/basic.dynamic.test.js | 69 +++++++++++++++++++ 10 files changed, 115 insertions(+), 18 deletions(-) create mode 100644 test/dev/basic.dynamic.test.js diff --git a/packages/config/src/options.js b/packages/config/src/options.js index badbe0bde8..8820c29b8d 100644 --- a/packages/config/src/options.js +++ b/packages/config/src/options.js @@ -4,8 +4,8 @@ import { defaultsDeep, pick, uniq } from 'lodash' import defu from 'defu' import consola from 'consola' import destr from 'destr' -import { TARGETS, MODES, guardDir, isNonEmptyString, isPureObject, isUrl, getMainModule, urlJoin, getPKG } from '@nuxt/utils' -import { normalizeURL, withTrailingSlash } from '@nuxt/ufo' +import { TARGETS, MODES, guardDir, isNonEmptyString, isPureObject, isUrl, getMainModule, getPKG } from '@nuxt/utils' +import { joinURL, normalizeURL, withTrailingSlash } from '@nuxt/ufo' import { defaultNuxtConfigFile, getDefaultNuxtConfig } from './config' export function getNuxtConfig (_options) { @@ -450,17 +450,27 @@ export function getNuxtConfig (_options) { .map(([path, handler]) => ({ path, handler })) } + // App config (internal for nuxt2 at this stage) + const useCDN = isUrl(options.build.publicPath) && !options.dev + options.app = defu(options.app, { + basePath: options.router.base, + assetsPath: useCDN ? '/' : joinURL(options.router.base, options.build.publicPath), + cdnURL: useCDN ? options.build.publicPath : null + }) + // Expose app config to $config.app + options.publicRuntimeConfig = options.publicRuntimeConfig || {} + options.publicRuntimeConfig.app = options.app + // Generate staticAssets const { staticAssets } = options.generate if (!staticAssets.version) { staticAssets.version = String(Math.round(Date.now() / 1000)) } if (!staticAssets.base) { - const publicPath = isUrl(options.build.publicPath) ? '' : options.build.publicPath // "/_nuxt" or custom CDN URL - staticAssets.base = urlJoin(publicPath, staticAssets.dir) + staticAssets.base = joinURL(options.app.assetsPath, staticAssets.dir) } if (!staticAssets.versionBase) { - staticAssets.versionBase = urlJoin(staticAssets.base, staticAssets.version) + staticAssets.versionBase = joinURL(staticAssets.base, staticAssets.version) } // createRequire diff --git a/packages/config/test/__snapshots__/options.test.js.snap b/packages/config/test/__snapshots__/options.test.js.snap index 03043329a3..dd24512c3d 100644 --- a/packages/config/test/__snapshots__/options.test.js.snap +++ b/packages/config/test/__snapshots__/options.test.js.snap @@ -19,6 +19,11 @@ Object { "~": "/var/nuxt/test", "~~": "/var/nuxt/test", }, + "app": Object { + "assetsPath": "/_nuxt/", + "basePath": "/", + "cdnURL": null, + }, "appTemplatePath": "/var/nuxt/test/.nuxt/views/app.template.html", "build": Object { "_publicPath": "/_nuxt/", @@ -312,7 +317,13 @@ Object { }, "plugins": Array [], "privateRuntimeConfig": Object {}, - "publicRuntimeConfig": Object {}, + "publicRuntimeConfig": Object { + "app": Object { + "assetsPath": "/_nuxt/", + "basePath": "/", + "cdnURL": null, + }, + }, "render": Object { "bundleRenderer": Object { "runInNewContext": false, diff --git a/packages/generator/src/generator.js b/packages/generator/src/generator.js index f558fa417c..11a0d234b2 100644 --- a/packages/generator/src/generator.js +++ b/packages/generator/src/generator.js @@ -29,11 +29,9 @@ export default class Generator { ) // Payloads for full static if (this.isFullStatic) { - const { build: { publicPath: _publicPath }, router: { base } } = this.options - const publicPath = isUrl(_publicPath) ? _publicPath : base const { staticAssets, manifest } = this.options.generate this.staticAssetsDir = path.resolve(this.distNuxtPath, staticAssets.dir, staticAssets.version) - this.staticAssetsBase = urlJoin(publicPath, this.options.generate.staticAssets.versionBase) + this.staticAssetsBase = urlJoin(this.options.app.cdnURL || '/', this.options.generate.staticAssets.versionBase) if (manifest) { this.manifest = defu(manifest, { routes: [] diff --git a/packages/vue-app/template/client.js b/packages/vue-app/template/client.js index 6cc887cf50..85bf82c052 100644 --- a/packages/vue-app/template/client.js +++ b/packages/vue-app/template/client.js @@ -15,7 +15,8 @@ import { compile, getQueryDiff, globalHandleError, - isSamePath + isSamePath, + urlJoin } from './utils.js' import { createApp<% if (features.layouts) { %>, NuxtError<% } %> } from './index.js' <% if (features.fetch) { %>import fetchMixin from './mixins/fetch.client'<% } %> @@ -45,6 +46,11 @@ let router // Try to rehydrate SSR data from window const NUXT = window.<%= globals.context %> || {} +const $config = NUXT.config || {} +if ($config.app) { + __webpack_public_path__ = urlJoin($config.app.cdnURL || '/', $config.app.assetsPath) +} + Object.assign(Vue.config, <%= serialize(vue.config) %>)<%= isTest ? '// eslint-disable-line' : '' %> <% if (nuxtOptions.render.ssrLog) { %> diff --git a/packages/vue-app/template/index.js b/packages/vue-app/template/index.js index 71ec6f0d41..50c11c90a7 100644 --- a/packages/vue-app/template/index.js +++ b/packages/vue-app/template/index.js @@ -88,7 +88,7 @@ function registerModule (path, rawModule, options = {}) { <% } %> async function createApp(ssrContext, config = {}) { - const router = await createRouter(ssrContext) + const router = await createRouter(ssrContext, config) <% if (store) { %> const store = createStore(ssrContext) diff --git a/packages/vue-app/template/router.js b/packages/vue-app/template/router.js index d639c6784d..86becdff01 100644 --- a/packages/vue-app/template/router.js +++ b/packages/vue-app/template/router.js @@ -109,8 +109,9 @@ function decodeObj(obj) { } } -export function createRouter () { - const router = new Router(routerOptions) +export function createRouter (ssrContext, config) { + const base = (config.app && config.app.basePath) || routerOptions.base + const router = new Router({ ...routerOptions, base }) // TODO: remove in Nuxt 3 const originalPush = router.push diff --git a/packages/vue-app/template/server.js b/packages/vue-app/template/server.js index 2456301cf9..d449ce8eaa 100644 --- a/packages/vue-app/template/server.js +++ b/packages/vue-app/template/server.js @@ -46,7 +46,8 @@ const createNext = ssrContext => (opts) => { } opts.query = stringify(opts.query) opts.path = opts.path + (opts.query ? '?' + opts.query : '') - const routerBase = '<%= router.base %>' + const $config = ssrContext.runtimeConfig || {} + const routerBase = ($config.app && $config.app.basePath) || '<%= router.base %>' if (!opts.path.startsWith('http') && (routerBase !== '/' && !opts.path.startsWith(routerBase))) { opts.path = urlJoin(routerBase, opts.path) } diff --git a/packages/vue-app/template/utils.js b/packages/vue-app/template/utils.js index b4cda63c57..3390c53de2 100644 --- a/packages/vue-app/template/utils.js +++ b/packages/vue-app/template/utils.js @@ -180,7 +180,7 @@ export async function setContext (app, context) { <%= (store ? 'store: app.store,' : '') %> payload: context.payload, error: context.error, - base: '<%= router.base %>', + base: app.router.options.base, env: <%= JSON.stringify(env) %><%= isTest ? '// eslint-disable-line' : '' %> } // Only set once diff --git a/packages/vue-renderer/src/renderer.js b/packages/vue-renderer/src/renderer.js index b6ffb8f091..598331c6bb 100644 --- a/packages/vue-renderer/src/renderer.js +++ b/packages/vue-renderer/src/renderer.js @@ -2,7 +2,7 @@ import path from 'path' import fs from 'fs-extra' import consola from 'consola' import { template } from 'lodash' -import { TARGETS, isModernRequest, waitFor } from '@nuxt/utils' +import { TARGETS, isModernRequest, urlJoin, waitFor } from '@nuxt/utils' import { normalizeURL } from '@nuxt/ufo' import SPARenderer from './renderers/spa' @@ -310,14 +310,15 @@ export default class VueRenderer { } get resourceMap () { + const publicPath = urlJoin(this.options.app.cdnURL || '/', this.options.app.assetsPath) return { clientManifest: { fileName: 'client.manifest.json', - transform: src => JSON.parse(src) + transform: src => Object.assign(JSON.parse(src), { publicPath }) }, modernManifest: { fileName: 'modern.manifest.json', - transform: src => JSON.parse(src) + transform: src => Object.assign(JSON.parse(src), { publicPath }) }, serverManifest: { fileName: 'server.manifest.json', diff --git a/test/dev/basic.dynamic.test.js b/test/dev/basic.dynamic.test.js new file mode 100644 index 0000000000..70e4fedea5 --- /dev/null +++ b/test/dev/basic.dynamic.test.js @@ -0,0 +1,69 @@ +import path from 'path' +import { readFileSync } from 'fs' +import { ResourceLoader } from 'jsdom' + +import { loadFixture, getPort, Nuxt, Builder } from '../utils' + +let fetchCount = 0 + +class ProxyLoader extends ResourceLoader { + fetch (url, options) { + if (url.startsWith('https://cdn.nuxtjs.org')) { + fetchCount++ + const param = url.slice('https://cdn.nuxtjs.org'.length + 1) + const file = path.join(nuxt.options.buildDir, 'dist/client', param) + const fileContents = readFileSync(file, 'utf-8') + return Promise.resolve(Buffer.from(fileContents)) + } + + return super.fetch(url, options) + } +} +const resourceLoader = new ProxyLoader() + +let port +let nuxt +const url = route => 'http://localhost:' + port + route + +describe('basic ssr', () => { + beforeAll(async () => { + const options = await loadFixture('basic') + const builderNuxt = new Nuxt(options) + await builderNuxt.ready() + + const builder = new Builder(builderNuxt) + + await builder.build() + + const runOptions = await loadFixture('basic', { + router: { + base: '/path' + }, + build: { + publicPath: 'https://cdn.nuxtjs.org' + } + }) + nuxt = new Nuxt(runOptions) + await nuxt.ready() + + port = await getPort() + await nuxt.server.listen(port, '0.0.0.0') + }) + + test('dynamic config is injected', async () => { + const window = await nuxt.server.renderAndGetWindow(url('/path/'), { resources: resourceLoader }) + + expect(window.document.body.innerHTML).toContain('

Index page

') + + expect(window.__NUXT__.config.app.basePath).toBe('/path/') + expect(window.__NUXT__.config.app.cdnURL).toBe('https://cdn.nuxtjs.org/') + expect(window.__NUXT__.config.app.assetsPath).toBe('/') + + expect(fetchCount).toBeGreaterThan(0) + }) + + // Close server and ask nuxt to stop listening to file changes + afterAll(async () => { + await nuxt.close() + }) +})