-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for multiple bundlers and module systems (#3)
To add support for multiple module systems, the project needs to be compiled in both Commonjs and ESM formats. tsup will now be used for all compilation purposes. The setup for compilation during development and production is different to minimize workflow disruptions. In development, we only compile to ESM while for the package distribution, we compile to both module systems together using tsup's multi-format transpilation. This project is now an ESM module, however, we still support commonjs projects and have been tested to work with Nextjs build tools. One can both require or import the project's package without issues. Multi-bundler support will now be possible with unplugin. For starters, we only support webpack, but in the future support for other bundlers will added.
- Loading branch information
Showing
14 changed files
with
4,594 additions
and
3,141 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 |
---|---|---|
@@ -1,6 +1,9 @@ | ||
**/node_modules/ | ||
dev/ | ||
demo-project/ | ||
|
||
# test projects for testing and prototyping | ||
test-projects/ | ||
|
||
.vscode/ | ||
**/coverage/ | ||
dist/ |
This file was deleted.
Oops, something went wrong.
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 |
---|---|---|
@@ -1,14 +1,9 @@ | ||
/* | ||
* For a detailed explanation regarding each configuration property and type check, visit: | ||
* https://jestjs.io/docs/configuration | ||
*/ | ||
import type { Config } from "jest"; | ||
|
||
export default { | ||
preset: "ts-jest", | ||
globals: { | ||
"ts-jest": { | ||
tsconfig: "tsconfig.json", | ||
}, | ||
}, | ||
testPathIgnorePatterns: ["/node_modules/", "dev/"], | ||
} | ||
const config: Config = { | ||
preset: "ts-jest/presets/default-esm", | ||
testEnvironment: "node", | ||
extensionsToTreatAsEsm: [".ts"], | ||
}; | ||
|
||
export default config; |
Large diffs are not rendered by default.
Oops, something went wrong.
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 |
---|---|---|
@@ -1,32 +1,56 @@ | ||
{ | ||
"name": "jsx-css-module-transforms", | ||
"version": "0.2.2-beta", | ||
"version": "0.3.2-beta", | ||
"description": "A babel plugin to transform string classnames into css-module classnames", | ||
"exports": "./dist/index.js", | ||
"license": "MIT", | ||
"author": "Tushar Singh", | ||
"repository": "github:tusharsnn/jsx-css-module-transforms", | ||
"type": "module", | ||
"module": "dist/index.js", | ||
"types": "dist/index.d.ts", | ||
"main": "dist/index.cjs", | ||
"exports": { | ||
".": { | ||
"import": "./dist/index.js", | ||
"require": "./dist/index.cjs", | ||
"types": "./dist/index.d.ts" | ||
} | ||
}, | ||
"files": [ | ||
"dist" | ||
], | ||
"keywords": [ | ||
"module-classname-transforms" | ||
], | ||
"author": "Tushar Singh", | ||
"license": "MIT", | ||
"repository": "github:tusharsnn/jsx-css-module-transforms", | ||
"scripts": { | ||
"build": "tsc -p tsconfig.build.json", | ||
"test": "jest --verbose", | ||
"test:cov": "jest --verbose --coverage", | ||
"dev": "tsc -p tsconfig.json" | ||
"build": "NODE_ENV=production npx tsup-node --config tsup.build.config.ts", | ||
"dev": "npx tsup-node --watch", | ||
"dev:nowatch": "npx tsup-node", | ||
"test": "npm run dev:nowatch && NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest --verbose", | ||
"test:cov": "npm run dev:nowatch && NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest --verbose --coverage" | ||
}, | ||
"dependencies": { | ||
"@babel/core": "^7.17.10", | ||
"chalk": "^4.1.2" | ||
"@babel/core": "^7.24.3", | ||
"chalk": "^4.1.2", | ||
"unplugin": "^1.10.1" | ||
}, | ||
"peerDependencies": { | ||
"@babel/plugin-syntax-jsx": "^7.24.1", | ||
"webpack": "^5.91.0" | ||
}, | ||
"peerDependenciesMeta": { | ||
"webpack": { | ||
"optional": true | ||
} | ||
}, | ||
"devDependencies": { | ||
"@babel/plugin-syntax-jsx": "^7.17.12", | ||
"@types/babel__core": "^7.1.19", | ||
"@types/jest": "^27.5.0", | ||
"jest": "^28.1.0", | ||
"ts-jest": "^28.0.2", | ||
"ts-node": "^10.8.0", | ||
"ts-node-dev": "^1.1.8", | ||
"typescript": "^4.6.4" | ||
"@types/babel__core": "^7.20.5", | ||
"@types/jest": "^29.5.12", | ||
"@types/node": "^20.14.2", | ||
"jest": "^29.7.0", | ||
"ts-jest": "^29.1.4", | ||
"tsup": "^8.0.2", | ||
"typescript": "^4.9.5" | ||
} | ||
} | ||
} |
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 |
---|---|---|
@@ -1,134 +1,33 @@ | ||
import type { NodePath, PluginObj } from "@babel/core"; | ||
import { types as t } from "@babel/core"; | ||
import type babel from "@babel/core"; | ||
import chalk from "chalk"; | ||
import { transformAsync } from "@babel/core"; | ||
import { createWebpackPlugin } from "unplugin"; | ||
import type { UnpluginOptions } from "unplugin"; | ||
import plugin from "./plugin.js"; | ||
import { CSSModuleError } from "./utils.js"; | ||
|
||
import { getImportInfo, getTemplFromStrCls } from "./transforms"; | ||
import { CSSModuleError } from "./utils"; | ||
|
||
function ImportDeclaration(path: NodePath<t.ImportDeclaration>, state: PluginPass) { | ||
// we're only interested in scss/sass/css imports | ||
if (!/.module.(s[ac]ss|css)(:.*)?$/iu.test(path.node.source.value)) { | ||
return; | ||
} | ||
|
||
// 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`); | ||
} | ||
|
||
// 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`); | ||
} | ||
|
||
let importSpecifier = path.scope.generateUidIdentifier("style"); | ||
let newSpecifiers = [t.importDefaultSpecifier(importSpecifier)]; | ||
let newImportDeclaration = t.importDeclaration(newSpecifiers, t.stringLiteral(path.node.source.value)); | ||
path.replaceWith<t.ImportDeclaration>(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`); | ||
} | ||
|
||
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<t.ImportDeclaration>(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 | ||
} | ||
|
||
function JSXAttribute(path: NodePath<t.JSXAttribute>, state: PluginPass) { | ||
// we only support className attribute having a string value | ||
if (path.node.name.name != "className" || !t.isStringLiteral(path.node.value)) { | ||
return; | ||
} | ||
// className values should be transformed only if we ever found a css module | ||
if (!state.pluginState.modules.defaultModule && !state.pluginState.modules.namedModules) { | ||
return; | ||
} | ||
|
||
// saving path for error messages | ||
CSSModuleError.path = path; | ||
function unpluginFactory(): UnpluginOptions { | ||
return { | ||
name: "jsx-css-module-transforms", | ||
|
||
// if no default modules is available, make the first modules as default | ||
if (!state.pluginState.modules.defaultModule) { | ||
let firstNamedModule = getFirstNamedModule(state.pluginState.modules.namedModules); | ||
if (firstNamedModule) { | ||
state.pluginState.modules.defaultModule = state.pluginState.modules.namedModules[firstNamedModule]; | ||
} | ||
} | ||
transformInclude(id) { | ||
const result = /\.tsx?$/i.test(id); | ||
return result; | ||
}, | ||
|
||
let fileCSSModules = state.pluginState.modules; | ||
let templateLiteral = getTemplFromStrCls(path.node.value.value, fileCSSModules); | ||
let jsxExpressionContainer = t.jsxExpressionContainer(templateLiteral); | ||
let newJSXAttr = t.jsxAttribute(t.jsxIdentifier("className"), jsxExpressionContainer); | ||
path.replaceWith(newJSXAttr); | ||
path.skip(); | ||
} | ||
async transform(code, id) { | ||
// babel's transformSync cannot be used with ESM based plugin | ||
const result = await transformAsync(code, { | ||
filename: id, | ||
plugins: ["@babel/plugin-syntax-jsx", plugin], | ||
sourceMaps: process.env.NODE_ENV == "production" ? false : "inline", | ||
}); | ||
|
||
function API({ types: t }: typeof babel): PluginObj<PluginPass> { | ||
/** | ||
* Sets up the initial state of the plugin | ||
*/ | ||
function pre(this: PluginPass): void { | ||
this.pluginState = { | ||
modules: { | ||
namedModules: {}, | ||
}, | ||
}; | ||
} | ||
if (!result?.code) { | ||
throw new CSSModuleError(`Could not transform ${id}`); | ||
} | ||
|
||
return { | ||
pre, | ||
visitor: { | ||
ImportDeclaration, | ||
JSXAttribute, | ||
return result.code; | ||
}, | ||
}; | ||
} | ||
|
||
export default API; | ||
|
||
function getFirstNamedModule(namedModules: Modules["namedModules"]): string | null { | ||
for (let module in namedModules) return module; | ||
return null; | ||
} | ||
|
||
export type Modules = { | ||
defaultModule?: string; | ||
namedModules: { [moduleName: string]: string }; | ||
}; | ||
|
||
type PluginState = { | ||
modules: Modules; | ||
}; | ||
|
||
interface PluginPass extends babel.PluginPass { | ||
pluginState: PluginState; | ||
} | ||
export default createWebpackPlugin(unpluginFactory); |
Oops, something went wrong.