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

Support CSS from CDN in esm-views #1757

Merged
merged 20 commits into from
May 30, 2022
Merged

Conversation

cristiano-belloni
Copy link
Contributor

@cristiano-belloni cristiano-belloni commented May 20, 2022

Problem

Suppose we have a CSS import from an external dependency like:

import 'regular-table/dist/css/material.css';

In an esm-view, we want to load the dependency from CDN instead of bundling it from local dependencies. In the future, we will also be able to use CSS Module Scripts to add styles to the page (and possibly deduplicate them).
The problem, with Webpack, is that we can't just rewrite the import like we do with js modules. We need to use an helper function that fetches the css (or imports it with an assertion) from the CDN.

Solution

I used a Webpack pitching loader on top of our chain of style loaders. A pitching loader has a pitch method that gets evaluated left-to-right in the array of loaders. If it returns something truthy, it prevents the subsequent loaders to run; otherwise, all the other loaders for the rule run right-to-left (so the top loader is last). It works in our case because:

  • If we detect that a CSS import is within our rewritable dependency map in the pitch method, we can rewrite it with some code that fetches and injects the desired file from CDN and, by returning from pitch, prevent all the other style loaders to process it.
  • In all other cases (local CSS files or external CSS files that need to be bundled), we return undefined from the pitch function, letting the whole loader chain process it, and when our loader runs last, we just act as a bypass and return whatever output the loader chain has processed before us.

The pros of this approach is that it uses the correct mechanism to load a dependency (a loader) and the solution is quite compact. The cons are that we re-create the import submodule "manually" (by looking at the Webpack relative path of the requested file) and that the loader is mandatorily positional (it must sit before the others).

Alternative solutions

  • plugins: Plugins are more powerful than loaders. Theoretically we could scan the AST of every file and carefully rewrite it to fetch styles from a CDN where needed. In practice, it would be an incredibly complex solution that is much more difficult to debug.
  • non-pitching loader: if it existed a way to "redirect" an import to a loader, using the inline syntax and a query to specify the import statement value, we could use a much simple standalone loader. Unfortunately, it doesn't seem possible to do that when marking imports as externals. Any help would be appreciated here.

Other

This PR also includes a bit more debug logging when filtering external dependencies and a small bugfix (wildcard value in allow list is ** instead of * to include scoped packages)

@changeset-bot
Copy link

changeset-bot bot commented May 20, 2022

🦋 Changeset detected

Latest commit: 3e6d3e7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
modular-scripts Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coveralls
Copy link
Collaborator

coveralls commented May 20, 2022

Coverage Status

Coverage increased (+0.4%) to 26.801% when pulling 3e6d3e7 on feature/webpack-cdn-css into 813c941 on main.

@cristiano-belloni cristiano-belloni marked this pull request as ready for review May 23, 2022 10:06
@cristiano-belloni cristiano-belloni added this to the ESM View Support milestone May 23, 2022
@cristiano-belloni cristiano-belloni marked this pull request as ready for review May 24, 2022 17:28
return `
const link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
Copy link
Contributor

Choose a reason for hiding this comment

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

What about .less files?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think we're expecting to process any files coming from the CDN at runtime - the expectation is that the CDN provides files that are already processed by the build process of the dependency.

link.rel = 'stylesheet';
link.type = 'text/css';
link.href = '${url}';
document.getElementsByTagName('HEAD')[0].appendChild(link);
Copy link
Contributor

Choose a reason for hiding this comment

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

Edge case: the <head> tag is optional, so it may not be present. I suggest creating it if it doesn't exist. https://google.github.io/styleguide/htmlcssguide.html#Optional_Tags

Copy link
Contributor Author

Choose a reason for hiding this comment

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

TL;DR: Done.

It seems that Webpack adds <head> even when we don't specify it in the template, but I added a check and replacement for missing head anyway. Better safe than sorry.

: isEnvDevelopment,
},
undefined,
true,
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be isEsmView?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, thanks!

});
});

describe('WHEN we specify an allow / block list', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

This test file is very long, and these unit tests may get lost. I think it would be better to put them in a file that makes it clear this is testing filterDependencies.ts.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Factored out in partitionDependencies.test.ts. Why didn't I call it filterDependencies? Because we're testing only one method in the file (partitionDependencies) and the other one - that deals with env variable parsing - is already tested in esmView.test.ts

Copy link
Contributor

Choose a reason for hiding this comment

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

That's fair enough, though I would have been happy if you'd created a to-be-completed test file as we can always flesh it out later!

export function matchDependencies({
packageDependencies,
// By default, everything in allow list
allowList = ['**'],
Copy link
Contributor

Choose a reason for hiding this comment

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

Re-reading this, I think "allow", "block", and even "matchDependencies" (and "filterDependencies") are not self-descriptive. Really what you're looking for is external vs bundled. It'll be very easy to forget what this function is trying to do or what it's for.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@benpryke I changed terminology to reflect extarnal / bundled in the function that was introduced in this PR, and renamed it to partitionDependencies.
I didn't change it in the rest of the file, since EXTERNAL_BLOCK_LIST and EXTERNAL_ALLOW_LIST were approved months ago, are part of the user interface and not in scope of this PR.

@cristiano-belloni cristiano-belloni merged commit 1e72dc4 into main May 30, 2022
@cristiano-belloni cristiano-belloni deleted the feature/webpack-cdn-css branch May 30, 2022 22:02
@github-actions github-actions bot mentioned this pull request May 30, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants