Skip to content

implement $css rune #127

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

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const defaultOptions = (): PluginOptions => {
includePaths: [],
localIdentName: '[local]-[hash:base64:6]',
mode: 'native',
rune: false,
parseExternalStylesheet: false,
parseStyleTag: true,
useAsDefaultScoping: false,
Expand Down
2 changes: 2 additions & 0 deletions src/parsers/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export { default as parseImportDeclaration } from './importDeclaration';
export { default as parseTemplate } from './template';
export { default as parseRune } from './rune';

55 changes: 55 additions & 0 deletions src/parsers/rune.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { BaseNode, walk } from 'estree-walker';
import Processor from '../processors/processor';
import { updateMultipleClasses } from './template';

export const replaceRune = (processor: Processor, node: BaseNode) => {
const callExpr = node as {
type: 'CallExpression';
callee?: { type: string; name: string };
start: number;
end: number;
arguments?: Array<{ type: string; value?: string; start: number; end: number }>;
};
if (
callExpr.type === 'CallExpression' &&
callExpr.callee?.type === 'Identifier' &&
callExpr.callee?.name === '$css'
) {
const args = callExpr.arguments as Array<{
type: string;
value: string;
start: number;
end: number;
}>;

if (args.length !== 1 || args[0].type !== 'Literal') {
throw new Error('Invalid $css call');
}
const classNames = args[0].value;

const generatedClassNames = updateMultipleClasses(processor, classNames);
processor.magicContent.overwrite(callExpr.start, callExpr.end, `"${generatedClassNames}"`);
}
};

/**
* Parse the template markup and script to process the $css rune
* @param processor The CSS Module Processor
*/

export default (processor: Processor): void => {
if (processor.ast.fragment) {
walk(processor.ast.fragment, {
enter(baseNode) {
replaceRune(processor, baseNode);
},
});
}
if (processor.ast.instance) {
walk(processor.ast.instance, {
enter(baseNode) {
replaceRune(processor, baseNode);
},
});
}
};
2 changes: 1 addition & 1 deletion src/parsers/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface CssVariables {
* @param classNames The attribute value containing one or multiple classes
* @returns the CSS Modules classnames
*/
const updateMultipleClasses = (processor: Processor, classNames: string): string => {
export const updateMultipleClasses = (processor: Processor, classNames: string): string => {
const classes: string[] = classNames.split(' ');
const generatedClassNames: string = classes.reduce((accumulator, currentValue, currentIndex) => {
let value: string = currentValue;
Expand Down
6 changes: 5 additions & 1 deletion src/processors/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
hasModuleAttribute,
hasModuleImports,
} from '../lib';
import { parseImportDeclaration, parseTemplate } from '../parsers';
import { parseImportDeclaration, parseTemplate, parseRune } from '../parsers';

export default class Processor {
public filename: string;
Expand Down Expand Up @@ -100,6 +100,10 @@ export default class Processor {
parseImportDeclaration(this);
}

if(this.options.rune) {
parseRune(this);
}

if (Object.keys(this.cssModuleList).length > 0 || Object.keys(this.cssVarList).length > 0) {
parseTemplate(this);
}
Expand Down
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type PluginOptions = {
includePaths: string[];
localIdentName: string;
mode: 'native' | 'mixed' | 'scoped';
rune: boolean;
parseExternalStylesheet: boolean;
parseStyleTag: boolean;
useAsDefaultScoping: boolean;
Expand Down
117 changes: 117 additions & 0 deletions test/runes/stylesRune.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
const compiler = require('../compiler.js');

const source = '<style module>.red { color: red; }</style><span test={$css("red")}>Red</span>';

describe('Rune', () => {
test('Works in markup', async () => {
const output = await compiler(
{
source,
},
{
localIdentName: '[local]-123',
rune: true
}
);

expect(output).toBe(
'<style module>:global(.red-123) { color: red; }</style><span test={"red-123"}>Red</span>'
);
});

test('Works in combination with class', async () => {
const output = await compiler(
{
source: '<style module>.red { color: red; }</style><span class={$css("red")}>Red</span>'
},
{
localIdentName: '[local]-123',
rune: true
}
);

expect(output).toBe(
'<style module>:global(.red-123) { color: red; }</style><span class={"red-123"}>Red</span>'
);
});

test('Works in script', async () => {
const output = await compiler(
{
source: '<style module>.red { color: red; }</style><script>const someClass = $css("red")</script>'
},
{
localIdentName: '[local]-123',
rune: true
}
);

expect(output).toBe(
'<style module>:global(.red-123) { color: red; }</style><script>const someClass = "red-123"</script>'
);
});

test('Respects option', async () => {
const output = await compiler(
{
source: source + '<script>const someClass = $css("red")</script>'
},
{
localIdentName: '[local]-123',
rune: false
}
);

expect(output).toBe(
'<style module>:global(.red-123) { color: red; }</style><span test={$css("red")}>Red</span><script>const someClass = $css("red")</script>'
);
});

test('Large component', async () => {
const source = `
<script>
const test = $css("red");
const statement = true? $css("blue") : $css("green");
function testFunction() {
return $css("red");
}
</script>
<style module>
.red { color: red; }
.blue { color: blue; }
.green { color: green; }
</style>
<span test={$css("red")}>Red</span>
<span class={$css("blue")}>Blue</span>
<span lol={$css("green")}>Green</span>
`;
const result = `
<script>
const test = "red-123";
const statement = true? "blue-123" : "green-123";
function testFunction() {
return "red-123";
}
</script>
<style module>
:global(.red-123) { color: red; }
:global(.blue-123) { color: blue; }
:global(.green-123) { color: green; }
</style>
<span test={"red-123"}>Red</span>
<span class={"blue-123"}>Blue</span>
<span lol={"green-123"}>Green</span>
`;
const output = await compiler(
{
source
},
{
localIdentName: '[local]-123',
rune: true
}
);

expect(output).toBe(result);
});
});