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

[heft-web-rig] Add an "app" profile, upgrade to Webpack 5, implement new CSS rules #3204

Merged
merged 43 commits into from
Feb 11, 2022

Conversation

octogonz
Copy link
Collaborator

@octogonz octogonz commented Feb 4, 2022

Summary

Update @rushstack/heft-web-rig with a bunch of new features.

Details

Related ideas that are outside the scope of this PR:

  • Replace node-sass with sass for heft-sass-plugin
  • Replace lib-commonjs with emitCjsExtensionForCommonJS
  • Enable Webpack bundling of web libraries
  • Add more profiles to @rushstack/heft-node-rig

strictPeerDependencies fix

While trying to get the various Webpack 5 plugin versions straightened out, I encountered a bunch of peer dependency issues. The strictPeerDependencies validation for this repo apparently got disabled by accident a few months ago, and a bunch of warnings accumulated in the interim. Ideally that should be solved in a separate PR with its own code review, but in the interest of time it is more efficient to solve the equation with the final set of package.json files, so I've rolled it into this PR. Sorry for the huge diff. :-)

How it was tested

The new @rushstack/heft-web-rig was prototyped in the rushstack.io-service repo.

CC @iclanton @elliot-nelson @dmichon-msft

localIdentName: production ? '[hash:base64]' : '[local]__[hash:base64:5]'
},

sourceMap: !production
Copy link
Collaborator Author

@octogonz octogonz Feb 5, 2022

Choose a reason for hiding this comment

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

One weirdness of the above approach is that global .css files get sourcemaps injected, which you can see in the Chrome F12 debugger. (Does that even make sense when CSS modules are not being used?)

However because we are using 'mini-css-extract-plugin', the problem disappears when the CSS is extracted into a separate .css bundle.

DEBUG: !production
}),

process.env.REPORT ? new BundleAnalyzerPlugin({ analyzerMode: 'static' }) : undefined
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The idea is to generate the .html reports optionally, only if the REPORT environment variable is set. (Other ideas welcome.) Currently we are using dotenv at HBO.

// this allows projects to override the default rig behavior if applicable (ex. when targeting older browsers)
customizeArray: unique(
'plugins',
['HtmlWebpackPlugin'],
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

(This is slightly awkward -- is there a better way?)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm starting to think we should invent a more rigorous inheritance mechanism for webpack.config.js than these copy+paste createWebpackConfig() functions. When we tackle that, we can also figure out a better way to handle these overrides.

build-tests/heft-sass-test/package.json Show resolved Hide resolved
Comment on lines 5 to 9
"outputFolderNames": ["dist", "lib", "lib-commonjs", "temp"]
},
{
"operationName": "build",
"outputFolderNames": ["dist", "lib", "lib-commonjs", "temp"]
Copy link
Member

Choose a reason for hiding this comment

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

These lists should probably match the folders that heft clean deletes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good feedback, fixed!

Copy link
Contributor

Choose a reason for hiding this comment

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

Not in scope for this, but we should update Rush to always clean the output folders if it is not skipping the operation, not only if there is a cache hit. That way we get consistent behavior regarding rush cleaning regardless of the build cache.

Comment on lines 44 to 57
{
// The source-map-loader extracts existing source maps from all JavaScript entries. This includes both
// inline source maps as well as those linked via URL. All source map data is passed to Webpack for
// processing as per a chosen source map style specified by the devtool option in webpack.config.js.
// https://www.npmjs.com/package/source-map-loader
test: /\.js$/,
include: [path.resolve(projectRoot, 'lib')],
enforce: 'pre',
use: [
{
loader: require.resolve('source-map-loader')
}
]
},
Copy link
Member

Choose a reason for hiding this comment

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

This isn't built-in to Webpack at this point?

Copy link
Collaborator Author

@octogonz octogonz Feb 11, 2022

Choose a reason for hiding this comment

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

This rule still appears in the docs for Webpack 5:

https://webpack.js.org/loaders/source-map-loader/#getting-started

👉 Seems we should keep the above config as-is.

{
// Compiles SASS syntax into CSS
// https://www.npmjs.com/package/sass-loader
loader: require.resolve('sass-loader'),
Copy link
Member

Choose a reason for hiding this comment

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

I kinda think our recommended approach should be to compile SASS to CSS with heft-sass-plugin so we don't have to parse it twice.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@iclanton is that possible today?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ian said that Heft has this feature today. However, compiling src/*.scss -> lib/*.css today will conflict with this requirement:

// Handling of formats:
// - *.scss: sass, autoprefixer, modules <--- recommended
// - *.global.scss: sass, autoprefixer, NO modules
// - *.css: NO sass, NO autoprefixer, NO modules
//
// If someone wants CSS with modules, they can simply use the .sass file extension, since SASS
// is essentially a superset of CSS. (There's a small performance penalty for applying SASS to
// a plain CSS file, but the extra syntax validation probably justifies that cost.)

To handle that, we need to add a heft-sass-plugin setting that allows the file extensions to be remapped (e.g. src/*.scss -> lib/*.module.css BUT src/*.global.scss --> lib/*.global.css).

That needs to be implemented in a separate PR.

@dmichon-msft @jonasb @halfnibble

Copy link
Contributor

Choose a reason for hiding this comment

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

When using heft-sass-plugin to compile SASS to CSS, the mapping is:

  • src/**/*.scss -> lib/**/*.scss.css
  • src/**/*.sass -> lib/**/*.sass.css
  • src/**/*.css -> lib/**/*.css
    This was done so that source code imports like:
import styles from './Foo.scss';

resolve correctly, whether you compile the file in advance or have webpack point at the source code. The only required change in webpack is to append ".css" to resolve.extensions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@dmichon-msft I tried to add this, however it turns out that sass.json is not riggable because "cssOutputFolders" gets resolved relative to the rig project rather than the app project. This is a design problem with heft-sass-plugin.

We should do a separate PR to fix heft-sass-plugin, then we can follow up with heft-web-rig to enable this optimization.

rigs/heft-web-rig/shared/webpack-base.config.js Outdated Show resolved Hide resolved
Comment on lines 14 to 15
// IMPORTANT: To simplify versioning, 'webpack-dev-server' is a devDependency, not a regular dependency
import type { Configuration as WebpackDevServerConfig } from 'webpack-dev-server';

Choose a reason for hiding this comment

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

Why is it important for the reader to know, in this context, that 'webpack-dev-server' is a devDependency? I see "IMPORTANT:", and a fact -- but no explanation of why that fact is important, or what I-the-reader need to do about it.

My apologies if this question merely reflects my own ignorance, as is entirely possible.

Copy link
Contributor

Choose a reason for hiding this comment

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

If you try to import the value and reference it, it will compile and work locally, but fail at runtime when published. Only the type information is safe to use, hence the choice of type-only import.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@MickeyPhoenix Right, normally when you import from a package, that package must be a regular dependency (since a dev dependency will be missing for library consumers, as David explained). So the IMPORTANT note is explaining why we are doing import type.

It's a common situation for published libraries, but I can add a bit more context to the comment.

Comment on lines +69 to +70
// "For production builds it's recommended to extract the CSS from your bundle being able to
// use parallel loading of CSS/JS resources later on. This can be achieved by using the

Choose a reason for hiding this comment

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

Should this maybe be

Suggested change
// "For production builds it's recommended to extract the CSS from your bundle being able to
// use parallel loading of CSS/JS resources later on. This can be achieved by using the
// "For production builds it's recommended to extract the CSS from your bundle so that you are
// able to use parallel loading of CSS/JS resources later on. This can be achieved by using the

or some such? I'm not sure how to parse it as it is.

Copy link
Contributor

Choose a reason for hiding this comment

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

The optimization is really less about parallelization and more that it lets you use the browser's optimized <link rel="stylesheet" href="..." /> hot path for importing your CSS. Also improves caching.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The quotation marks indicate that it is an excerpt from the Webpack website. The main point was that Webpack recommends to do this.

Comment on lines +113 to +119
// Enable CSS modules...
const useCssModules =
// ...UNLESS the filename opts out using a file extension like "filename.global.scss"
!/\.global\.\w+$/i.test(resourcePath) &&
// ...UNLESS this is a .css file.
!/\.css$/i.test(resourcePath);
return useCssModules;

Choose a reason for hiding this comment

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

This is weird enough, maybe:

Suggested change
// Enable CSS modules...
const useCssModules =
// ...UNLESS the filename opts out using a file extension like "filename.global.scss"
!/\.global\.\w+$/i.test(resourcePath) &&
// ...UNLESS this is a .css file.
!/\.css$/i.test(resourcePath);
return useCssModules;
// Files with ".global." in the name should NOT use modules
const isGlobalFileByName = /\.global\.\w+$/i.test(resourcePath);
// Files with the ".css" extension should NOT use modules
const isCssFile = /\.css$/i.test(resourcePath);
// Use modules (i.e.return `true`) if NEITHER of the above exceptions applies
const useCssModules = !isGlobalFileByName && !isCssFile;
return useCssModules;

Copy link
Contributor

Choose a reason for hiding this comment

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

Why shouldn't you be allowed to write raw CSS modules? They're perfectly valid.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@halfnibble had the viewpoint that if you want to make raw CSS modules, you should use the .scss file extension. The comments in this file explain the rationale:

// If someone wants CSS with modules, they can simply use the .sass file extension, since SASS
// is essentially a superset of CSS. (There's a small performance penalty for applying SASS to
// a plain CSS file, but the extra syntax validation probably justifies that cost.)

So the idea is that .css should only be used for actual traditional .css files without any fancy features.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

However I chatted with @iclanton and we've decided to enable autoprefixer for .css files.

Comment on lines +100 to +111
// The "auto" setting has a confusing design:
// - "false" disables CSS modules, i.e. ":local" and ":global" selectors can't be used at all
// - "true" means magically disable CSS modules if the file extension isn't like ".module.css"
// or ".module.scss"
// - a lambda disables CSS modules only if the lambda returns false; the function parameter is
// the resource path
// - a RegExp disables CSS modules only if the resource path does not match the RegExp
//
// NOTE: Counterintuitively, if you instead set "modules=true" then CSS modules are enabled
// without magic, equivalent to "auto: () => true" instead of "auto: true"
//
// DEFAULT: "true" (i.e. path based magic)

Choose a reason for hiding this comment

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

This feels like it belongs in the css-modules documentation, not in our webpack-base.config. I suggest, instead:

Suggested change
// The "auto" setting has a confusing design:
// - "false" disables CSS modules, i.e. ":local" and ":global" selectors can't be used at all
// - "true" means magically disable CSS modules if the file extension isn't like ".module.css"
// or ".module.scss"
// - a lambda disables CSS modules only if the lambda returns false; the function parameter is
// the resource path
// - a RegExp disables CSS modules only if the resource path does not match the RegExp
//
// NOTE: Counterintuitively, if you instead set "modules=true" then CSS modules are enabled
// without magic, equivalent to "auto: () => true" instead of "auto: true"
//
// DEFAULT: "true" (i.e. path based magic)
// We define `auto` as a function so that we can selectively disable CSS modules only in the
// specific cases of (1) files with ".global." in the filename and (2) ".css" files.
// For all other files, we enable CSS modules. See the css-modules documentation for details.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This feels like it belongs in the css-modules documentation, not in our webpack-base.config.

We don't own in the css-modules project. I added these notes for posterity, to save someone else the headache of trying to understand the docs for that setting. :-)

@octogonz octogonz merged commit 39ec31f into master Feb 11, 2022
@octogonz octogonz deleted the octogonz/heft-web-rig-revamp branch February 11, 2022 10:18
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