From 66d19530f3aeebfa9a1baf7c8263cd2dbd391c87 Mon Sep 17 00:00:00 2001 From: Tushar Singh Date: Fri, 5 Jul 2024 07:06:17 +0000 Subject: [PATCH 1/2] refactor to simplify plugin logic --- .vscode/launch.json | 15 ++ src/plugin.ts | 105 +++++----- src/transforms.ts | 247 ++++++++++++----------- src/utils.ts | 51 ----- test-projects/nextjs/src/app/globals.css | 4 + test-projects/nextjs/src/app/page.tsx | 8 +- tests/transforms.test.ts | 76 ++++--- tests/utils.test.ts | 79 -------- tsconfig.json | 2 +- 9 files changed, 241 insertions(+), 346 deletions(-) delete mode 100644 tests/utils.test.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 813ec7c..9967124 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,21 @@ { "version": "0.2.0", "configurations": [ + { + "name": "Debug tests", + "preLaunchTask": "Build", + "request": "launch", + "cwd": "${workspaceFolder}", + "type": "node", + "runtimeExecutable": "npm", + "args": [ + "run", + "test" + ], + "env": { + "NODE_OPTIONS": "--inspect", + }, + }, { "name": "Next.js: debug server-side", "preLaunchTask": "Build", diff --git a/src/plugin.ts b/src/plugin.ts index 5daa365..0c38d6f 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -3,10 +3,10 @@ import { types as t } from "@babel/core"; import type babel from "@babel/core"; import chalk from "chalk"; -import { getImportInfo, getTemplFromStrCls } from "./transforms.js"; +import { transformClassNames, transformImport } from "./transforms.js"; import { CSSModuleError } from "./utils.js"; -function ImportDeclaration(path: NodePath, state: PluginPass) { +function ImportDeclaration(path: NodePath, { pluginState }: PluginPass) { // we're only interested in scss/sass/css imports if (!/.module.(s[ac]ss|css)(:.*)?$/iu.test(path.node.source.value)) { return; @@ -15,65 +15,44 @@ function ImportDeclaration(path: NodePath, state: PluginPas // saving path for error messages CSSModuleError.path = path; - if (path.node.specifiers.length > 1 && !t.isImportDefaultSpecifier(path.node.specifiers[0])) { - // Syntax: import { classA, classB } from "./m1.module.css" - throw new CSSModuleError(`Import CSS-Module as a default import on '${chalk.cyan(path.node.source.value)}'`); - } - if (path.node.specifiers.length > 1) { - // Syntax: import style, { classA, classB } from "./m1.module.css" - throw new CSSModuleError(`More than one import found on '${chalk.cyan(path.node.source.value)}'`); - } - - let moduleInfo = getImportInfo(path.node); - if (moduleInfo.hasSpecifier) { - let importSpecifier = path.node.specifiers[0].local; - if (importSpecifier.name in state.pluginState.modules.namedModules) { - throw new CSSModuleError(`CSS-Module ${chalk.yellow(`'${importSpecifier.name}'`)} has already been declared`); - } + // 1. Transform import declaration + const idGenerator = (hint: string) => path.scope.generateUidIdentifier(hint); + const res = transformImport(path.node, idGenerator); + path.replaceWith(res.transformedNode); + path.skip(); - // saving new module - state.pluginState.modules.namedModules[importSpecifier.name] = importSpecifier.name; - } else if (moduleInfo.default) { - if (state.pluginState.modules.defaultModule) { - throw new CSSModuleError(`Only one default css-module import is allowed. Provide names for all except the default module`); + // 2. Add CSS module to the list + const importSpecifier = res.transformedNode.specifiers[0].local.name; + if (res.generatedSpecifier) { + if (res.moduleLabel) { + addCheckedModule(res.moduleLabel, importSpecifier, pluginState.modules); + } else { + // this is a default module + addCheckedDefaultModule(importSpecifier, pluginState.modules); } - - let importSpecifier = path.scope.generateUidIdentifier("style"); - let newSpecifiers = [t.importDefaultSpecifier(importSpecifier)]; - let newImportDeclaration = t.importDeclaration(newSpecifiers, t.stringLiteral(path.node.source.value)); - path.replaceWith(newImportDeclaration); - - // saving this module as the default module for the current translation unit. - state.pluginState.modules.defaultModule = importSpecifier.name; } else { - if (moduleInfo.moduleName in state.pluginState.modules.namedModules) { - throw new CSSModuleError(`CSS-Module ${chalk.yellow(`'${moduleInfo.moduleName}'`)} has already been declared`); + // Verify that the module label is unique. + // Prevents scenarios where the same value is used as both a module + // label and an import specifier in different import declarations. + addCheckedModule(importSpecifier, importSpecifier, pluginState.modules); + + if (res.moduleLabel && res.moduleLabel != importSpecifier) { + // Make module label an alias to the provided specifier + addCheckedModule(res.moduleLabel, importSpecifier, pluginState.modules); } - - let importSpecifier = path.scope.generateUidIdentifier(moduleInfo.moduleName); - let newSpecifiers = [t.importDefaultSpecifier(importSpecifier)]; - let newImportDeclaration = t.importDeclaration(newSpecifiers, t.stringLiteral(path.node.source.value)); - path.replaceWith(newImportDeclaration); - - // saving new module - state.pluginState.modules.namedModules[moduleInfo.moduleName] = importSpecifier.name; } - - // strips away module name from the source - path.node.source.value = moduleInfo.moduleSource; // this inplace replacment does not causes any problem with the ast - path.skip(); } -function JSXAttribute(path: NodePath, state: PluginPass) { - const firstNamedModule = getFirstNamedModule(state.pluginState.modules.namedModules); +function JSXAttribute(path: NodePath, { pluginState }: PluginPass) { + const firstNamedModule = getFirstNamedModule(pluginState.modules.namedModules); // we only support className attribute having a string value - if (path.node.name.name != "className" || !t.isStringLiteral(path.node.value)) { + if (path.node.name.name != "className" || !path.node.value || !t.isStringLiteral(path.node.value)) { return; } // className values should be transformed only if we ever found a css module. // FirstNamedModule signifies that we found at least one named css module. - if (!state.pluginState.modules.defaultModule && !firstNamedModule) { + if (!pluginState.modules.defaultModule && !firstNamedModule) { return; } @@ -81,14 +60,14 @@ function JSXAttribute(path: NodePath, state: PluginPass) { CSSModuleError.path = path; // if no default modules is available, make the first modules as default - if (!state.pluginState.modules.defaultModule) { + if (!pluginState.modules.defaultModule) { if (firstNamedModule) { - state.pluginState.modules.defaultModule = state.pluginState.modules.namedModules[firstNamedModule]; + pluginState.modules.defaultModule = pluginState.modules.namedModules[firstNamedModule]; } } - let fileCSSModules = state.pluginState.modules; - let templateLiteral = getTemplFromStrCls(path.node.value.value, fileCSSModules); + let classNames = path.node.value.value; + let templateLiteral = transformClassNames(classNames, pluginState.modules); let jsxExpressionContainer = t.jsxExpressionContainer(templateLiteral); let newJSXAttr = t.jsxAttribute(t.jsxIdentifier("className"), jsxExpressionContainer); path.replaceWith(newJSXAttr); @@ -96,9 +75,7 @@ function JSXAttribute(path: NodePath, state: PluginPass) { } function API(): PluginObj { - /** - * Sets up the initial state of the plugin - */ + // Set up the initial state for the plugin function pre(this: PluginPass): void { this.pluginState = { modules: { @@ -116,6 +93,20 @@ function API(): PluginObj { }; } +function addCheckedModule(moduleLabel: string, module: string, modules: Modules) { + if (moduleLabel in modules.namedModules) { + throw new CSSModuleError(`Duplicate CSS module '${chalk.yellow(module)}' found`); + } + modules.namedModules[moduleLabel] = module; +} + +function addCheckedDefaultModule(module: string, modules: Modules) { + if (modules.defaultModule) { + throw new CSSModuleError(`Only one default css-module import is allowed. Provide names for all except the default module`); + } + modules.defaultModule = module; +} + export default API; function getFirstNamedModule(namedModules: Modules["namedModules"]): string | null { @@ -123,9 +114,11 @@ function getFirstNamedModule(namedModules: Modules["namedModules"]): string | nu return null; } +type CSSModuleLabel = string; +type CSSModuleIdentifier = string; export type Modules = { defaultModule?: string; - namedModules: { [moduleName: string]: string }; + namedModules: { [moduleLabel: CSSModuleLabel]: CSSModuleIdentifier }; }; type PluginState = { diff --git a/src/transforms.ts b/src/transforms.ts index 967a33b..1e48f62 100644 --- a/src/transforms.ts +++ b/src/transforms.ts @@ -1,145 +1,148 @@ import * as t from "@babel/types"; import chalk from "chalk"; -import { CSSModuleError, splitClsName, splitModuleSource } from "./utils.js"; +import { CSSModuleError } from "./utils.js"; import type { Modules } from "./plugin.js"; +type TransformImportResult = { + transformedNode: t.ImportDeclaration; + generatedSpecifier: boolean; + moduleLabel?: string; +}; + /** - * generates template literal using css-module classes as expressions - * and global classes as quasis - * - * @param cssModExpr array of string(representing global class) and memberExpression(representing css-module class) + * Generates a replacement node for the given import declaration after + * removing any plugin specific syntax. + * @param importDecalaration The import declaration to transform + * @param idGenerator A helper callback to generate a unique identifier(/variable) + * to use as the import specifier in case none was provided */ -function createTemplateLiteral(cssModExpr: (string | t.MemberExpression)[]) { - let templateLiteral: t.TemplateLiteral = t.templateLiteral([t.templateElement({ raw: "", cooked: "" })], []); // quasis must be 1 more than expression while creating templateLiteral - - cssModExpr.forEach((expression) => { - if (typeof expression === "string") { - // overwrite the previous quasis element to include this classname - templateLiteral.quasis[templateLiteral.quasis.length - 1].value.raw += expression + " "; - templateLiteral.quasis[templateLiteral.quasis.length - 1].value.cooked += expression + " "; // assigning cooked value is not needed but it saves from weird edge cases where plugins only uses cooked value and ignores raw value (eg. @babel/preset-env). - return; - } +export function transformImport(node: t.ImportDeclaration, idGenerator: (hint: string) => t.Identifier): TransformImportResult { + let modSrc = node.source.value; + + // We only support having only a default specifier in the + // import declaration. It's okay to have no specifiers. + const nSpecifiers = node.specifiers.length; + if (nSpecifiers > 1 || (nSpecifiers == 1 && !t.isImportDefaultSpecifier(node.specifiers[0]))) { + throw new CSSModuleError(`Invalid CSS module import '${chalk.yellow(modSrc)}': Only default specifiers are allowed`); + } - let spaceTemplateElement = t.templateElement({ raw: " ", cooked: " " }); - templateLiteral.expressions.push(expression); - templateLiteral.quasis.push(spaceTemplateElement); - }); + // Extract module label and real source from the given import source + const SourcePattern = /(?.+?):(?.+)/; + const matches = modSrc.match(SourcePattern); - // removing extra spaces, this way we don't have to figure out the last quasis added was a class or a space - templateLiteral.quasis[templateLiteral.quasis.length - 1].value.raw = templateLiteral.quasis[templateLiteral.quasis.length - 1].value.raw.trimEnd(); - templateLiteral.quasis[templateLiteral.quasis.length - 1].value.cooked = templateLiteral.quasis[templateLiteral.quasis.length - 1].value.cooked?.trimEnd(); - // last quasis element should have tail as true - templateLiteral.quasis[templateLiteral.quasis.length - 1].tail = true; + let modLabel: string | undefined; + if (matches && matches.groups) { + // Update the module source with the real module source + modSrc = matches.groups["moduleSource"]; + modLabel = matches.groups["moduleLabel"]; + } - return templateLiteral; + const hasSpecifier = !!nSpecifiers; + const newImportSpecId = hasSpecifier ? t.identifier(node.specifiers[0].local.name) : idGenerator(modLabel || "style"); + const newImportDefSpec = t.importDefaultSpecifier(newImportSpecId); + const specifiers = [newImportDefSpec]; + const newModSrc = t.stringLiteral(modSrc); + const newNode = t.importDeclaration(specifiers, newModSrc); + + return { + transformedNode: newNode, + generatedSpecifier: !hasSpecifier, + moduleLabel: modLabel, + }; } + /** - * creates MemberExpression using module as object and classname as property. - * - * eg. `[]` + * Generates a template literal as a result of transformation applied to the given classNames + * @param classNames The string we got from the `className` JSX attribute + * @param modules The CSS modules found in the file */ -export function createModuleMemberExpression(classname: string, module: string, modules: Modules): t.MemberExpression { - let moduleIdentifier: t.Identifier; - let classnameStringLiteral = t.stringLiteral(classname); - - if (module == modules.defaultModule) { - moduleIdentifier = t.identifier(modules.defaultModule); - } else { - if (!(module in modules.namedModules)) { - throw new CSSModuleError(`Module '${chalk.green(module)}' on class '${chalk.cyan(classname)}' not found`); +export function transformClassNames(classNames: string, modules: Modules): t.TemplateLiteral { + let quasis: t.TemplateElement[] = []; + let expressions: t.Expression[] = []; + + // Template literals uses alternating quasis and expressions to build + // the final template literal. They start and end with a quasis. + // So, if quasis = [q1, q2, q3] and expressions = [e1, e2], the + // final template literal will be: + let quasisStr = ""; + + const addExpression = (mod: string, cn: string) => { + // Add the accumulated quasis string. + // Always add the cooked value as some transpilers exclusively + // use it while ignoring the raw value. + let ele = t.templateElement({ raw: quasisStr, cooked: quasisStr }); + quasis.push(ele); + + // Reset the quasis string -- but with a space so that there's always + // a space between two expressions in the final template string. + quasisStr = " "; + + // A classname like '.my-class-a' can be accessed from the css module style object in two ways: + // - Using dot notation, but remove the dashes from the classname and camelCase it: style.myClassA. + // - Using bracket notation: style["my-class-a"]. + // The latter has the benefit that we don't have to transform the classname, and it can be used directly + // in the member expression as the property name. Set `computed` to true to use the bracket notation. + const expr = t.memberExpression(t.identifier(mod), t.stringLiteral(cn), true); + + expressions.push(expr); + }; + + classNames.split(" ").forEach((className) => { + if (!className) { + return; } - moduleIdentifier = t.identifier(modules.namedModules[module]); - } + if (className.startsWith(":") || className.endsWith(":")) { + throw new CSSModuleError(`Invalid classname '${className}': Classname can't start or end with ':'`); + } - // When a class has dashes in its name, e.g. `.my-class`, it can be - // read from the imported style object in two ways: - // - Remove dash and camelCase the class name: `style.myClass`. - // - Use the bracket syntax for reading object property: `style["my-class"]`. - // The latter has the benefit that we can avoid transforming the classname string - // and just use it in the generated MemberExpression. Set `computed` to true to use the bracket syntax - return t.memberExpression(moduleIdentifier, classnameStringLiteral, true); -} + const ClassNamePattern = /(?.+?):(?.+)/; + const matches = className.match(ClassNamePattern); -/** - * - * generates template literal from string classes - * - * @param classString string containing classes for the className attribute (eg. "classA classB") - * @returns templateLiteral based on string classes and modules - */ -export function getTemplFromStrCls(classString: string, modules: Modules): t.TemplateLiteral { - if (!modules.defaultModule) { - throw new CSSModuleError("No default css-module found"); - } + if (!matches || !matches.groups) { + // ClassName uses default module as no module label was provided - let classList = classString.split(" "); - let splittedClass = classList.map((classname) => { - return splitClsName(classname, modules.defaultModule ?? ""); - }); - let classAsModule = splittedClass.map((classObj) => { - if (classObj.module) { - return createModuleMemberExpression(classObj.classname, classObj.module, modules); - } else { - return classObj.classname; + if (!modules.defaultModule) { + throw new CSSModuleError(`No default module found in the file to use with class '${className}'`); + } + + addExpression(modules.defaultModule, className); + return; } - }); - return createTemplateLiteral(classAsModule); -} -/** - * A helper function to identify Kind of import source. - * @param statement import statement from the source - * @returns object representing type of import used and specifier present - */ -export function getImportInfo(statement: t.ImportDeclaration): DefaultModule | ModuleWithSpecifier | NamedModule { - let module = splitModuleSource(statement.source.value); - - // .length is either 0 or 1. - if (statement.specifiers.length) { - // Syntax: import style from "./m1.module.css" - // - // all the checks are done inside the visitor for the - // presence of only default specifier in case if any specifier is present - return { - moduleSource: module.moduleSource, - default: false, - hasSpecifier: true, - }; - } else if (!module.moduleName) { - // Syntax: import "./moduleA.module.css" - return { - moduleSource: module.moduleSource, - default: true, - hasSpecifier: false, - }; - } else { - // Syntax: import "./moduleA.module.css:m1" - return { - moduleSource: module.moduleSource, - moduleName: module.moduleName, - default: false, - hasSpecifier: false, - }; - } -} + // Named or global module -type DefaultModule = { - moduleSource: string; - default: true; - hasSpecifier: false; -}; + let modLabel = matches.groups["moduleLabel"]; + let cn = matches.groups["className"]; -type NamedModule = { - moduleSource: string; - moduleName: string; - default: false; - hasSpecifier: false; -}; + if (!modLabel || !cn) { + throw new CSSModuleError(`Invalid classname: '${className}'`); + } -type ModuleWithSpecifier = { - moduleSource: string; - default: false; - hasSpecifier: true; -}; + // Global classname + const GlobalClassNameLabel = "g"; + if (modLabel === GlobalClassNameLabel) { + // Classnames tagged with the global label are left unchanged. + // Add them to the quasis string and bail. + quasisStr += cn + " "; + return; + } + + // Named module + let module = modules.namedModules[modLabel]; + if (!module) { + throw new CSSModuleError(`Module matching label '${chalk.green(modLabel)}' on '${chalk.cyan(className)}' not found`); + } + addExpression(module, cn); + }); + + // Trim extra spaces at the end of the quasis + quasisStr = quasisStr.trimEnd(); + // Add the last quasis to compelete the alternating sequence. + // Also, mark it as the tail element. + const elem = t.templateElement({ raw: quasisStr, cooked: quasisStr }, true); + quasis.push(elem); + + return t.templateLiteral(quasis, expressions); +} diff --git a/src/utils.ts b/src/utils.ts index b595653..2fd57ba 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,57 +1,6 @@ import { NodePath } from "@babel/core"; import chalk from "chalk"; -/** - * splits full classname (with ':') into classname and module name - * - * @param classname full classname with module - * @param defaultModule default module for the file - * @returns classname and module name used - */ -export function splitClsName(classname: string, defaultModule: string): { classname: string; module?: string } { - if (shouldTransform(classname)) { - // TODO: throw error if more than one sep is present, or use last sep in the classname to split - let [splittedClassName, module] = classname.split(":"); - if (module === "") { - // TODO: silently use defaultModule? - throw new CSSModuleError(`no module name found after ':' on ${CSSModuleError.cls(classname)}`); - } - return { - classname: splittedClassName.trim(), - module: module || defaultModule, - }; - } else { - // global class - return { - classname: classname.slice(0, classname.length - 2), - }; - } -} - -export function shouldTransform(classname: string) { - return !classname.endsWith(":g"); -} - -export function splitClassnames(classes: string) { - return classes.split(" "); -} - -/** - * Splits module source into module source and user-provided module name - * eg. `"moduleA.module.css:m1"` -> `{ moduleSource: "moduleA.module.css", moduleName: "m1" }` - * @param source - module spec that contains a module source path and a user provided module name eg. `"moduleA.module.css:m1"` - * @returns object with module source and user provided module name - */ -export function splitModuleSource(source: string): { moduleSource: string; moduleName?: string } { - if (!source.includes(":")) { - return { - moduleSource: source, - }; - } - let [moduleSource, moduleName] = source.split(":"); - return { moduleSource, moduleName }; -} - export class CSSModuleError extends Error { errorMessage: string; static path: NodePath | undefined; diff --git a/test-projects/nextjs/src/app/globals.css b/test-projects/nextjs/src/app/globals.css index 52c1fc7..af8d7e8 100644 --- a/test-projects/nextjs/src/app/globals.css +++ b/test-projects/nextjs/src/app/globals.css @@ -62,6 +62,10 @@ a { text-decoration: none; } +.glow { + text-shadow: 0 0 10px #fff, 0 0 20px #fff, 0 0 30px #e60073, 0 0 40px #e60073, 0 0 50px #e60073, 0 0 60px #e60073, 0 0 70px #e60073; +} + @media (prefers-color-scheme: dark) { html { color-scheme: dark; diff --git a/test-projects/nextjs/src/app/page.tsx b/test-projects/nextjs/src/app/page.tsx index 25f7ed6..dd38bda 100644 --- a/test-projects/nextjs/src/app/page.tsx +++ b/test-projects/nextjs/src/app/page.tsx @@ -1,5 +1,5 @@ import Image from "next/image"; -import "./page.module.css"; +import style from "style1:./page.module.css"; export default function Home() { return ( @@ -21,14 +21,14 @@ export default function Home() {
- +

Docs ->

Find in-depth information about Next.js features and API.

- +

Learn ->

@@ -42,7 +42,7 @@ export default function Home() {

Explore starter templates for Next.js.

- +

Deploy ->

diff --git a/tests/transforms.test.ts b/tests/transforms.test.ts index bb3ffea..8aafc59 100644 --- a/tests/transforms.test.ts +++ b/tests/transforms.test.ts @@ -31,21 +31,21 @@ describe("single imports", () => { }); test("with specifier (ignore names)", async () => { - let source = `import style from "./foo.module.scss:m1"`; + let source = `import style from "m1:./foo.module.scss"`; let code = await runWithBabel(source); expect(code).toMatchInlineSnapshot(`"import style from "./foo.module.scss";"`); - source = `import style from "./foo.module.css:m1"`; + source = `import style from "m1:./foo.module.css"`; code = await runWithBabel(source); expect(code).toMatchInlineSnapshot(`"import style from "./foo.module.css";"`); }); test("with named-module", async () => { - let source = `import "./foo.module.scss:m1"`; + let source = `import "m1:./foo.module.scss"`; let code = await runWithBabel(source); expect(code).toMatchInlineSnapshot(`"import _m from "./foo.module.scss";"`); - source = `import "./foo.module.css:m1"`; + source = `import "m1:./foo.module.css"`; code = await runWithBabel(source); expect(code).toMatchInlineSnapshot(`"import _m from "./foo.module.css";"`); }); @@ -62,7 +62,7 @@ describe("single imports", () => { }); describe("imports multiple module", () => { - test("default module", async () => { + test("multiple default module", async () => { let source = ` import "./foo.module.css" import "./bar.module.css" @@ -72,8 +72,8 @@ describe("imports multiple module", () => { test("with named-modules", async () => { let source = ` - import "./module1.module.css:m1" - import "./module2.module.css:m2" + import "m1:./module1.module.css" + import "m2:./module2.module.css" `; let code = await runWithBabel(source); expect(code).toMatchInlineSnapshot(` @@ -84,16 +84,16 @@ import _m2 from "./module2.module.css";" test("with same named-modules twice", async () => { let source = ` - import "./module1.module.css:m1" - import "./module2.module.css:m1" + import "m1:./module1.module.css" + import "m1:./module2.module.css" `; await expect(runWithBabel(source)).rejects.toThrow(CSSModuleError); }); test("with specifier", async () => { let source = ` - import style from "./module1.module.css:m1" - import style1 from "./module2.module.css:m2" + import style from "m1:./module1.module.css" + import style1 from "m2:./module2.module.css" `; let code = await runWithBabel(source); expect(code).toMatchInlineSnapshot(` @@ -104,8 +104,8 @@ import style1 from "./module2.module.css";" test("with same specifier twice", async () => { let source = ` - import style from "./module1.module.css:m1" - import style from "./module2.module.css:m2" + import style from "m1:./module1.module.css" + import style from "m2:./module2.module.css" `; expect(runWithBabel(source)).rejects.toThrow(SyntaxError); }); @@ -115,7 +115,7 @@ describe("different kinds together", () => { test("each kind once", async () => { let source = ` import style from "./module1.module.css" - import "./module2.module.css:m2" + import "m2:./module2.module.css" import "./module3.module.css" `; let code = await runWithBabel(source); @@ -130,7 +130,7 @@ import _style from "./module3.module.css";" let source = ` import "./component.module.css" import layout from "./layout.module.css" - import "./layout2.module.css:layout" + import "layout:./layout2.module.css" `; await expect(runWithBabel(source)).rejects.toThrow(CSSModuleError); }); @@ -138,7 +138,7 @@ import _style from "./module3.module.css";" test("same module name used twice on different kinds (2)", async () => { let source = ` import "./component.module.css" - import "./layout2.module.css:layout" + import "layout:./layout2.module.css" import layout from "./layout.module.css" `; await expect(runWithBabel(source)).rejects.toThrow(CSSModuleError); @@ -182,10 +182,10 @@ function Component() { test("with named-module", async () => { let source = ` - import "./component.module.css:m1" + import "m1:./component.module.css" function Component() { - return

+ return

} `; let code = await runWithBabel(source); @@ -197,7 +197,17 @@ function Component() { `); }); - test("named module class with colon only", async () => { + test("named module class with colon only at the front", async () => { + let source = ` + import "./component.module.css" + function Component() { + return

+ } + `; + await expect(() => runWithBabel(source)).rejects.toThrow(CSSModuleError); + }); + + test("named module class with colon only at the end", async () => { let source = ` import "./component.module.css" @@ -235,7 +245,7 @@ function component() { import layout from "./layout.module.css" function Component() { - return

+ return

} `; let code = await runWithBabel(source); @@ -250,11 +260,11 @@ function Component() { test("named-module", async () => { let source = ` - import "./component.module.css:style" - import "./layout.module.css:layout" + import "style:./component.module.css" + import "layout:./layout.module.css" function Component() { - return

+ return

} `; let code = await runWithBabel(source); @@ -273,7 +283,7 @@ function Component() { import layout from "./layout.module.css" function Component() { - return

+ return

} `; await expect(runWithBabel(source)).rejects.toThrow(CSSModuleError); @@ -281,11 +291,11 @@ function Component() { test("named-module (class uses non-existent module)", async () => { let source = ` - import "./component.module.css:style" - import "./layout.module.css:layout" + import "style:./component.module.css" + import "layout:./layout.module.css" function component() { - return

+ return

} `; await expect(runWithBabel(source)).rejects.toThrow(CSSModuleError); @@ -297,12 +307,12 @@ describe("jsx with multiple kinds of module", () => { let source = ` import style from "./component.module.css" import layout from "./layout.module.css" - import "./layout2.module.css:altLayout" + import "altLayout:./layout2.module.css" function Component() { return ( -
-

+
+

) } @@ -324,12 +334,12 @@ function Component() { let source = ` import style from "./component.module.css" import layout from "./layout.module.css" - import "./layout2.module.css:altLayout" + import "altLayout:./layout2.module.css" function Component() { return ( -
-

+
+

) } diff --git a/tests/utils.test.ts b/tests/utils.test.ts deleted file mode 100644 index bd0a4f0..0000000 --- a/tests/utils.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import * as t from "@babel/types"; - -import { splitClsName, shouldTransform, CSSModuleError } from "../dev/utils.js"; -import { createModuleMemberExpression } from "../dev/transforms.js"; - -// testing splitModuleName -describe("split string into module and classnames", () => { - test("class without modifier", () => { - let { classname, module } = splitClsName("classB:m1", "m1"); - - expect(classname).toBe("classB"); - expect(module).toBe("m1"); - }); - - test("class with modifier", () => { - let { classname, module } = splitClsName("classB-modifier:m2", "m1"); - - expect(classname).toBe("classB-modifier"); - expect(module).toBe("m2"); - }); - - test("global module", () => { - let { classname, module } = splitClsName("classA:g", "m1"); - - expect(classname).toBe("classA"); - expect(module).toBeUndefined(); - }); - - test("classname with separator only", () => { - expect(() => splitClsName("classA:", "m1")).toThrow(CSSModuleError); - }); - - test("use default module", () => { - let { classname, module } = splitClsName("classA", "m1"); - - expect(classname).toBe("classA"); - expect(module).toBe("m1"); - }); -}); - -// testing checkShouldTransform -describe("transform are applied correctly", () => { - test("should transform", () => { - let transform = shouldTransform("foo-bar"); - expect(transform).toBe(true); - }); - - test("should transform (only sep)", () => { - let transform = shouldTransform("foo-bar:"); - expect(transform).toBe(true); - }); - - test("should not transform", () => { - let transform = shouldTransform("foo-bar:g"); - expect(transform).toBe(false); - }); -}); - -// testing createModuleMemberExpression -describe("member expression from module and classname created as expected", () => { - test("creating MemberExpression from module and classname", () => { - let classname = "foo-bar", - module = "m1", - modules = { - namedModules: { - m1: "_module", - }, - }; - let memberExpressionNode = createModuleMemberExpression(classname, module, modules); - expect(t.isMemberExpression(memberExpressionNode)).toBe(true); - expect(memberExpressionNode).toHaveProperty("computed", true); - - // checking .object and .property to have expected props - expect(t.isIdentifier(memberExpressionNode.object)).toBe(true); - expect(memberExpressionNode.object).toHaveProperty("name", "_module"); - expect(t.isStringLiteral(memberExpressionNode.property)).toBe(true); - expect(memberExpressionNode.property).toHaveProperty("value", "foo-bar"); - }); -}); diff --git a/tsconfig.json b/tsconfig.json index 68e3d08..3023c8c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "forceConsistentCasingInFileNames": true, "module": "Node16", "moduleResolution": "Node16", - "target": "ES6", + "target": "ES2018", "esModuleInterop": true, "noImplicitAny": true, "noUnusedLocals": true, From 13151e6f93a277cd804ace8217ae7a11e96062f6 Mon Sep 17 00:00:00 2001 From: Tushar Singh Date: Fri, 5 Jul 2024 07:43:52 +0000 Subject: [PATCH 2/2] update docs --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a4898fe..39ad153 100644 --- a/README.md +++ b/README.md @@ -85,13 +85,13 @@ This improves readability and follows the same pattern as regular CSS. When the plugin finds `'.module.css'` import in the file, it will transform **all** CSS classnames to use the imported CSS module. However, you may want to use regular CSS classnames and prevent transformations on them. This -can be done by adding `:g` at the end of the classname: +can be done by adding `g:` at the start of the classname: ```jsx import "./style.module.css" function Component() { - return
+ return
} ``` @@ -120,11 +120,11 @@ function Component() { You can use multiple CSS module within a file using Named modules. To use Named CSS modules, you can add labels to each CSS module import -in the file by adding `:` at the end of the path: +in the file by adding `:` at the end of the path: ```jsx -import "./layout.module.css:layout" -import "./component.module.css:com" +import "layout:./layout.module.css" +import "com:./component.module.css" ``` And use the same labels for writing your classnames: @@ -132,9 +132,9 @@ And use the same labels for writing your classnames: ```jsx function Component() { return ( -
    -
  • -
  • +
      +
    • +
    ) }