@vesk/plugin-tailwind 0.0.1

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 ADDED
@@ -0,0 +1,13 @@
1
+ # @vesk/plugin-tailwind
2
+
3
+ Tailwind CSS v4 plugin for Vesk. Extracts Tailwind directives from `.vsk` files and compiles them at build time.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install @vesk/plugin-tailwind
9
+ ```
10
+
11
+ ## License
12
+
13
+ MIT
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Vesk Tailwind CSS plugin.
3
+ * Scans .vsk files for class attributes, builds a purge-able content list,
4
+ * runs the Tailwind CLI to generate the final CSS, and handles HMR rebuilds.
5
+ * @module plugin-tailwind
6
+ */
7
+ export interface TailwindOptions {
8
+ entry?: string;
9
+ appDir?: string;
10
+ }
11
+ declare function extractTailwindDirectives(css: string): {
12
+ directives: string;
13
+ userCSS: string;
14
+ };
15
+ export { extractTailwindDirectives };
16
+ interface TailwindPlugin {
17
+ name: string;
18
+ dependencies: Set<string>;
19
+ onBuildStart: () => Promise<void>;
20
+ onCSS: (content: string, filePath: string) => Promise<string | null>;
21
+ onFileWatch: (filePath: string) => Promise<{
22
+ handled: boolean;
23
+ }>;
24
+ }
25
+ export default function tailwindcss(options?: TailwindOptions): TailwindPlugin;
package/dist/index.js ADDED
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Vesk Tailwind CSS plugin.
3
+ * Scans .vsk files for class attributes, builds a purge-able content list,
4
+ * runs the Tailwind CLI to generate the final CSS, and handles HMR rebuilds.
5
+ * @module plugin-tailwind
6
+ */
7
+ import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
8
+ import { resolve, join } from 'path';
9
+ const CLASS_RE = /class(?:\w+)?=["']([^"']*)["']/g;
10
+ const SCAN_EXT = /\.(vsk|js|ts|jsx|tsx)$/;
11
+ function walkFiles(dir) {
12
+ const out = [];
13
+ for (const name of readdirSync(dir)) {
14
+ const p = join(dir, name);
15
+ if (name === 'node_modules' || name === 'dist' || name === '.vesk' || name === '.vercel' || name === '.git')
16
+ continue;
17
+ let st;
18
+ try {
19
+ st = statSync(p);
20
+ }
21
+ catch {
22
+ continue;
23
+ }
24
+ if (st.isDirectory()) {
25
+ out.push(...walkFiles(p));
26
+ }
27
+ else if (SCAN_EXT.test(name)) {
28
+ out.push(p);
29
+ }
30
+ }
31
+ return out;
32
+ }
33
+ function scanCandidates(dir) {
34
+ const candidates = new Set();
35
+ const base = resolve(dir);
36
+ if (!existsSync(base))
37
+ return [];
38
+ for (const file of walkFiles(base)) {
39
+ try {
40
+ const content = readFileSync(file, 'utf-8');
41
+ let match;
42
+ while ((match = CLASS_RE.exec(content)) !== null) {
43
+ const classes = match[1].split(/\s+/);
44
+ for (const cls of classes) {
45
+ if (cls && cls.length > 1 && !cls.startsWith('{') && !cls.startsWith('}')) {
46
+ candidates.add(cls);
47
+ }
48
+ }
49
+ }
50
+ }
51
+ catch { }
52
+ }
53
+ return [...candidates];
54
+ }
55
+ async function loadFallbackStylesheet(id, base) {
56
+ const { readFile } = await import('fs/promises');
57
+ const path = await import('path');
58
+ const { createRequire } = await import('module');
59
+ if (id.startsWith('.')) {
60
+ const p = path.resolve(base, id);
61
+ const content = await readFile(p, 'utf-8');
62
+ return { content, base: path.dirname(p) };
63
+ }
64
+ const req = createRequire(path.join(base, 'noop.mjs'));
65
+ const pkgJson = req.resolve(id + '/package.json');
66
+ const pkgDir = path.dirname(pkgJson);
67
+ const pkg = JSON.parse(await readFile(pkgJson, 'utf-8'));
68
+ let cssPath = null;
69
+ if (pkg.style) {
70
+ cssPath = path.resolve(pkgDir, pkg.style);
71
+ }
72
+ else if (pkg.exports?.['.']?.style) {
73
+ cssPath = path.resolve(pkgDir, pkg.exports['.'].style);
74
+ }
75
+ if (cssPath === null)
76
+ cssPath = path.resolve(pkgDir, 'index.css');
77
+ if (!existsSync(cssPath))
78
+ throw new Error(`Cannot resolve stylesheet for package '${id}'`);
79
+ const content = await readFile(cssPath, 'utf-8');
80
+ return { content, base: path.dirname(cssPath) };
81
+ }
82
+ async function loadFallbackModule(id, base) {
83
+ const path = await import('path');
84
+ const { createRequire } = await import('module');
85
+ if (id.startsWith('.')) {
86
+ const p = path.resolve(base, id);
87
+ const mod = await import(p);
88
+ return { module: mod.default ?? mod, base: path.dirname(p) };
89
+ }
90
+ const req = createRequire(path.join(base, 'noop.mjs'));
91
+ const pkgPath = req.resolve(id);
92
+ const mod = await import(pkgPath);
93
+ return { module: mod.default ?? mod, base: path.dirname(pkgPath) };
94
+ }
95
+ const TAILWIND_BLOCK = /^\s*@(theme\s*\{|layer\s+(base|components|utilities)\s*\{|utility\s+\w+\s*\{)/m;
96
+ /**
97
+ * Scans a CSS block starting at `start` (an opening `{`) and returns the
98
+ * index just past its matching `}`. Quoted strings and `/* ... *​/`
99
+ * comments are skipped so braces inside them (e.g. `url("data:...{}")`)
100
+ * do not confuse the nesting count. (Mirrors `cssBlockEnd` in
101
+ * `@vesk/compiler/src/scan.ts` — kept local so this package stays
102
+ * independent of the compiler.)
103
+ */
104
+ function cssBlockEnd(css, start) {
105
+ let depth = 0;
106
+ let i = start;
107
+ while (i < css.length) {
108
+ const c = css[i];
109
+ if (c === '"' || c === "'") {
110
+ i++;
111
+ while (i < css.length && css[i] !== c)
112
+ i++;
113
+ i++;
114
+ continue;
115
+ }
116
+ if (c === '/' && css[i + 1] === '*') {
117
+ i += 2;
118
+ while (i < css.length && !(css[i] === '*' && css[i + 1] === '/'))
119
+ i++;
120
+ i += 2;
121
+ continue;
122
+ }
123
+ if (c === '{')
124
+ depth++;
125
+ else if (c === '}') {
126
+ depth--;
127
+ if (depth === 0)
128
+ return i + 1;
129
+ }
130
+ i++;
131
+ }
132
+ return css.length;
133
+ }
134
+ function extractTailwindDirectives(css) {
135
+ const tailwindChunks = [];
136
+ const userChunks = [];
137
+ let pos = 0;
138
+ while (pos < css.length) {
139
+ const lineStart = pos;
140
+ const nl = css.indexOf('\n', pos);
141
+ const lineEnd = nl === -1 ? css.length : nl + 1;
142
+ const line = css.slice(lineStart, lineEnd);
143
+ const trimmed = line.trim();
144
+ const isImport = trimmed.startsWith('@import') && trimmed.includes('tailwindcss');
145
+ const isSource = trimmed.startsWith('@source ');
146
+ const isBlockStart = TAILWIND_BLOCK.test(trimmed);
147
+ if (isImport || isSource) {
148
+ tailwindChunks.push(line);
149
+ pos = lineEnd;
150
+ continue;
151
+ }
152
+ if (isBlockStart) {
153
+ const end = cssBlockEnd(css, lineStart);
154
+ tailwindChunks.push(css.slice(lineStart, end));
155
+ pos = end;
156
+ continue;
157
+ }
158
+ userChunks.push(line);
159
+ pos = lineEnd;
160
+ }
161
+ return {
162
+ directives: tailwindChunks.join('').trim(),
163
+ userCSS: userChunks.join('').trim(),
164
+ };
165
+ }
166
+ export { extractTailwindDirectives };
167
+ export default function tailwindcss(options = {}) {
168
+ const entry = options.entry || 'src/global.css';
169
+ const appDir = options.appDir || 'app';
170
+ let compileReady = null;
171
+ async function getCompile() {
172
+ if (compileReady)
173
+ return compileReady;
174
+ try {
175
+ const tw = await import('@tailwindcss/node');
176
+ compileReady = { compile: tw.compile, node: true };
177
+ }
178
+ catch {
179
+ const tw = await import('tailwindcss');
180
+ compileReady = { compile: tw.compile, node: false };
181
+ }
182
+ return compileReady;
183
+ }
184
+ return {
185
+ name: '@vesk/plugin-tailwind',
186
+ dependencies: new Set(),
187
+ async onBuildStart() {
188
+ this.dependencies = new Set();
189
+ },
190
+ async onCSS(content, filePath) {
191
+ const baseDir = process.cwd();
192
+ const entryPath = resolve(baseDir, entry);
193
+ const resolvedFilePath = filePath ? resolve(baseDir, filePath) : '';
194
+ if (resolvedFilePath !== entryPath && !(filePath && filePath.endsWith(entry.replace(/^\.\//, '')))) {
195
+ return null;
196
+ }
197
+ const { directives } = extractTailwindDirectives(content);
198
+ if (!directives)
199
+ return content;
200
+ try {
201
+ const { compile, node } = await getCompile();
202
+ const compileOpts = {
203
+ base: baseDir,
204
+ from: entryPath,
205
+ onDependency: (path) => {
206
+ if (path)
207
+ this.dependencies.add(path);
208
+ },
209
+ };
210
+ if (!node) {
211
+ compileOpts.loadStylesheet = (id, base) => loadFallbackStylesheet(id, base);
212
+ compileOpts.loadModule = (id, base) => loadFallbackModule(id, base);
213
+ }
214
+ const result = await compile(directives, compileOpts);
215
+ const candidates = scanCandidates(baseDir);
216
+ const finalCss = result.build(candidates);
217
+ return finalCss;
218
+ }
219
+ catch (err) {
220
+ console.error(`[vesk/plugin-tailwind] Compile error:`, err instanceof Error ? err.message : String(err));
221
+ return content;
222
+ }
223
+ },
224
+ async onFileWatch(filePath) {
225
+ if (this.dependencies.has(filePath)) {
226
+ return { handled: true };
227
+ }
228
+ return { handled: false };
229
+ },
230
+ };
231
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@vesk/plugin-tailwind",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "Tailwind CSS v4 plugin for Vesk",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc -p tsconfig.build.json",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "dependencies": {
19
+ "@tailwindcss/node": "^4.0.0",
20
+ "@tailwindcss/oxide": "^4.0.0"
21
+ },
22
+ "license": "MIT",
23
+ "author": "emeraldlinks",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/emeraldlinks/veskTs.git"
27
+ },
28
+ "homepage": "https://github.com/emeraldlinks/veskTs#readme",
29
+ "bugs": {
30
+ "url": "https://github.com/emeraldlinks/veskTs/issues"
31
+ },
32
+ "engines": {
33
+ "node": ">=20.0.0"
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "README.md"
38
+ ],
39
+ "publishConfig": {
40
+ "access": "public"
41
+ }
42
+ }