@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.
Files changed (2) hide show
  1. package/package.json +25 -0
  2. 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();