@human-synthesis/norns 0.0.9 → 0.0.11
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 +1 -1
- package/src/auto-import.js +3 -24
- package/src/vite.js +151 -1
package/package.json
CHANGED
package/src/auto-import.js
CHANGED
|
@@ -653,12 +653,6 @@ function renderImports(entries) {
|
|
|
653
653
|
* and excluded from auto-import — forcing an explicit import to disambiguate.
|
|
654
654
|
*
|
|
655
655
|
* Default `[]` (off; explicit imports for service-layer code).
|
|
656
|
-
* @param {string[] | false} [options.exportDirs]
|
|
657
|
-
* DEPRECATED. Equivalent to `exportGlobs: dirs.map(d => '${d}/**\/*.{c,civet,js}')`,
|
|
658
|
-
* which scans every file under those dirs. Path-scoping is still applied,
|
|
659
|
-
* so server exports won't leak into client files — but the broad scan
|
|
660
|
-
* surfaces every internal export as a potential auto-import. Migrate to
|
|
661
|
-
* `exportGlobs: ['src/lib/**\/public.c']` for an intentional API surface.
|
|
662
656
|
* @param {string[]} [options.exportExtensions]
|
|
663
657
|
* File extensions accepted for exports (defence-in-depth on top of the
|
|
664
658
|
* glob). Default `['.c', '.civet', '.js']` — `.ts` excluded because
|
|
@@ -667,8 +661,8 @@ function renderImports(entries) {
|
|
|
667
661
|
* @param {string} [options.libAlias] Default `'$lib'`.
|
|
668
662
|
* @param {string} [options.root] Default `process.cwd()`.
|
|
669
663
|
* @param {(msg: string) => void} [options.log]
|
|
670
|
-
* Channel for conflict
|
|
671
|
-
*
|
|
664
|
+
* Channel for conflict warnings. Default `console.warn`. Tests pass a
|
|
665
|
+
* stub to assert behavior without polluting output.
|
|
672
666
|
*/
|
|
673
667
|
export function nornsAutoImport(options = {}) {
|
|
674
668
|
const root = options.root ?? process.cwd();
|
|
@@ -682,23 +676,8 @@ export function nornsAutoImport(options = {}) {
|
|
|
682
676
|
const componentSpecs = options.components ?? null;
|
|
683
677
|
const log = options.log ?? console.warn;
|
|
684
678
|
|
|
685
|
-
|
|
686
|
-
// `exportDirs` is shimmed in for backward compatibility with a one-time
|
|
687
|
-
// deprecation notice per init.
|
|
688
|
-
const explicitGlobs =
|
|
679
|
+
const effectiveGlobs =
|
|
689
680
|
options.exportGlobs === false || options.exportGlobs == null ? [] : options.exportGlobs;
|
|
690
|
-
const legacyDirs =
|
|
691
|
-
options.exportDirs === false || options.exportDirs == null ? [] : options.exportDirs;
|
|
692
|
-
let effectiveGlobs = [...explicitGlobs];
|
|
693
|
-
if (legacyDirs.length > 0) {
|
|
694
|
-
log(
|
|
695
|
-
'[norns-auto-import] `exportDirs` is deprecated. Migrate to `exportGlobs`, e.g. ' +
|
|
696
|
-
"`exportGlobs: ['src/lib/**/public.c']` — barrel-file scope is the safe default."
|
|
697
|
-
);
|
|
698
|
-
effectiveGlobs = effectiveGlobs.concat(
|
|
699
|
-
legacyDirs.map((d) => `${d.replace(/\\/g, '/').replace(/\/+$/, '')}/**/*.{c,civet,js}`)
|
|
700
|
-
);
|
|
701
|
-
}
|
|
702
681
|
|
|
703
682
|
const components =
|
|
704
683
|
componentDirs.length === 0
|
package/src/vite.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile, stat, realpath } from 'node:fs/promises';
|
|
1
|
+
import { readFile, stat, realpath, readdir, mkdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { compile as compileCivet } from '@danielx/civet';
|
|
@@ -140,3 +140,153 @@ export function nornsCivetPlugin() {
|
|
|
140
140
|
}
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
|
+
|
|
144
|
+
/* === pugTailwindExtract ==================================================
|
|
145
|
+
*
|
|
146
|
+
* Tailwind v4's content extractor tokenizes candidates against a fixed
|
|
147
|
+
* non-class alphabet. Pug class-shorthand chains followed directly by an
|
|
148
|
+
* attribute paren — `.grid.gap-6(class="…")` — fail that tokenizer: the
|
|
149
|
+
* substring `.gap-6(` reads as one non-class token, so `gap-6` never
|
|
150
|
+
* reaches the candidate set and the CSS rule is never generated.
|
|
151
|
+
*
|
|
152
|
+
* Because the `tag.cls.cls(attrs)` form is idiomatic Pug, asking authors
|
|
153
|
+
* to either move every utility into `class="…"` or hand-maintain a
|
|
154
|
+
* safelist is a paper cut on every page they touch.
|
|
155
|
+
*
|
|
156
|
+
* This plugin walks every `.n` file under `root`, extracts each `.cls`
|
|
157
|
+
* segment via a permissive regex, and writes the deduplicated set into a
|
|
158
|
+
* single sidecar HTML file. Consumers reference the file from their CSS
|
|
159
|
+
* via `@source "./.tailwind-pug-classes.html";` so Tailwind picks it up
|
|
160
|
+
* like any other content source.
|
|
161
|
+
*
|
|
162
|
+
* The regex is permissive on purpose — it captures every `.candidate`
|
|
163
|
+
* segment in the source, including occasional false positives like
|
|
164
|
+
* `.svelte` inside template text. Those are free: Tailwind's own
|
|
165
|
+
* candidate-validation step drops anything that isn't a real utility, so
|
|
166
|
+
* the only cost is a few extra bytes in the sidecar.
|
|
167
|
+
*
|
|
168
|
+
* Runs with `enforce: 'pre'` so the scan sees the raw Pug source, not the
|
|
169
|
+
* Svelte output that the rest of the chain emits.
|
|
170
|
+
* ========================================================================
|
|
171
|
+
*/
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Match every `.candidate` segment. Class names may contain Tailwind's
|
|
175
|
+
* full alphabet — letters, digits, `-`, `_`, `:`, `/`, and arbitrary-value
|
|
176
|
+
* brackets `[...]`. Pug shorthand never contains a `.` inside a class
|
|
177
|
+
* (the dot is the delimiter), so `text-[1.5rem]`-style values never appear
|
|
178
|
+
* in shorthand — those always live inside `class="…"`, which Tailwind
|
|
179
|
+
* extracts directly.
|
|
180
|
+
*/
|
|
181
|
+
const SEGMENT_RE = /\.([A-Za-z][\w\-:/]*(?:\[[^\]]*\])?)/g;
|
|
182
|
+
|
|
183
|
+
function extractPugClasses(source) {
|
|
184
|
+
const out = new Set();
|
|
185
|
+
let m;
|
|
186
|
+
SEGMENT_RE.lastIndex = 0;
|
|
187
|
+
while ((m = SEGMENT_RE.exec(source))) {
|
|
188
|
+
if (m[1]) out.add(m[1]);
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function walkNFiles(dir, ext, out = []) {
|
|
194
|
+
let entries;
|
|
195
|
+
try {
|
|
196
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
197
|
+
} catch {
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
for (const entry of entries) {
|
|
201
|
+
const full = join(dir, entry.name);
|
|
202
|
+
if (entry.isDirectory()) {
|
|
203
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
|
|
204
|
+
await walkNFiles(full, ext, out);
|
|
205
|
+
} else if (entry.isFile() && full.endsWith(ext)) {
|
|
206
|
+
out.push(full);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Vite plugin that extracts Tailwind class candidates from Pug shorthand
|
|
214
|
+
* in `.n` files and writes them to a sidecar file Tailwind can scan.
|
|
215
|
+
*
|
|
216
|
+
* @param {object} [options]
|
|
217
|
+
* @param {string} [options.root] Directory to scan (default `src`).
|
|
218
|
+
* @param {string} [options.ext] File extension (default `.n`).
|
|
219
|
+
* @param {string} [options.outFile] Sidecar path relative to root
|
|
220
|
+
* (default `.tailwind-pug-classes.html`).
|
|
221
|
+
* Reference it from your CSS with:
|
|
222
|
+
* @source "./.tailwind-pug-classes.html";
|
|
223
|
+
*
|
|
224
|
+
* @returns {import('vite').Plugin}
|
|
225
|
+
*/
|
|
226
|
+
export function pugTailwindExtract({
|
|
227
|
+
root = 'src',
|
|
228
|
+
ext = '.n',
|
|
229
|
+
outFile = '.tailwind-pug-classes.html'
|
|
230
|
+
} = {}) {
|
|
231
|
+
let projectRoot = process.cwd();
|
|
232
|
+
const fileClasses = new Map(); // absolute path -> Set<string>
|
|
233
|
+
|
|
234
|
+
async function writeSidecar() {
|
|
235
|
+
const all = new Set();
|
|
236
|
+
for (const set of fileClasses.values()) {
|
|
237
|
+
for (const c of set) all.add(c);
|
|
238
|
+
}
|
|
239
|
+
const sorted = [...all].sort();
|
|
240
|
+
const html =
|
|
241
|
+
'<!-- AUTO-GENERATED by @human-synthesis/norns/vite pugTailwindExtract. Do not edit. -->\n' +
|
|
242
|
+
`<div class="${sorted.join(' ')}"></div>\n`;
|
|
243
|
+
const out = join(projectRoot, root, outFile);
|
|
244
|
+
await mkdir(dirname(out), { recursive: true });
|
|
245
|
+
await writeFile(out, html, 'utf8');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function scanFile(abs) {
|
|
249
|
+
try {
|
|
250
|
+
const content = await readFile(abs, 'utf8');
|
|
251
|
+
fileClasses.set(abs, extractPugClasses(content));
|
|
252
|
+
} catch {
|
|
253
|
+
fileClasses.delete(abs);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function scanAll() {
|
|
258
|
+
const dir = join(projectRoot, root);
|
|
259
|
+
const files = await walkNFiles(dir, ext);
|
|
260
|
+
await Promise.all(files.map(scanFile));
|
|
261
|
+
await writeSidecar();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
name: 'norns:pug-tailwind-extract',
|
|
266
|
+
// Run before the Civet/Pug transform so the scan sees raw shorthand.
|
|
267
|
+
enforce: 'pre',
|
|
268
|
+
configResolved(config) {
|
|
269
|
+
projectRoot = config.root || process.cwd();
|
|
270
|
+
},
|
|
271
|
+
async buildStart() {
|
|
272
|
+
await scanAll();
|
|
273
|
+
},
|
|
274
|
+
async handleHotUpdate({ file }) {
|
|
275
|
+
if (!file.endsWith(ext)) return;
|
|
276
|
+
await scanFile(file);
|
|
277
|
+
await writeSidecar();
|
|
278
|
+
},
|
|
279
|
+
configureServer(server) {
|
|
280
|
+
server.watcher.on('add', async (file) => {
|
|
281
|
+
if (!file.endsWith(ext)) return;
|
|
282
|
+
await scanFile(file);
|
|
283
|
+
await writeSidecar();
|
|
284
|
+
});
|
|
285
|
+
server.watcher.on('unlink', async (file) => {
|
|
286
|
+
if (!file.endsWith(ext)) return;
|
|
287
|
+
fileClasses.delete(file);
|
|
288
|
+
await writeSidecar();
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
}
|