Skip to content

Commit c0423f5

Browse files
jmsjtunolanlawson
andauthored
refactor(template-compiler): move implicit stylesheet import and style token to template compiler (#4147)
Co-authored-by: Nolan Lawson <nlawson@salesforce.com>
1 parent e7934d1 commit c0423f5

File tree

347 files changed

+4392
-500
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

347 files changed

+4392
-500
lines changed

packages/@lwc/compiler/src/transformers/template.ts

Lines changed: 5 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
* SPDX-License-Identifier: MIT
55
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
66
*/
7-
import * as path from 'path';
8-
import { APIFeature, APIVersion, isAPIFeatureEnabled } from '@lwc/shared';
97
import {
108
CompilerError,
119
normalizeToCompilerError,
@@ -52,7 +50,9 @@ export default function templateTransform(
5250

5351
let result;
5452
try {
55-
result = compile(src, {
53+
result = compile(src, filename, {
54+
name,
55+
namespace,
5656
experimentalDynamicDirective,
5757
// TODO [#3370]: remove experimental template expression flag
5858
experimentalComplexExpressions,
@@ -76,121 +76,12 @@ export default function templateTransform(
7676
// thrown above. As for "Log" and "Fatal", they are currently unused.
7777
const warnings = result.warnings.filter((_) => _.level === DiagnosticLevel.Warning);
7878

79-
// TODO [#3733]: remove support for legacy scope tokens
80-
const { scopeToken, legacyScopeToken } = generateScopeTokens(filename, namespace, name);
81-
8279
// Rollup only cares about the mappings property on the map. Since producing a source map for
8380
// the template doesn't make sense, the transform returns an empty mappings.
8481
return {
85-
code: serialize(result.code, filename, scopeToken, legacyScopeToken, apiVersion),
82+
code: result.code,
8683
map: { mappings: '' },
8784
warnings,
88-
cssScopeTokens: [
89-
scopeToken,
90-
`${scopeToken}-host`, // implicit scope token created by `makeHostToken()` in `@lwc/engine-core`
91-
// The legacy tokens must be returned as well since we technically don't know what we're going to render
92-
// This is not strictly required since this is only used for Jest serialization (as of this writing),
93-
// and people are unlikely to set runtime flags in Jest, but it is technically correct to include this.
94-
legacyScopeToken,
95-
`${legacyScopeToken}-host`,
96-
],
85+
cssScopeTokens: result.cssScopeTokens,
9786
};
9887
}
99-
100-
// The reason this hash code implementation [1] is chosen is because:
101-
// 1. It has a very low hash collision rate - testing a list of 466,551 English words [2], it generates no collisions
102-
// 2. It is fast - it can hash those 466k words in 70ms (Node 16, 2020 MacBook Pro)
103-
// 3. The output size is reasonable (32-bit - this can be base-32 encoded at 10-11 characters)
104-
//
105-
// Also note that the reason we're hashing rather than generating a random number is because
106-
// we want the output to be predictable given the input, which helps with caching.
107-
//
108-
// [1]: https://stackoverflow.com/a/52171480
109-
// [2]: https://github.com/dwyl/english-words/blob/a77cb15f4f5beb59c15b945f2415328a6b33c3b0/words.txt
110-
function generateHashCode(str: string) {
111-
const seed = 0;
112-
let h1 = 0xdeadbeef ^ seed;
113-
let h2 = 0x41c6ce57 ^ seed;
114-
for (let i = 0, ch; i < str.length; i++) {
115-
ch = str.charCodeAt(i);
116-
h1 = Math.imul(h1 ^ ch, 2654435761);
117-
h2 = Math.imul(h2 ^ ch, 1597334677);
118-
}
119-
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
120-
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
121-
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
122-
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
123-
124-
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
125-
}
126-
127-
function escapeScopeToken(input: string) {
128-
// Minimal escape for strings containing the "@" and "#" characters, which are disallowed
129-
// in certain cases in attribute names
130-
return input.replace(/@/g, '___at___').replace(/#/g, '___hash___');
131-
}
132-
133-
function generateScopeTokens(
134-
filename: string,
135-
namespace: string | undefined,
136-
name: string | undefined
137-
) {
138-
const uniqueToken = `${namespace}-${name}_${path.basename(filename, path.extname(filename))}`;
139-
140-
// This scope token is all lowercase so that it works correctly in case-sensitive namespaces (e.g. SVG).
141-
// It is deliberately designed to discourage people from relying on it by appearing somewhat random.
142-
// (But not totally random, because it's nice to have stable scope tokens for our own tests.)
143-
// Base-32 is chosen because it is not case-sensitive (0-v), and generates short strings with the given hash
144-
// code implementation (10-11 characters).
145-
const hashCode = generateHashCode(uniqueToken);
146-
const scopeToken = `lwc-${hashCode.toString(32)}`;
147-
148-
// This scope token is based on the namespace and name, and contains a mix of uppercase/lowercase chars
149-
const legacyScopeToken = escapeScopeToken(uniqueToken);
150-
151-
return {
152-
scopeToken,
153-
legacyScopeToken,
154-
};
155-
}
156-
157-
function serialize(
158-
code: string,
159-
filename: string,
160-
scopeToken: string,
161-
legacyScopeToken: string,
162-
apiVersion: APIVersion
163-
): string {
164-
const cssRelPath = `./${path.basename(filename, path.extname(filename))}.css`;
165-
const scopedCssRelPath = `./${path.basename(filename, path.extname(filename))}.scoped.css`;
166-
167-
let buffer = '';
168-
buffer += `import { freezeTemplate } from "lwc";\n\n`;
169-
buffer += `import _implicitStylesheets from "${cssRelPath}";\n\n`;
170-
buffer += `import _implicitScopedStylesheets from "${scopedCssRelPath}?scoped=true";\n\n`;
171-
buffer += code;
172-
buffer += '\n\n';
173-
buffer += 'if (_implicitStylesheets) {\n';
174-
buffer += ` tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitStylesheets);\n`;
175-
buffer += `}\n`;
176-
buffer += 'if (_implicitScopedStylesheets) {\n';
177-
buffer += ` tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitScopedStylesheets);\n`;
178-
buffer += `}\n`;
179-
180-
if (isAPIFeatureEnabled(APIFeature.LOWERCASE_SCOPE_TOKENS, apiVersion)) {
181-
// Include both the new and legacy tokens, so that the runtime can decide based on a flag whether
182-
// we need to render the legacy one. This is designed for cases where the legacy one is required
183-
// for backwards compat (e.g. global stylesheets that rely on the legacy format for a CSS selector).
184-
buffer += `tmpl.stylesheetToken = "${scopeToken}";\n`;
185-
buffer += `tmpl.legacyStylesheetToken = "${legacyScopeToken}";\n`;
186-
} else {
187-
// In old API versions, we can just keep doing what we always did
188-
buffer += `tmpl.stylesheetToken = "${legacyScopeToken}";\n`;
189-
}
190-
191-
// Note that `renderMode` and `slots` are already rendered in @lwc/template-compiler and appear
192-
// as `code` above. At this point, no more expando props should be added to `tmpl`.
193-
buffer += 'freezeTemplate(tmpl);\n';
194-
195-
return buffer;
196-
}

packages/@lwc/template-compiler/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ yarn add --dev @lwc/template-compiler
1313
```js
1414
import { compile } from '@lwc/template-compiler';
1515

16+
const filename = 'component.html';
1617
const options = {};
1718
const { code, warnings } = compile(
1819
`
1920
<template>
2021
<h1>Hello World!</h1>
2122
</template>
2223
`,
24+
filename,
2325
options
2426
);
2527

@@ -44,10 +46,13 @@ const { code, warnings } = compile(`<template><h1>Hello World!</h1></template>`,
4446
**Parameters:**
4547

4648
- `source` (string, required) - the HTML template source to compile.
49+
- `filename` (string, required) - the source filename with extension.
4750
- `options` (object, required) - the options to used to compile the HTML template source.
4851

4952
**Options:**
5053

54+
- `name` (type: `string`, optional, `undefined` by default) - name of the component, e.g. `foo` in `x/foo`.
55+
- `namespace` (type: `string`, optional, `undefined` by default) - namespace of the component, e.g. `x` in `x/foo`.
5156
- `experimentalComputedMemberExpression` (boolean, optional, `false` by default) - set to `true` to enable computed member expression in the template, eg: `{list[0].name}`.
5257
- `experimentalComplexExpressions` (boolean, optional, `false` by default) - set to `true` to enable use of (a subset of) JavaScript expressions in place of template bindings.
5358
- `experimentalDynamicDirective` (boolean, optional, `false` by default) - set to `true` to allow the usage of `lwc:dynamic` directives in the template.

packages/@lwc/template-compiler/src/__tests__/fixtures.spec.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ describe('fixtures', () => {
2020
},
2121
({ src, dirname }) => {
2222
const configPath = path.resolve(dirname, 'config.json');
23+
const filename = path.basename(dirname);
2324

24-
let config: Config = {};
25+
let config: Config = { namespace: 'x', name: filename };
2526
if (fs.existsSync(configPath)) {
26-
config = require(configPath);
27+
config = { ...config, ...require(configPath) };
2728
}
2829

29-
const compiled = compiler(src, config);
30+
const compiled = compiler(src, filename, config);
3031
const { warnings, root } = compiled;
3132

3233
// Replace LWC's version with X.X.X so the snapshots don't frequently change
Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { parseFragment, registerTemplate } from "lwc";
1+
import _implicitStylesheets from "./attribute-allow.css";
2+
import _implicitScopedStylesheets from "./attribute-allow.scoped.css?scoped=true";
3+
import { freezeTemplate, parseFragment, registerTemplate } from "lwc";
24
const $fragment1 = parseFragment`<iframe allow="geolocation https://google-developers.appspot.com"${3}></iframe>`;
35
function tmpl($api, $cmp, $slotset, $ctx) {
46
const { st: api_static_fragment } = $api;
@@ -7,3 +9,12 @@ function tmpl($api, $cmp, $slotset, $ctx) {
79
}
810
export default registerTemplate(tmpl);
911
tmpl.stylesheets = [];
12+
tmpl.stylesheetToken = "lwc-q2oidhauj2";
13+
tmpl.legacyStylesheetToken = "x-attribute-allow_attribute-allow";
14+
if (_implicitStylesheets) {
15+
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitStylesheets);
16+
}
17+
if (_implicitScopedStylesheets) {
18+
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitScopedStylesheets);
19+
}
20+
freezeTemplate(tmpl);

packages/@lwc/template-compiler/src/__tests__/fixtures/attributes/attribute-crossorigin/expected.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { parseFragment, registerTemplate } from "lwc";
1+
import _implicitStylesheets from "./attribute-crossorigin.css";
2+
import _implicitScopedStylesheets from "./attribute-crossorigin.scoped.css?scoped=true";
3+
import { freezeTemplate, parseFragment, registerTemplate } from "lwc";
24
const $fragment1 = parseFragment`<img src="http://www.example.com/image.png" crossorigin="anonymous"${3}>`;
35
const $fragment2 = parseFragment`<video src="http://www.example.com/video.mp4" crossorigin="anonymous"${3}></video>`;
46
const $fragment3 = parseFragment`<audio src="http://www.example.com/video.mp3" crossorigin="anonymous"${3}></audio>`;
@@ -13,3 +15,12 @@ function tmpl($api, $cmp, $slotset, $ctx) {
1315
}
1416
export default registerTemplate(tmpl);
1517
tmpl.stylesheets = [];
18+
tmpl.stylesheetToken = "lwc-2bplc35dp3p";
19+
tmpl.legacyStylesheetToken = "x-attribute-crossorigin_attribute-crossorigin";
20+
if (_implicitStylesheets) {
21+
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitStylesheets);
22+
}
23+
if (_implicitScopedStylesheets) {
24+
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitScopedStylesheets);
25+
}
26+
freezeTemplate(tmpl);

packages/@lwc/template-compiler/src/__tests__/fixtures/attributes/attribute-href-with-id-expression/expected.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { parseFragment, registerTemplate } from "lwc";
1+
import _implicitStylesheets from "./attribute-href-with-id-expression.css";
2+
import _implicitScopedStylesheets from "./attribute-href-with-id-expression.scoped.css?scoped=true";
3+
import { freezeTemplate, parseFragment, registerTemplate } from "lwc";
24
const $fragment1 = parseFragment`<a${"a0:href"}${3}>KIX</a>`;
35
const stc0 = {
46
key: 2,
@@ -53,3 +55,13 @@ function tmpl($api, $cmp, $slotset, $ctx) {
5355
}
5456
export default registerTemplate(tmpl);
5557
tmpl.stylesheets = [];
58+
tmpl.stylesheetToken = "lwc-4s0jmj9uli4";
59+
tmpl.legacyStylesheetToken =
60+
"x-attribute-href-with-id-expression_attribute-href-with-id-expression";
61+
if (_implicitStylesheets) {
62+
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitStylesheets);
63+
}
64+
if (_implicitScopedStylesheets) {
65+
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitScopedStylesheets);
66+
}
67+
freezeTemplate(tmpl);

packages/@lwc/template-compiler/src/__tests__/fixtures/attributes/attribute-href-with-id/expected.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { registerTemplate } from "lwc";
1+
import _implicitStylesheets from "./attribute-href-with-id.css";
2+
import _implicitScopedStylesheets from "./attribute-href-with-id.scoped.css?scoped=true";
3+
import { freezeTemplate, registerTemplate } from "lwc";
24
const stc0 = {
35
key: 1,
46
};
@@ -49,3 +51,12 @@ function tmpl($api, $cmp, $slotset, $ctx) {
4951
}
5052
export default registerTemplate(tmpl);
5153
tmpl.stylesheets = [];
54+
tmpl.stylesheetToken = "lwc-k7u0u67but";
55+
tmpl.legacyStylesheetToken = "x-attribute-href-with-id_attribute-href-with-id";
56+
if (_implicitStylesheets) {
57+
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitStylesheets);
58+
}
59+
if (_implicitScopedStylesheets) {
60+
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitScopedStylesheets);
61+
}
62+
freezeTemplate(tmpl);

packages/@lwc/template-compiler/src/__tests__/fixtures/attributes/attribute-href/expected.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { registerTemplate } from "lwc";
1+
import _implicitStylesheets from "./attribute-href.css";
2+
import _implicitScopedStylesheets from "./attribute-href.scoped.css?scoped=true";
3+
import { freezeTemplate, registerTemplate } from "lwc";
24
const stc0 = {
35
attrs: {
46
href: "#yasaka-taxi",
@@ -33,3 +35,12 @@ function tmpl($api, $cmp, $slotset, $ctx) {
3335
}
3436
export default registerTemplate(tmpl);
3537
tmpl.stylesheets = [];
38+
tmpl.stylesheetToken = "lwc-5jt7h1qitjc";
39+
tmpl.legacyStylesheetToken = "x-attribute-href_attribute-href";
40+
if (_implicitStylesheets) {
41+
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitStylesheets);
42+
}
43+
if (_implicitScopedStylesheets) {
44+
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitScopedStylesheets);
45+
}
46+
freezeTemplate(tmpl);
Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { parseFragment, registerTemplate } from "lwc";
1+
import _implicitStylesheets from "./attribute-part.css";
2+
import _implicitScopedStylesheets from "./attribute-part.scoped.css?scoped=true";
3+
import { freezeTemplate, parseFragment, registerTemplate } from "lwc";
24
const $fragment1 = parseFragment`<div part="foo"${3}></div>`;
35
function tmpl($api, $cmp, $slotset, $ctx) {
46
const { st: api_static_fragment } = $api;
@@ -7,3 +9,12 @@ function tmpl($api, $cmp, $slotset, $ctx) {
79
}
810
export default registerTemplate(tmpl);
911
tmpl.stylesheets = [];
12+
tmpl.stylesheetToken = "lwc-4plj0m3hj0";
13+
tmpl.legacyStylesheetToken = "x-attribute-part_attribute-part";
14+
if (_implicitStylesheets) {
15+
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitStylesheets);
16+
}
17+
if (_implicitScopedStylesheets) {
18+
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitScopedStylesheets);
19+
}
20+
freezeTemplate(tmpl);

packages/@lwc/template-compiler/src/__tests__/fixtures/attributes/attribute-props-transform/expected.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import _implicitStylesheets from "./attribute-props-transform.css";
2+
import _implicitScopedStylesheets from "./attribute-props-transform.scoped.css?scoped=true";
13
import _xFoo from "x/foo";
2-
import { registerTemplate } from "lwc";
4+
import { freezeTemplate, registerTemplate } from "lwc";
35
function tmpl($api, $cmp, $slotset, $ctx) {
46
const { gid: api_scoped_id, c: api_custom_element } = $api;
57
return [
@@ -81,3 +83,13 @@ function tmpl($api, $cmp, $slotset, $ctx) {
8183
}
8284
export default registerTemplate(tmpl);
8385
tmpl.stylesheets = [];
86+
tmpl.stylesheetToken = "lwc-6dvmnra325n";
87+
tmpl.legacyStylesheetToken =
88+
"x-attribute-props-transform_attribute-props-transform";
89+
if (_implicitStylesheets) {
90+
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitStylesheets);
91+
}
92+
if (_implicitScopedStylesheets) {
93+
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitScopedStylesheets);
94+
}
95+
freezeTemplate(tmpl);

0 commit comments

Comments
 (0)