@nativele/ts-dom 0.5.2 → 0.5.4
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 +55 -0
- package/README.md +4 -2
- package/README.zh-TW.md +4 -2
- package/dist/{a11y-CV3nBgBn.mjs → a11y-CiEJj_e6.mjs} +74 -28
- package/dist/cli.mjs +182 -36
- package/dist/{lint-Bwytj7L8.mjs → lint-Did4w-NL.mjs} +2 -2
- package/dist/oxlint-plugin.mjs +2 -2
- package/dist/vite-plugin.mjs +21 -26
- package/llms.txt +4 -2
- package/package.json +1 -1
- package/skills/README.md +3 -2
- package/skills/nativele-angular/SKILL.md +1 -3
- package/skills/nativele-architecture/SKILL.md +541 -0
- package/skills/nativele-integration/SKILL.md +4 -6
- package/skills/nativele-react/SKILL.md +8 -12
- package/skills/nativele-svelte/SKILL.md +8 -12
- package/skills/nativele-testing/SKILL.md +1 -1
- package/skills/nativele-vite/SKILL.md +1 -1
- package/skills/nativele-vue/SKILL.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,61 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version
|
|
|
10
10
|
- Adding a lint rule is a minor change; lint rules are warnings or opt-in and do not count as breaking.
|
|
11
11
|
- Before every release run `npm run verify:pack` (`publint` and `attw`), `tsc`, the unit tests, the browser tests, `npm run smoke` and `npm run smoke:no-bundler`; all of them run in `.github/workflows/ci.yml`.
|
|
12
12
|
|
|
13
|
+
## 0.5.4
|
|
14
|
+
|
|
15
|
+
The guides, the Skills and the tools agree with each other: wrong statements in the guides are fixed, facts the Skills repeated now live in the guides, and the tools report the traps the Skill trials hit instead of staying silent.
|
|
16
|
+
|
|
17
|
+
### New checks
|
|
18
|
+
|
|
19
|
+
- `ts-dom doctor` reports `typecheck-before-gen` (warning): a package.json script, with its `pre` script and the scripts it runs through `npm run` (or pnpm, yarn, bun), that runs `tsc`, `vue-tsc`, `svelte-check` or `ng build`/`serve`/`test` before any `ts-dom gen` or `ts-dom check` while `.gitignore` leaves out `*.html.ts`; on a clean checkout it fails with TS2307. A `tsc -p` whose config covers none of the scanned folders is skipped.
|
|
20
|
+
- `ts-dom doctor` reports `framework-template` (error): an `.html` in a scanned folder that a TypeScript file names as an Angular `templateUrl`. The `invalid-hole` and `no-importer` hints now also say that the folder may not be meant to be scanned.
|
|
21
|
+
- `formatter-whitespace` also reports generated files written with `preserve` while a package.json script or the Vite config asks for `--whitespace=collapse`, with the command to regenerate them.
|
|
22
|
+
- `vite-plugin-missing` also reports a `vitest.config.*` that replaces the Vite config for Vitest without the `tsDom()` plugin and without importing `./vite.config`.
|
|
23
|
+
|
|
24
|
+
### Fixes
|
|
25
|
+
|
|
26
|
+
- The Vite plugin warns (`dir-not-found`) when a `tsDom({ dirs })` entry does not exist under Vite's `root`, instead of skipping it silently.
|
|
27
|
+
- A generated `ts-dom-tags.ts` in a subfolder that is not itself scanned (left over after components moved) is an orphan: `gen` deletes it, and `gen --check`, `check --no-write` and `doctor` report it as `stale-generated`.
|
|
28
|
+
- A `tsc` error located in a generated `x.html.ts` or `ts-dom-tags.ts` now says to run `ts-dom gen` or delete the leftover file, not to change the TypeScript there.
|
|
29
|
+
- `ts-dom check` leaves out `tsc` errors inside the body of a class it already reported for a contract error (for example an implicit `any` on a `bind` callback's parameter next to a `{{ hole }}` that names nothing).
|
|
30
|
+
- `ts-dom --help` lists `--include`, `--exclude` and `--whitespace` under `check` and `doctor`, which accept them.
|
|
31
|
+
|
|
32
|
+
### Documentation
|
|
33
|
+
|
|
34
|
+
- `docs/components.md` gains:
|
|
35
|
+
- Light DOM slots and children managed by a framework;
|
|
36
|
+
- when a Light or Shadow component builds its content;
|
|
37
|
+
- the `props` and `"~emits"` members that framework declarations use;
|
|
38
|
+
- `EmitEvent` with an undeclared name;
|
|
39
|
+
- `ReadableStore`.
|
|
40
|
+
- Five framework Skills link to the slot section instead of repeating it.
|
|
41
|
+
- Corrected:
|
|
42
|
+
- the import map in `docs/design.md`;
|
|
43
|
+
- the generated files being "already in `.gitignore`" (a project adds them itself);
|
|
44
|
+
- duplicate and misplaced reordering notes;
|
|
45
|
+
- `llms.txt` lists every guide.
|
|
46
|
+
- The Skills agree on one build-script rule: a project whose type check is plain `tsc` runs `ts-dom check` before the bundler, and a framework type checker gets `ts-dom gen` in front. The README Skill tables show the right tiers.
|
|
47
|
+
- `test/skills.test.ts` checks the README Skill tables against each Skill's tier, and every link and anchor in the READMEs, `llms.txt`, the guides and the Skills.
|
|
48
|
+
- `docs/tooling.md`: the new checks; the bare specifier to map is `@nativele/ts-dom`; the Vite plugin generates when the build, the dev server or Vitest starts, never type-checks, prints rejected templates only in the terminal, and sends a full reload to every client; a run without `--whitespace` keeps each file's recorded mode; `check` runs `tsc` once per referenced project of a solution-style tsconfig.
|
|
49
|
+
|
|
50
|
+
## 0.5.3
|
|
51
|
+
|
|
52
|
+
How to lay out an app with several pages: a new architecture guide, an Official Skill that applies it, and a medium example app checked in CI. Building the example found a codegen bug with several scanned folders, fixed here.
|
|
53
|
+
|
|
54
|
+
### Guide and Skill
|
|
55
|
+
|
|
56
|
+
- New guide `docs/architecture.md`:
|
|
57
|
+
- feature folders with their components, stores and tests, and a shared folder whose components' interface is their props and events;
|
|
58
|
+
- pages as custom elements mounted by an external router: route params as props, leaving a page, lazy page modules;
|
|
59
|
+
- where state lives at each level, and the split between unit, real-browser and end-to-end tests;
|
|
60
|
+
- one scanned folder for the generated files, checks and tsconfig.
|
|
61
|
+
- New `nativele-architecture` (Official): the procedure for starting a multi-page Nativele app or restructuring one into this layout. The recommended router uses the Navigation API and `URLPattern` with no dependency; universal-router is the alternative. It covers the abort check after a lazy `import()` and the test split.
|
|
62
|
+
- Its example, a project tracker with lazy pages, shared components, feature and app-wide stores, unit, browser and end-to-end tests, runs in CI.
|
|
63
|
+
|
|
64
|
+
### Fixes
|
|
65
|
+
|
|
66
|
+
- When several folders are scanned (`ts-dom gen a b`, `ts-dom check a b`, `ts-dom doctor a b`, the Vite plugin's `dirs`), each folder was generated on its own, so a template in one folder that used a component defined in another got no import of it and the build silently left the component out, while the types still passed. The folders now share their `define()` calls: the template imports the child by its relative path, `gen --check` and `doctor` agree with what `gen` writes, and the Vite plugin keeps the imports across folders when a template is saved or a `define()` is added or removed. A tag defined with different classes in two scanned folders is now reported as `duplicate-define` at the second `define()` call.
|
|
67
|
+
|
|
13
68
|
## 0.5.2
|
|
14
69
|
|
|
15
70
|
Skills for the libraries a Nativele app most often sits next to: state stores, testing, data fetching, form validation and Tailwind CSS. Each was accepted through agent trials, and the trials found two bugs in the library, fixed here.
|
package/README.md
CHANGED
|
@@ -63,10 +63,11 @@ npx ts-dom skills add nativele-core # copy it into .claude/skills/ (when the
|
|
|
63
63
|
| `nativele-core` | building components: the `ts-dom check` loop, templates, state, lists, data flow |
|
|
64
64
|
| `nativele-react` | Nativele components in React 19: props, events, children and slots, lifecycle, TSX types |
|
|
65
65
|
| `nativele-vue` | Nativele components in Vue 3: props, events, `v-model`, slots, `<KeepAlive>`, template types |
|
|
66
|
-
| `nativele-svelte` | Nativele components in Svelte 5: props, events, slots, template types |
|
|
66
|
+
| `nativele-svelte` | Nativele components in Svelte 5: props, events, slots, template types (Experimental) |
|
|
67
67
|
| `nativele-angular` | Nativele components in Angular: bindings, change detection, the build, content projection (Experimental) |
|
|
68
|
+
| `nativele-architecture` | app layout: feature folders, shared components and stores, pages mounted by a router, lazy pages, the test split |
|
|
68
69
|
| `nativele-vite` | Nativele in a Vite app: the plugin and `root`, multi-page builds, hot updates |
|
|
69
|
-
| `nativele-integration` | Nativele components in a framework without its own Skill (Preact, Solid, ...): properties, events, slots, lifecycle, template types
|
|
70
|
+
| `nativele-integration` | Nativele components in a framework without its own Skill (Preact, Solid, ...): properties, events, slots, lifecycle, template types |
|
|
70
71
|
| `nativele-state-management` | where state lives; `createStore`, and Zustand, Redux Toolkit or nanostores through `bind` |
|
|
71
72
|
| `nativele-testing` | component tests with Vitest, real-browser tests |
|
|
72
73
|
| `nativele-tanstack-query` | data fetching with `@tanstack/query-core` (Experimental) |
|
|
@@ -228,6 +229,7 @@ The full guide is in English:
|
|
|
228
229
|
- [Components and data flow: state, props, derive, events, composition, stores](https://github.com/Pixmi/nativele/blob/main/docs/components.md)
|
|
229
230
|
- [Recipes: loading and error states, store data, filtered lists, dialogs, form validation](https://github.com/Pixmi/nativele/blob/main/docs/recipes.md)
|
|
230
231
|
- [Lists: lists of components with `items =`, plain lists with `replace()`, moving items](https://github.com/Pixmi/nativele/blob/main/docs/lists.md)
|
|
232
|
+
- [Architecture: feature folders, shared components and stores, pages mounted by a router, where state lives, testing strategy](https://github.com/Pixmi/nativele/blob/main/docs/architecture.md)
|
|
231
233
|
- [Setup, checks and lint: installation, Vite, oxlint, `ts-dom check`, `ts-dom doctor`, accessibility](https://github.com/Pixmi/nativele/blob/main/docs/tooling.md)
|
|
232
234
|
- [Performance: the large list benchmark](https://github.com/Pixmi/nativele/blob/main/docs/performance.md)
|
|
233
235
|
- [Design: principles, deliberate decisions, limitations](https://github.com/Pixmi/nativele/blob/main/docs/design.md)
|
package/README.zh-TW.md
CHANGED
|
@@ -63,10 +63,11 @@ npx ts-dom skills add nativele-core # 複製到 .claude/skills/(專案有 .
|
|
|
63
63
|
| `nativele-core` | 開發元件:`ts-dom check` 循環、範本、state、清單、資料流 |
|
|
64
64
|
| `nativele-react` | 在 React 19 中使用 Nativele 元件:props、事件、子元素與 slot、生命週期、TSX 型別 |
|
|
65
65
|
| `nativele-vue` | 在 Vue 3 中使用 Nativele 元件:props、事件、`v-model`、slot、`<KeepAlive>`、範本型別 |
|
|
66
|
-
| `nativele-svelte` | 在 Svelte 5 中使用 Nativele 元件:props、事件、slot
|
|
66
|
+
| `nativele-svelte` | 在 Svelte 5 中使用 Nativele 元件:props、事件、slot、範本型別(Experimental) |
|
|
67
67
|
| `nativele-angular` | 在 Angular 中使用 Nativele 元件:綁定、變更偵測、建置流程、內容投影(Experimental) |
|
|
68
|
+
| `nativele-architecture` | 應用的結構:功能資料夾、共用元件與 store、由 router 掛載的頁面、延遲載入、測試分層 |
|
|
68
69
|
| `nativele-vite` | 在 Vite 應用中使用 Nativele:外掛與 `root`、多頁建置、熱更新 |
|
|
69
|
-
| `nativele-integration` | 在沒有專屬 Skill 的框架(Preact、Solid 等)中使用 Nativele 元件:property、事件、slot
|
|
70
|
+
| `nativele-integration` | 在沒有專屬 Skill 的框架(Preact、Solid 等)中使用 Nativele 元件:property、事件、slot、生命週期、範本型別 |
|
|
70
71
|
| `nativele-state-management` | 狀態放在哪裡;`createStore`,以及透過 `bind` 接上 Zustand、Redux Toolkit、nanostores |
|
|
71
72
|
| `nativele-testing` | 用 Vitest 做元件測試、真實瀏覽器測試 |
|
|
72
73
|
| `nativele-tanstack-query` | 用 `@tanstack/query-core` 取資料(Experimental) |
|
|
@@ -228,6 +229,7 @@ define("task-board", TaskBoard);
|
|
|
228
229
|
- [Components and data flow: state, props, derive, events, composition, stores](https://github.com/Pixmi/nativele/blob/main/docs/components.md)
|
|
229
230
|
- [Recipes: loading and error states, store data, filtered lists, dialogs, form validation](https://github.com/Pixmi/nativele/blob/main/docs/recipes.md)
|
|
230
231
|
- [Lists: lists of components with `items =`, plain lists with `replace()`, moving items](https://github.com/Pixmi/nativele/blob/main/docs/lists.md)
|
|
232
|
+
- [Architecture: feature folders, shared components and stores, pages mounted by a router, where state lives, testing strategy](https://github.com/Pixmi/nativele/blob/main/docs/architecture.md)
|
|
231
233
|
- [Setup, checks and lint: installation, Vite, oxlint, `ts-dom check`, `ts-dom doctor`, accessibility](https://github.com/Pixmi/nativele/blob/main/docs/tooling.md)
|
|
232
234
|
- [Performance: the large list benchmark](https://github.com/Pixmi/nativele/blob/main/docs/performance.md)
|
|
233
235
|
- [Design: principles, deliberate decisions, limitations](https://github.com/Pixmi/nativele/blob/main/docs/design.md)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as RESERVED_NAMES, n as IDENT, r as holeName, t as HOLE_ANY } from "./holes-DTBtTZF_.mjs";
|
|
2
2
|
import { existsSync, readFileSync, readdirSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join, relative, sep } from "node:path";
|
|
3
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { parseFragment } from "parse5";
|
|
5
5
|
// src/lib/codegen.ts
|
|
6
6
|
/** Every diagnostic that `ts-dom gen` can report while analyzing a template. */
|
|
@@ -29,7 +29,7 @@ const CODEGEN_RULES = {
|
|
|
29
29
|
"forbidden-tag": "<script> cannot be in a template (it would run code)",
|
|
30
30
|
"forbidden-attribute": "on* event attributes and srcdoc cannot be in a template (they run code or are blocked by Trusted Types)",
|
|
31
31
|
"unsafe-url": "href / src / action / formaction cannot start with javascript:",
|
|
32
|
-
"duplicate-define": "a custom element tag can be defined only once
|
|
32
|
+
"duplicate-define": "a custom element tag can be defined only once across the scanned directories"
|
|
33
33
|
};
|
|
34
34
|
const RESERVED_ELEMENT_NAMES = /* @__PURE__ */ new Set([
|
|
35
35
|
"annotation-xml",
|
|
@@ -104,7 +104,7 @@ function analyze(html) {
|
|
|
104
104
|
mark(listName ? `text:${listName}/${name}` : `text:${name}`, at);
|
|
105
105
|
} else {
|
|
106
106
|
const inner = (m[1] ?? "").trim();
|
|
107
|
-
report("invalid-hole", "error", `{{ ${inner} }} is not a valid hole: a hole must be a single identifier, paths and expressions are not supported. Fix: compute the value in TypeScript (state, derive or this.text) and use a named hole such as {{ name }}.`, at);
|
|
107
|
+
report("invalid-hole", "error", `{{ ${inner} }} is not a valid hole: a hole must be a single identifier, paths and expressions are not supported. Fix: compute the value in TypeScript (state, derive or this.text) and use a named hole such as {{ name }}. If the file is another framework's template, this folder is not meant to be scanned: scan only the folder with the Nativele templates, or leave the file out with --exclude.`, at);
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
}
|
|
@@ -406,13 +406,15 @@ function nonCodeRanges(code) {
|
|
|
406
406
|
return out;
|
|
407
407
|
}
|
|
408
408
|
/**
|
|
409
|
-
* Scan
|
|
409
|
+
* Scan one or more directories for every `define("my-tag", MyClass)` call; a file under two of them is read once.
|
|
410
410
|
* Calls that only appear inside a string or a comment (for example sample code in a test or in documentation) are ignored.
|
|
411
411
|
*/
|
|
412
|
-
function scanDefines(
|
|
412
|
+
function scanDefines(dirs) {
|
|
413
413
|
const defs = [];
|
|
414
414
|
const re = /\bdefine\(\s*(["'])([a-z][a-z0-9]*(?:-[a-z0-9]+)+)\1\s*,\s*([A-Za-z_$][\w$]*)\s*\)/g;
|
|
415
|
-
|
|
415
|
+
const files = /* @__PURE__ */ new Map();
|
|
416
|
+
for (const dir of typeof dirs === "string" ? [dirs] : dirs) for (const f of listTs(dir)) if (!files.has(resolve(f))) files.set(resolve(f), f);
|
|
417
|
+
for (const file of files.values()) {
|
|
416
418
|
const code = readFileSync(file, "utf8");
|
|
417
419
|
const skip = nonCodeRanges(code);
|
|
418
420
|
const inCode = (pos) => !skip.some(([a, b]) => pos >= a && pos < b);
|
|
@@ -422,12 +424,15 @@ function scanDefines(dir) {
|
|
|
422
424
|
const [, , tag, className] = m;
|
|
423
425
|
const named = has(new RegExp(`export\\s+class\\s+${className}\\b`, "g")) || has(new RegExp(`export\\s*\\{[^}]*\\b${className}\\b[^}]*\\}`, "g"));
|
|
424
426
|
const isDefault = has(new RegExp(`export\\s+default\\s+class\\s+${className}\\b`, "g"));
|
|
427
|
+
const before = code.slice(0, m.index);
|
|
425
428
|
defs.push({
|
|
426
429
|
tag,
|
|
427
430
|
className,
|
|
428
431
|
file,
|
|
429
432
|
exported: named || isDefault,
|
|
430
|
-
isDefault
|
|
433
|
+
isDefault,
|
|
434
|
+
line: before.split("\n").length,
|
|
435
|
+
col: m.index - before.lastIndexOf("\n")
|
|
431
436
|
});
|
|
432
437
|
}
|
|
433
438
|
}
|
|
@@ -437,6 +442,30 @@ const relImport = (fromDir, file) => {
|
|
|
437
442
|
const r = relative(fromDir, file).split(sep).join("/").replace(/\.ts$/, ".js");
|
|
438
443
|
return r.startsWith(".") ? r : `./${r}`;
|
|
439
444
|
};
|
|
445
|
+
/**
|
|
446
|
+
* A tag that two of the directories define with different classes, reported at its second define() call.
|
|
447
|
+
* A tag defined twice within one directory is reported by `generateTags` instead.
|
|
448
|
+
*/
|
|
449
|
+
function crossDirDuplicates(dirs) {
|
|
450
|
+
const byTag = /* @__PURE__ */ new Map();
|
|
451
|
+
for (const d of scanDefines(dirs)) byTag.set(d.tag, [...byTag.get(d.tag) ?? [], d]);
|
|
452
|
+
const inside = (dir, file) => resolve(file).startsWith(resolve(dir) + sep);
|
|
453
|
+
const out = [];
|
|
454
|
+
for (const [tag, defs] of byTag) {
|
|
455
|
+
const [first, second] = defs;
|
|
456
|
+
if (defs.length < 2 || dirs.some((dir) => defs.every((d) => inside(dir, d.file)))) continue;
|
|
457
|
+
const at = (d) => `${relative(process.cwd(), d.file)}:${d.line ?? 1}`;
|
|
458
|
+
out.push({
|
|
459
|
+
file: second.file,
|
|
460
|
+
line: second.line ?? 1,
|
|
461
|
+
col: second.col ?? 1,
|
|
462
|
+
severity: "error",
|
|
463
|
+
rule: "duplicate-define",
|
|
464
|
+
message: `The tag <${tag}> is defined more than once in the scanned folders (${at(first)} and ${at(second)}), so the second define() fails at runtime. Fix: each custom element name can be defined only once; rename one of them.`
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
return out;
|
|
468
|
+
}
|
|
440
469
|
/** Scan `define("my-tag", MyClass)` calls and return the components a type can be generated for (the class must be exported). */
|
|
441
470
|
function scanTags(dir) {
|
|
442
471
|
return scanDefines(dir).filter((d) => d.exported).map((d) => ({
|
|
@@ -488,11 +517,11 @@ function renderTags(dir) {
|
|
|
488
517
|
/**
|
|
489
518
|
* Everything `generateAll` would write for a directory, in memory: generated path to content, or null for a file that should not exist.
|
|
490
519
|
* Templates with error diagnostics are left out and their messages returned in `errors`.
|
|
520
|
+
* `scanned` are all the directories of the run: a ts-dom-tags.ts in a subfolder is an orphan unless that subfolder is one of them.
|
|
491
521
|
*/
|
|
492
|
-
function planGenerated(dir, options = {}) {
|
|
522
|
+
function planGenerated(dir, options = {}, defs = scanDefines(dir), scanned = [dir]) {
|
|
493
523
|
const files = /* @__PURE__ */ new Map();
|
|
494
524
|
const errors = [];
|
|
495
|
-
const defs = scanDefines(dir);
|
|
496
525
|
for (const f of listHtml(dir)) {
|
|
497
526
|
if (!isSelected(dir, f, options)) continue;
|
|
498
527
|
try {
|
|
@@ -510,26 +539,26 @@ function planGenerated(dir, options = {}) {
|
|
|
510
539
|
if (e instanceof CodegenError) errors.push(e.message);
|
|
511
540
|
else throw e;
|
|
512
541
|
}
|
|
513
|
-
for (const f of listGenerated(dir)) if (!files.has(f) && !existsSync(f.slice(0, -3))) files.set(f, null);
|
|
542
|
+
for (const f of listGenerated(dir, scanned)) if (!files.has(f) && !existsSync(f.slice(0, -3))) files.set(f, null);
|
|
514
543
|
return {
|
|
515
544
|
files,
|
|
516
545
|
errors
|
|
517
546
|
};
|
|
518
547
|
}
|
|
519
548
|
/** 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;
|
|
549
|
+
function staleGenerated(dir, options = {}, defs, scanned) {
|
|
550
|
+
return checkGenerated(dir, options, defs, scanned).stale;
|
|
522
551
|
}
|
|
523
552
|
/** `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);
|
|
553
|
+
function checkGenerated(dir, options = {}, defs, scanned) {
|
|
554
|
+
const plan = planGenerated(dir, options, defs, scanned);
|
|
526
555
|
const out = [];
|
|
527
556
|
for (const [file, next] of plan.files) {
|
|
528
557
|
const prev = existsSync(file) ? readFileSync(file, "utf8") : null;
|
|
529
558
|
if (next === null) {
|
|
530
559
|
if (prev !== null && prev.startsWith(TAGS_HEADER)) out.push({
|
|
531
560
|
file,
|
|
532
|
-
reason: file
|
|
561
|
+
reason: file === join(dir, TAGS_FILE) ? "outdated" : "orphan"
|
|
533
562
|
});
|
|
534
563
|
} else if (prev === null) out.push({
|
|
535
564
|
file,
|
|
@@ -545,21 +574,24 @@ function checkGenerated(dir, options = {}) {
|
|
|
545
574
|
errors: plan.errors
|
|
546
575
|
};
|
|
547
576
|
}
|
|
548
|
-
function listGenerated(dir) {
|
|
577
|
+
function listGenerated(dir, scanned, top = true) {
|
|
549
578
|
const out = [];
|
|
579
|
+
const ownTags = top || scanned.some((d) => resolve(d) === resolve(dir));
|
|
550
580
|
for (const name of readdirSync(dir)) {
|
|
551
581
|
if (name === "node_modules") continue;
|
|
552
582
|
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);
|
|
583
|
+
if (statSync(full).isDirectory()) out.push(...listGenerated(full, scanned, false));
|
|
584
|
+
else if ((name.endsWith(".html.ts") || name === TAGS_FILE && !ownTags) && readFileSync(full, "utf8").startsWith(TAGS_HEADER)) out.push(full);
|
|
555
585
|
}
|
|
556
586
|
return out;
|
|
557
587
|
}
|
|
558
|
-
/**
|
|
559
|
-
|
|
588
|
+
/**
|
|
589
|
+
* Generate every `x.html.ts` (and the tag map) under a directory; collects all errors and throws them together.
|
|
590
|
+
* `defs` are the define() calls templates import child components from (default: those under `dir`).
|
|
591
|
+
*/
|
|
592
|
+
function generateAll(dir, options = {}, defs = scanDefines(dir), scanned = [dir]) {
|
|
560
593
|
const written = [];
|
|
561
594
|
const errors = [];
|
|
562
|
-
const defs = scanDefines(dir);
|
|
563
595
|
for (const f of listHtml(dir)) {
|
|
564
596
|
if (!isSelected(dir, f, options)) continue;
|
|
565
597
|
try {
|
|
@@ -576,7 +608,22 @@ function generateAll(dir, options = {}) {
|
|
|
576
608
|
if (e instanceof CodegenError) errors.push(e.message);
|
|
577
609
|
else throw e;
|
|
578
610
|
}
|
|
579
|
-
for (const f of listGenerated(dir)) if (!existsSync(f.slice(0, -3))) unlinkSync(f);
|
|
611
|
+
for (const f of listGenerated(dir, scanned)) if (!existsSync(f.slice(0, -3))) unlinkSync(f);
|
|
612
|
+
if (errors.length) throw new CodegenError(errors.join("\n"));
|
|
613
|
+
return written;
|
|
614
|
+
}
|
|
615
|
+
/** `generateAll` for every directory, with the define() calls of all of them: a template imports a child component defined in another directory. */
|
|
616
|
+
function generateDirs(dirs, options = {}) {
|
|
617
|
+
const defs = scanDefines(dirs);
|
|
618
|
+
const written = [];
|
|
619
|
+
const errors = [];
|
|
620
|
+
for (const dir of dirs) try {
|
|
621
|
+
written.push(...generateAll(dir, options, defs, dirs));
|
|
622
|
+
} catch (e) {
|
|
623
|
+
if (e instanceof CodegenError) errors.push(e.message);
|
|
624
|
+
else throw e;
|
|
625
|
+
}
|
|
626
|
+
for (const d of crossDirDuplicates(dirs)) errors.push(formatDiagnostics(d.file, [d]));
|
|
580
627
|
if (errors.length) throw new CodegenError(errors.join("\n"));
|
|
581
628
|
return written;
|
|
582
629
|
}
|
|
@@ -586,11 +633,10 @@ function watchAll(dirs, opts = {}) {
|
|
|
586
633
|
const watchers = dirs.map((dir) => watch(dir, { recursive: true }, (_event, name) => {
|
|
587
634
|
if (!name || name.includes("node_modules")) return;
|
|
588
635
|
if (name.endsWith(".ts") && !name.endsWith(".html.ts") && !name.endsWith(".d.ts") && !name.endsWith(TAGS_FILE)) {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
timers.set(key, setTimeout(() => {
|
|
636
|
+
clearTimeout(timers.get("tags"));
|
|
637
|
+
timers.set("tags", setTimeout(() => {
|
|
592
638
|
try {
|
|
593
|
-
for (const f of
|
|
639
|
+
for (const f of generateDirs(dirs, opts)) opts.onGenerate?.(f);
|
|
594
640
|
} catch (e) {
|
|
595
641
|
if (e instanceof CodegenError) opts.onError?.(e.message);
|
|
596
642
|
else throw e;
|
|
@@ -604,7 +650,7 @@ function watchAll(dirs, opts = {}) {
|
|
|
604
650
|
timers.set(file, setTimeout(() => {
|
|
605
651
|
try {
|
|
606
652
|
if (!existsSync(file) || !isSelected(dir, file, opts) || isFullPage(readFileSync(file, "utf8"))) return;
|
|
607
|
-
opts.onGenerate?.(generateFile(file, opts,
|
|
653
|
+
opts.onGenerate?.(generateFile(file, opts, scanDefines(dirs)));
|
|
608
654
|
} catch (e) {
|
|
609
655
|
if (e instanceof CodegenError) opts.onError?.(e.message);
|
|
610
656
|
else throw e;
|
|
@@ -814,4 +860,4 @@ function checkA11y(html, ignore = []) {
|
|
|
814
860
|
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.`);
|
|
815
861
|
return out.sort((a, b) => a.line - b.line || a.col - b.col);
|
|
816
862
|
}
|
|
817
|
-
export {
|
|
863
|
+
export { staleGenerated as _, analyze as a, formatDiagnostics as c, isFullPage as d, isSelected as f, scanDefines as g, renderTags as h, CodegenError as i, generateDirs as l, listTs as m, checkA11y as n, checkGenerated as o, listHtml as p, CODEGEN_RULES as r, crossDirDuplicates as s, A11Y_RULES as t, generateFile as u, watchAll as v };
|