-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(transform): add stylis plugin to handle ":global()" (#37)
* feat(transform): add stylis plugin to handle ":global()" * Create nice-dingos-peel.md
- Loading branch information
1 parent
f986670
commit ec051b7
Showing
5 changed files
with
315 additions
and
53 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@wyw-in-js/transform": patch | ||
--- | ||
|
||
feat: add stylis plugin to handle ":global()" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
132 changes: 132 additions & 0 deletions
132
packages/transform/src/transform/generators/__tests__/createStylisProcessor.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
import dedent from 'dedent'; | ||
import { compile, middleware, serialize, stringify } from 'stylis'; | ||
|
||
import { | ||
createStylisUrlReplacePlugin, | ||
stylisGlobalPlugin, | ||
} from '../createStylisPreprocessor'; | ||
|
||
describe('stylisUrlReplacePlugin', () => { | ||
const filename = '/path/to/src/file.js'; | ||
const outputFilename = '/path/to/assets/file.css'; | ||
|
||
const stylisUrlReplacePlugin = createStylisUrlReplacePlugin( | ||
filename, | ||
outputFilename | ||
); | ||
|
||
function compileRule(rule: string): string { | ||
return serialize( | ||
compile(rule), | ||
middleware([stylisUrlReplacePlugin, stringify]) | ||
); | ||
} | ||
|
||
it('should replace relative paths in url() expressions', () => { | ||
expect( | ||
compileRule('.component { background-image: url(./image.png) }') | ||
).toMatchInlineSnapshot( | ||
`".component{background-image:url(../src/image.png);}"` | ||
); | ||
}); | ||
}); | ||
|
||
describe('stylisGlobalPlugin', () => { | ||
function compileRule(rule: string): string { | ||
return serialize( | ||
compile(rule), | ||
middleware([stylisGlobalPlugin, stringify]) | ||
); | ||
} | ||
|
||
describe('inner part of :global()', () => { | ||
it('single selector', () => { | ||
expect( | ||
compileRule('.component :global(.global) { color: red }') | ||
).toMatchInlineSnapshot(`".global {color:red;}"`); | ||
|
||
expect( | ||
compileRule('.component &:global(.global) { color: red }') | ||
).toMatchInlineSnapshot(`".global.component {color:red;}"`); | ||
|
||
expect( | ||
compileRule('.component & :global(.global) { color: red }') | ||
).toMatchInlineSnapshot(`".global .component {color:red;}"`); | ||
}); | ||
|
||
it('multiple selectors', () => { | ||
expect( | ||
compileRule('.component :global(.globalA.globalB) { color: red }') | ||
).toMatchInlineSnapshot(`".globalA.globalB {color:red;}"`); | ||
|
||
expect( | ||
compileRule('.component &:global(.globalA.globalB) { color: red }') | ||
).toMatchInlineSnapshot(`".globalA.globalB.component {color:red;}"`); | ||
|
||
expect( | ||
compileRule('.component & :global(.globalA.globalB) { color: red }') | ||
).toMatchInlineSnapshot(`".globalA.globalB .component {color:red;}"`); | ||
}); | ||
|
||
it('data selector', () => { | ||
expect( | ||
compileRule('.component :global([data-global-style]) { color: red }') | ||
).toMatchInlineSnapshot(`"[data-global-style] {color:red;}"`); | ||
|
||
expect( | ||
compileRule('.component &:global([data-global-style]) { color: red }') | ||
).toMatchInlineSnapshot(`"[data-global-style].component {color:red;}"`); | ||
|
||
expect( | ||
compileRule('.component & :global([data-global-style]) { color: red }') | ||
).toMatchInlineSnapshot(`"[data-global-style] .component {color:red;}"`); | ||
}); | ||
}); | ||
|
||
describe('nested part of :global()', () => { | ||
it('single selector', () => { | ||
expect( | ||
compileRule('.component :global() { .global { color: red } }') | ||
).toMatchInlineSnapshot(`".global {color:red;}"`); | ||
|
||
expect( | ||
compileRule('.component &:global() { .global { color: red } }') | ||
).toMatchInlineSnapshot(`".global.component {color:red;}"`); | ||
|
||
expect( | ||
compileRule('.component & :global() { .global { color: red } }') | ||
).toMatchInlineSnapshot(`".global .component {color:red;}"`); | ||
}); | ||
|
||
it('multiple selectors', () => { | ||
const cssRuleA = dedent(` | ||
.component :global() { | ||
.globalA { color: red } | ||
.globalB { color: blue } | ||
} | ||
`); | ||
const cssRuleB = dedent(` | ||
.component &:global() { | ||
.globalA { color: red } | ||
.globalB { color: blue } | ||
} | ||
`); | ||
const cssRuleC = dedent(` | ||
.component & :global() { | ||
.globalA { color: red } | ||
.globalB { color: blue } | ||
} | ||
`); | ||
|
||
expect(compileRule(cssRuleA)).toMatchInlineSnapshot( | ||
`".globalA {color:red;}.globalB {color:blue;}"` | ||
); | ||
expect(compileRule(cssRuleB)).toMatchInlineSnapshot( | ||
`".globalA.component {color:red;}.globalB.component {color:blue;}"` | ||
); | ||
expect(compileRule(cssRuleC)).toMatchInlineSnapshot( | ||
`".globalA .component {color:red;}.globalB .component {color:blue;}"` | ||
); | ||
}); | ||
}); | ||
}); |
176 changes: 176 additions & 0 deletions
176
packages/transform/src/transform/generators/createStylisPreprocessor.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
import * as path from 'path'; | ||
import { | ||
compile, | ||
middleware, | ||
prefixer, | ||
serialize, | ||
stringify, | ||
tokenize, | ||
RULESET, | ||
} from 'stylis'; | ||
import type { Middleware } from 'stylis'; | ||
|
||
import type { Options } from '../../types'; | ||
|
||
const POSIX_SEP = path.posix.sep; | ||
|
||
export function transformUrl( | ||
url: string, | ||
outputFilename: string, | ||
sourceFilename: string, | ||
platformPath: typeof path = path | ||
) { | ||
// Replace asset path with new path relative to the output CSS | ||
const relative = platformPath.relative( | ||
platformPath.dirname(outputFilename), | ||
// Get the absolute path to the asset from the path relative to the JS file | ||
platformPath.resolve(platformPath.dirname(sourceFilename), url) | ||
); | ||
|
||
if (platformPath.sep === POSIX_SEP) { | ||
return relative; | ||
} | ||
|
||
return relative.split(platformPath.sep).join(POSIX_SEP); | ||
} | ||
|
||
/** | ||
* Stylis plugin that mimics :global() selector behavior from Stylis v3. | ||
*/ | ||
export const stylisGlobalPlugin: Middleware = (element) => { | ||
function getGlobalSelectorModifiers(value: string) { | ||
const match = value.match(/(&\f( )?)?:global\(/); | ||
|
||
if (match === null) { | ||
throw new Error( | ||
`Failed to match :global() selector in "${value}". Please report a bug if it happens.` | ||
); | ||
} | ||
|
||
const [, baseSelector, spaceDelimiter] = match; | ||
|
||
return { | ||
includeBaseSelector: !!baseSelector, | ||
includeSpaceDelimiter: !!spaceDelimiter, | ||
}; | ||
} | ||
|
||
switch (element.type) { | ||
case RULESET: | ||
if (typeof element.props === 'string') { | ||
if (process.env.NODE_ENV !== 'production') { | ||
throw new Error( | ||
`"element.props" has type "string" (${JSON.stringify( | ||
element.props, | ||
null, | ||
2 | ||
)}), it's not expected. Please report a bug if it happens.` | ||
); | ||
} | ||
|
||
return; | ||
} | ||
|
||
Object.assign(element, { | ||
props: element.props.map((cssSelector) => { | ||
// Avoids calling tokenize() on every string | ||
if (!cssSelector.includes(':global(')) { | ||
return cssSelector; | ||
} | ||
|
||
if (element.children.length === 0) { | ||
Object.assign(element, { | ||
global: getGlobalSelectorModifiers(element.value), | ||
}); | ||
return cssSelector; | ||
} | ||
|
||
const { includeBaseSelector, includeSpaceDelimiter } = | ||
( | ||
element.parent as unknown as | ||
| (Element & { | ||
global: ReturnType<typeof getGlobalSelectorModifiers>; | ||
}) | ||
| undefined | ||
)?.global || getGlobalSelectorModifiers(element.value); | ||
|
||
const tokens = tokenize(cssSelector); | ||
let selector = ''; | ||
|
||
for (let i = 0, len = tokens.length; i < len; i++) { | ||
const token = tokens[i]; | ||
|
||
// | ||
// Match for ":global(" | ||
if (token === ':' && tokens[i + 1] === 'global') { | ||
// | ||
// Match for ":global()" | ||
if (tokens[i + 2] === '()') { | ||
selector = [ | ||
...tokens.slice(i + 4), | ||
includeSpaceDelimiter ? ' ' : '', | ||
...(includeBaseSelector ? tokens.slice(0, i - 1) : []), | ||
includeSpaceDelimiter ? '' : ' ', | ||
].join(''); | ||
|
||
break; | ||
} | ||
|
||
// | ||
// Match for ":global(selector)" | ||
selector = [ | ||
tokens[i + 2].slice(1, -1), | ||
includeSpaceDelimiter ? ' ' : '', | ||
...(includeBaseSelector ? tokens.slice(0, i - 1) : []), | ||
includeSpaceDelimiter ? '' : ' ', | ||
].join(''); | ||
|
||
break; | ||
} | ||
} | ||
|
||
return selector; | ||
}), | ||
}); | ||
|
||
break; | ||
|
||
default: | ||
} | ||
}; | ||
|
||
export function createStylisUrlReplacePlugin( | ||
filename: string, | ||
outputFilename: string | undefined | ||
): Middleware { | ||
return (element) => { | ||
if (element.type === 'decl' && outputFilename) { | ||
// When writing to a file, we need to adjust the relative paths inside url(..) expressions. | ||
// It'll allow css-loader to resolve an imported asset properly. | ||
// eslint-disable-next-line no-param-reassign | ||
element.return = element.value.replace( | ||
/\b(url\((["']?))(\.[^)]+?)(\2\))/g, | ||
(match, p1, p2, p3, p4) => | ||
p1 + transformUrl(p3, outputFilename, filename) + p4 | ||
); | ||
} | ||
}; | ||
} | ||
|
||
export function createStylisPreprocessor(options: Options) { | ||
function stylisPreprocess(selector: string, text: string): string { | ||
const compiled = compile(`${selector} {${text}}\n`); | ||
|
||
return serialize( | ||
compiled, | ||
middleware([ | ||
createStylisUrlReplacePlugin(options.filename, options.outputFilename), | ||
stylisGlobalPlugin, | ||
prefixer, | ||
stringify, | ||
]) | ||
); | ||
} | ||
|
||
return stylisPreprocess; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters