diff --git a/demos/vite-react/src/App.tsx b/demos/vite-react/src/App.tsx
index 1f9bc11..13b0cbf 100644
--- a/demos/vite-react/src/App.tsx
+++ b/demos/vite-react/src/App.tsx
@@ -1,5 +1,5 @@
import { useState } from 'react';
-import { css } from '@acab/ecsstatic';
+import { css } from '@acab/ecsstatic/modules';
import { Logo } from './Logo.js';
import { Button } from './Button.js';
@@ -7,22 +7,24 @@ export const App = () => {
const [count, setCount] = useState(0);
return (
-
+
- Edit any .tsx file to test HMR
+ Edit any .tsx file to test HMR
);
};
-const wrapper = css`
- display: grid;
- place-items: center;
-`;
+const styles = css`
+ .wrapper {
+ display: grid;
+ place-items: center;
+ }
-const code = css`
- font-size: 0.9em;
- font-family: ui-monospace, monospace;
+ .code {
+ font-size: 0.9em;
+ font-family: ui-monospace, monospace;
+ }
`;
diff --git a/package/modules.ts b/package/modules.ts
new file mode 100644
index 0000000..79d96d1
--- /dev/null
+++ b/package/modules.ts
@@ -0,0 +1,67 @@
+/**
+ * Returns an object containing CSS-modules-like scoped class names for the
+ * CSS inside the template string.
+ *
+ * @example
+ * import { css } from '@acab/ecsstatic/modules';
+ *
+ * const styles = css`
+ * .wrapper {
+ * display: grid;
+ * place-items: center;
+ * }
+ * .button {
+ * font: inherit;
+ * color: hotpink;
+ * }
+ * `;
+ *
+ * export () => (
+ *
+ *
+ *
+ * );
+ */
+export function css(
+ templates: TemplateStringsArray,
+ ...args: Array
+): Record {
+ throw new Error(
+ `If you're seeing this error, it is likely your bundler isn't configured correctly.`
+ );
+}
+
+/**
+ * Returns an object containing CSS-modules-like scoped class names for the
+ * SCSS inside the template string.
+ *
+ * @example
+ * import { scss } from '@acab/ecsstatic/modules';
+ *
+ * const styles = scss`
+ * $accent: hotpink;
+ *
+ * .wrapper {
+ * display: grid;
+ * place-items: center;
+ * }
+ * .button {
+ * font: inherit;
+ * color: $accent;
+ * }
+ * `;
+ *
+ * export () => (
+ *
+ *
+ *
+ * );
+ */
+export function scss(
+ templates: TemplateStringsArray,
+ ...args: Array
+): Record {
+ throw new Error(
+ `If you're seeing this error, it is likely your bundler isn't configured correctly.`
+ );
+}
diff --git a/package/package.json b/package/package.json
index 07d6b88..58749c5 100644
--- a/package/package.json
+++ b/package/package.json
@@ -1,7 +1,7 @@
{
"name": "@acab/ecsstatic",
"description": "The predefinite CSS-in-JS library for Vite.",
- "version": "0.2.0",
+ "version": "0.3.0-dev.1",
"license": "MIT",
"repository": {
"type": "git",
@@ -33,6 +33,11 @@
"types": "./vite.d.ts",
"import": "./vite.js",
"require": "./vite.cjs"
+ },
+ "./modules": {
+ "types": "./modules.d.ts",
+ "import": "./modules.js",
+ "require": "./modules.cjs"
}
},
"dependencies": {
@@ -42,6 +47,7 @@
"esbuild-plugin-noexternal": "^0.1.4",
"magic-string": "^0.27.0",
"postcss": "^8.4.19",
+ "postcss-modules": "^6.0.0",
"postcss-nested": "^6.0.0",
"postcss-scss": "^4.0.6"
},
diff --git a/package/tsup.config.ts b/package/tsup.config.ts
index 743a97f..a54beab 100644
--- a/package/tsup.config.ts
+++ b/package/tsup.config.ts
@@ -1,7 +1,7 @@
import type { Options } from 'tsup';
export default {
- entryPoints: ['index.ts', 'vite.ts'],
+ entryPoints: ['index.ts', 'vite.ts', 'modules.ts'],
clean: false,
format: ['cjs', 'esm'],
dts: true,
diff --git a/package/vite.ts b/package/vite.ts
index 2cbbe9b..7d5f492 100644
--- a/package/vite.ts
+++ b/package/vite.ts
@@ -3,6 +3,7 @@ import externalizeAllPackagesExcept from 'esbuild-plugin-noexternal';
import MagicString from 'magic-string';
import path from 'path';
import postcss from 'postcss';
+import postcssModules from 'postcss-modules';
import postcssNested from 'postcss-nested';
import postcssScss from 'postcss-scss';
import { ancestor as walk } from 'acorn-walk';
@@ -107,6 +108,7 @@ export function ecsstatic(options: Options = {}) {
for (const node of cssTemplateLiterals) {
const { start, end, quasi, tag, _originalName } = node;
const isScss = tag.type === 'Identifier' && ecsstaticImports.get(tag.name)?.isScss;
+ const isModule = tag.type === 'Identifier' && ecsstaticImports.get(tag.name)?.isModule;
// lazy populate inlinedVars until we need it, to delay problems that come with this mess
if (quasi.expressions.length && !inlinedVars) {
@@ -117,23 +119,30 @@ export function ecsstatic(options: Options = {}) {
const templateContents = quasi.expressions.length
? await processTemplateLiteral(rawTemplate, { inlinedVars })
: rawTemplate.slice(1, rawTemplate.length - 2);
- const [css, className] = processCss(templateContents, isScss);
+
+ // do the scoping!
+ const [css, modulesOrClass] = await processCss(templateContents, { isScss, isModule });
+
+ let returnValue = ''; // what we will replace the tagged template literal with
+ if (isModule) {
+ returnValue = JSON.stringify(modulesOrClass);
+ } else {
+ returnValue = `"${modulesOrClass}"`;
+ // add the original variable name in DEV mode
+ if (_originalName && viteConfigObj.command === 'serve') {
+ returnValue = `"🎈-${_originalName} ${modulesOrClass}"`;
+ }
+ }
// add processed css to a .css file
const extension = isScss ? 'scss' : 'css';
- const cssFilename = `${className}.acab.${extension}`.toLowerCase();
+ const cssFilename = `${hash(templateContents.trim())}.acab.${extension}`.toLowerCase();
magicCode.append(`import "./${cssFilename}";\n`);
const fullCssPath = normalizePath(path.join(path.dirname(id), cssFilename));
cssList.set(fullCssPath, css);
- // add the original variable name in DEV mode
- let _className = `"${className}"`;
- if (_originalName && viteConfigObj.command === 'serve') {
- _className = `"🎈-${_originalName} ${className}"`;
- }
-
- // replace the tagged template literal with the generated className
- magicCode.update(start, end, _className);
+ // replace the tagged template literal with the generated class names
+ magicCode.update(start, end, returnValue);
}
// remove ecsstatic imports, we don't need them anymore
@@ -147,11 +156,8 @@ export function ecsstatic(options: Options = {}) {
};
}
-/**
- * processes template strings using postcss and
- * returns it along with a hashed classname based on the string contents.
- */
-function processCss(templateContents: string, isScss = false) {
+/** processes css and returns it along with hashed classeses */
+async function processCss(templateContents: string, { isScss = false, isModule = false }) {
const isImportOrUse = (line: string) =>
line.trim().startsWith('@import') || line.trim().startsWith('@use');
@@ -166,15 +172,37 @@ function processCss(templateContents: string, isScss = false) {
.join('\n');
const className = `🎈-${hash(templateContents.trim())}`;
- const unprocessedCss = `${importsAndUses}\n.${className}{${codeWithoutImportsAndUses}}`;
+ const unprocessedCss = isModule
+ ? templateContents
+ : `${importsAndUses}\n.${className}{${codeWithoutImportsAndUses}}`;
- const plugins = !isScss
- ? [postcssNested(), autoprefixer(autoprefixerOptions)]
- : [autoprefixer(autoprefixerOptions)];
- const options = isScss ? { parser: postcssScss } : {};
- const { css } = postcss(plugins).process(unprocessedCss, options);
+ const { css, modules } = await postprocessCss(unprocessedCss, { isScss, isModule });
+
+ if (isModule) {
+ return [css, modules] as const;
+ }
- return [css, className];
+ return [css, className] as const;
+}
+
+/** runs postcss with autoprefixer and optionally css-modules */
+async function postprocessCss(rawCss: string, { isScss = false, isModule = false }) {
+ let modules: Record = {};
+
+ const plugins = [
+ !isScss && postcssNested(),
+ autoprefixer(autoprefixerOptions),
+ isModule &&
+ postcssModules({
+ generateScopedName: '🎈-[local]-[hash:base64:6]',
+ getJSON: (_, json) => void (modules = json),
+ }),
+ ].flatMap((value) => (value ? [value] : []));
+
+ const options = isScss ? { parser: postcssScss, from: undefined } : { from: undefined };
+ const { css } = await postcss(plugins).process(rawCss, options);
+
+ return { css, modules };
}
/** resolves all expressions in the template literal and returns a plain string */
@@ -190,13 +218,17 @@ async function processTemplateLiteral(rawTemplate: string, { inlinedVars = '' })
/** parses ast and returns info about all css/scss ecsstatic imports */
function findEcsstaticImports(ast: ESTree.Program) {
- const statements = new Map();
+ const statements = new Map<
+ string,
+ { isScss: boolean; isModule: boolean; start: number; end: number }
+ >();
for (const node of ast.body.filter((node) => node.type === 'ImportDeclaration')) {
if (
node.type === 'ImportDeclaration' &&
node.source.value?.toString().startsWith('@acab/ecsstatic')
) {
+ const isModule = node.source.value?.toString().endsWith('modules');
const { start, end } = node;
node.specifiers.forEach((specifier) => {
if (
@@ -205,7 +237,7 @@ function findEcsstaticImports(ast: ESTree.Program) {
) {
const tagName = specifier.local.name;
const isScss = specifier.imported.name === 'scss';
- statements.set(tagName, { isScss, start, end });
+ statements.set(tagName, { isScss, isModule, start, end });
}
});
}
@@ -316,25 +348,29 @@ function findCssTaggedTemplateLiterals(ast: ESTree.Program, tagNames: string[])
function loadDummyEcsstatic() {
const hashStr = hash.toString();
const getHashFromTemplateStr = getHashFromTemplate.toString();
- const contents = `${hashStr}\n${getHashFromTemplateStr}\n
+ const indexContents = `${hashStr}\n${getHashFromTemplateStr}\n
export const css = getHashFromTemplate;
export const scss = getHashFromTemplate;
`;
+ const modulesContents = `new Proxy({}, {
+ get() { throw 'please don't do this. css modules are hard to evaluate inside other strings :(' }
+ })`;
return {
name: 'load-dummy-ecsstatic',
setup(build) {
build.onResolve({ filter: /^@acab\/ecsstatic$/ }, (args) => {
- return {
- namespace: 'ecsstatic',
- path: args.path,
- };
+ return { namespace: 'ecsstatic', path: args.path };
});
build.onLoad({ filter: /(.*)/, namespace: 'ecsstatic' }, () => {
- return {
- contents,
- loader: 'js',
- };
+ return { contents: indexContents, loader: 'js' };
+ });
+
+ build.onResolve({ filter: /^@acab\/ecsstatic\/modules$/ }, (args) => {
+ return { namespace: 'ecsstatic-modules', path: args.path };
+ });
+ build.onLoad({ filter: /(.*)/, namespace: 'ecsstatic-modules' }, () => {
+ return { contents: modulesContents, loader: 'js' };
});
},
};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9e66782..de3e91d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -139,6 +139,7 @@ importers:
esbuild-plugin-noexternal: ^0.1.4
magic-string: ^0.27.0
postcss: ^8.4.19
+ postcss-modules: ^6.0.0
postcss-nested: ^6.0.0
postcss-scss: ^4.0.6
tsup: ^6.5.0
@@ -150,6 +151,7 @@ importers:
esbuild-plugin-noexternal: 0.1.4
magic-string: 0.27.0
postcss: 8.4.21
+ postcss-modules: 6.0.0_postcss@8.4.21
postcss-nested: 6.0.0_postcss@8.4.21
postcss-scss: 4.0.6_postcss@8.4.21
devDependencies:
@@ -2406,6 +2408,12 @@ packages:
/function-bind/1.1.1:
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
+ /generic-names/4.0.0:
+ resolution: {integrity: sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==}
+ dependencies:
+ loader-utils: 3.2.1
+ dev: false
+
/gensync/1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
@@ -2611,6 +2619,15 @@ packages:
engines: {node: '>=12.20.0'}
dev: false
+ /icss-utils/5.1.0_postcss@8.4.21:
+ resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==}
+ engines: {node: ^10 || ^12 || >= 14}
+ peerDependencies:
+ postcss: ^8.1.0
+ dependencies:
+ postcss: 8.4.21
+ dev: false
+
/ieee754/1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
dev: false
@@ -2822,6 +2839,11 @@ packages:
strip-bom: 3.0.0
dev: false
+ /loader-utils/3.2.1:
+ resolution: {integrity: sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==}
+ engines: {node: '>= 12.13.0'}
+ dev: false
+
/locate-path/3.0.0:
resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==}
engines: {node: '>=6'}
@@ -2844,6 +2866,10 @@ packages:
p-locate: 5.0.0
dev: false
+ /lodash.camelcase/4.3.0:
+ resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
+ dev: false
+
/lodash.sortby/4.7.0:
resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
dev: true
@@ -3555,6 +3581,63 @@ packages:
yaml: 1.10.2
dev: true
+ /postcss-modules-extract-imports/3.0.0_postcss@8.4.21:
+ resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==}
+ engines: {node: ^10 || ^12 || >= 14}
+ peerDependencies:
+ postcss: ^8.1.0
+ dependencies:
+ postcss: 8.4.21
+ dev: false
+
+ /postcss-modules-local-by-default/4.0.0_postcss@8.4.21:
+ resolution: {integrity: sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==}
+ engines: {node: ^10 || ^12 || >= 14}
+ peerDependencies:
+ postcss: ^8.1.0
+ dependencies:
+ icss-utils: 5.1.0_postcss@8.4.21
+ postcss: 8.4.21
+ postcss-selector-parser: 6.0.10
+ postcss-value-parser: 4.2.0
+ dev: false
+
+ /postcss-modules-scope/3.0.0_postcss@8.4.21:
+ resolution: {integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==}
+ engines: {node: ^10 || ^12 || >= 14}
+ peerDependencies:
+ postcss: ^8.1.0
+ dependencies:
+ postcss: 8.4.21
+ postcss-selector-parser: 6.0.10
+ dev: false
+
+ /postcss-modules-values/4.0.0_postcss@8.4.21:
+ resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==}
+ engines: {node: ^10 || ^12 || >= 14}
+ peerDependencies:
+ postcss: ^8.1.0
+ dependencies:
+ icss-utils: 5.1.0_postcss@8.4.21
+ postcss: 8.4.21
+ dev: false
+
+ /postcss-modules/6.0.0_postcss@8.4.21:
+ resolution: {integrity: sha512-7DGfnlyi/ju82BRzTIjWS5C4Tafmzl3R79YP/PASiocj+aa6yYphHhhKUOEoXQToId5rgyFgJ88+ccOUydjBXQ==}
+ peerDependencies:
+ postcss: ^8.0.0
+ dependencies:
+ generic-names: 4.0.0
+ icss-utils: 5.1.0_postcss@8.4.21
+ lodash.camelcase: 4.3.0
+ postcss: 8.4.21
+ postcss-modules-extract-imports: 3.0.0_postcss@8.4.21
+ postcss-modules-local-by-default: 4.0.0_postcss@8.4.21
+ postcss-modules-scope: 3.0.0_postcss@8.4.21
+ postcss-modules-values: 4.0.0_postcss@8.4.21
+ string-hash: 1.1.3
+ dev: false
+
/postcss-nested/6.0.0_postcss@8.4.21:
resolution: {integrity: sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==}
engines: {node: '>=12.0'}
@@ -3999,6 +4082,10 @@ packages:
engines: {node: '>=10.0.0'}
dev: false
+ /string-hash/1.1.3:
+ resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==}
+ dev: false
+
/string-width/4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}