@tok124/tokcss 1.0.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/package.json +25 -0
- package/tokcss.js +201 -0
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tok124/tokcss",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A stupidly simple CSS preprocessor with one directive: #include:\"file.css\"; — optionally wrapped in @layer/@supports/@media, just like @import.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"tokcss": "tokcss.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "tokcss.js",
|
|
9
|
+
"files": [
|
|
10
|
+
"tokcss.js"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"css",
|
|
14
|
+
"preprocessor",
|
|
15
|
+
"include",
|
|
16
|
+
"cli",
|
|
17
|
+
"layer",
|
|
18
|
+
"cascade-layers"
|
|
19
|
+
],
|
|
20
|
+
"author": "Tokdev",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=14"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/tokcss.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* tokcss — a stupidly simple CSS "preprocessor"
|
|
4
|
+
*
|
|
5
|
+
* The only thing it understands is:
|
|
6
|
+
*
|
|
7
|
+
* #include:"path/to/file.css";
|
|
8
|
+
*
|
|
9
|
+
* optionally followed by the same trailing conditions @import supports:
|
|
10
|
+
*
|
|
11
|
+
* #include:"reset.css" layer(reset);
|
|
12
|
+
* #include:"grid.css" supports(display: grid);
|
|
13
|
+
* #include:"wide.css" (width >= 50rem);
|
|
14
|
+
* #include:"a.css" layer(base) supports(display: grid) (min-width: 500px);
|
|
15
|
+
*
|
|
16
|
+
* Each one wraps the included file's compiled content in the matching
|
|
17
|
+
* @layer / @supports / @media block, nested in that order.
|
|
18
|
+
*
|
|
19
|
+
* It reads an entry .tokcss file, follows every #include, and glues
|
|
20
|
+
* everything together into one plain .css file — like SCSS's @use,
|
|
21
|
+
* but with a single directive and zero other opinions.
|
|
22
|
+
*
|
|
23
|
+
* Usage:
|
|
24
|
+
* node tokcss.js entry.tokcss -> prints result to stdout
|
|
25
|
+
* node tokcss.js entry.tokcss -o out.css -> writes result to out.css
|
|
26
|
+
* node tokcss.js entry.tokcss -o out.css -w -> watches all included files, recompiles on change
|
|
27
|
+
* node tokcss.js entry.tokcss -o out.css --no-banner -> omit the "from: ..." source markers
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const fs = require('fs');
|
|
31
|
+
const path = require('path');
|
|
32
|
+
|
|
33
|
+
const INCLUDE_RE = /#include:"([^"]+)"\s*([^;\n]*);?/g;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parses the text after the quoted path (e.g. `layer(reset) (width >= 50rem)`)
|
|
37
|
+
* into its layer / supports / media parts, mirroring @import's grammar.
|
|
38
|
+
* Returns null values for anything not present.
|
|
39
|
+
*/
|
|
40
|
+
function parseModifiers(raw) {
|
|
41
|
+
let rest = raw.trim();
|
|
42
|
+
let layer = null; // null = no layer; '' = anonymous layer; else a layer name
|
|
43
|
+
let supports = null;
|
|
44
|
+
|
|
45
|
+
let m = rest.match(/^layer\(([^)]*)\)/i);
|
|
46
|
+
if (m) {
|
|
47
|
+
layer = m[1].trim();
|
|
48
|
+
rest = rest.slice(m[0].length).trim();
|
|
49
|
+
} else if (/^layer\b/i.test(rest)) {
|
|
50
|
+
layer = '';
|
|
51
|
+
rest = rest.replace(/^layer\b/i, '').trim();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
m = rest.match(/^supports\(([^)]*)\)/i);
|
|
55
|
+
if (m) {
|
|
56
|
+
supports = m[1].trim();
|
|
57
|
+
rest = rest.slice(m[0].length).trim();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Whatever's left is the media condition, e.g. "(width >= 50rem)" or "screen and (min-width: 500px)".
|
|
61
|
+
const media = rest.trim() || null;
|
|
62
|
+
|
|
63
|
+
return { layer, supports, media };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Indents every non-blank line of `text` by `spaces` spaces. */
|
|
67
|
+
function indent(text, spaces = 2) {
|
|
68
|
+
const pad = ' '.repeat(spaces);
|
|
69
|
+
return text
|
|
70
|
+
.split('\n')
|
|
71
|
+
.map((line) => (line.trim() ? pad + line : line))
|
|
72
|
+
.join('\n');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Wraps compiled content in @layer / @supports / @media blocks per the parsed modifiers. */
|
|
76
|
+
function applyModifiers(content, mods) {
|
|
77
|
+
let out = content;
|
|
78
|
+
if (mods.media) out = `@media ${mods.media} {\n${indent(out)}}\n`;
|
|
79
|
+
if (mods.supports !== null) out = `@supports (${mods.supports}) {\n${indent(out)}}\n`;
|
|
80
|
+
if (mods.layer !== null) out = `@layer${mods.layer ? ' ' + mods.layer : ''} {\n${indent(out)}}\n`;
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Recursively resolves #include statements starting from entryFile.
|
|
86
|
+
* `seen` tracks absolute paths already inlined so:
|
|
87
|
+
* - circular includes don't infinite-loop
|
|
88
|
+
* - the same file included twice only gets inlined once (like @use)
|
|
89
|
+
* `touched` collects every file that was actually read, so --watch
|
|
90
|
+
* knows what to keep an eye on.
|
|
91
|
+
*/
|
|
92
|
+
function compile(entryFile, { seen = new Set(), touched = new Set(), banner = true } = {}) {
|
|
93
|
+
const fullPath = path.resolve(entryFile);
|
|
94
|
+
|
|
95
|
+
if (seen.has(fullPath)) {
|
|
96
|
+
return banner ? `/* skipped duplicate include: ${relative(fullPath)} */\n` : '';
|
|
97
|
+
}
|
|
98
|
+
seen.add(fullPath);
|
|
99
|
+
|
|
100
|
+
if (!fs.existsSync(fullPath)) {
|
|
101
|
+
throw new Error(`#include target not found: ${fullPath}`);
|
|
102
|
+
}
|
|
103
|
+
touched.add(fullPath);
|
|
104
|
+
|
|
105
|
+
const dir = path.dirname(fullPath);
|
|
106
|
+
const raw = fs.readFileSync(fullPath, 'utf8');
|
|
107
|
+
|
|
108
|
+
const body = raw.replace(INCLUDE_RE, (match, includePath, modifierText) => {
|
|
109
|
+
const resolved = path.resolve(dir, includePath);
|
|
110
|
+
const compiled = compile(resolved, { seen, touched, banner });
|
|
111
|
+
const mods = parseModifiers(modifierText);
|
|
112
|
+
return applyModifiers(compiled, mods);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return banner ? `/* --- from: ${relative(fullPath)} --- */\n${body}\n` : body;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function relative(p) {
|
|
119
|
+
return path.relative(process.cwd(), p) || path.basename(p);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseArgs(argv) {
|
|
123
|
+
const args = { input: null, output: null, watch: false, banner: true };
|
|
124
|
+
for (let i = 0; i < argv.length; i++) {
|
|
125
|
+
const a = argv[i];
|
|
126
|
+
if (a === '-o' || a === '--output') args.output = argv[++i];
|
|
127
|
+
else if (a === '-w' || a === '--watch') args.watch = true;
|
|
128
|
+
else if (a === '--no-banner') args.banner = false;
|
|
129
|
+
else if (!args.input) args.input = a;
|
|
130
|
+
}
|
|
131
|
+
return args;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function run(args) {
|
|
135
|
+
const touched = new Set();
|
|
136
|
+
let result;
|
|
137
|
+
try {
|
|
138
|
+
result = compile(args.input, { touched, banner: args.banner });
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.error(`tokcss: ${err.message}`);
|
|
141
|
+
return { ok: false, touched };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (args.output) {
|
|
145
|
+
fs.mkdirSync(path.dirname(path.resolve(args.output)), { recursive: true });
|
|
146
|
+
fs.writeFileSync(args.output, result);
|
|
147
|
+
console.log(`tokcss: compiled -> ${args.output} (${touched.size} file${touched.size === 1 ? '' : 's'})`);
|
|
148
|
+
} else {
|
|
149
|
+
process.stdout.write(result);
|
|
150
|
+
}
|
|
151
|
+
return { ok: true, touched };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function main() {
|
|
155
|
+
const args = parseArgs(process.argv.slice(2));
|
|
156
|
+
if (!args.input) {
|
|
157
|
+
console.error('Usage: node tokcss.js <entry.tokcss> [-o out.css] [-w] [--no-banner]');
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const { ok, touched } = run(args);
|
|
162
|
+
|
|
163
|
+
if (args.watch && !ok) {
|
|
164
|
+
console.error('tokcss: fix the error above, then re-run to start watching.');
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (args.watch) {
|
|
169
|
+
if (!args.output) {
|
|
170
|
+
console.error('tokcss: --watch requires -o <output file>');
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
console.log(`tokcss: watching ${touched.size} file(s) for changes... (Ctrl+C to stop)`);
|
|
174
|
+
const watchers = new Map();
|
|
175
|
+
|
|
176
|
+
const rewatch = () => {
|
|
177
|
+
// Recompute the file set after every recompile, since edits can
|
|
178
|
+
// add or remove #include lines and change what needs watching.
|
|
179
|
+
for (const w of watchers.values()) w.close();
|
|
180
|
+
watchers.clear();
|
|
181
|
+
|
|
182
|
+
const { touched: newTouched } = run(args);
|
|
183
|
+
for (const file of newTouched) {
|
|
184
|
+
const watcher = fs.watch(file, { persistent: true }, () => {
|
|
185
|
+
// Debounce-ish: editors often fire multiple events per save.
|
|
186
|
+
setTimeout(rewatch, 50);
|
|
187
|
+
});
|
|
188
|
+
watchers.set(file, watcher);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
for (const file of touched) {
|
|
193
|
+
const watcher = fs.watch(file, { persistent: true }, () => {
|
|
194
|
+
setTimeout(rewatch, 50);
|
|
195
|
+
});
|
|
196
|
+
watchers.set(file, watcher);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
main();
|