-
Notifications
You must be signed in to change notification settings - Fork 394
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(ssr-compiler): implement scoped styles and scope tokens #4567
Changes from all commits
bbdfe8f
de2037a
f427370
7cb2715
a0f86b1
f61cfee
08d4a9a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,7 +12,7 @@ import { AriaPropNameToAttrNameMap } from '@lwc/shared'; | |
|
||
import { replaceLwcImport } from './lwc-import'; | ||
import { catalogTmplImport } from './catalog-tmpls'; | ||
import { addStylesheetImports, catalogStaticStylesheets, catalogStyleImport } from './stylesheets'; | ||
import { catalogStaticStylesheets, catalogStyleImport } from './stylesheets'; | ||
import { addGenerateMarkupExport } from './generate-markup'; | ||
|
||
import type { Identifier as EsIdentifier, Program as EsProgram } from 'estree'; | ||
|
@@ -145,8 +145,15 @@ export default function compileJS(src: string, filename: string) { | |
}; | ||
} | ||
|
||
if (state.cssExplicitImports || state.staticStylesheetIds) { | ||
throw new Error( | ||
`Unimplemented static stylesheets, but found:\n${[...state.cssExplicitImports!].join( | ||
' \n' | ||
)}` | ||
); | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just moved this error message from elsewhere to here. |
||
addGenerateMarkupExport(ast, state, filename); | ||
addStylesheetImports(ast, state, filename); | ||
|
||
return { | ||
code: generate(ast, {}), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { is } from 'estree-toolkit'; | ||
import { generateScopeTokens } from '@lwc/template-compiler'; | ||
import { builders as b } from 'estree-toolkit/dist/builders'; | ||
import { esTemplate } from '../estemplate'; | ||
import type { BlockStatement, ExportNamedDeclaration, Program, VariableDeclaration } from 'estree'; | ||
|
||
function generateStylesheetScopeToken(filename: string) { | ||
// FIXME: we should be getting the namespace/name from the config options, | ||
// since these actually come from the component filename, not the template filename. | ||
const split = filename.split('/'); | ||
const namespace = split.at(-3)!; | ||
const baseName = split.at(-1)!; | ||
|
||
const componentName = baseName.replace(/\.[^.]+$/, ''); | ||
const { | ||
// FIXME: handle legacy scope token for older API versions | ||
scopeToken, | ||
} = generateScopeTokens(filename, namespace, componentName); | ||
|
||
return scopeToken; | ||
} | ||
|
||
const bStylesheetTokenDeclaration = esTemplate<VariableDeclaration>` | ||
const stylesheetScopeToken = '${is.literal}'; | ||
`; | ||
|
||
const bAdditionalDeclarations = [ | ||
esTemplate<VariableDeclaration>` | ||
const hasScopedStylesheets = defaultScopedStylesheets && defaultScopedStylesheets.length > 0; | ||
`, | ||
esTemplate<ExportNamedDeclaration>` | ||
const stylesheetScopeTokenClass = hasScopedStylesheets ? \` class="\${stylesheetScopeToken}"\` : ''; | ||
`, | ||
esTemplate<ExportNamedDeclaration>` | ||
const stylesheetScopeTokenHostClass = hasScopedStylesheets ? \` class="\${stylesheetScopeToken}-host"\` : ''; | ||
`, | ||
esTemplate<ExportNamedDeclaration>` | ||
const stylesheetScopeTokenClassPrefix = hasScopedStylesheets ? (stylesheetScopeToken + ' ') : ''; | ||
`, | ||
]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is probably a better way to do this, but I like the |
||
|
||
// Scope tokens are associated with a given template. This is assigned here so that it can be used in `generateMarkup`. | ||
const tmplAssignmentBlock = esTemplate<BlockStatement>` | ||
${is.identifier}.stylesheetScopeTokenHostClass = stylesheetScopeTokenHostClass; | ||
`; | ||
|
||
export function addScopeTokenDeclarations(program: Program, filename: string) { | ||
const scopeToken = generateStylesheetScopeToken(filename); | ||
|
||
program.body.unshift( | ||
bStylesheetTokenDeclaration(b.literal(scopeToken)), | ||
...bAdditionalDeclarations.map((declaration) => declaration()) | ||
); | ||
|
||
program.body.push(tmplAssignmentBlock(b.identifier('tmpl'))); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,7 @@ export { CustomRendererConfig, CustomRendererElementConfig } from './shared/rend | |
export { Config } from './config'; | ||
export { toPropertyName } from './shared/utils'; | ||
export { kebabcaseToCamelcase } from './shared/naming'; | ||
export { generateScopeTokens } from './scopeTokens'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are now exposing this from the template compiler... may the gods forgive us. |
||
|
||
/** | ||
* Parses HTML markup into an AST | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,12 +46,19 @@ export type scopeTokens = { | |
cssScopeTokens: string[]; | ||
}; | ||
|
||
/** | ||
* Generate the scope tokens for a given component. Note that this API is NOT stable and should be | ||
* considered internal to the LWC framework. | ||
* @param filename - full filename, e.g. `path/to/x/foo/foo.js` | ||
* @param namespace - namespace, e.g. 'x' for `x/foo/foo.js` | ||
* @param componentName - component name, e.g. 'foo' for `x/foo/foo.js` | ||
*/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a big warning here that this is not stable. I can't imagine why someone would use it, but you never know... 🤷 |
||
export function generateScopeTokens( | ||
filename: string, | ||
namespace: string | undefined, | ||
name: string | undefined | ||
componentName: string | undefined | ||
): scopeTokens { | ||
const uniqueToken = `${namespace}-${name}_${path.basename(filename, path.extname(filename))}`; | ||
const uniqueToken = `${namespace}-${componentName}_${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. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Scope tokens are associated with templates, not with components. This is the main mistake that the previous code was making. Here I am grabbing the scope token from the
tmplFn
, and I'm also not passing it into thetmplFn
below because thetmplFn
should already know what its stylesheets are.(This is not true for
static stylesheets
but we'll cross that bridge when we get there.)