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

refactor(template-compiler): move implicit stylesheet import and style token to template compiler #4147

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
119 changes: 5 additions & 114 deletions packages/@lwc/compiler/src/transformers/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import * as path from 'path';
import { APIFeature, APIVersion, isAPIFeatureEnabled } from '@lwc/shared';
import {
CompilerError,
normalizeToCompilerError,
Expand Down Expand Up @@ -52,7 +50,9 @@ export default function templateTransform(

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

// TODO [#3733]: remove support for legacy scope tokens
const { scopeToken, legacyScopeToken } = generateScopeTokens(filename, namespace, name);

// Rollup only cares about the mappings property on the map. Since producing a source map for
// the template doesn't make sense, the transform returns an empty mappings.
return {
code: serialize(result.code, filename, scopeToken, legacyScopeToken, apiVersion),
code: result.code,
map: { mappings: '' },
warnings,
cssScopeTokens: [
scopeToken,
`${scopeToken}-host`, // implicit scope token created by `makeHostToken()` in `@lwc/engine-core`
// The legacy tokens must be returned as well since we technically don't know what we're going to render
// This is not strictly required since this is only used for Jest serialization (as of this writing),
// and people are unlikely to set runtime flags in Jest, but it is technically correct to include this.
legacyScopeToken,
`${legacyScopeToken}-host`,
],
cssScopeTokens: result.cssScopeTokens,
};
}

// The reason this hash code implementation [1] is chosen is because:
// 1. It has a very low hash collision rate - testing a list of 466,551 English words [2], it generates no collisions
// 2. It is fast - it can hash those 466k words in 70ms (Node 16, 2020 MacBook Pro)
// 3. The output size is reasonable (32-bit - this can be base-32 encoded at 10-11 characters)
//
// Also note that the reason we're hashing rather than generating a random number is because
// we want the output to be predictable given the input, which helps with caching.
//
// [1]: https://stackoverflow.com/a/52171480
// [2]: https://github.com/dwyl/english-words/blob/a77cb15f4f5beb59c15b945f2415328a6b33c3b0/words.txt
function generateHashCode(str: string) {
const seed = 0;
let h1 = 0xdeadbeef ^ seed;
let h2 = 0x41c6ce57 ^ seed;
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);

return 4294967296 * (2097151 & h2) + (h1 >>> 0);
}

function escapeScopeToken(input: string) {
// Minimal escape for strings containing the "@" and "#" characters, which are disallowed
// in certain cases in attribute names
return input.replace(/@/g, '___at___').replace(/#/g, '___hash___');
}

function generateScopeTokens(
filename: string,
namespace: string | undefined,
name: string | undefined
) {
const uniqueToken = `${namespace}-${name}_${path.basename(filename, path.extname(filename))}`;

// This scope token is all lowercase so that it works correctly in case-sensitive namespaces (e.g. SVG).
// It is deliberately designed to discourage people from relying on it by appearing somewhat random.
// (But not totally random, because it's nice to have stable scope tokens for our own tests.)
// Base-32 is chosen because it is not case-sensitive (0-v), and generates short strings with the given hash
// code implementation (10-11 characters).
const hashCode = generateHashCode(uniqueToken);
const scopeToken = `lwc-${hashCode.toString(32)}`;

// This scope token is based on the namespace and name, and contains a mix of uppercase/lowercase chars
const legacyScopeToken = escapeScopeToken(uniqueToken);

return {
scopeToken,
legacyScopeToken,
};
}

function serialize(
code: string,
filename: string,
scopeToken: string,
legacyScopeToken: string,
apiVersion: APIVersion
): string {
const cssRelPath = `./${path.basename(filename, path.extname(filename))}.css`;
const scopedCssRelPath = `./${path.basename(filename, path.extname(filename))}.scoped.css`;

let buffer = '';
buffer += `import { freezeTemplate } from "lwc";\n\n`;
buffer += `import _implicitStylesheets from "${cssRelPath}";\n\n`;
buffer += `import _implicitScopedStylesheets from "${scopedCssRelPath}?scoped=true";\n\n`;
buffer += code;
buffer += '\n\n';
buffer += 'if (_implicitStylesheets) {\n';
buffer += ` tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitStylesheets);\n`;
buffer += `}\n`;
buffer += 'if (_implicitScopedStylesheets) {\n';
buffer += ` tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitScopedStylesheets);\n`;
buffer += `}\n`;

if (isAPIFeatureEnabled(APIFeature.LOWERCASE_SCOPE_TOKENS, apiVersion)) {
// Include both the new and legacy tokens, so that the runtime can decide based on a flag whether
// we need to render the legacy one. This is designed for cases where the legacy one is required
// for backwards compat (e.g. global stylesheets that rely on the legacy format for a CSS selector).
buffer += `tmpl.stylesheetToken = "${scopeToken}";\n`;
buffer += `tmpl.legacyStylesheetToken = "${legacyScopeToken}";\n`;
} else {
// In old API versions, we can just keep doing what we always did
buffer += `tmpl.stylesheetToken = "${legacyScopeToken}";\n`;
}

// Note that `renderMode` and `slots` are already rendered in @lwc/template-compiler and appear
// as `code` above. At this point, no more expando props should be added to `tmpl`.
buffer += 'freezeTemplate(tmpl);\n';

return buffer;
}
5 changes: 5 additions & 0 deletions packages/@lwc/template-compiler/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ yarn add --dev @lwc/template-compiler
```js
import { compile } from '@lwc/template-compiler';

const filename = 'component.html';
const options = {};
const { code, warnings } = compile(
`
<template>
<h1>Hello World!</h1>
</template>
`,
filename,
options
);

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

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

**Options:**

- `name` (type: `string`, optional, `undefined` by default) - name of the component, e.g. `foo` in `x/foo`.
- `namespace` (type: `string`, optional, `undefined` by default) - namespace of the component, e.g. `x` in `x/foo`.
- `experimentalComputedMemberExpression` (boolean, optional, `false` by default) - set to `true` to enable computed member expression in the template, eg: `{list[0].name}`.
- `experimentalComplexExpressions` (boolean, optional, `false` by default) - set to `true` to enable use of (a subset of) JavaScript expressions in place of template bindings.
- `experimentalDynamicDirective` (boolean, optional, `false` by default) - set to `true` to allow the usage of `lwc:dynamic` directives in the template.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ describe('fixtures', () => {
},
({ src, dirname }) => {
const configPath = path.resolve(dirname, 'config.json');
const filename = path.basename(dirname);

let config: Config = {};
let config: Config = { namespace: 'x', name: filename };
if (fs.existsSync(configPath)) {
config = require(configPath);
config = { ...config, ...require(configPath) };
}

const compiled = compiler(src, config);
const compiled = compiler(src, filename, config);
Copy link
Member Author

Choose a reason for hiding this comment

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

The new snapshots all have the implicit imports, which can be noisey.

As an alternative we could add something to cut out the imports for certain snapshots.

I opted to leave it as-is for now to get opinions.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm fine with the snapshots being a bit noisy. I actually prefer to see what the code actually looks like, rather than trying to hide boilerplate/repetition.

const { warnings, root } = compiled;

// Replace LWC's version with X.X.X so the snapshots don't frequently change
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { parseFragment, registerTemplate } from "lwc";
import _implicitStylesheets from "./attribute-allow.css";
import _implicitScopedStylesheets from "./attribute-allow.scoped.css?scoped=true";
import { freezeTemplate, parseFragment, registerTemplate } from "lwc";
const $fragment1 = parseFragment`<iframe allow="geolocation https://google-developers.appspot.com"${3}></iframe>`;
function tmpl($api, $cmp, $slotset, $ctx) {
const { st: api_static_fragment } = $api;
Expand All @@ -7,3 +9,12 @@ function tmpl($api, $cmp, $slotset, $ctx) {
}
export default registerTemplate(tmpl);
tmpl.stylesheets = [];
tmpl.stylesheetToken = "lwc-q2oidhauj2";
tmpl.legacyStylesheetToken = "x-attribute-allow_attribute-allow";
if (_implicitStylesheets) {
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitStylesheets);
}
if (_implicitScopedStylesheets) {
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitScopedStylesheets);
}
freezeTemplate(tmpl);
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { parseFragment, registerTemplate } from "lwc";
import _implicitStylesheets from "./attribute-crossorigin.css";
import _implicitScopedStylesheets from "./attribute-crossorigin.scoped.css?scoped=true";
import { freezeTemplate, parseFragment, registerTemplate } from "lwc";
const $fragment1 = parseFragment`<img src="http://www.example.com/image.png" crossorigin="anonymous"${3}>`;
const $fragment2 = parseFragment`<video src="http://www.example.com/video.mp4" crossorigin="anonymous"${3}></video>`;
const $fragment3 = parseFragment`<audio src="http://www.example.com/video.mp3" crossorigin="anonymous"${3}></audio>`;
Expand All @@ -13,3 +15,12 @@ function tmpl($api, $cmp, $slotset, $ctx) {
}
export default registerTemplate(tmpl);
tmpl.stylesheets = [];
tmpl.stylesheetToken = "lwc-2bplc35dp3p";
tmpl.legacyStylesheetToken = "x-attribute-crossorigin_attribute-crossorigin";
if (_implicitStylesheets) {
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitStylesheets);
}
if (_implicitScopedStylesheets) {
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitScopedStylesheets);
}
freezeTemplate(tmpl);
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { parseFragment, registerTemplate } from "lwc";
import _implicitStylesheets from "./attribute-href-with-id-expression.css";
import _implicitScopedStylesheets from "./attribute-href-with-id-expression.scoped.css?scoped=true";
import { freezeTemplate, parseFragment, registerTemplate } from "lwc";
const $fragment1 = parseFragment`<a${"a0:href"}${3}>KIX</a>`;
const stc0 = {
key: 2,
Expand Down Expand Up @@ -53,3 +55,13 @@ function tmpl($api, $cmp, $slotset, $ctx) {
}
export default registerTemplate(tmpl);
tmpl.stylesheets = [];
tmpl.stylesheetToken = "lwc-4s0jmj9uli4";
tmpl.legacyStylesheetToken =
"x-attribute-href-with-id-expression_attribute-href-with-id-expression";
if (_implicitStylesheets) {
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitStylesheets);
}
if (_implicitScopedStylesheets) {
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitScopedStylesheets);
}
freezeTemplate(tmpl);
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { registerTemplate } from "lwc";
import _implicitStylesheets from "./attribute-href-with-id.css";
import _implicitScopedStylesheets from "./attribute-href-with-id.scoped.css?scoped=true";
import { freezeTemplate, registerTemplate } from "lwc";
const stc0 = {
key: 1,
};
Expand Down Expand Up @@ -49,3 +51,12 @@ function tmpl($api, $cmp, $slotset, $ctx) {
}
export default registerTemplate(tmpl);
tmpl.stylesheets = [];
tmpl.stylesheetToken = "lwc-k7u0u67but";
tmpl.legacyStylesheetToken = "x-attribute-href-with-id_attribute-href-with-id";
if (_implicitStylesheets) {
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitStylesheets);
}
if (_implicitScopedStylesheets) {
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitScopedStylesheets);
}
freezeTemplate(tmpl);
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { registerTemplate } from "lwc";
import _implicitStylesheets from "./attribute-href.css";
import _implicitScopedStylesheets from "./attribute-href.scoped.css?scoped=true";
import { freezeTemplate, registerTemplate } from "lwc";
const stc0 = {
attrs: {
href: "#yasaka-taxi",
Expand Down Expand Up @@ -33,3 +35,12 @@ function tmpl($api, $cmp, $slotset, $ctx) {
}
export default registerTemplate(tmpl);
tmpl.stylesheets = [];
tmpl.stylesheetToken = "lwc-5jt7h1qitjc";
tmpl.legacyStylesheetToken = "x-attribute-href_attribute-href";
if (_implicitStylesheets) {
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitStylesheets);
}
if (_implicitScopedStylesheets) {
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitScopedStylesheets);
}
freezeTemplate(tmpl);
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { parseFragment, registerTemplate } from "lwc";
import _implicitStylesheets from "./attribute-part.css";
import _implicitScopedStylesheets from "./attribute-part.scoped.css?scoped=true";
import { freezeTemplate, parseFragment, registerTemplate } from "lwc";
const $fragment1 = parseFragment`<div part="foo"${3}></div>`;
function tmpl($api, $cmp, $slotset, $ctx) {
const { st: api_static_fragment } = $api;
Expand All @@ -7,3 +9,12 @@ function tmpl($api, $cmp, $slotset, $ctx) {
}
export default registerTemplate(tmpl);
tmpl.stylesheets = [];
tmpl.stylesheetToken = "lwc-4plj0m3hj0";
tmpl.legacyStylesheetToken = "x-attribute-part_attribute-part";
if (_implicitStylesheets) {
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitStylesheets);
}
if (_implicitScopedStylesheets) {
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitScopedStylesheets);
}
freezeTemplate(tmpl);
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import _implicitStylesheets from "./attribute-props-transform.css";
import _implicitScopedStylesheets from "./attribute-props-transform.scoped.css?scoped=true";
import _xFoo from "x/foo";
import { registerTemplate } from "lwc";
import { freezeTemplate, registerTemplate } from "lwc";
function tmpl($api, $cmp, $slotset, $ctx) {
const { gid: api_scoped_id, c: api_custom_element } = $api;
return [
Expand Down Expand Up @@ -81,3 +83,13 @@ function tmpl($api, $cmp, $slotset, $ctx) {
}
export default registerTemplate(tmpl);
tmpl.stylesheets = [];
tmpl.stylesheetToken = "lwc-6dvmnra325n";
tmpl.legacyStylesheetToken =
"x-attribute-props-transform_attribute-props-transform";
if (_implicitStylesheets) {
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitStylesheets);
}
if (_implicitScopedStylesheets) {
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitScopedStylesheets);
}
freezeTemplate(tmpl);
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import _implicitStylesheets from "./attribute-underscore.css";
import _implicitScopedStylesheets from "./attribute-underscore.scoped.css?scoped=true";
import _xButton from "x/button";
import { registerTemplate } from "lwc";
import { freezeTemplate, registerTemplate } from "lwc";
const stc0 = {
props: {
foo__bar: "underscore",
Expand Down Expand Up @@ -29,3 +31,12 @@ function tmpl($api, $cmp, $slotset, $ctx) {
}
export default registerTemplate(tmpl);
tmpl.stylesheets = [];
tmpl.stylesheetToken = "lwc-36q7c97l1ni";
tmpl.legacyStylesheetToken = "x-attribute-underscore_attribute-underscore";
if (_implicitStylesheets) {
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitStylesheets);
}
if (_implicitScopedStylesheets) {
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitScopedStylesheets);
}
freezeTemplate(tmpl);
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import _implicitStylesheets from "./attribute-uppercase.css";
import _implicitScopedStylesheets from "./attribute-uppercase.scoped.css?scoped=true";
import _xButton from "x/button";
import { registerTemplate } from "lwc";
import { freezeTemplate, registerTemplate } from "lwc";
const stc0 = {
props: {
Class: "r",
Expand All @@ -19,3 +21,12 @@ function tmpl($api, $cmp, $slotset, $ctx) {
}
export default registerTemplate(tmpl);
tmpl.stylesheets = [];
tmpl.stylesheetToken = "lwc-uguh0pjd13";
tmpl.legacyStylesheetToken = "x-attribute-uppercase_attribute-uppercase";
if (_implicitStylesheets) {
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitStylesheets);
}
if (_implicitScopedStylesheets) {
tmpl.stylesheets.push.apply(tmpl.stylesheets, _implicitScopedStylesheets);
}
freezeTemplate(tmpl);