gemcss 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli/gen.d.ts +17 -0
- package/dist/cli/gen.js +39 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +58 -116
- package/dist/core/compile.d.ts +15 -0
- package/dist/core/compile.js +17 -0
- package/dist/core/dts.d.ts +6 -0
- package/dist/core/dts.js +35 -0
- package/dist/core/model.d.ts +16 -0
- package/dist/core/model.js +0 -0
- package/dist/core/parser.d.ts +17 -0
- package/dist/core/parser.js +91 -0
- package/dist/core/runtime-path.d.ts +3 -0
- package/dist/core/runtime-path.js +6 -0
- package/dist/plugin/core/dts.d.ts +6 -0
- package/dist/plugin/core/dts.js +38 -0
- package/dist/plugin/core/model.d.ts +16 -0
- package/dist/plugin/core/model.js +2 -0
- package/dist/plugin/core/parser.d.ts +17 -0
- package/dist/plugin/core/parser.js +100 -0
- package/dist/plugin/package.json +1 -0
- package/dist/plugin/ts-plugin/generate-dts.d.ts +2 -0
- package/dist/plugin/ts-plugin/generate-dts.js +30 -0
- package/dist/plugin/ts-plugin.cjs +53 -0
- package/dist/{ts-plugin.d.cts → plugin/ts-plugin.d.cts} +1 -3
- package/dist/runtime.d.ts +3 -5
- package/dist/runtime.js +26 -23
- package/dist/vite.d.ts +5 -7
- package/dist/vite.js +58 -56
- package/dist/webpack.d.ts +5 -8
- package/dist/webpack.js +22 -30
- package/package.json +8 -9
- package/dist/chunk-CNXDOGPX.js +0 -89
- package/dist/chunk-DMYDRHTS.js +0 -31
- package/dist/compile-Br7cHtI1.d.ts +0 -6
- package/dist/ts-plugin.cjs +0 -206
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# gemcss
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/gemcss)
|
|
4
|
-
[](https://github.com/vkalinichev/gemcss/actions/workflows/ci.yml)
|
|
5
5
|
|
|
6
6
|
Type-safe BEM classes from CSS modules. Write a plain `*.module.css` in BEM
|
|
7
7
|
notation and get back an object where every block is a modifier-combinator
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Default pattern — every CSS module in the project. */
|
|
2
|
+
export declare const DEFAULT_PATTERNS: string[];
|
|
3
|
+
export interface RunOptions {
|
|
4
|
+
cwd?: string;
|
|
5
|
+
}
|
|
6
|
+
/** Declaration file name for a `*.module.css`.
|
|
7
|
+
* `x.module.css` -> `x.module.d.css.ts` (TS `allowArbitraryExtensions`). */
|
|
8
|
+
export declare function dtsPathFor(cssFile: string): string;
|
|
9
|
+
/** Generate the `.d.css.ts` next to one CSS file. Returns the declaration path. */
|
|
10
|
+
export declare function generateOne(cssFile: string): Promise<string>;
|
|
11
|
+
/** Remove the `.d.css.ts` of a CSS file (silent when it does not exist). */
|
|
12
|
+
export declare function removeOne(cssFile: string): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Find CSS modules by pattern and generate a `.d.css.ts` for each one.
|
|
15
|
+
* Returns the paths of the written declarations.
|
|
16
|
+
*/
|
|
17
|
+
export declare function run(patterns: string[], opts?: RunOptions): Promise<string[]>;
|
package/dist/cli/gen.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { glob, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { generateDts } from '../core/dts.js';
|
|
4
|
+
import { parseCss } from '../core/parser.js';
|
|
5
|
+
/** Default pattern — every CSS module in the project. */
|
|
6
|
+
export const DEFAULT_PATTERNS = ['**/*.module.css'];
|
|
7
|
+
/** Declaration file name for a `*.module.css`.
|
|
8
|
+
* `x.module.css` -> `x.module.d.css.ts` (TS `allowArbitraryExtensions`). */
|
|
9
|
+
export function dtsPathFor(cssFile) {
|
|
10
|
+
return cssFile.replace(/\.css$/, '.d.css.ts');
|
|
11
|
+
}
|
|
12
|
+
/** Generate the `.d.css.ts` next to one CSS file. Returns the declaration path. */
|
|
13
|
+
export async function generateOne(cssFile) {
|
|
14
|
+
const css = await readFile(cssFile, 'utf8');
|
|
15
|
+
const dts = generateDts(parseCss(css, { from: cssFile }));
|
|
16
|
+
const out = dtsPathFor(cssFile);
|
|
17
|
+
await writeFile(out, dts);
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
/** Remove the `.d.css.ts` of a CSS file (silent when it does not exist). */
|
|
21
|
+
export async function removeOne(cssFile) {
|
|
22
|
+
await rm(dtsPathFor(cssFile), { force: true });
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Find CSS modules by pattern and generate a `.d.css.ts` for each one.
|
|
26
|
+
* Returns the paths of the written declarations.
|
|
27
|
+
*/
|
|
28
|
+
export async function run(patterns, opts = {}) {
|
|
29
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
30
|
+
const files = glob(patterns.length ? patterns : DEFAULT_PATTERNS, {
|
|
31
|
+
cwd,
|
|
32
|
+
exclude: ['**/node_modules/**'],
|
|
33
|
+
});
|
|
34
|
+
const written = [];
|
|
35
|
+
for await (const file of files) {
|
|
36
|
+
written.push(await generateOne(resolve(cwd, file)));
|
|
37
|
+
}
|
|
38
|
+
return written;
|
|
39
|
+
}
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -1,78 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import { stat } from "fs/promises";
|
|
10
|
-
import { resolve as resolve2 } from "path";
|
|
11
|
-
import { parseArgs } from "util";
|
|
12
|
-
|
|
13
|
-
// src/cli/gen.ts
|
|
14
|
-
import { glob, readFile, rm, writeFile } from "fs/promises";
|
|
15
|
-
import { resolve } from "path";
|
|
16
|
-
|
|
17
|
-
// src/core/dts.ts
|
|
18
|
-
var IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
19
|
-
function propKey(name) {
|
|
20
|
-
return IDENT.test(name) ? name : JSON.stringify(name);
|
|
21
|
-
}
|
|
22
|
-
function strLit(value) {
|
|
23
|
-
return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
24
|
-
}
|
|
25
|
-
function propsParam(block) {
|
|
26
|
-
const keys = Object.keys(block.modifiers);
|
|
27
|
-
if (keys.length === 0) return "";
|
|
28
|
-
const fields = keys.map((k) => {
|
|
29
|
-
const mod = block.modifiers[k];
|
|
30
|
-
const type = mod.kind === "bool" ? "boolean" : mod.values.map(strLit).join(" | ");
|
|
31
|
-
return `${propKey(k)}?: ${type}`;
|
|
32
|
-
});
|
|
33
|
-
return `props?: { ${fields.join("; ")} }`;
|
|
34
|
-
}
|
|
35
|
-
function generateDts(module) {
|
|
36
|
-
const lines = ["declare const gems: {"];
|
|
37
|
-
for (const block of Object.values(module.blocks)) {
|
|
38
|
-
lines.push(` ${propKey(block.name)}(${propsParam(block)}): string;`);
|
|
39
|
-
}
|
|
40
|
-
lines.push("};");
|
|
41
|
-
lines.push("export default gems;");
|
|
42
|
-
lines.push("");
|
|
43
|
-
return lines.join("\n");
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// src/cli/gen.ts
|
|
47
|
-
var DEFAULT_PATTERNS = ["**/*.module.css"];
|
|
48
|
-
function dtsPathFor(cssFile) {
|
|
49
|
-
return cssFile.replace(/\.css$/, ".d.css.ts");
|
|
50
|
-
}
|
|
51
|
-
async function generateOne(cssFile) {
|
|
52
|
-
const css = await readFile(cssFile, "utf8");
|
|
53
|
-
const dts = generateDts(parseCss(css, { from: cssFile }));
|
|
54
|
-
const out = dtsPathFor(cssFile);
|
|
55
|
-
await writeFile(out, dts);
|
|
56
|
-
return out;
|
|
57
|
-
}
|
|
58
|
-
async function removeOne(cssFile) {
|
|
59
|
-
await rm(dtsPathFor(cssFile), { force: true });
|
|
60
|
-
}
|
|
61
|
-
async function run(patterns, opts = {}) {
|
|
62
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
63
|
-
const files = glob(patterns.length ? patterns : DEFAULT_PATTERNS, {
|
|
64
|
-
cwd,
|
|
65
|
-
exclude: ["**/node_modules/**"]
|
|
66
|
-
});
|
|
67
|
-
const written = [];
|
|
68
|
-
for await (const file of files) {
|
|
69
|
-
written.push(await generateOne(resolve(cwd, file)));
|
|
70
|
-
}
|
|
71
|
-
return written;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// src/cli.ts
|
|
75
|
-
var USAGE = `gemcss \u2014 generate .d.css.ts declarations for CSS modules
|
|
2
|
+
import { watch as fsWatch } from 'node:fs';
|
|
3
|
+
import { stat } from 'node:fs/promises';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { parseArgs } from 'node:util';
|
|
6
|
+
import { DEFAULT_PATTERNS, generateOne, removeOne, run } from './cli/gen.js';
|
|
7
|
+
import { GemcssParseError } from './core/parser.js';
|
|
8
|
+
const USAGE = `gemcss — generate .d.css.ts declarations for CSS modules
|
|
76
9
|
|
|
77
10
|
Usage:
|
|
78
11
|
gemcss [patterns...] [options]
|
|
@@ -82,56 +15,65 @@ Options:
|
|
|
82
15
|
--cwd <dir> working directory (default: process.cwd())
|
|
83
16
|
-h, --help show this help
|
|
84
17
|
|
|
85
|
-
Default pattern: ${DEFAULT_PATTERNS.join(
|
|
18
|
+
Default pattern: ${DEFAULT_PATTERNS.join(' ')}
|
|
86
19
|
Watch mode follows every *.module.css under cwd (node_modules ignored).`;
|
|
87
20
|
async function main() {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
21
|
+
const { values, positionals } = parseArgs({
|
|
22
|
+
allowPositionals: true,
|
|
23
|
+
options: {
|
|
24
|
+
watch: { type: 'boolean', short: 'w' },
|
|
25
|
+
cwd: { type: 'string' },
|
|
26
|
+
help: { type: 'boolean', short: 'h' },
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
if (values.help) {
|
|
30
|
+
console.log(USAGE);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const cwd = resolve(values.cwd ?? process.cwd());
|
|
34
|
+
try {
|
|
35
|
+
const written = await run(positionals, { cwd });
|
|
36
|
+
for (const file of written)
|
|
37
|
+
console.log(`generated ${file}`);
|
|
38
|
+
console.log(`gemcss: ${written.length} file(s)`);
|
|
94
39
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const written = await run(positionals, { cwd });
|
|
103
|
-
for (const file of written) console.log(`generated ${file}`);
|
|
104
|
-
console.log(`gemcss: ${written.length} file(s)`);
|
|
105
|
-
} catch (err) {
|
|
106
|
-
fail(err);
|
|
107
|
-
if (!values.watch) process.exit(1);
|
|
108
|
-
}
|
|
109
|
-
if (values.watch) watch(cwd);
|
|
40
|
+
catch (err) {
|
|
41
|
+
fail(err);
|
|
42
|
+
if (!values.watch)
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
if (values.watch)
|
|
46
|
+
watch(cwd);
|
|
110
47
|
}
|
|
48
|
+
/** Watch `*.module.css` under cwd: file exists — regenerate the declaration, gone — drop it. */
|
|
111
49
|
function watch(cwd) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
50
|
+
fsWatch(cwd, { recursive: true }, (_event, rel) => {
|
|
51
|
+
if (!rel || !rel.endsWith('.module.css') || rel.includes('node_modules'))
|
|
52
|
+
return;
|
|
53
|
+
void onChange(resolve(cwd, rel));
|
|
54
|
+
});
|
|
55
|
+
console.log(`gemcss: watching ${cwd}`);
|
|
117
56
|
}
|
|
118
57
|
async function onChange(file) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
58
|
+
try {
|
|
59
|
+
await stat(file);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return removeOne(file);
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
console.log(`generated ${await generateOne(file)}`);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
fail(err);
|
|
69
|
+
}
|
|
129
70
|
}
|
|
130
71
|
function fail(err) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
72
|
+
if (err instanceof GemcssParseError) {
|
|
73
|
+
console.error(`gemcss: ${err.message}`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
console.error(err);
|
|
77
|
+
}
|
|
136
78
|
}
|
|
137
79
|
void main();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Module } from './model.js';
|
|
2
|
+
export interface CompileResult {
|
|
3
|
+
/** Transformed CSS with scoped class names — handed to the bundler for injection. */
|
|
4
|
+
css: string;
|
|
5
|
+
/** Map of raw name → scoped name (postcss-modules output). */
|
|
6
|
+
scoped: Record<string, string>;
|
|
7
|
+
/** The gemcss model built from the raw class names. */
|
|
8
|
+
module: Module;
|
|
9
|
+
}
|
|
10
|
+
export interface CompileOptions {
|
|
11
|
+
/** Scoped name template for postcss-modules. */
|
|
12
|
+
generateScopedName?: string;
|
|
13
|
+
}
|
|
14
|
+
/** Run a CSS module through postcss-modules and build the gemcss model. */
|
|
15
|
+
export declare function compile(code: string, id: string, opts?: CompileOptions): Promise<CompileResult>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import postcss from 'postcss';
|
|
2
|
+
import postcssModules from 'postcss-modules';
|
|
3
|
+
import { parseCss } from './parser.js';
|
|
4
|
+
/** Run a CSS module through postcss-modules and build the gemcss model. */
|
|
5
|
+
export async function compile(code, id, opts = {}) {
|
|
6
|
+
let scoped = {};
|
|
7
|
+
const result = await postcss([
|
|
8
|
+
postcssModules({
|
|
9
|
+
generateScopedName: opts.generateScopedName ?? '[name]__[local]__[hash:base64:5]',
|
|
10
|
+
getJSON: (_file, json) => {
|
|
11
|
+
scoped = json;
|
|
12
|
+
},
|
|
13
|
+
}),
|
|
14
|
+
]).process(code, { from: id });
|
|
15
|
+
const module = parseCss(code, { from: id });
|
|
16
|
+
return { css: result.css, scoped, module };
|
|
17
|
+
}
|
package/dist/core/dts.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
2
|
+
/** Property name: bare if a valid identifier, quoted otherwise. */
|
|
3
|
+
function propKey(name) {
|
|
4
|
+
return IDENT.test(name) ? name : JSON.stringify(name);
|
|
5
|
+
}
|
|
6
|
+
/** TS string literal for an enum value. */
|
|
7
|
+
function strLit(value) {
|
|
8
|
+
return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
9
|
+
}
|
|
10
|
+
/** Argument signature of a block's gem function (empty when it has no modifiers). */
|
|
11
|
+
function propsParam(block) {
|
|
12
|
+
const keys = Object.keys(block.modifiers);
|
|
13
|
+
if (keys.length === 0)
|
|
14
|
+
return '';
|
|
15
|
+
const fields = keys.map((k) => {
|
|
16
|
+
const mod = block.modifiers[k];
|
|
17
|
+
const type = mod.kind === 'bool' ? 'boolean' : mod.values.map(strLit).join(' | ');
|
|
18
|
+
return `${propKey(k)}?: ${type}`;
|
|
19
|
+
});
|
|
20
|
+
return `props?: { ${fields.join('; ')} }`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Generate the `.d.ts` text for a model.
|
|
24
|
+
* Every block is a `(props?) => string` function; default export is `gems`.
|
|
25
|
+
*/
|
|
26
|
+
export function generateDts(module) {
|
|
27
|
+
const lines = ['declare const gems: {'];
|
|
28
|
+
for (const block of Object.values(module.blocks)) {
|
|
29
|
+
lines.push(` ${propKey(block.name)}(${propsParam(block)}): string;`);
|
|
30
|
+
}
|
|
31
|
+
lines.push('};');
|
|
32
|
+
lines.push('export default gems;');
|
|
33
|
+
lines.push('');
|
|
34
|
+
return lines.join('\n');
|
|
35
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** A single block modifier: either a boolean flag or an enum with a set of values. */
|
|
2
|
+
export type Modifier = {
|
|
3
|
+
kind: 'bool';
|
|
4
|
+
} | {
|
|
5
|
+
kind: 'enum';
|
|
6
|
+
values: string[];
|
|
7
|
+
};
|
|
8
|
+
/** A BEM block — the first segment of a class name. */
|
|
9
|
+
export interface Block {
|
|
10
|
+
name: string;
|
|
11
|
+
modifiers: Record<string, Modifier>;
|
|
12
|
+
}
|
|
13
|
+
/** The model of one `*.module.css`. */
|
|
14
|
+
export interface Module {
|
|
15
|
+
blocks: Record<string, Block>;
|
|
16
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Module } from './model.js';
|
|
2
|
+
/** Thrown when a class name violates the naming convention. */
|
|
3
|
+
export declare class GemcssParseError extends Error {
|
|
4
|
+
constructor(message: string);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Extract class selectors from CSS source and build the model.
|
|
8
|
+
* Classes keep the order of their first appearance.
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseCss(css: string, opts?: {
|
|
11
|
+
from?: string;
|
|
12
|
+
}): Module;
|
|
13
|
+
/**
|
|
14
|
+
* Build the model from a list of raw class names.
|
|
15
|
+
* Convention: `block`, `block_bool`, `block_key_value`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildModule(classNames: string[], from?: string): Module;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import postcss from 'postcss';
|
|
2
|
+
import selectorParser from 'postcss-selector-parser';
|
|
3
|
+
/** Thrown when a class name violates the naming convention. */
|
|
4
|
+
export class GemcssParseError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'GemcssParseError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function ctx(from) {
|
|
11
|
+
return from ? ` (in ${from})` : '';
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Extract class selectors from CSS source and build the model.
|
|
15
|
+
* Classes keep the order of their first appearance.
|
|
16
|
+
*/
|
|
17
|
+
export function parseCss(css, opts = {}) {
|
|
18
|
+
const root = postcss.parse(css, { from: opts.from });
|
|
19
|
+
const classNames = [];
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
root.walkRules((rule) => {
|
|
22
|
+
selectorParser((selectors) => {
|
|
23
|
+
selectors.walkClasses((cls) => {
|
|
24
|
+
if (!seen.has(cls.value)) {
|
|
25
|
+
seen.add(cls.value);
|
|
26
|
+
classNames.push(cls.value);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}).processSync(rule.selector);
|
|
30
|
+
});
|
|
31
|
+
return buildModule(classNames, opts.from);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build the model from a list of raw class names.
|
|
35
|
+
* Convention: `block`, `block_bool`, `block_key_value`.
|
|
36
|
+
*/
|
|
37
|
+
export function buildModule(classNames, from) {
|
|
38
|
+
const blocks = {};
|
|
39
|
+
for (const raw of classNames) {
|
|
40
|
+
const segments = raw.split('_');
|
|
41
|
+
const blockName = segments[0];
|
|
42
|
+
if (!blockName) {
|
|
43
|
+
throw new GemcssParseError(`Empty block name in class ".${raw}"${ctx(from)}`);
|
|
44
|
+
}
|
|
45
|
+
const block = (blocks[blockName] ??= {
|
|
46
|
+
name: blockName,
|
|
47
|
+
modifiers: {},
|
|
48
|
+
});
|
|
49
|
+
const tail = segments.slice(1);
|
|
50
|
+
if (tail.length === 0)
|
|
51
|
+
continue;
|
|
52
|
+
if (tail.length === 1) {
|
|
53
|
+
const key = tail[0];
|
|
54
|
+
if (!key)
|
|
55
|
+
throw new GemcssParseError(`Empty modifier in class ".${raw}"${ctx(from)}`);
|
|
56
|
+
assignBool(block, key, raw, from);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (tail.length === 2) {
|
|
60
|
+
const key = tail[0];
|
|
61
|
+
const value = tail[1];
|
|
62
|
+
if (!key || !value) {
|
|
63
|
+
throw new GemcssParseError(`Empty modifier key/value in class ".${raw}"${ctx(from)}`);
|
|
64
|
+
}
|
|
65
|
+
assignEnum(block, key, value, raw, from);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
throw new GemcssParseError(`Class ".${raw}" has more than one modifier segment after block "${blockName}". ` +
|
|
69
|
+
`Allowed: block, block_bool, block_key_value.${ctx(from)}`);
|
|
70
|
+
}
|
|
71
|
+
return { blocks };
|
|
72
|
+
}
|
|
73
|
+
function assignBool(block, key, raw, from) {
|
|
74
|
+
const existing = block.modifiers[key];
|
|
75
|
+
if (existing && existing.kind === 'enum') {
|
|
76
|
+
throw new GemcssParseError(`Modifier "${key}" of block "${block.name}" used as both boolean (".${raw}") and enum.${ctx(from)}`);
|
|
77
|
+
}
|
|
78
|
+
block.modifiers[key] = { kind: 'bool' };
|
|
79
|
+
}
|
|
80
|
+
function assignEnum(block, key, value, raw, from) {
|
|
81
|
+
const existing = block.modifiers[key];
|
|
82
|
+
if (existing && existing.kind === 'bool') {
|
|
83
|
+
throw new GemcssParseError(`Modifier "${key}" of block "${block.name}" used as both enum (".${raw}") and boolean.${ctx(from)}`);
|
|
84
|
+
}
|
|
85
|
+
const mod = existing && existing.kind === 'enum'
|
|
86
|
+
? existing
|
|
87
|
+
: (block.modifiers[key] = { kind: 'enum', values: [] });
|
|
88
|
+
if (!mod.values.includes(value)) {
|
|
89
|
+
mod.values.push(value);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
/** Absolute path to the gemcss runtime: generated and virtual modules cannot
|
|
3
|
+
* resolve a bare specifier, so the emitted import gets a ready-made path. */
|
|
4
|
+
export function runtimePath() {
|
|
5
|
+
return createRequire(import.meta.url).resolve('gemcss/runtime');
|
|
6
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateDts = generateDts;
|
|
4
|
+
const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
5
|
+
/** Property name: bare if a valid identifier, quoted otherwise. */
|
|
6
|
+
function propKey(name) {
|
|
7
|
+
return IDENT.test(name) ? name : JSON.stringify(name);
|
|
8
|
+
}
|
|
9
|
+
/** TS string literal for an enum value. */
|
|
10
|
+
function strLit(value) {
|
|
11
|
+
return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
12
|
+
}
|
|
13
|
+
/** Argument signature of a block's gem function (empty when it has no modifiers). */
|
|
14
|
+
function propsParam(block) {
|
|
15
|
+
const keys = Object.keys(block.modifiers);
|
|
16
|
+
if (keys.length === 0)
|
|
17
|
+
return '';
|
|
18
|
+
const fields = keys.map((k) => {
|
|
19
|
+
const mod = block.modifiers[k];
|
|
20
|
+
const type = mod.kind === 'bool' ? 'boolean' : mod.values.map(strLit).join(' | ');
|
|
21
|
+
return `${propKey(k)}?: ${type}`;
|
|
22
|
+
});
|
|
23
|
+
return `props?: { ${fields.join('; ')} }`;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Generate the `.d.ts` text for a model.
|
|
27
|
+
* Every block is a `(props?) => string` function; default export is `gems`.
|
|
28
|
+
*/
|
|
29
|
+
function generateDts(module) {
|
|
30
|
+
const lines = ['declare const gems: {'];
|
|
31
|
+
for (const block of Object.values(module.blocks)) {
|
|
32
|
+
lines.push(` ${propKey(block.name)}(${propsParam(block)}): string;`);
|
|
33
|
+
}
|
|
34
|
+
lines.push('};');
|
|
35
|
+
lines.push('export default gems;');
|
|
36
|
+
lines.push('');
|
|
37
|
+
return lines.join('\n');
|
|
38
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** A single block modifier: either a boolean flag or an enum with a set of values. */
|
|
2
|
+
export type Modifier = {
|
|
3
|
+
kind: 'bool';
|
|
4
|
+
} | {
|
|
5
|
+
kind: 'enum';
|
|
6
|
+
values: string[];
|
|
7
|
+
};
|
|
8
|
+
/** A BEM block — the first segment of a class name. */
|
|
9
|
+
export interface Block {
|
|
10
|
+
name: string;
|
|
11
|
+
modifiers: Record<string, Modifier>;
|
|
12
|
+
}
|
|
13
|
+
/** The model of one `*.module.css`. */
|
|
14
|
+
export interface Module {
|
|
15
|
+
blocks: Record<string, Block>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Module } from './model.js';
|
|
2
|
+
/** Thrown when a class name violates the naming convention. */
|
|
3
|
+
export declare class GemcssParseError extends Error {
|
|
4
|
+
constructor(message: string);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Extract class selectors from CSS source and build the model.
|
|
8
|
+
* Classes keep the order of their first appearance.
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseCss(css: string, opts?: {
|
|
11
|
+
from?: string;
|
|
12
|
+
}): Module;
|
|
13
|
+
/**
|
|
14
|
+
* Build the model from a list of raw class names.
|
|
15
|
+
* Convention: `block`, `block_bool`, `block_key_value`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildModule(classNames: string[], from?: string): Module;
|