Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fastboot chunk preloading fix #1109

Merged
merged 1 commit into from
Feb 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 25 additions & 22 deletions packages/core/src/html-entrypoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export class HTMLEntrypoint {

// bundles maps from input asset to a per-variant map of output assets
render(stats: BundleSummary): string {
let insertedLazy = false;
let insertedLazy = new Set<string>();
let fastbootVariant = stats.variants.findIndex(v => Boolean(v.runtime === 'fastboot'));
let supportsFastboot = stats.variants.some(v => v.runtime === 'fastboot' || v.runtime === 'all');

Expand All @@ -90,10 +90,14 @@ export class HTMLEntrypoint {
let matchingFastbootBundles = fastbootVariant >= 0 ? match.get(fastbootVariant) || [] : [];

for (let placeholder of placeholders) {
if (supportsFastboot) {
if (supportsFastboot && placeholder.isScript()) {
// if there is any fastboot involved, we will emit the lazy bundles
// right before our first script.
insertedLazy = maybeInsertLazyBundles(insertedLazy, stats.lazyBundles, placeholder, this.publicAssetURL);
let lazyMatch = stats.lazyBundles.get(src);
if (lazyMatch && !insertedLazy.has(src)) {
insertLazyBundles(lazyMatch, placeholder, this.publicAssetURL);
insertedLazy.add(src);
}
}
for (let [base, fastboot] of zip(matchingBundles, matchingFastbootBundles)) {
if (!base) {
Expand Down Expand Up @@ -131,11 +135,19 @@ export class HTMLEntrypoint {

export interface BundleSummary {
// entrypoints.get(inputAsset).get(variantIndex) === outputAssets
//
// these are the output assets that are needed eagerly to boot the given input
// asset
entrypoints: Map<string, Map<number, string[]>>;

// lazyBundles are tracked specifically for fastboot, so these always come
// from the fastboot variant, if any
lazyBundles: Set<string>;
// lazyBundles.get(inputAsset) === lazyOutputAssets
//
// these are the output assets that might be loaded lazyily at runtime by the
// given input asset.
//
// These are tracked specifically for the fastboot variant, because that's
// where we need to be responsble for them.
lazyBundles: Map<string, string[]>;

variants: Variant[];
}
Expand All @@ -146,22 +158,13 @@ function isAbsoluteURL(url: string) {

// we (somewhat arbitrarily) decide to put the lazy bundles before the very
// first <script> that we have rewritten
function maybeInsertLazyBundles(
insertedLazy: boolean,
lazyBundles: Set<string>,
placeholder: Placeholder,
publicAssetURL: string
): boolean {
if (!insertedLazy && placeholder.isScript()) {
for (let bundle of lazyBundles) {
if (bundle.endsWith('.js')) {
let element = placeholder.start.ownerDocument.createElement('fastboot-script');
element.setAttribute('src', publicAssetURL + bundle);
placeholder.insert(element);
placeholder.insertNewline();
}
function insertLazyBundles(lazyBundles: string[], placeholder: Placeholder, publicAssetURL: string) {
for (let bundle of lazyBundles) {
if (bundle.endsWith('.js')) {
let element = placeholder.start.ownerDocument.createElement('fastboot-script');
element.setAttribute('src', publicAssetURL + bundle);
placeholder.insert(element);
placeholder.insertNewline();
}
return true;
}
return insertedLazy;
}
35 changes: 15 additions & 20 deletions packages/webpack/src/ember-webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,14 +396,14 @@ const Webpack: PackagerConstructor<Options> = class Webpack implements Packager
private summarizeStats(multiStats: webpack.MultiStats): BundleSummary {
let output: BundleSummary = {
entrypoints: new Map(),
lazyBundles: new Set(),
lazyBundles: new Map(),
variants: this.variants,
};
for (let [variantIndex, variant] of this.variants.entries()) {
let { entrypoints, assets } = multiStats.stats[variantIndex].toJson({
let { entrypoints, chunks } = multiStats.stats[variantIndex].toJson({
all: false,
entrypoints: true,
assets: true,
chunks: true,
});

// webpack's types are written rather loosely, implying that these two
Expand All @@ -412,11 +412,10 @@ const Webpack: PackagerConstructor<Options> = class Webpack implements Packager
if (!entrypoints) {
throw new Error(`unexpected webpack output: no entrypoints`);
}
if (!assets) {
throw new Error(`unexpected webpack output: no assets`);
if (!chunks) {
throw new Error(`unexpected webpack output: no chunks`);
}

let nonLazyAssets: Set<string> = new Set();
for (let id of Object.keys(entrypoints)) {
let { assets: entrypointAssets } = entrypoints[id];
if (!entrypointAssets) {
Expand All @@ -427,20 +426,16 @@ const Webpack: PackagerConstructor<Options> = class Webpack implements Packager
variantIndex,
entrypointAssets.map(asset => 'assets/' + asset.name)
);

for (let asset of entrypointAssets) {
nonLazyAssets.add(asset.name);
}
}
if (variant.runtime !== 'browser') {
// in the browser we don't need to worry about lazy assets (they will be
// handled automatically by webpack as needed), but in any other runtime
// we need the ability to preload them
output.lazyBundles = new Set();
for (let asset of assets) {
if (!nonLazyAssets.has(asset.name)) {
output.lazyBundles.add('assets/' + asset.name);
}
if (variant.runtime !== 'browser') {
// in the browser we don't need to worry about lazy assets (they will be
// handled automatically by webpack as needed), but in any other runtime
// we need the ability to preload them
output.lazyBundles.set(
id,
chunks
.filter(chunk => chunk.runtime?.includes(id) && !entrypointAssets?.find(a => a.name === chunk.files?.[0]))
Copy link
Contributor Author

@ef4 ef4 Feb 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't find any obvious docs about chunk.runtime but it appears to mean what we need it to mean: which entrypoints can use this chunk at runtime.

.map(chunk => `assets/${chunk.files?.[0]}`)
);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
<div data-test="lazy-component">{{this.message}}</div>
<div data-test='lazy-component'>{{this.message}}</div>
<div data-test='lazy-component-second'>{{this.secondMessage}}</div>
10 changes: 10 additions & 0 deletions tests/fixtures/fastboot-app/app/components/lazy-component.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { tracked } from '@glimmer/tracking';
export default class LazyComponent extends Component {
@inject fastboot;
@tracked message = 'loading...';
@tracked secondMessage = 'loading...';

constructor(...args) {
super(...args);
Expand All @@ -16,8 +17,17 @@ export default class LazyComponent extends Component {
}

async loadLibrary() {
// we're loading two libraries here to exercise two different code paths.

// this one is only used here, so it will be a lazy dependency of the app
let library = (await import('@embroider/sample-lib')).default;
this.message = library();

// this one is used *lazily* here and also used *eagerly* in the test suite.
// Embroider needs to keep the different straight as its figuring out which
// lazy chunks to preload for fastboot.
let secondLib = (await import('@embroider/second-sample-lib')).default;
this.secondMessage = secondLib();
window.lazyComponentDone = true;
}
}
12 changes: 12 additions & 0 deletions tests/fixtures/fastboot-app/ember-cli-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,17 @@ module.exports = function (defaults) {
package: 'qunit',
},
],
packagerOptions: {
webpackConfig: {
optimization: {
splitChunks: {
// In these tests we want to guarantee that the lazily imported
// things really get handled lazily by webpack, even if they're too
// small for the optimizer to normally bother with
minSize: 1,
},
},
},
},
});
};
6 changes: 6 additions & 0 deletions tests/fixtures/fastboot-app/tests/acceptance/basic-test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { module, test } from 'qunit';
import { visit, waitUntil } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import secondSampleLib from '@embroider/second-sample-lib';

module('Acceptance | runtime basics', function (hooks) {
setupApplicationTest(hooks);
Expand Down Expand Up @@ -32,5 +33,10 @@ module('Acceptance | runtime basics', function (hooks) {

test('a component lazily loaded some code', async function (assert) {
assert.dom('[data-test="lazy-component"]').containsText('From sample-lib');
assert.dom('[data-test="lazy-component-second"]').containsText('From second-sample-lib');
});

test('the tests suite eagerly loads some code that the app uses only lazily', async function (assert) {
assert.equal(secondSampleLib(), 'From second-sample-lib');
});
});
22 changes: 17 additions & 5 deletions tests/scenarios/fastboot-app-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,26 @@ const { module: Qmodule, test } = QUnit;

appScenarios
.map('fastboot-app-test', project => {
let sampleLib = new Project('@embroider/sample-lib', '0.0.0');
merge(sampleLib.files, {
'index.js': `export default function () {
project.addDependency(
new Project('@embroider/sample-lib', '0.0.0', {
files: {
'index.js': `export default function () {
return 'From sample-lib';
}`,
});
},
})
);

project.addDependency(
new Project('@embroider/second-sample-lib', '0.0.0', {
files: {
'index.js': `export default function () {
return 'From second-sample-lib';
}`,
},
})
);

project.addDependency(sampleLib);
project.linkDependency('ember-cli-fastboot', { baseDir: __dirname });
project.linkDependency('fastboot', { baseDir: __dirname });

Expand Down