Skip to content

Commit

Permalink
fix(runtime): add lazy load for addtl bundlers (#3364)
Browse files Browse the repository at this point in the history
* fix(runtime): add lazy load for addtl bundlers

this commit adds experimental support for using stencil component
libraries in projects that use bundlers such as vite. prior to this
commit, stencil component libraries that were used in projects that used
such bundlers would have issues lazily-loading components at runtime.
this is due to restrictions the bundlers themselves place on the
filepaths that can be used in dynamic import statements.

this commit does not introduce the ability for stencil's compiler to use
bundlers other than rollup under the hood. it only permits a compiled
component library (that uses the `dist` output target) to be used in an
application that uses a bundler built atop of rollup.

due to the restrictions that rollup may impose on dynamic imports, this
commit adds the ability to add an explicit `import()` statement for each
lazily-loadable bundle. in order to keep the runtime small, this feature
is hidden behind a new feature flag, `experimentalImportInjection`

this pr build's atop the work done by @johnjenkins in
#2959 and the test cases
provided by @PrinceManfred in
#2959 (comment).
Without their contributions, this commit would not have been possible.

STENCIL-339: Integrate Bundler Functionality

* review(ap): remove unnecessary nested `Promise.all` calls

remove two nested `Promise.all` calls - after moving writing lazily
loadable chunks out of the outermost `Promise.all` call, we were
awaiting a single promise wrapped in an array. this commit simplifies
the code
  • Loading branch information
rwaskiewicz committed May 10, 2022
1 parent de7ec55 commit 25cef1e
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 33 deletions.
2 changes: 2 additions & 0 deletions src/client/client-load-module.ts
Expand Up @@ -22,7 +22,9 @@ export const loadModule = (
if (module) {
return module[exportName];
}
/*!__STENCIL_STATIC_IMPORT_SWITCH__*/
return import(
/* @vite-ignore */
/* webpackInclude: /\.entry\.js$/ */
/* webpackExclude: /\.system\.entry\.js$/ */
/* webpackMode: "lazy" */
Expand Down
147 changes: 114 additions & 33 deletions src/compiler/output-targets/dist-lazy/generate-lazy-module.ts
Expand Up @@ -30,39 +30,42 @@ export const generateLazyModules = async (
const entryComponentsResults = rollupResults.filter((rollupResult) => rollupResult.isComponent);
const chunkResults = rollupResults.filter((rollupResult) => !rollupResult.isComponent && !rollupResult.isEntry);

const [bundleModules] = await Promise.all([
Promise.all(
entryComponentsResults.map((rollupResult) => {
return generateLazyEntryModule(
config,
compilerCtx,
buildCtx,
rollupResult,
outputTargetType,
destinations,
sourceTarget,
shouldMinify,
isBrowserBuild,
sufix
);
})
),
Promise.all(
chunkResults.map((rollupResult) => {
return writeLazyChunk(
config,
compilerCtx,
buildCtx,
rollupResult,
outputTargetType,
destinations,
sourceTarget,
shouldMinify,
isBrowserBuild
);
})
),
]);
const bundleModules = await Promise.all(
entryComponentsResults.map((rollupResult) => {
return generateLazyEntryModule(
config,
compilerCtx,
buildCtx,
rollupResult,
outputTargetType,
destinations,
sourceTarget,
shouldMinify,
isBrowserBuild,
sufix
);
})
);

if (!!config.extras?.experimentalImportInjection && !isBrowserBuild) {
addStaticImports(rollupResults, bundleModules);
}

await Promise.all(
chunkResults.map((rollupResult) => {
return writeLazyChunk(
config,
compilerCtx,
buildCtx,
rollupResult,
outputTargetType,
destinations,
sourceTarget,
shouldMinify,
isBrowserBuild
);
})
);

const lazyRuntimeData = formatLazyBundlesRuntimeMeta(bundleModules);
const entryResults = rollupResults.filter((rollupResult) => !rollupResult.isComponent && rollupResult.isEntry);
Expand Down Expand Up @@ -98,6 +101,84 @@ export const generateLazyModules = async (
return bundleModules;
};

/**
* Add imports for each bundle to Stencil's lazy loader. Some bundlers that are built atop of Rollup strictly impose
* the limitations that are laid out in https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#limitations.
* This function injects an explicit import statement for each bundle that can be lazily loaded.
* @param rollupChunkResults the results of running Rollup across a Stencil project
* @param bundleModules lazy-loadable modules that can be resolved at runtime
*/
const addStaticImports = (rollupChunkResults: d.RollupChunkResult[], bundleModules: d.BundleModule[]): void => {
rollupChunkResults.filter(isStencilCoreResult).forEach((index: d.RollupChunkResult) => {
const generateCjs = isCjsFormat(index) ? generateCaseClauseCjs : generateCaseClause;
index.code = index.code.replace(
'/*!__STENCIL_STATIC_IMPORT_SWITCH__*/',
`
if (!hmrVersionId || !BUILD.hotModuleReplacement) {
const processMod = importedModule => {
cmpModules.set(bundleId, importedModule);
return importedModule[exportName];
}
switch(bundleId) {
${bundleModules.map((mod) => generateCjs(mod.output.bundleId)).join('')}
}
}`
);
});
};

/**
* Determine if a Rollup output chunk contains Stencil runtime code
* @param rollupChunkResult the rollup chunk output to test
* @returns true if the output chunk contains Stencil runtime code, false otherwise
*/
const isStencilCoreResult = (rollupChunkResult: d.RollupChunkResult): boolean => {
return (
rollupChunkResult.isCore &&
rollupChunkResult.entryKey === 'index' &&
(rollupChunkResult.moduleFormat === 'es' ||
rollupChunkResult.moduleFormat === 'esm' ||
isCjsFormat(rollupChunkResult))
);
};

/**
* Helper function to determine if a Rollup chunk has a commonjs module format
* @param rollupChunkResult the Rollup result to test
* @returns true if the Rollup chunk has a commonjs module format, false otherwise
*/
const isCjsFormat = (rollupChunkResult: d.RollupChunkResult): boolean => {
return rollupChunkResult.moduleFormat === 'cjs' || rollupChunkResult.moduleFormat === 'commonjs';
};

/**
* Generate a 'case' clause to be used within a `switch` statement. The case clause generated will key-off the provided
* bundle ID for a component, and load a file (tied to that ID) at runtime.
* @param bundleId the name of the bundle to load
* @returns the case clause that will load the component's file at runtime
*/
const generateCaseClause = (bundleId: string): string => {
return `
case '${bundleId}':
return import(
/* webpackMode: "lazy" */
'./${bundleId}.entry.js').then(processMod, consoleError);`;
};

/**
* Generate a 'case' clause to be used within a `switch` statement. The case clause generated will key-off the provided
* bundle ID for a component, and load a CommonJS file (tied to that ID) at runtime.
* @param bundleId the name of the bundle to load
* @returns the case clause that will load the component's file at runtime
*/
const generateCaseClauseCjs = (bundleId: string): string => {
return `
case '${bundleId}':
return Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require(
/* webpackMode: "lazy" */
'./${bundleId}.entry.js')); }).then(processMod, consoleError);`;
};

const generateLazyEntryModule = async (
config: d.Config,
compilerCtx: d.CompilerCtx,
Expand Down
8 changes: 8 additions & 0 deletions src/declarations/stencil-public-compiler.ts
Expand Up @@ -277,6 +277,14 @@ export interface ConfigExtras {
*/
dynamicImportShim?: boolean;

/**
* Experimental flag. Projects that use a Stencil library built using the `dist` output target may have trouble lazily
* loading components when using a bundler such as Vite or Parcel. Setting this flag to `true` will change how Stencil
* lazily loads components in a way that works with additional bundlers. Setting this flag to `true` will increase
* the size of the compiled output. Defaults to `false`.
*/
experimentalImportInjection?: boolean;

/**
* Dispatches component lifecycle events. Mainly used for testing. Defaults to `false`.
*/
Expand Down
3 changes: 3 additions & 0 deletions test/bundler/component-library/stencil.config.ts
Expand Up @@ -15,4 +15,7 @@ export const config: Config = {
serviceWorker: null, // disable service workers
},
],
extras: {
experimentalImportInjection: true,
},
};

0 comments on commit 25cef1e

Please sign in to comment.