From d8fcb60caedff6ac4a8b166e51f14143abe0c73a Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Wed, 16 Sep 2020 13:04:27 -0500 Subject: [PATCH] feat(prerender): server-side only bundle modules w/ .server directory Any modules within a directory that ends with ".server" will not be bundled into browser builds, but ".server" modules will get included within the dist-hydrate-script output target. Additionally, any external module referenced from a ".server" module will not be bundled within the hydrate output target. For example, lodash or moment would not get bundled if they were only referenced from within "src/data.server/index.ts", but instead they'll be traditional nodejs require() imports. --- src/compiler/bundle/bundle-output.ts | 27 ++++++-- src/compiler/bundle/plugin-helper.ts | 4 +- src/compiler/bundle/server-plugin.ts | 64 +++++++++++++++++++ .../hydrate-factory-closure.ts | 2 + .../dist-hydrate-script/index.ts | 4 -- 5 files changed, 91 insertions(+), 10 deletions(-) create mode 100644 src/compiler/bundle/server-plugin.ts diff --git a/src/compiler/bundle/bundle-output.ts b/src/compiler/bundle/bundle-output.ts index 82ac82f971e..655cc33321f 100644 --- a/src/compiler/bundle/bundle-output.ts +++ b/src/compiler/bundle/bundle-output.ts @@ -14,10 +14,16 @@ import { pluginHelper } from './plugin-helper'; import { resolveIdWithTypeScript, typescriptPlugin } from './typescript-plugin'; import { rollupCommonjsPlugin, rollupJsonPlugin, rollupNodeResolvePlugin, rollupReplacePlugin } from '@compiler-deps'; import { RollupOptions, TreeshakingOptions, rollup } from 'rollup'; +import { serverPlugin } from './server-plugin'; import { userIndexPlugin } from './user-index-plugin'; import { workerPlugin } from './worker-plugin'; -export const bundleOutput = async (config: d.Config, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx, bundleOpts: BundleOptions) => { +export const bundleOutput = async ( + config: d.Config, + compilerCtx: d.CompilerCtx, + buildCtx: d.BuildCtx, + bundleOpts: BundleOptions, +) => { try { const rollupOptions = getRollupOptions(config, compilerCtx, buildCtx, bundleOpts); const rollupBuild = await rollup(rollupOptions); @@ -32,8 +38,20 @@ export const bundleOutput = async (config: d.Config, compilerCtx: d.CompilerCtx, return undefined; }; -export const getRollupOptions = (config: d.Config, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx, bundleOpts: BundleOptions) => { - const customResolveOptions = createCustomResolverAsync(config.sys, compilerCtx.fs, ['.tsx', '.ts', '.js', '.mjs', '.json', '.d.ts']); +export const getRollupOptions = ( + config: d.Config, + compilerCtx: d.CompilerCtx, + buildCtx: d.BuildCtx, + bundleOpts: BundleOptions, +) => { + const customResolveOptions = createCustomResolverAsync(config.sys, compilerCtx.fs, [ + '.tsx', + '.ts', + '.js', + '.mjs', + '.json', + '.d.ts', + ]); const nodeResolvePlugin = rollupNodeResolvePlugin({ mainFields: ['collection:main', 'jsnext:main', 'es2017', 'es2015', 'module', 'main'], customResolveOptions, @@ -78,6 +96,7 @@ export const getRollupOptions = (config: d.Config, compilerCtx: d.CompilerCtx, b extFormatPlugin(config), extTransformsPlugin(config, compilerCtx, buildCtx, bundleOpts), workerPlugin(config, compilerCtx, buildCtx, bundleOpts.platform, !!bundleOpts.inlineWorkers), + serverPlugin(config, bundleOpts.platform), ...beforePlugins, nodeResolvePlugin, resolveIdWithTypeScript(config, compilerCtx), @@ -88,7 +107,7 @@ export const getRollupOptions = (config: d.Config, compilerCtx: d.CompilerCtx, b ...config.commonjs, }), ...afterPlugins, - pluginHelper(config, buildCtx), + pluginHelper(config, buildCtx, bundleOpts.platform), rollupJsonPlugin({ preferConst: true, }), diff --git a/src/compiler/bundle/plugin-helper.ts b/src/compiler/bundle/plugin-helper.ts index bfadfe03a54..fc63e735400 100644 --- a/src/compiler/bundle/plugin-helper.ts +++ b/src/compiler/bundle/plugin-helper.ts @@ -2,7 +2,7 @@ import type * as d from '../../declarations'; import { buildError } from '@utils'; import { relative } from 'path'; -export const pluginHelper = (config: d.Config, builtCtx: d.BuildCtx) => { +export const pluginHelper = (config: d.Config, builtCtx: d.BuildCtx, platform: string) => { return { name: 'pluginHelper', resolveId(importee: string, importer: string): null { @@ -22,7 +22,7 @@ export const pluginHelper = (config: d.Config, builtCtx: d.BuildCtx) => { } const diagnostic = buildError(builtCtx.diagnostics); diagnostic.header = `Node Polyfills Required`; - diagnostic.messageText = `For the import "${importee}" to be bundled${fromMsg}, ensure the "rollup-plugin-node-polyfills" plugin is installed and added to the stencil config plugins. Please see the bundling docs for more information. + diagnostic.messageText = `For the import "${importee}" to be bundled${fromMsg}, ensure the "rollup-plugin-node-polyfills" plugin is installed and added to the stencil config plugins (${platform}). Please see the bundling docs for more information. Further information: https://stenciljs.com/docs/module-bundling`; } return null; diff --git a/src/compiler/bundle/server-plugin.ts b/src/compiler/bundle/server-plugin.ts new file mode 100644 index 00000000000..69cab0b61fc --- /dev/null +++ b/src/compiler/bundle/server-plugin.ts @@ -0,0 +1,64 @@ +import type * as d from '../../declarations'; +import { isString, normalizeFsPath } from '@utils'; +import type { Plugin } from 'rollup'; +import { isOutputTargetHydrate } from '../output-targets/output-utils'; +import { isAbsolute } from 'path'; + +export const serverPlugin = (config: d.Config, platform: string): Plugin => { + const isHydrateBundle = platform === 'hydrate'; + const serverVarid = `@removed-server-code`; + + const isServerOnlyModule = (id: string) => { + if (isString(id)) { + id = normalizeFsPath(id); + return id.includes('.server/') || id.endsWith('.server'); + } + return false; + }; + + const externals = isHydrateBundle ? config.outputTargets.filter(isOutputTargetHydrate).flatMap(o => o.external) : []; + + return { + name: 'serverPlugin', + + resolveId(id, importer) { + if (id === serverVarid) { + return id; + } + if (isHydrateBundle) { + if (externals.includes(id)) { + // don't attempt to bundle node builtins for the hydrate bundle + return { + id, + external: true, + }; + } + if (isServerOnlyModule(importer) && !id.startsWith('.') && !isAbsolute(id)) { + // do not bundle if the importer is a server-only module + // and the module it is importing is a node module + return { + id, + external: true, + }; + } + } else { + if (isServerOnlyModule(id)) { + // any path that has .server in it shouldn't actually + // be bundled in the web build, only the hydrate build + return serverVarid; + } + } + return null; + }, + + load(id) { + if (id === serverVarid) { + return { + code: 'export default {};', + syntheticNamedExports: true, + }; + } + return null; + }, + }; +}; diff --git a/src/compiler/output-targets/dist-hydrate-script/hydrate-factory-closure.ts b/src/compiler/output-targets/dist-hydrate-script/hydrate-factory-closure.ts index d96277da2d1..40b376ab3cf 100644 --- a/src/compiler/output-targets/dist-hydrate-script/hydrate-factory-closure.ts +++ b/src/compiler/output-targets/dist-hydrate-script/hydrate-factory-closure.ts @@ -17,6 +17,7 @@ export function hydrateFactory($stencilWindow, $stencilHydrateOpts, $stencilHydr var close = () => {}; var confirm = $stencilWindow.confirm.bind($stencilWindow); var dispatchEvent = $stencilWindow.dispatchEvent.bind($stencilWindow); + var fetch = $stencilWindow.fetch.bind($stencilWindow); var focus = $stencilWindow.focus.bind($stencilWindow); var getComputedStyle = $stencilWindow.getComputedStyle.bind($stencilWindow); var matchMedia = $stencilWindow.matchMedia.bind($stencilWindow); @@ -37,6 +38,7 @@ export function hydrateFactory($stencilWindow, $stencilHydrateOpts, $stencilHydr var DOMTokenList = $stencilWindow.DOMTokenList; var Element = $stencilWindow.Element; var Event = $stencilWindow.Event; + var FetchError = $stencilWindow.FetchError; var Headers = $stencilWindow.Headers; var HTMLAnchorElement = $stencilWindow.HTMLAnchorElement; var HTMLBaseElement = $stencilWindow.HTMLBaseElement; diff --git a/src/compiler/output-targets/dist-hydrate-script/index.ts b/src/compiler/output-targets/dist-hydrate-script/index.ts index eecb8ad01b2..0a67192afa9 100644 --- a/src/compiler/output-targets/dist-hydrate-script/index.ts +++ b/src/compiler/output-targets/dist-hydrate-script/index.ts @@ -3,10 +3,6 @@ import { generateHydrateApp } from './generate-hydrate-app'; import { isOutputTargetHydrate } from '../output-utils'; export const outputHydrateScript = async (config: d.Config, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx) => { - if (!config.buildDist) { - return; - } - const hydrateOutputTargets = config.outputTargets.filter(isOutputTargetHydrate); if (hydrateOutputTargets.length > 0) { const timespan = buildCtx.createTimeSpan(`generate hydrate app started`);