@nativele/ts-dom 0.4.0 → 0.5.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/CHANGELOG.md +84 -0
- package/README.md +95 -811
- package/README.zh-TW.md +95 -816
- package/dist/{a11y-Bn9pDvWV.mjs → a11y-wWL9rie7.mjs} +123 -21
- package/dist/cli.mjs +1326 -22
- package/dist/index.d.mts +31 -13
- package/dist/index.mjs +12 -4
- package/dist/{lint-CqWk4U5P.mjs → lint-5vo_ifIc.mjs} +19 -3
- package/dist/oxlint-plugin.d.mts +12 -1
- package/dist/oxlint-plugin.mjs +59 -19
- package/dist/vite-plugin.mjs +1 -1
- package/llms.txt +21 -8
- package/package.json +6 -3
- package/skills/README.md +114 -0
- package/skills/_template/SKILL.md +57 -0
- package/skills/nativele-core/SKILL.md +92 -0
- package/skills/nativele-react/SKILL.md +197 -0
|
@@ -227,6 +227,14 @@ function rootElementType(html) {
|
|
|
227
227
|
const el = elements[0];
|
|
228
228
|
return el.namespaceURI === HTML_NS ? `El<${JSON.stringify(el.tagName)}>` : "SVGElement";
|
|
229
229
|
}
|
|
230
|
+
/** The whitespace mode an existing generated module was written with; "preserve" when there is none. */
|
|
231
|
+
function recordedWhitespace(out) {
|
|
232
|
+
try {
|
|
233
|
+
return readFileSync(out, "utf8").split("\n", 3).includes(COLLAPSE_MARK) ? "collapse" : "preserve";
|
|
234
|
+
} catch {
|
|
235
|
+
return "preserve";
|
|
236
|
+
}
|
|
237
|
+
}
|
|
230
238
|
function globToRegExp(glob) {
|
|
231
239
|
let re = "";
|
|
232
240
|
for (let i = 0; i < glob.length; i++) {
|
|
@@ -302,8 +310,9 @@ function generateModule(html, options = {}, imports = [], defs = []) {
|
|
|
302
310
|
events: ${union(a.events)};
|
|
303
311
|
}`;
|
|
304
312
|
const needsEl = type.includes("El<");
|
|
305
|
-
|
|
306
|
-
|
|
313
|
+
const side = imports.map((i) => `import ${q(i)};\n`).join("");
|
|
314
|
+
return `// Generated by ts-dom. Do not edit.${options.whitespace === "collapse" ? `\n${COLLAPSE_MARK}` : ""}
|
|
315
|
+
${side}import { template } from ${q(runtime)};
|
|
307
316
|
${needsEl ? `import type { El } from ${q(runtime)};\n` : ""}export default template<${type}>(${JSON.stringify(toNodes(parseFragment(html), options.whitespace === "collapse"))});
|
|
308
317
|
`;
|
|
309
318
|
}
|
|
@@ -320,21 +329,31 @@ function formatDiagnostics(file, ds) {
|
|
|
320
329
|
return ds.map((d) => `${file}:${d.line}:${d.col} ${d.severity} [${d.rule}] ${d.message}`).join("\n");
|
|
321
330
|
}
|
|
322
331
|
var CodegenError = class extends Error {};
|
|
323
|
-
/**
|
|
324
|
-
|
|
325
|
-
* `defs` is either a directory to scan or a precomputed scan result: custom elements used by the template
|
|
326
|
-
* are imported automatically from the modules that define them.
|
|
327
|
-
*/
|
|
328
|
-
function generateFile(htmlPath, options = {}, defs) {
|
|
332
|
+
/** What `generateFile` would write for a template, without writing it; throws CodegenError when the template has error diagnostics. */
|
|
333
|
+
function renderFile(htmlPath, options = {}, defs) {
|
|
329
334
|
const html = readFileSync(htmlPath, "utf8");
|
|
335
|
+
if (options.whitespace === void 0) options = {
|
|
336
|
+
...options,
|
|
337
|
+
whitespace: recordedWhitespace(modulePathFor(htmlPath))
|
|
338
|
+
};
|
|
330
339
|
const analysis = analyze(html);
|
|
331
340
|
const errors = analysis.diagnostics.filter((d) => d.severity === "error");
|
|
332
341
|
if (errors.length) throw new CodegenError(formatDiagnostics(htmlPath, errors));
|
|
333
342
|
const known = typeof defs === "string" ? scanDefines(defs) : defs ?? [];
|
|
334
343
|
const here = dirname(htmlPath);
|
|
335
344
|
const imports = [...new Set(known.filter((d) => analysis.customTags.has(d.tag)).map((d) => relImport(here, d.file)))].sort();
|
|
336
|
-
|
|
337
|
-
|
|
345
|
+
return {
|
|
346
|
+
out: modulePathFor(htmlPath),
|
|
347
|
+
next: generateModule(html, options, imports, known)
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Generate a single `x.html.ts`; throws CodegenError (and writes nothing) when there are error diagnostics.
|
|
352
|
+
* `defs` is either a directory to scan or a precomputed scan result: custom elements used by the template
|
|
353
|
+
* are imported automatically from the modules that define them.
|
|
354
|
+
*/
|
|
355
|
+
function generateFile(htmlPath, options = {}, defs) {
|
|
356
|
+
const { out, next } = renderFile(htmlPath, options, defs);
|
|
338
357
|
let prev = "";
|
|
339
358
|
try {
|
|
340
359
|
prev = readFileSync(out, "utf8");
|
|
@@ -345,7 +364,7 @@ function generateFile(htmlPath, options = {}, defs) {
|
|
|
345
364
|
function listHtml(dir) {
|
|
346
365
|
const out = [];
|
|
347
366
|
for (const name of readdirSync(dir)) {
|
|
348
|
-
if (name === "node_modules") continue;
|
|
367
|
+
if (name === "node_modules" || name === "dist") continue;
|
|
349
368
|
const full = join(dir, name);
|
|
350
369
|
if (statSync(full).isDirectory()) out.push(...listHtml(full));
|
|
351
370
|
else if (name.endsWith(".html") && !isFullPage(readFileSync(full, "utf8"))) out.push(full);
|
|
@@ -354,6 +373,7 @@ function listHtml(dir) {
|
|
|
354
373
|
}
|
|
355
374
|
const TAGS_FILE = "ts-dom-tags.ts";
|
|
356
375
|
const TAGS_HEADER = "// Generated by ts-dom. Do not edit.";
|
|
376
|
+
const COLLAPSE_MARK = "// whitespace: collapse";
|
|
357
377
|
function listTs(dir) {
|
|
358
378
|
const out = [];
|
|
359
379
|
for (const name of readdirSync(dir)) {
|
|
@@ -431,6 +451,20 @@ function scanTags(dir) {
|
|
|
431
451
|
* so that `refs.x` for `<tag data-ref="x">` in a template is fully typed (props, methods and emitted events).
|
|
432
452
|
*/
|
|
433
453
|
function generateTags(dir) {
|
|
454
|
+
const { out, next } = renderTags(dir);
|
|
455
|
+
if (next === null) {
|
|
456
|
+
if (existsSync(out) && readFileSync(out, "utf8").startsWith(TAGS_HEADER)) unlinkSync(out);
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
let prev = "";
|
|
460
|
+
try {
|
|
461
|
+
prev = readFileSync(out, "utf8");
|
|
462
|
+
} catch {}
|
|
463
|
+
if (prev !== next) writeFileSync(out, next);
|
|
464
|
+
return out;
|
|
465
|
+
}
|
|
466
|
+
/** What `generateTags` would write, without writing it; `next` is null when the directory defines no exported component. */
|
|
467
|
+
function renderTags(dir) {
|
|
434
468
|
const defs = scanTags(dir);
|
|
435
469
|
const out = join(dir, TAGS_FILE);
|
|
436
470
|
const seen = /* @__PURE__ */ new Map();
|
|
@@ -439,19 +473,86 @@ function generateTags(dir) {
|
|
|
439
473
|
if (prev && (prev.from !== d.from || prev.className !== d.className)) throw new CodegenError(`${dir}: [duplicate-define] the tag <${d.tag}> is defined more than once (${prev.from} and ${d.from}). Fix: each custom element name can be defined only once; rename one of them.`);
|
|
440
474
|
seen.set(d.tag, d);
|
|
441
475
|
}
|
|
442
|
-
if (!defs.length) {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
476
|
+
if (!defs.length) return {
|
|
477
|
+
out,
|
|
478
|
+
next: null
|
|
479
|
+
};
|
|
446
480
|
const alias = (d) => `Tag${[...seen.keys()].indexOf(d.tag)}`;
|
|
447
481
|
const imports = [...seen.values()].map((d) => `import type { ${d.isDefault ? "default" : d.className} as ${alias(d)} } from ${JSON.stringify(d.from)};`);
|
|
448
482
|
const entries = [...seen.values()].map((d) => ` ${JSON.stringify(d.tag)}: ${alias(d)};`);
|
|
449
|
-
|
|
450
|
-
|
|
483
|
+
return {
|
|
484
|
+
out,
|
|
485
|
+
next: `${TAGS_HEADER}\n${imports.join("\n")}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n${entries.join("\n")}\n }\n}\n\nexport {};\n`
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Everything `generateAll` would write for a directory, in memory: generated path to content, or null for a file that should not exist.
|
|
490
|
+
* Templates with error diagnostics are left out and their messages returned in `errors`.
|
|
491
|
+
*/
|
|
492
|
+
function planGenerated(dir, options = {}) {
|
|
493
|
+
const files = /* @__PURE__ */ new Map();
|
|
494
|
+
const errors = [];
|
|
495
|
+
const defs = scanDefines(dir);
|
|
496
|
+
for (const f of listHtml(dir)) {
|
|
497
|
+
if (!isSelected(dir, f, options)) continue;
|
|
498
|
+
try {
|
|
499
|
+
const { out, next } = renderFile(f, options, defs);
|
|
500
|
+
files.set(out, next);
|
|
501
|
+
} catch (e) {
|
|
502
|
+
if (e instanceof CodegenError) errors.push(e.message);
|
|
503
|
+
else throw e;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
451
506
|
try {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
507
|
+
const tags = renderTags(dir);
|
|
508
|
+
files.set(tags.out, tags.next);
|
|
509
|
+
} catch (e) {
|
|
510
|
+
if (e instanceof CodegenError) errors.push(e.message);
|
|
511
|
+
else throw e;
|
|
512
|
+
}
|
|
513
|
+
for (const f of listGenerated(dir)) if (!files.has(f) && !existsSync(f.slice(0, -3))) files.set(f, null);
|
|
514
|
+
return {
|
|
515
|
+
files,
|
|
516
|
+
errors
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
/** Generated files under `dir` that differ from what `ts-dom gen` would write now. */
|
|
520
|
+
function staleGenerated(dir, options = {}) {
|
|
521
|
+
return checkGenerated(dir, options).stale;
|
|
522
|
+
}
|
|
523
|
+
/** `staleGenerated`, plus the errors that would make `ts-dom gen` fail (template diagnostics, a tag defined twice). */
|
|
524
|
+
function checkGenerated(dir, options = {}) {
|
|
525
|
+
const plan = planGenerated(dir, options);
|
|
526
|
+
const out = [];
|
|
527
|
+
for (const [file, next] of plan.files) {
|
|
528
|
+
const prev = existsSync(file) ? readFileSync(file, "utf8") : null;
|
|
529
|
+
if (next === null) {
|
|
530
|
+
if (prev !== null && prev.startsWith(TAGS_HEADER)) out.push({
|
|
531
|
+
file,
|
|
532
|
+
reason: file.endsWith(".html.ts") ? "orphan" : "outdated"
|
|
533
|
+
});
|
|
534
|
+
} else if (prev === null) out.push({
|
|
535
|
+
file,
|
|
536
|
+
reason: "missing"
|
|
537
|
+
});
|
|
538
|
+
else if (prev !== next) out.push({
|
|
539
|
+
file,
|
|
540
|
+
reason: "outdated"
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
stale: out.sort((a, b) => a.file.localeCompare(b.file)),
|
|
545
|
+
errors: plan.errors
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
function listGenerated(dir) {
|
|
549
|
+
const out = [];
|
|
550
|
+
for (const name of readdirSync(dir)) {
|
|
551
|
+
if (name === "node_modules") continue;
|
|
552
|
+
const full = join(dir, name);
|
|
553
|
+
if (statSync(full).isDirectory()) out.push(...listGenerated(full));
|
|
554
|
+
else if (name.endsWith(".html.ts") && readFileSync(full, "utf8").startsWith(TAGS_HEADER)) out.push(full);
|
|
555
|
+
}
|
|
455
556
|
return out;
|
|
456
557
|
}
|
|
457
558
|
/** Generate every `x.html.ts` (and the tag map) under a directory; collects all errors and throws them together. */
|
|
@@ -475,6 +576,7 @@ function generateAll(dir, options = {}) {
|
|
|
475
576
|
if (e instanceof CodegenError) errors.push(e.message);
|
|
476
577
|
else throw e;
|
|
477
578
|
}
|
|
579
|
+
for (const f of listGenerated(dir)) if (!existsSync(f.slice(0, -3))) unlinkSync(f);
|
|
478
580
|
if (errors.length) throw new CodegenError(errors.join("\n"));
|
|
479
581
|
return written;
|
|
480
582
|
}
|
|
@@ -712,4 +814,4 @@ function checkA11y(html, ignore = []) {
|
|
|
712
814
|
for (const { id, el } of labelFor) if (!ids.has(id)) report("label-for", el, `<label for="${id}"> points to an id that does not exist in this template. Fix: add id="${id}" to the control, or wrap the control in the label.`);
|
|
713
815
|
return out.sort((a, b) => a.line - b.line || a.col - b.col);
|
|
714
816
|
}
|
|
715
|
-
export { analyze as a,
|
|
817
|
+
export { watchAll as _, analyze as a, generateAll as c, isSelected as d, listHtml as f, staleGenerated as g, scanDefines as h, CodegenError as i, generateFile as l, renderTags as m, checkA11y as n, checkGenerated as o, listTs as p, CODEGEN_RULES as r, formatDiagnostics as s, A11Y_RULES as t, isFullPage as u };
|