Skip to content

Commit

Permalink
Merge pull request #1140 from embroider-build/fastboot-lazy-css
Browse files Browse the repository at this point in the history
Fastboot lazy css support
  • Loading branch information
ef4 authored Mar 4, 2022
2 parents 5b923af + 6a7bd23 commit 13273f4
Show file tree
Hide file tree
Showing 17 changed files with 272 additions and 128 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"types/ember-cli-htmlbars",
"tests/scenarios",
"tests/app-template",
"tests/addon-template"
"tests/addon-template",
"tests/v2-addon-template"
],
"nohoist": [
"**/@types/broccoli-plugin"
Expand Down
22 changes: 18 additions & 4 deletions packages/core/src/html-entrypoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,11 @@ export class HTMLEntrypoint {
if (supportsFastboot && placeholder.isScript()) {
// if there is any fastboot involved, we will emit the lazy bundles
// right before our first script.

let lazyMatch = stats.lazyBundles.get(src);
if (lazyMatch && !insertedLazy.has(src)) {
insertLazyBundles(lazyMatch, placeholder, this.publicAssetURL);
insertLazyJavascript(lazyMatch, placeholder, this.publicAssetURL);
insertLazyStyles(lazyMatch, placeholder, this.publicAssetURL);
insertedLazy.add(src);
}
}
Expand Down Expand Up @@ -156,9 +158,9 @@ function isAbsoluteURL(url: string) {
return /^(?:[a-z]+:)?\/\//i.test(url);
}

// we (somewhat arbitrarily) decide to put the lazy bundles before the very
// first <script> that we have rewritten
function insertLazyBundles(lazyBundles: string[], placeholder: Placeholder, publicAssetURL: string) {
// we (somewhat arbitrarily) decide to put the lazy javascript bundles before
// the very first <script> that we have rewritten
function insertLazyJavascript(lazyBundles: string[], placeholder: Placeholder, publicAssetURL: string) {
for (let bundle of lazyBundles) {
if (bundle.endsWith('.js')) {
let element = placeholder.start.ownerDocument.createElement('fastboot-script');
Expand All @@ -168,3 +170,15 @@ function insertLazyBundles(lazyBundles: string[], placeholder: Placeholder, publ
}
}
}

function insertLazyStyles(lazyBundles: string[], placeholder: Placeholder, publicAssetURL: string) {
for (let bundle of lazyBundles) {
if (bundle.endsWith('.css')) {
let element = placeholder.start.ownerDocument.createElement('link');
element.setAttribute('href', publicAssetURL + bundle);
element.setAttribute('rel', 'stylesheet');
placeholder.insert(element);
placeholder.insertNewline();
}
}
}
15 changes: 12 additions & 3 deletions packages/webpack/src/ember-webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,9 +428,12 @@ const Webpack: PackagerConstructor<Options> = class Webpack implements Packager
// 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]))
.map(chunk => `assets/${chunk.files?.[0]}`)
flatMap(
chunks.filter(chunk => chunk.runtime?.includes(id)),
chunk => chunk.files
)
.filter(file => !entrypointAssets?.find(a => a.name === file))
.map(file => `assets/${file}`)
);
}
}
Expand Down Expand Up @@ -511,6 +514,12 @@ const Webpack: PackagerConstructor<Options> = class Webpack implements Packager
new MiniCssExtractPlugin({
filename: `chunk.[chunkhash].css`,
chunkFilename: `chunk.[chunkhash].css`,
// in the browser, MiniCssExtractPlugin can manage it's own runtime
// lazy loading of stylesheets.
//
// but in fastboot, we need to disable that in favor of doing our
// own insertion of `<link>` tags in the HTML
runtime: variant.runtime === 'browser',
}),
],
};
Expand Down
2 changes: 1 addition & 1 deletion test-packages/support/suite-setup-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ async function githubMatrix() {
let dir = resolve(__dirname, '..', '..', 'tests', 'scenarios');
let { stdout } = await execa(
'scenario-tester',
['list', '--require', 'ts-node/register', '--files', '*-test.ts', '--matrix', 'yarn test --filter %s:'],
['list', '--require', 'ts-node/register', '--files', '*-test.ts', '--matrix', 'yarn test --filter %s'],
{
cwd: dir,
preferLocal: true,
Expand Down
21 changes: 3 additions & 18 deletions tests/app-template/ember-cli-build.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,12 @@
'use strict';

const EmberApp = require('ember-cli/lib/broccoli/ember-app');
const { maybeEmbroider } = require('@embroider/test-setup');

module.exports = function (defaults) {
let app = new EmberApp(defaults, {
// Add options here
});

// Use `app.import` to add additional libraries to the generated
// output files.
//
// If you need to use different assets in different
// environments, specify an object as the first parameter. That
// object's keys should be the environment name and the values
// should be the asset to use in that environment.
//
// If the library that you are including contains AMD or ES6
// modules that you would like to import into your application
// please specify an object with the list of modules as keys
// along with the exports of each module as its value.
let app = new EmberApp(defaults, {});

const { Webpack } = require('@embroider/webpack');
return require('@embroider/compat').compatBuild(app, Webpack, {
return maybeEmbroider(app, {
skipBabel: [
{
package: 'qunit',
Expand Down
1 change: 1 addition & 0 deletions tests/app-template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@embroider/compat": "1.2.0",
"@embroider/core": "1.2.0",
"@embroider/router": "1.2.0",
"@embroider/test-setup": "1.2.0",
"@embroider/webpack": "1.2.0",
"@glimmer/component": "^1.0.4",
"@glimmer/tracking": "^1.0.4",
Expand Down
75 changes: 46 additions & 29 deletions tests/fixtures/engines-host-app/tests/acceptance/basics-test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { module, test } from 'qunit';
import { visit, currentURL } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import { dependencySatisfies } from '@embroider/macros';

function arrayOfCSSRules(styleSheets, cssSelector, cssProperty) {
let values = [];
Expand All @@ -16,6 +17,10 @@ function arrayOfCSSRules(styleSheets, cssSelector, cssProperty) {
return values;
}

// We don't yet support lazy CSS in apps that are using fastboot. This test
// application runs both with and without fastboot.
const ensureCSSisLazy = !dependencySatisfies('ember-cli-fastboot', '*');

module('Acceptance | basics', function (hooks) {
setupApplicationTest(hooks);

Expand All @@ -33,11 +38,13 @@ module('Acceptance | basics', function (hooks) {
let entriesBefore = Object.entries(window.require.entries).length;
let rules = arrayOfCSSRules(document.styleSheets, '.shared-style-target', 'content');

assert.deepEqual(rules, [
'engines-host-app/vendor/styles.css',
'eager-engine/addon/styles/addon.css',
'engines-host-app/app/styles/app.css',
]);
if (ensureCSSisLazy) {
assert.deepEqual(rules, [
'engines-host-app/vendor/styles.css',
'eager-engine/addon/styles/addon.css',
'engines-host-app/app/styles/app.css',
]);
}

await visit('/style-check');
assert.dom('.shared-style-target').exists();
Expand All @@ -48,17 +55,19 @@ module('Acceptance | basics', function (hooks) {
'eager-engine styles are present'
);

assert.equal(
getComputedStyle(document.querySelector('.shared-style-target'))['border-right-width'],
'0px',
'lazy-engine addon styles are not present'
);

assert.equal(
getComputedStyle(document.querySelector('.shared-style-target'))['border-top-width'],
'0px',
'lazy-engine vendor styles are not present'
);
if (ensureCSSisLazy) {
assert.equal(
getComputedStyle(document.querySelector('.shared-style-target'))['border-right-width'],
'0px',
'lazy-engine addon styles are not present'
);

assert.equal(
getComputedStyle(document.querySelector('.shared-style-target'))['border-top-width'],
'0px',
'lazy-engine vendor styles are not present'
);
}

// TODO: uncomment once we fix this appearing too eagerly
//assert.notOk(!!window.require.entries['lazy-engine/helpers/duplicated-helper']);
Expand All @@ -73,13 +82,17 @@ module('Acceptance | basics', function (hooks) {

rules = arrayOfCSSRules(document.styleSheets, '.shared-style-target', 'content');

assert.deepEqual(rules, [
'engines-host-app/vendor/styles.css',
'eager-engine/addon/styles/addon.css',
'engines-host-app/app/styles/app.css',
'macro-sample-addon/addon/styles/addon.css',
'lazy-engine/addon/styles/addon.css',
]);
assert.deepEqual(
rules,
[
'engines-host-app/vendor/styles.css',
'eager-engine/addon/styles/addon.css',
'engines-host-app/app/styles/app.css',
'macro-sample-addon/addon/styles/addon.css',
'lazy-engine/addon/styles/addon.css',
ensureCSSisLazy ? undefined : 'lazy-in-repo-engine/addon/styles/addon.css',
].filter(Boolean)
);

await visit('/style-check');

Expand Down Expand Up @@ -113,7 +126,9 @@ module('Acceptance | basics', function (hooks) {
const entriesBefore = Object.entries(window.require.entries).length;
let rules = arrayOfCSSRules(document.styleSheets, '.shared-style-target', 'content');

assert.notOk(rules.includes('lazy-in-repo-engine/addon/styles/addon.css'));
if (ensureCSSisLazy) {
assert.notOk(rules.includes('lazy-in-repo-engine/addon/styles/addon.css'));
}

await visit('/style-check');
assert.dom('.shared-style-target').exists();
Expand All @@ -124,11 +139,13 @@ module('Acceptance | basics', function (hooks) {
'eager-engine styles are present'
);

assert.equal(
getComputedStyle(document.querySelector('.shared-style-target'))['border-bottom-width'],
'0px',
'lazy-in-repo-engine addon styles are not present'
);
if (ensureCSSisLazy) {
assert.equal(
getComputedStyle(document.querySelector('.shared-style-target'))['border-bottom-width'],
'0px',
'lazy-in-repo-engine addon styles are not present'
);
}

await visit('/use-lazy-in-repo-engine');
const entriesAfter = Object.entries(window.require.entries).length;
Expand Down
4 changes: 4 additions & 0 deletions tests/fixtures/fastboot-app/app/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@ export default class IndexRoute extends Route {
// merged by Embroider. So this serves as a reproduction of https://github.com/embroider-build/embroider/issues/160
return this.fastboot.isFastBoot ? this.fastboot.request.host : null;
}

async model() {
await import('v2-example/components/extra-styles.css');
}
}
5 changes: 3 additions & 2 deletions tests/fixtures/fastboot-app/app/templates/index.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<div data-test="hello">Hello from fastboot-app</div>
<div data-test='hello'>Hello from fastboot-app</div>
<Example />
<AddonExample />
<CheckService />
<LazyComponent />
<LazyComponent />
<V2ExampleComponent />
18 changes: 18 additions & 0 deletions tests/fixtures/fastboot-app/tests/acceptance/basic-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,22 @@ module('Acceptance | runtime basics', function (hooks) {
test('the tests suite eagerly loads some code that the app uses only lazily', async function (assert) {
assert.equal(secondSampleLib(), 'From second-sample-lib');
});

test('a component from a v2 addon with eager css', async function (assert) {
assert.dom('[data-test-v2-example]').containsText('it worked');
assert.equal(
getComputedStyle(document.querySelector('[data-test-v2-example]')).color,
'rgb(0, 128, 0)',
'style was applied'
);
});

test('a component from a v2 addon with lazy css', async function (assert) {
assert.dom('[data-test-v2-example]').containsText('it worked');
assert.equal(
getComputedStyle(document.querySelector('[data-test-v2-example]')).backgroundColor,
'rgb(0, 0, 255)',
'style was applied'
);
});
});
26 changes: 16 additions & 10 deletions tests/scenarios/engines-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,8 @@ appScenarios
project.addDevDependency(eagerEngine);
project.addDevDependency(lazyEngine);

project.linkDependency('ember-cli-fastboot', { baseDir: __dirname });
project.linkDependency('fastboot', { baseDir: __dirname });
project.linkDependency('ember-truth-helpers', { baseDir: __dirname });
project.linkDependency('@embroider/macros', { baseDir: __dirname });
project.addDependency(emberEngines());
eagerEngine.linkDependency('ember-truth-helpers', { baseDir: __dirname });
eagerEngine.addDependency(emberEngines());
Expand All @@ -76,24 +75,31 @@ appScenarios
let engineTestFiles = loadFromFixtureData('engines-host-app');
merge(project.files, engineTestFiles);
})
.expand({
'with-fastboot': project => {
project.linkDependency('ember-cli-fastboot', { baseDir: __dirname });
project.linkDependency('fastboot', { baseDir: __dirname });
},
'without-fastboot': () => {},
})
.forEachScenario(scenario => {
Qmodule(scenario.name, function (hooks) {
let app: PreparedApp;
hooks.before(async () => {
app = await scenario.prepare();
});

['development'].forEach(env => {
test(`yarn test: ${env}`, async function (assert) {
let result = await app.execute('yarn test');
assert.equal(result.exitCode, 0, result.output);
});
test(`yarn test`, async function (assert) {
let result = await app.execute('yarn test');
assert.equal(result.exitCode, 0, result.output);
});

Qmodule(`fastboot: ${env}`, function (hooks) {
if (/with-fastboot/.test(scenario.name)) {
Qmodule(`fastboot`, function (hooks) {
let visit: any;

hooks.before(async () => {
({ visit } = await setupFastboot(app, env));
({ visit } = await setupFastboot(app));
});

test('host-app', async function (assert) {
Expand All @@ -119,6 +125,6 @@ appScenarios
);
});
});
});
}
});
});
Loading

0 comments on commit 13273f4

Please sign in to comment.