Skip to content

Commit

Permalink
fix(bundler): prevent vite bundling errors in downstream projects (#3349
Browse files Browse the repository at this point in the history
)

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.

add a stencil component library to be used in tests that verify
applications that consume the library and are bundled with vite, parcel,
etc.

add an application that is built using vite to the bundler
test directory. it consumes a small stencil library build using the
`dist` output target, and verifies that the application can load the web
component when the application has been built using vite.

add infrastructure for running the bundler tests in karma.
karma was chosen to align with existing parts of our technical stack
(see the `test/karma` directory), and to expedite the initial
implementation phase of these tests. karma can be difficult to
configure, and even more difficult to add new (i.e. different) testing
paradigms and testing strategies to. given that these tests do not use
browserstack and are a significant departure from the existing karma
tests, it felt 'ok' to split these off into a separate set of tests
(with their own configuration).

in order to get tests up and running, a utilities file,
`test/bundler/karma-stencil-utils.ts` has been created. this file is
largely based off of `test/karma/test-app/util.ts`. parts of the
existing utility file were not ported over if they were deemed
unnecessary, and attempts were made to clean up the existing code to
improve their readability.

wire the bundler tests to github actions. these tests are
kept in a new reusable workflow that can run in parallel with existing
analysis, unit and e2e tests

STENCIL-339: Integrate Bundler Functionality
  • Loading branch information
rwaskiewicz committed May 13, 2022
1 parent d134830 commit 4c8d8c0
Show file tree
Hide file tree
Showing 32 changed files with 8,592 additions and 33 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ jobs:
needs: [ build_core ]
uses: ./.github/workflows/test-analysis.yml

bundler_tests:
name: Bundler Tests
needs: [ build_core ]
uses: ./.github/workflows/test-bundlers.yml

e2e_tests:
name: E2E Tests
needs: [ build_core ]
Expand Down
28 changes: 28 additions & 0 deletions .github/workflows/test-bundlers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Bundler Tests

on:
workflow_call:
# Make this a reusable workflow, no value needed
# https://docs.github.com/en/actions/using-workflows/reusing-workflows

jobs:
bundler_tests:
name: Verify Bundlers
runs-on: 'ubuntu-latest'
steps:
- name: Checkout Code
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0

- name: Get Core Dependencies
uses: ./.github/workflows/actions/get-core-dependencies

- name: Download Build Archive
uses: ./.github/workflows/actions/download-archive
with:
name: stencil-core
path: .
filename: stencil-core-build.zip

- name: Bundler Tests
run: npm run test.bundlers
shell: bash
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"prettier.dry-run": "npm run prettier.base -- --list-different",
"test": "jest --coverage",
"test.analysis": "cd test && npm run analysis.build-and-analyze",
"test.bundlers": "cd test && npm run bundlers",
"test.dist": "node scripts --validate-build",
"test.end-to-end": "cd test/end-to-end && npm ci && npm test && npm run test.dist",
"test.jest": "jest",
Expand Down
2 changes: 2 additions & 0 deletions src/client/client-load-module.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
5 changes: 5 additions & 0 deletions test/bundler/component-library/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
dist/
loader/
www/

node_modules/
16 changes: 16 additions & 0 deletions test/bundler/component-library/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# component-library

This directory contains a small Stencil library to be consumed by other applications for testing purposes.

The library consists of a single component, `<my-component></my-component>`.
Documentation for using this component can be found in the [README.md file](./src/components/my-component/readme.md) for
the component.

## scripts

This library contains three NPM scripts:

- `build` - builds the project for use in other applications
- `clean` - removes previously created build artifacts
- `start` - starts up a local dev server to validate the component looks/behaves as expected (without having to
consume it in an application)
13 changes: 13 additions & 0 deletions test/bundler/component-library/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions test/bundler/component-library/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "component-library",
"version": "0.0.1",
"description": "Stencil Component Starter",
"main": "dist/index.cjs.js",
"module": "dist/index.js",
"es2015": "dist/esm/index.mjs",
"es2017": "dist/esm/index.mjs",
"types": "dist/types/index.d.ts",
"collection": "dist/collection/collection-manifest.json",
"collection:main": "dist/collection/index.js",
"unpkg": "dist/component-library/component-library.esm.js",
"files": [
"dist/",
"loader/"
],
"scripts": {
"build": "node ../../../bin/stencil build --docs",
"clean": "rm -rf dist loader www",
"start": "node ../../../bin/stencil build --dev --watch --serve"
},
"license": "MIT"
}
61 changes: 61 additions & 0 deletions test/bundler/component-library/src/components.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/* eslint-disable */
/* tslint:disable */
/**
* This is an autogenerated file created by the Stencil compiler.
* It contains typing information for all components that exist in this project.
*/
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
export namespace Components {
interface MyComponent {
/**
* The first name
*/
"first": string;
/**
* The last name
*/
"last": string;
/**
* The middle name
*/
"middle": string;
}
}
declare global {
interface HTMLMyComponentElement extends Components.MyComponent, HTMLStencilElement {
}
var HTMLMyComponentElement: {
prototype: HTMLMyComponentElement;
new (): HTMLMyComponentElement;
};
interface HTMLElementTagNameMap {
"my-component": HTMLMyComponentElement;
}
}
declare namespace LocalJSX {
interface MyComponent {
/**
* The first name
*/
"first"?: string;
/**
* The last name
*/
"last"?: string;
/**
* The middle name
*/
"middle"?: string;
}
interface IntrinsicElements {
"my-component": MyComponent;
}
}
export { LocalJSX as JSX };
declare module "@stencil/core" {
export namespace JSX {
interface IntrinsicElements {
"my-component": LocalJSX.MyComponent & JSXBase.HTMLAttributes<HTMLMyComponentElement>;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:host {
display: block;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Component, Prop, h } from '@stencil/core';
import { format } from '../../utils/utils';

@Component({
tag: 'my-component',
styleUrl: 'my-component.css',
shadow: true,
})
export class MyComponent {
/**
* The first name
*/
@Prop() first: string;

/**
* The middle name
*/
@Prop() middle: string;

/**
* The last name
*/
@Prop() last: string;

private getText(): string {
return format(this.first, this.middle, this.last);
}

render() {
return <div>Hello, World! I'm {this.getText()}</div>;
}
}
Loading

0 comments on commit 4c8d8c0

Please sign in to comment.