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

fix(compiler): support multiple styleUrls in components #5090

Merged
merged 17 commits into from
Nov 30, 2023

Conversation

christian-bromann
Copy link
Member

What is the current behavior?

Currently if multiple style urls are provided, as described in the docs:

@Component({
  tag: 'my-component',
  styleUrls: ['reset.scss', 'roboto.scss', 'css-variables.scss', 'core.scss'] 
})
export class MyComponent {
  // ...
}

Only the first (reset.css) style would get applied to the component.

GitHub Issue Number: #5016

What is the new behavior?

Instead of just compiling the first external style, I make it loop through all external styles and join the style strings together. Here is an example:

const barCss = ":host{display:block}";
const fooCss = ":host{display:none}";

const MyComponent = class {
    // ...
};
MyComponent.style = barCss + fooCss;

export { MyComponent as my_component };

Does this introduce a breaking change?

  • Yes
  • No

Testing

Working on adding unit tests and integration tests for it.

Other information

Copy link
Contributor

github-actions bot commented Nov 20, 2023

--strictNullChecks error report

Typechecking with --strictNullChecks resulted in 1364 errors on this branch.

Unfortunately, it looks like that's an increase of 1 over main 😞.

Violations Not on `main` (may be more than the count above)
Path Location Error Message
src/compiler/transformers/stencil-import-path.ts (68, 5) TS2322
src/compiler/transformers/stencil-import-path.ts (69, 5) TS2322
src/compiler/transformers/stencil-import-path.ts (70, 5) TS2322
src/compiler/transformers/transform-utils.ts (1112, 5) TS2532

reports and statistics

Our most error-prone files
Path Error Count
src/dev-server/index.ts 37
src/mock-doc/serialize-node.ts 36
src/dev-server/server-process.ts 32
src/compiler/build/build-stats.ts 23
src/compiler/style/test/optimize-css.spec.ts 23
src/testing/puppeteer/puppeteer-element.ts 23
src/compiler/output-targets/dist-lazy/generate-lazy-module.ts 22
src/compiler/prerender/prerender-main.ts 22
src/runtime/client-hydrate.ts 20
src/screenshot/connector-base.ts 19
src/runtime/vdom/vdom-render.ts 18
src/compiler/config/test/validate-paths.spec.ts 16
src/dev-server/request-handler.ts 15
src/compiler/prerender/prerender-optimize.ts 14
src/compiler/sys/stencil-sys.ts 14
src/compiler/transpile/transpile-module.ts 14
src/runtime/vdom/vdom-annotations.ts 14
src/sys/node/node-sys.ts 14
src/compiler/build/build-finish.ts 13
src/compiler/prerender/prerender-queue.ts 13
Our most common errors
Typescript Error Code Count
TS2345 403
TS2322 385
TS18048 304
TS18047 101
TS2722 38
TS2532 35
TS2531 23
TS2454 14
TS2352 13
TS2769 10
TS2790 10
TS2538 8
TS2416 6
TS2344 5
TS2493 3
TS2488 2
TS18046 2
TS2684 1
TS2430 1

Unused exports report

There are 14 unused exports on this PR. That's the same number of errors on main, so at least we're not creating new ones!

Unused exports
File Line Identifier
src/runtime/bootstrap-lazy.ts 21 setNonce
src/screenshot/screenshot-fs.ts 18 readScreenshotData
src/testing/testing-utils.ts 198 withSilentWarn
src/utils/index.ts 145 CUSTOM
src/utils/index.ts 269 normalize
src/utils/index.ts 7 escapeRegExpSpecialCharacters
src/compiler/app-core/app-data.ts 25 BUILD
src/compiler/app-core/app-data.ts 115 Env
src/compiler/app-core/app-data.ts 117 NAMESPACE
src/compiler/fs-watch/fs-watch-rebuild.ts 123 updateCacheFromRebuild
src/compiler/types/validate-primary-package-output-target.ts 61 satisfies
src/compiler/types/validate-primary-package-output-target.ts 61 Record
src/testing/puppeteer/puppeteer-declarations.ts 485 WaitForEventOptions
src/compiler/sys/fetch/write-fetch-success.ts 7 writeFetchSuccessSync

@@ -88,17 +89,3 @@ const createStyleLiteral = (cmp: d.ComponentCompilerMeta, style: d.StyleCompiler

return ts.factory.createStringLiteral(style.styleStr);
};

const createStyleIdentifierFromUrl = (cmp: d.ComponentCompilerMeta, style: d.StyleCompiler) => {
Copy link
Member Author

Choose a reason for hiding this comment

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

It seems to be the same function as in src/compiler/transformers/add-static-style.ts. Please let me know if there is any reason why the code was duplicated. Other functions in this file have also the same implementation with minor nuances. My concern here is that maybe some fixes were applied in one file but not the other.

Copy link
Member

Choose a reason for hiding this comment

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

I think this is a safe move to make 👍 Good find!

@christian-bromann christian-bromann marked this pull request as ready for review November 21, 2023 00:23
@christian-bromann christian-bromann requested a review from a team as a code owner November 21, 2023 00:23
@@ -28,7 +29,7 @@ export const serializeImportPath = (data: SerializeImportData, styleImportData:
p = './' + p;
}

if (styleImportData === 'queryparams' || styleImportData === undefined) {
if (moduleSystem !== 'cjs' && (styleImportData === 'queryparams' || styleImportData === undefined)) {
Copy link
Member Author

@christian-bromann christian-bromann Nov 27, 2023

Choose a reason for hiding this comment

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

I saw the following failures in unit tests:

Cannot find module './my-component.css?tag=my-component&encapsulation=shadow' from 'src/components/my-component/my-component.tsx'

as Jest tried to parse and run the component that would require the css files with queryparams don't have any function CJS land and confuse Jests resolver. This tweak makes sure we don't attach them when we are explicitly transforming to CJS.

Copy link
Contributor

Choose a reason for hiding this comment

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

is it possible this would cause problems if a user is trying to transpile to commonjs?

Copy link
Member Author

Choose a reason for hiding this comment

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

It depends I guess. Using query params in require calls is actually not supported and would fail, e.g.

CJS:

const bar = require('./b?foo=bar')
// throws `Error: Cannot find module './b?foo=bar'`

ESM:

import b from './b.js?foo=bar'
// works 👍

Now, I can imagine that there are use cases where folks compile it into CJS which then gets picked up by another bundler that compiles it back in ESM. If we assume that the compiler respects the require params and ports them along than it could introduce issues.

Copy link
Contributor

Choose a reason for hiding this comment

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

ok cool, I'm not worried then!

@christian-bromann
Copy link
Member Author

This is ready for review now 🙌

Copy link
Contributor

@alicewriteswrongs alicewriteswrongs left a comment

Choose a reason for hiding this comment

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

tried it out locally and it looks great! Just had some comments and questions when reading through

Comment on lines +170 to 171
return ts.factory.createIdentifier(getIdentifierFromResourceUrl(externalStyles[0].absolutePath));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

is it right to say that we could just use the pascal-case version of the tag name for the identifier before because we were always just using the first entry, so we didn't have to worry about name collisions or anything? I guess know we know that the filenames specified in styleUrls have to be unique so if we have a one-to-one map from the absolutePath to the style identifier then we'll never have a collision

Copy link
Member Author

Choose a reason for hiding this comment

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

That is correct, before we used the component name as identifier which we can't anymore when importing multiple style sheets. getIdentifierFromResourceUrl is intended to return a unique identifier based on the absolute path. Now this has its own challenges, re special characters etc. An alternative would be to create a md5 sha based on the absolute path.

Note that the code that handles the imports and the one handling attaching them to the Component class are located in different functions so the only common identifier information is the absolute path.

return ts.factory.createBinaryExpression(
createStyleIdentifierFromUrl([firstExternalStyle]),
ts.SyntaxKind.PlusToken,
createStyleIdentifierFromUrl(externalStyles.slice(1)),
Copy link
Contributor

Choose a reason for hiding this comment

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

so if we have, say, 3 styleUrls this will just recursively build up a string by adding another binary expression? nice!

Copy link
Member Author

Choose a reason for hiding this comment

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

Correct 😉

src/compiler/transformers/add-static-style.ts Show resolved Hide resolved
Comment on lines +161 to +162
* Note: style imports are made in [`createEsmStyleImport`](src/compiler/transformers/style-imports.ts).
*
Copy link
Contributor

Choose a reason for hiding this comment

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

oh I guess this explains that haha

@@ -28,7 +29,7 @@ export const serializeImportPath = (data: SerializeImportData, styleImportData:
p = './' + p;
}

if (styleImportData === 'queryparams' || styleImportData === undefined) {
if (moduleSystem !== 'cjs' && (styleImportData === 'queryparams' || styleImportData === undefined)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

is it possible this would cause problems if a user is trying to transpile to commonjs?

Comment on lines 95 to 98
// e.g. `import _ImportPathStyle from './import-path.css';`
const importName = getIdentifierFromResourceUrl(externalStyle.absolutePath);
const importIdentifier = ts.factory.createIdentifier(importName);
const importPath = getStyleImportPath(transformOpts, tsSourceFile, cmp, style, externalStyle.absolutePath);
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe we should add a note here that we have implicit coupling between the creation of the import identifier here and the style getters we create elsewhere? I'd just want to make sure that anyone who might want to change this code in the future knows that the content of importIdentifier here is quite important, and if it's changed we'd need to make changes in a bunch of other spots too

Copy link
Member Author

Choose a reason for hiding this comment

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

I will add additional comments to address this.

Comment on lines 1107 to 1120
const SPECIAL_CHARS = /[\s~`!@#$%\^&*+=\-\[\]\\';,/{}|\\":<>\?()\._]/g;
/**
* transform any path to a valid identifier, e.g.
* - `/foo/bar/loo.css` -> `_fooBarLooCssStyle`
* - `C:\\foo\bar\loo.css` -> `_cFooBarLooCssStyle`
*
* @param absolutePath windows or linux based path
* @returns a valid identifier to be used as variable name
*/
export const getIdentifierFromResourceUrl = (absolutePath: string): string => {
return `_${dashToPascalCase(
absolutePath.split('?').shift().toLocaleLowerCase().replace(SPECIAL_CHARS, '-').replace(/--/, '-'),
)}Style`;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

question - do you think we need to worry about escaping special characters here, rather than just removing them? I was able to generate a collision between two different valid POSIX paths like so (in the spec file):

    expect(
      getIdentifierFromResourceUrl("/foo--bar.css")
    ).not.toEqual(
      getIdentifierFromResourceUrl("/foo-bar.css")
    )

they both get transformed to _FooBarCssStyle.css

Copy link
Contributor

Choose a reason for hiding this comment

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

I can trigger this condition from a component if I have a styleUrls array like so:

Screenshot 2023-11-29 at 12 12 53 PM

I get a rollup error because of variable redeclaration (or import identifier re-declaration I guess)

Screenshot 2023-11-29 at 12 12 41 PM

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch! I will make a tweak to the function to address this.

Copy link
Member

@tanner-reits tanner-reits left a comment

Choose a reason for hiding this comment

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

LGTM w/ changes from Alice's comments! Appreciate all the code commenting you did as well!

@@ -88,17 +89,3 @@ const createStyleLiteral = (cmp: d.ComponentCompilerMeta, style: d.StyleCompiler

return ts.factory.createStringLiteral(style.styleStr);
};

const createStyleIdentifierFromUrl = (cmp: d.ComponentCompilerMeta, style: d.StyleCompiler) => {
Copy link
Member

Choose a reason for hiding this comment

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

I think this is a safe move to make 👍 Good find!

Copy link
Contributor

@alicewriteswrongs alicewriteswrongs left a comment

Choose a reason for hiding this comment

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

looks good to me!

@christian-bromann christian-bromann added this pull request to the merge queue Nov 30, 2023
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Nov 30, 2023
@christian-bromann christian-bromann added this pull request to the merge queue Nov 30, 2023
Merged via the queue into main with commit 54e52da Nov 30, 2023
120 checks passed
@christian-bromann christian-bromann deleted the cb/fix-5016 branch November 30, 2023 19:42
christian-bromann added a commit that referenced this pull request Dec 4, 2023
We recently landed changes in main (not released) related to support multiple style urls. These changes however have caused failures when building upstream project, e.g. in Ionic framework (see https://github.com/ionic-team/ionic-framework/actions/runs/7064837482/job/19233697164).

This patch reverts these changes to unblock upcoming releases.

Revert "fix(compiler): ensure not to import duplicate style identifier (#5119)"

This reverts commit 7591dce.

Revert "fix(compiler): support multiple styleUrls in components (#5090)"

This reverts commit 54e52da.
github-merge-queue bot pushed a commit that referenced this pull request Dec 4, 2023
We recently landed changes in main (not released) related to support multiple style urls. These changes however have caused failures when building upstream project, e.g. in Ionic framework (see https://github.com/ionic-team/ionic-framework/actions/runs/7064837482/job/19233697164).

This patch reverts these changes to unblock upcoming releases.

Revert "fix(compiler): ensure not to import duplicate style identifier (#5119)"

This reverts commit 7591dce.

Revert "fix(compiler): support multiple styleUrls in components (#5090)"

This reverts commit 54e52da.
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.

3 participants