@nativele/ts-dom 0.5.4 → 0.6.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 +69 -0
- package/README.md +6 -1
- package/README.zh-TW.md +6 -1
- package/dist/{a11y-CiEJj_e6.mjs → a11y-KXbo-8o3.mjs} +119 -27
- package/dist/cli.mjs +273 -50
- package/dist/{lint-Did4w-NL.mjs → lint-DUsdDKZ-.mjs} +33 -8
- package/dist/oxlint-plugin.mjs +3 -3
- package/dist/vite-plugin.mjs +1 -1
- package/docs/architecture.md +188 -0
- package/docs/components.md +367 -0
- package/docs/contributing.md +80 -0
- package/docs/design.md +55 -0
- package/docs/lists.md +150 -0
- package/docs/performance.md +24 -0
- package/docs/recipes.md +189 -0
- package/docs/templates.md +73 -0
- package/docs/tooling.md +251 -0
- package/llms.txt +5 -5
- package/package.json +2 -1
- package/skills/README.md +51 -14
- package/skills/nativele-angular/SKILL.md +15 -11
- package/skills/nativele-architecture/SKILL.md +29 -16
- package/skills/nativele-ci/SKILL.md +148 -0
- package/skills/nativele-core/SKILL.md +16 -13
- package/skills/nativele-formatting/SKILL.md +167 -0
- package/skills/nativele-integration/SKILL.md +12 -9
- package/skills/nativele-linting/SKILL.md +174 -0
- package/skills/nativele-monorepo/SKILL.md +298 -0
- package/skills/nativele-react/SKILL.md +13 -10
- package/skills/nativele-state-management/SKILL.md +6 -6
- package/skills/nativele-svelte/SKILL.md +10 -8
- package/skills/nativele-tailwind/SKILL.md +8 -7
- package/skills/nativele-tanstack-query/SKILL.md +5 -5
- package/skills/nativele-testing/SKILL.md +22 -16
- package/skills/nativele-typescript/SKILL.md +186 -0
- package/skills/nativele-vite/SKILL.md +21 -17
- package/skills/nativele-vue/SKILL.md +10 -8
- package/skills/nativele-zod/SKILL.md +6 -6
package/dist/cli.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { _ as
|
|
3
|
-
import { c as
|
|
2
|
+
import { _ as scanTags, a as analyze, c as formatDiagnostics, f as isSelected, g as scanDefines, h as renderTags, i as CodegenError, l as generateDirs, m as listTs, n as checkA11y, o as checkGenerated, p as listHtml, r as CODEGEN_RULES, s as crossDirDuplicates, t as A11Y_RULES, v as staleGenerated, y as watchAll } from "./a11y-KXbo-8o3.mjs";
|
|
3
|
+
import { c as lintDataFlow, l as lintUsage, n as UNUSED_MARKUP_RULES, s as formatIssue, t as DATA_FLOW_RULES } from "./lint-DUsdDKZ-.mjs";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { cpSync, existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
6
6
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { homedir } from "node:os";
|
|
8
9
|
import { fileURLToPath } from "node:url";
|
|
9
10
|
// src/lib/tsconfig.ts
|
|
10
11
|
/** tsconfig is JSONC: drop comments and trailing commas before parsing. */
|
|
@@ -29,30 +30,71 @@ function resolveExtends(from, ext) {
|
|
|
29
30
|
return null;
|
|
30
31
|
}
|
|
31
32
|
/** Read a tsconfig and the ones it extends (a relative path, a package, or an array of them), the later and nearer one winning. */
|
|
32
|
-
function readTsConfig(file
|
|
33
|
+
function readTsConfig(file) {
|
|
34
|
+
const config = readChain(file, 0);
|
|
35
|
+
return config.include === void 0 && config.files ? {
|
|
36
|
+
...config,
|
|
37
|
+
include: []
|
|
38
|
+
} : config;
|
|
39
|
+
}
|
|
40
|
+
/** include, exclude and files are each taken from the nearest config that sets it, as tsc does; the default include is decided only for the whole chain. */
|
|
41
|
+
function readChain(file, depth) {
|
|
33
42
|
const json = parseJsonc(readFileSync(file, "utf8"));
|
|
34
43
|
let merged = { compilerOptions: {} };
|
|
35
44
|
const exts = typeof json.extends === "string" ? [json.extends] : Array.isArray(json.extends) ? json.extends : [];
|
|
36
45
|
for (const ext of depth < 5 ? exts : []) {
|
|
37
46
|
const target = resolveExtends(file, ext);
|
|
38
47
|
if (!target) continue;
|
|
39
|
-
const base =
|
|
48
|
+
const base = readChain(target, depth + 1);
|
|
40
49
|
merged = {
|
|
50
|
+
...merged,
|
|
51
|
+
...base,
|
|
41
52
|
compilerOptions: {
|
|
42
53
|
...merged.compilerOptions,
|
|
43
54
|
...base.compilerOptions
|
|
44
|
-
}
|
|
45
|
-
...base.include ?? merged.include ? { include: base.include ?? merged.include } : {}
|
|
55
|
+
}
|
|
46
56
|
};
|
|
47
57
|
}
|
|
48
|
-
const own = json.
|
|
49
|
-
const include
|
|
58
|
+
const own = (key) => Array.isArray(json[key]) ? json[key].map((p) => resolve(dirname(file), p).split("\\").join("/")) : merged[key];
|
|
59
|
+
const [include, exclude, files] = [
|
|
60
|
+
own("include"),
|
|
61
|
+
own("exclude"),
|
|
62
|
+
own("files")
|
|
63
|
+
];
|
|
50
64
|
return {
|
|
51
65
|
compilerOptions: {
|
|
52
66
|
...merged.compilerOptions,
|
|
53
67
|
...json.compilerOptions
|
|
54
68
|
},
|
|
55
|
-
...include ? { include } : {}
|
|
69
|
+
...include ? { include } : {},
|
|
70
|
+
...exclude ? { exclude } : {},
|
|
71
|
+
...files ? { files } : {}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** A tsconfig include or exclude pattern (absolute, forward slashes) as tsc matches it: `**` crosses folders, `*` and `?` stay within one, a last segment without a wildcard or an extension names a folder, and an exclude also removes everything under a folder it matches. */
|
|
75
|
+
function globRegExp(pattern, exclude) {
|
|
76
|
+
const last = pattern.slice(pattern.lastIndexOf("/") + 1);
|
|
77
|
+
const segments = (!exclude && !/[*?]/.test(last) && !/\.[^.]+$/.test(last) ? `${pattern.replace(/\/$/, "")}/**/*` : pattern).split("/");
|
|
78
|
+
const body = segments.map((seg, i) => {
|
|
79
|
+
const end = i === segments.length - 1;
|
|
80
|
+
if (seg === "**") return end ? ".*" : "(?:[^/]+/)*";
|
|
81
|
+
const text = seg.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]");
|
|
82
|
+
return end ? text : `${text}/`;
|
|
83
|
+
}).join("");
|
|
84
|
+
return new RegExp(`^${body}${exclude ? "(?:$|/)" : "$"}`);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Why tsc leaves `file` out of the program of a config whose folder is `configDir`, unless another file imports it: include does not match it, or an exclude pattern removes it.
|
|
88
|
+
* null when files, or include without a matching exclude, take it in.
|
|
89
|
+
*/
|
|
90
|
+
function leftOutBy(config, configDir, file) {
|
|
91
|
+
const f = file.split("\\").join("/");
|
|
92
|
+
if (config.files?.includes(f)) return null;
|
|
93
|
+
if (!(config.include ?? [`${configDir.split("\\").join("/")}/**/*`]).some((p) => globRegExp(p, false).test(f))) return { by: "include" };
|
|
94
|
+
const pattern = config.exclude?.find((p) => globRegExp(p, true).test(f));
|
|
95
|
+
return pattern === void 0 ? null : {
|
|
96
|
+
by: "exclude",
|
|
97
|
+
pattern
|
|
56
98
|
};
|
|
57
99
|
}
|
|
58
100
|
/**
|
|
@@ -81,7 +123,7 @@ const CHECK_RULES = {
|
|
|
81
123
|
"item-not-in-tag-map": "the item component is defined in a scanned folder but missing from ts-dom-tags.ts (found by tsc)",
|
|
82
124
|
"item-class-not-exported": "the item component class passed to define() is not exported (found by tsc)",
|
|
83
125
|
tsc: "any other TypeScript error, as tsc reports it",
|
|
84
|
-
"tsc-not-found": "TypeScript is not installed in the project, so types were not checked",
|
|
126
|
+
"tsc-not-found": "TypeScript is not installed in the project, so types were not checked and the check fails",
|
|
85
127
|
"dir-not-found": "a folder given on the command line does not exist, so nothing in it was checked",
|
|
86
128
|
"tsc-failed": "tsc did not run to completion (no tsconfig, a bad option, a crash), so types were not checked"
|
|
87
129
|
};
|
|
@@ -125,7 +167,7 @@ const CONTRACT = [
|
|
|
125
167
|
[
|
|
126
168
|
"itemComponentMissingFromTagMap",
|
|
127
169
|
"item-not-in-tag-map",
|
|
128
|
-
"run ts-dom gen so ts-dom-tags.ts lists the item component"
|
|
170
|
+
"run ts-dom gen so ts-dom-tags.ts lists the item component; if it already does, the tsconfig include or exclude leaves ts-dom-tags.ts out of the program (ts-dom doctor reports it as tsconfig-misses-tag-map): make include match it"
|
|
129
171
|
],
|
|
130
172
|
[
|
|
131
173
|
"itemComponentClassNotExported",
|
|
@@ -163,7 +205,7 @@ function mapTscErrors(errors, cwd) {
|
|
|
163
205
|
};
|
|
164
206
|
}
|
|
165
207
|
};
|
|
166
|
-
const brokenClasses = /* @__PURE__ */ new
|
|
208
|
+
const brokenClasses = /* @__PURE__ */ new Map();
|
|
167
209
|
const brokenBodies = [];
|
|
168
210
|
for (const e of errors) {
|
|
169
211
|
if (!/missingStateKeys|conflictingKeys|reservedPropNames|nativeEventNames/.test(e.message)) continue;
|
|
@@ -171,7 +213,7 @@ function mapTscErrors(errors, cwd) {
|
|
|
171
213
|
const lines = text.split("\n");
|
|
172
214
|
const m = /class\s+([A-Za-z_$][\w$]*)\s+extends\s+Component\s*\(/.exec(lines[e.line - 1] ?? "");
|
|
173
215
|
if (!m?.[1]) continue;
|
|
174
|
-
brokenClasses.
|
|
216
|
+
if (!brokenClasses.has(m[1])) brokenClasses.set(m[1], e);
|
|
175
217
|
const end = classBodyEnd(text, lines.slice(0, e.line - 1).join("\n").length + (e.line > 1 ? 1 : 0) + m.index + m[0].length - 1);
|
|
176
218
|
if (end !== -1) brokenBodies.push({
|
|
177
219
|
file: rel(resolve(cwd, e.file)),
|
|
@@ -179,8 +221,9 @@ function mapTscErrors(errors, cwd) {
|
|
|
179
221
|
to: text.slice(0, end).split("\n").length
|
|
180
222
|
});
|
|
181
223
|
}
|
|
182
|
-
const
|
|
224
|
+
const firstIssueOf = /* @__PURE__ */ new Map();
|
|
183
225
|
for (const e of errors) {
|
|
226
|
+
const before = out.length;
|
|
184
227
|
const file = resolve(cwd, e.file);
|
|
185
228
|
const where = `${rel(file)}:${e.line}:${e.col}`;
|
|
186
229
|
const issue = (rule, message, at) => out.push({
|
|
@@ -217,11 +260,50 @@ function mapTscErrors(errors, cwd) {
|
|
|
217
260
|
named = true;
|
|
218
261
|
issue(rule, `${e.message.split("\n")[0]} Fix: ${fix}.`);
|
|
219
262
|
}
|
|
220
|
-
if (named)
|
|
221
|
-
|
|
263
|
+
if (named) {
|
|
264
|
+
firstIssueOf.set(e, out[before]);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const tag = /Property '"([^"]+)"' must be of type/.exec(e.message)?.[1];
|
|
268
|
+
if (e.code === "TS2717" && tag && file.endsWith(`${sep}ts-dom-tags.ts`)) issue("duplicate-define", `The tag <${tag}> is declared with another class by a second tag map in the same program (another generated ts-dom-tags.ts, from a folder that is scanned separately or from a package), so two components share one custom element name, and at runtime only the class whose define() runs first is registered: the other define() is skipped without an error (tsc ${e.code} at ${where}). Fix: rename the tag in one of the define() calls and in the templates that use it; editing this generated file does not help.`);
|
|
269
|
+
else if (file.endsWith(".html.ts") || file.endsWith(`${sep}ts-dom-tags.ts`)) issue("tsc", `${e.code}: ${e.message} This file is generated, so an edit to it is overwritten. Fix: run ts-dom gen for the folder that holds it; if it is left over from a template or folder that is no longer scanned, delete it.`);
|
|
222
270
|
else issue("tsc", `${e.code}: ${e.message} Fix: change the TypeScript at this location so the types agree; ts-dom rules --explain=tsc.`);
|
|
223
271
|
}
|
|
224
|
-
|
|
272
|
+
const brokenFiles = new Set([...brokenClasses.values()].map((e) => rel(resolve(cwd, e.file))));
|
|
273
|
+
const classIn = (message) => {
|
|
274
|
+
const types = [...message.matchAll(/'([^']*)'/g)].flatMap((m) => topLevelMembers(m[1]).map((t) => t.replace(/^typeof\s+/, "")));
|
|
275
|
+
return [...brokenClasses.keys()].find((c) => types.includes(c));
|
|
276
|
+
};
|
|
277
|
+
return out.flatMap((i) => {
|
|
278
|
+
const cls = i.rule === "tsc" ? classIn(i.message) : void 0;
|
|
279
|
+
if (i.rule === "tsc" && brokenBodies.some((b) => b.file === i.file && i.line > b.from && i.line <= b.to)) return [];
|
|
280
|
+
if (cls === void 0) return [i];
|
|
281
|
+
if (brokenFiles.has(i.file) || /(^|[\\/])node_modules[\\/]/.test(i.file)) return [];
|
|
282
|
+
const cause = brokenClasses.get(cls);
|
|
283
|
+
const first = firstIssueOf.get(cause);
|
|
284
|
+
const at = first ? `${first.file}:${first.line}:${first.col} [${first.rule}]` : `${rel(resolve(cwd, cause.file))}:${cause.line}:${cause.col}`;
|
|
285
|
+
return [{
|
|
286
|
+
...i,
|
|
287
|
+
message: `${i.message.replace(/ Fix: .*$/s, "")} This error follows from the contract error of ${cls} at ${at}, which makes tsc type ${cls} as that error instead of an element. Fix: fix that error first; this one then goes away.`
|
|
288
|
+
}];
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
/** The members of a union or intersection type as tsc prints it (`A & { x: number; }` gives `A` and `{ x: number; }`); any other type is its only member. */
|
|
292
|
+
function topLevelMembers(type) {
|
|
293
|
+
const out = [];
|
|
294
|
+
let depth = 0;
|
|
295
|
+
let from = 0;
|
|
296
|
+
for (let i = 0; i < type.length; i++) {
|
|
297
|
+
const c = type[i];
|
|
298
|
+
if ("({[<".includes(c)) depth++;
|
|
299
|
+
else if (")}]".includes(c) || c === ">" && type[i - 1] !== "=") depth--;
|
|
300
|
+
else if (depth === 0 && (c === "&" || c === "|")) {
|
|
301
|
+
out.push(type.slice(from, i));
|
|
302
|
+
from = i + 1;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
out.push(type.slice(from));
|
|
306
|
+
return out.map((t) => t.trim());
|
|
225
307
|
}
|
|
226
308
|
/** The offset of the `}` that closes a class body, given the offset of the `(` of its `extends Component(`; -1 when it cannot be found. Strings and comments are skipped. */
|
|
227
309
|
function classBodyEnd(text, open) {
|
|
@@ -328,8 +410,8 @@ function runCheck(options) {
|
|
|
328
410
|
line: 1,
|
|
329
411
|
col: 1,
|
|
330
412
|
rule: "tsc-not-found",
|
|
331
|
-
severity: "
|
|
332
|
-
message: "TypeScript is not installed in this project, so types were
|
|
413
|
+
severity: "error",
|
|
414
|
+
message: "TypeScript is not installed in this project, so no types were checked, and a check without them would pass with nothing looked at. Fix: npm i -D typescript (in CI, install the dev dependencies before ts-dom check)."
|
|
333
415
|
});
|
|
334
416
|
else {
|
|
335
417
|
const configs = projectConfigs(cwd, options.project);
|
|
@@ -475,10 +557,11 @@ function libraryVersion(root = skillsRoot()) {
|
|
|
475
557
|
const DOCTOR_RULES = {
|
|
476
558
|
"dir-not-found": "a folder given on the command line does not exist, so nothing in it was checked",
|
|
477
559
|
"no-tsconfig": "there is no tsconfig.json, so tsc cannot check the templates against the components",
|
|
478
|
-
"tsconfig-not-strict": "strict is off, so tsc
|
|
560
|
+
"tsconfig-not-strict": "strict is off (set to false, or left out on TypeScript 5 or a version that cannot be determined), so tsc does not report possibly null values or implicit any",
|
|
479
561
|
"tsconfig-no-dom-lib": "lib leaves out DOM, so element types are missing",
|
|
480
|
-
"tsconfig-classic-resolution": "moduleResolution classic cannot resolve
|
|
562
|
+
"tsconfig-classic-resolution": "modules resolve the classic way (moduleResolution classic, or the TypeScript 5 default for module ES2015 to ESNext), which cannot resolve the runtime package the generated files import; TypeScript 6 deprecates it and 7 removes it",
|
|
481
563
|
"tsconfig-misses-templates": "include leaves out a folder with templates, so their generated types are not checked",
|
|
564
|
+
"tsconfig-misses-tag-map": "include or exclude leaves ts-dom-tags.ts out of the program and no file imports it, so custom element tags are typed as plain HTMLElement",
|
|
482
565
|
"runtime-not-installed": "the runtime package the generated files import cannot be resolved",
|
|
483
566
|
"template-import-without-js": "a template is imported as ./x.html instead of ./x.html.js",
|
|
484
567
|
"stale-generated": "a generated file is missing, outdated or orphaned",
|
|
@@ -518,6 +601,83 @@ function scriptSteps(scripts, name, depth = 0) {
|
|
|
518
601
|
...scriptSteps(scripts, `post${name}`, depth + 1)
|
|
519
602
|
];
|
|
520
603
|
}
|
|
604
|
+
/** The version of the TypeScript the project resolves, or null when it has none. */
|
|
605
|
+
function typescriptVersion(cwd) {
|
|
606
|
+
try {
|
|
607
|
+
return JSON.parse(readFileSync(createRequire(join(cwd, "package.json")).resolve("typescript/package.json"), "utf8")).version ?? null;
|
|
608
|
+
} catch {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
const TS5_CLASSIC_MODULES = /* @__PURE__ */ new Set([
|
|
613
|
+
"es6",
|
|
614
|
+
"es2015",
|
|
615
|
+
"es2020",
|
|
616
|
+
"es2022",
|
|
617
|
+
"esnext",
|
|
618
|
+
"amd",
|
|
619
|
+
"umd",
|
|
620
|
+
"system",
|
|
621
|
+
"none"
|
|
622
|
+
]);
|
|
623
|
+
const implicitlyClassic = (o) => {
|
|
624
|
+
if (o.module !== void 0) return TS5_CLASSIC_MODULES.has(String(o.module).toLowerCase());
|
|
625
|
+
return ![
|
|
626
|
+
"",
|
|
627
|
+
"es3",
|
|
628
|
+
"es5"
|
|
629
|
+
].includes(String(o.target ?? "").toLowerCase());
|
|
630
|
+
};
|
|
631
|
+
const readPackageJson = (file) => {
|
|
632
|
+
try {
|
|
633
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
634
|
+
} catch {
|
|
635
|
+
return {};
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
/** `cwd` and the folders above it up to the workspace root: the nearest folder with a package.json "workspaces" field or a pnpm-workspace.yaml, else the git root. Only `cwd` when neither is found below the home directory or the filesystem root. */
|
|
639
|
+
function upToWorkspaceRoot(cwd) {
|
|
640
|
+
const start = resolve(cwd);
|
|
641
|
+
const home = homedir();
|
|
642
|
+
const chain = [];
|
|
643
|
+
for (let dir = start;; dir = dirname(dir)) {
|
|
644
|
+
if (dir === home && dir !== start) return [start];
|
|
645
|
+
chain.push(dir);
|
|
646
|
+
if (existsSync(join(dir, "pnpm-workspace.yaml")) || "workspaces" in readPackageJson(join(dir, "package.json")) || existsSync(join(dir, ".git"))) return chain;
|
|
647
|
+
if (dirname(dir) === dir) return [start];
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
const SCRIPT_EXTENSIONS = [
|
|
651
|
+
"js",
|
|
652
|
+
"cjs",
|
|
653
|
+
"mjs",
|
|
654
|
+
"ts",
|
|
655
|
+
"cts",
|
|
656
|
+
"mts"
|
|
657
|
+
];
|
|
658
|
+
const OXFMT_CONFIGS = [
|
|
659
|
+
".oxfmtrc.json",
|
|
660
|
+
".oxfmtrc.jsonc",
|
|
661
|
+
...SCRIPT_EXTENSIONS.map((e) => `oxfmt.config.${e}`)
|
|
662
|
+
];
|
|
663
|
+
const PRETTIER_CONFIGS = [
|
|
664
|
+
".prettierrc",
|
|
665
|
+
...[
|
|
666
|
+
"json",
|
|
667
|
+
"json5",
|
|
668
|
+
"yaml",
|
|
669
|
+
"yml",
|
|
670
|
+
"toml",
|
|
671
|
+
...SCRIPT_EXTENSIONS
|
|
672
|
+
].map((e) => `.prettierrc.${e}`),
|
|
673
|
+
...SCRIPT_EXTENSIONS.map((e) => `prettier.config.${e}`)
|
|
674
|
+
];
|
|
675
|
+
const MODULE_SPECIFIERS = /\b(?:from|import)\s*\(?\s*["'](\.{1,2}\/[^"']+)["']|<reference\s+path\s*=\s*["']([^"']+)["']/g;
|
|
676
|
+
/** Whether `file` imports, re-exports or references `tagMap` through a relative path. */
|
|
677
|
+
function importsTagMap(file, tagMap) {
|
|
678
|
+
const target = tagMap.replace(/\.ts$/, "");
|
|
679
|
+
return [...(readText(file) ?? "").matchAll(MODULE_SPECIFIERS)].some((m) => resolve(dirname(file), m[1] ?? m[2]).replace(/\.[jt]s$/, "") === target);
|
|
680
|
+
}
|
|
521
681
|
const templateFolders = (dir) => [...new Set(listHtml(dir).map((f) => dirname(f)))];
|
|
522
682
|
const hasTemplates = (dir) => listHtml(dir).length > 0;
|
|
523
683
|
/** Check the project set-up an agent or a developer could get wrong; every issue names its rule and ends with a Fix hint. */
|
|
@@ -536,9 +696,14 @@ function runDoctor(options) {
|
|
|
536
696
|
const dirs = [];
|
|
537
697
|
for (const d of options.dirs.map((x) => resolve(cwd, x))) if (existsSync(d)) dirs.push(d);
|
|
538
698
|
else add("dir-not-found", "error", `The folder ${rel(d)} does not exist, so nothing in it was checked. Fix: pass the folder that holds the templates and components (default: src).`, { file: rel(d) });
|
|
539
|
-
const configs = projectConfigs(cwd);
|
|
699
|
+
const configs = projectConfigs(cwd, options.project);
|
|
540
700
|
let paths = {};
|
|
541
|
-
if (!configs.length)
|
|
701
|
+
if (!configs.length) {
|
|
702
|
+
if (options.project) add("no-tsconfig", "error", `The tsconfig ${options.project} given with --project does not exist. Fix: pass the path of the tsconfig the project type-checks with.`);
|
|
703
|
+
else add("no-tsconfig", "error", "There is no tsconfig.json. Fix: add one with \"strict\": true, \"lib\": [\"ES2022\", \"DOM\"] and \"moduleResolution\": \"bundler\" (or \"NodeNext\").");
|
|
704
|
+
}
|
|
705
|
+
const tsVersion = typescriptVersion(cwd);
|
|
706
|
+
const tsMajor = tsVersion === null ? null : Number.parseInt(tsVersion, 10);
|
|
542
707
|
const includes = [];
|
|
543
708
|
let everyConfigIncludes = true;
|
|
544
709
|
const scanned = dirs.map((dir) => ({
|
|
@@ -561,16 +726,42 @@ function runDoctor(options) {
|
|
|
561
726
|
if (config.include) includes.push(...config.include);
|
|
562
727
|
else everyConfigIncludes = false;
|
|
563
728
|
if (covering.length && !covering.some((r) => r.path === configPath)) continue;
|
|
564
|
-
|
|
729
|
+
const lost = "so tsc does not report values that may be null (a querySelector result or an optional ref used without a check) or parameters and variables that are implicitly any (a handler parameter without a type); the names in the templates are checked either way";
|
|
730
|
+
if (o.strict === false) add("tsconfig-not-strict", "warning", `compilerOptions.strict is false, ${lost}. Fix: set "strict": true.`, { file });
|
|
731
|
+
else if (o.strict === void 0 && tsMajor === null) add("tsconfig-not-strict", "warning", `compilerOptions.strict is not set, and the TypeScript version could not be determined (typescript is not resolvable from this project): TypeScript 5 leaves strict off by default (6 and later turn it on), ${lost}. Fix: set "strict": true.`, { file });
|
|
732
|
+
else if (o.strict === void 0 && tsMajor !== null && tsMajor < 6) add("tsconfig-not-strict", "warning", `compilerOptions.strict is not set, and TypeScript ${tsVersion} leaves it off by default (6 and later turn it on), ${lost}. Fix: set "strict": true.`, { file });
|
|
565
733
|
const lib = o.lib;
|
|
566
734
|
if (lib && !lib.some((l) => l.toLowerCase() === "dom")) add("tsconfig-no-dom-lib", "error", "compilerOptions.lib leaves out DOM, so element types such as HTMLButtonElement are missing. Fix: add \"DOM\" to lib.", { file });
|
|
567
|
-
|
|
735
|
+
const cannotResolve = `which cannot resolve the package "${runtime}" that every generated file imports (TS2792 Cannot find module), so no component gets its types`;
|
|
736
|
+
if (String(o.moduleResolution ?? "").toLowerCase() === "classic") add("tsconfig-classic-resolution", "error", `moduleResolution is "classic", ${tsMajor === null ? `${cannotResolve}; TypeScript 6 deprecates it (TS5107) and 7 removes it (TS5108)` : tsMajor >= 7 ? `which TypeScript ${tsVersion} no longer supports, so tsc stops with TS5108 before checking anything` : tsMajor === 6 ? `which TypeScript ${tsVersion} deprecates, so tsc stops with TS5107 before checking anything` : cannotResolve}. Fix: use "bundler" (or "NodeNext" with "module": "NodeNext").`, { file });
|
|
737
|
+
else if (o.moduleResolution === void 0 && tsMajor !== null && tsMajor < 6 && implicitlyClassic(o)) add("tsconfig-classic-resolution", "error", `${o.module !== void 0 ? `"module": ${JSON.stringify(o.module)}` : `"target": ${JSON.stringify(o.target)} (which makes module ES2015 or later)`} without "moduleResolution" makes TypeScript ${tsVersion} resolve modules the classic way, ${cannotResolve}. Fix: add "moduleResolution": "bundler" (or set "module" and "moduleResolution" to "NodeNext").`, { file });
|
|
568
738
|
}
|
|
569
739
|
if (configs.length && everyConfigIncludes) {
|
|
570
740
|
const missed = /* @__PURE__ */ new Set();
|
|
571
741
|
for (const folder of scanned.flatMap((s) => s.folders)) if (!includes.some((p) => covers(p, folder))) missed.add(rel(folder));
|
|
572
742
|
for (const folder of missed) add("tsconfig-misses-templates", "error", `No tsconfig include covers ${folder}, which has templates, so their generated types are never checked. Fix: add "${folder === "." ? "**/*" : `${folder}/**/*`}" to include.`, { file: rel(configs[0]) });
|
|
573
743
|
}
|
|
744
|
+
for (const { dir, folders } of scanned) {
|
|
745
|
+
const tags = scanTags(dir).map((t) => `<${t.tag}>`);
|
|
746
|
+
if (!tags.length) continue;
|
|
747
|
+
const tagMap = join(dir, "ts-dom-tags.ts");
|
|
748
|
+
const left = read.filter(({ config: { include } }) => !include || (folders.length ? folders : [dir]).some((t) => include.some((p) => covers(p, t)))).map(({ path, config }) => ({
|
|
749
|
+
path,
|
|
750
|
+
config,
|
|
751
|
+
why: leftOutBy(config, dirname(path), tagMap)
|
|
752
|
+
}));
|
|
753
|
+
const first = left[0];
|
|
754
|
+
if (!first?.why || left.some((l) => !l.why)) continue;
|
|
755
|
+
const configDir = dirname(first.path);
|
|
756
|
+
if ([
|
|
757
|
+
...listTs(dir),
|
|
758
|
+
...first.config.files ?? [],
|
|
759
|
+
...listTs(configDir).filter((f) => !leftOutBy(first.config, configDir, f))
|
|
760
|
+
].some((f) => importsTagMap(f, tagMap))) continue;
|
|
761
|
+
const own = (p) => relative(configDir, p).split("\\").join("/") || ".";
|
|
762
|
+
const effect = `so tsc types the tags defined in ${rel(dir)} (${tags.slice(0, 3).join(", ")}${tags.length > 3 ? ", ..." : ""}) as plain HTMLElement in document.createElement, querySelector and refs, and every list of item components fails with item-not-in-tag-map`;
|
|
763
|
+
add("tsconfig-misses-tag-map", "error", first.why.by === "include" ? `The include of ${rel(first.path)} does not match ${rel(tagMap)}, and no file imports it, ${effect}. Fix: add "${own(tagMap)}" to include.` : `The exclude pattern "${own(first.why.pattern)}" in ${rel(first.path)} removes ${rel(tagMap)} from the program, and no file imports it, ${effect}. Fix: narrow the pattern so it leaves ts-dom-tags.ts in, or add "${own(tagMap)}" to files (files are kept even when exclude matches them).`, { file: rel(first.path) });
|
|
764
|
+
}
|
|
574
765
|
let resolvable = Object.keys(paths).includes(runtime);
|
|
575
766
|
if (!resolvable) try {
|
|
576
767
|
createRequire(join(cwd, "package.json")).resolve(`${runtime}/package.json`);
|
|
@@ -615,7 +806,15 @@ function runDoctor(options) {
|
|
|
615
806
|
].map((ext) => join(cwd, `vitest.config.${ext}`)).find((f) => existsSync(f));
|
|
616
807
|
const vitest = vitestConfig ? readText(vitestConfig) : null;
|
|
617
808
|
if (vitestConfig && vitest !== null && !/@nativele\/ts-dom\/vite|vite-plugin(\.ts)?["']/.test(vitest) && !/["']\.\/vite\.config(\.[cm]?[jt]s)?["']/.test(vitest)) add("vite-plugin-missing", "warning", `${rel(vitestConfig)} replaces the Vite config for Vitest and does not use the ts-dom plugin, so the templates are not generated when the tests start and a clean checkout fails to resolve ./x.html.js. Fix: add tsDom() to its plugins, or merge the Vite config into it: mergeConfig(viteConfig, defineConfig({ test: { ... } })) with viteConfig imported from "./vite.config".`, { file: rel(vitestConfig) });
|
|
618
|
-
const
|
|
809
|
+
const workspace = upToWorkspaceRoot(cwd);
|
|
810
|
+
const nearest = (names) => workspace.flatMap((d) => names.map((n) => join(d, n))).find((f) => existsSync(f));
|
|
811
|
+
const oxlintrc = nearest([
|
|
812
|
+
".oxlintrc.json",
|
|
813
|
+
".oxlintrc.jsonc",
|
|
814
|
+
"oxlintrc.json",
|
|
815
|
+
"oxlint.config.ts",
|
|
816
|
+
"oxlint.config.mts"
|
|
817
|
+
]);
|
|
619
818
|
const lintConfigs = [oxlintrc ? readText(oxlintrc) : null, vite && /\blint\s*:/.test(vite) ? vite : null].filter((t) => t !== null);
|
|
620
819
|
if (lintConfigs.length && !lintConfigs.some((t) => /@nativele\/ts-dom\/oxlint|oxlint-plugin(\.ts)?["']/.test(t))) add("oxlint-plugin-missing", "warning", "The lint config does not load the ts-dom oxlint plugin, so unused markup, data flow and accessibility rules do not run in the editor. Fix: add { name: \"ts-dom\", specifier: \"@nativele/ts-dom/oxlint\" } to jsPlugins and enable \"ts-dom/unused-markup\".", { file: rel(oxlintrc ?? viteConfig) });
|
|
621
820
|
const pkgText = readText(join(cwd, "package.json"));
|
|
@@ -624,8 +823,7 @@ function runDoctor(options) {
|
|
|
624
823
|
...pkg.dependencies,
|
|
625
824
|
...pkg.devDependencies
|
|
626
825
|
};
|
|
627
|
-
|
|
628
|
-
if ((readText(join(cwd, ".gitignore")) ?? "").split("\n").some((l) => !l.trim().startsWith("!") && /\*\.html\.ts\s*$/.test(l)) && dirs.some(hasTemplates)) {
|
|
826
|
+
if (workspace.some((d) => (readText(join(d, ".gitignore")) ?? "").split("\n").some((l) => !l.trim().startsWith("!") && /\*\.html\.ts\s*$/.test(l))) && dirs.some(hasTemplates)) {
|
|
629
827
|
const all = pkg.scripts ?? {};
|
|
630
828
|
const checksTemplates = (step) => {
|
|
631
829
|
const project = TSC_PROJECT.exec(step)?.[1];
|
|
@@ -650,14 +848,21 @@ function runDoctor(options) {
|
|
|
650
848
|
});
|
|
651
849
|
}
|
|
652
850
|
}
|
|
653
|
-
const
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
".
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
851
|
+
const prettierIgnore = nearest([".prettierignore"]);
|
|
852
|
+
const ignoredHtml = /(^|\n)\s*(\*\*\/)?\*\.html\s*($|\n)/.test(prettierIgnore ? readText(prettierIgnore) ?? "" : "");
|
|
853
|
+
const formatterIn = (d) => {
|
|
854
|
+
const file = join(d, "package.json");
|
|
855
|
+
const json = readPackageJson(file);
|
|
856
|
+
const ds = {
|
|
857
|
+
...json.dependencies,
|
|
858
|
+
...json.devDependencies
|
|
859
|
+
};
|
|
860
|
+
const runs = (tool) => Object.values(json.scripts ?? {}).some((c) => tool.test(c));
|
|
861
|
+
const oxfmt = OXFMT_CONFIGS.map((n) => join(d, n)).find((f) => existsSync(f)) ?? ("oxfmt" in ds || runs(/(^|[\s;&|])(oxfmt|vp fmt)(\s|$)/) ? file : void 0);
|
|
862
|
+
if (oxfmt || ignoredHtml) return oxfmt;
|
|
863
|
+
return PRETTIER_CONFIGS.map((n) => join(d, n)).find((f) => existsSync(f)) ?? ("prettier" in json || "prettier" in ds || runs(/(^|[\s;&|])prettier(\s|$)/) ? file : void 0);
|
|
864
|
+
};
|
|
865
|
+
const formatter = workspace.map(formatterIn).find((f) => f !== void 0) ?? (vite !== null && /\bfmt\s*:/.test(vite) ? viteConfig : void 0);
|
|
661
866
|
const collapseScript = Object.entries(pkg.scripts ?? {}).find(([, c]) => c.includes("--whitespace=collapse"))?.[0];
|
|
662
867
|
const collapseVite = vite !== null && /whitespace\s*:\s*["']collapse["']/.test(vite);
|
|
663
868
|
const generatedModes = dirs.flatMap(listHtml).flatMap((f) => {
|
|
@@ -674,9 +879,9 @@ function runDoctor(options) {
|
|
|
674
879
|
add("formatter-whitespace", "warning", `${asker} asks for whitespace "collapse", but ${preserved.length === 1 ? "this generated file was" : `${preserved.length} generated files (this one first) were`} written with "preserve" (by a gen or check run without the flag, for example), and a run without --whitespace keeps the mode a file was last written with, so the page gets the whitespace as written instead of collapsed. Fix: regenerate with the flag once: ${rerun}.`, { file: rel(preserved[0].file) });
|
|
675
880
|
}
|
|
676
881
|
const collapsed = collapseScript !== void 0 || collapseVite || generatedModes.some((g) => g.collapse);
|
|
677
|
-
if (formatter && !collapsed && dirs.some(hasTemplates)) add("formatter-whitespace", "warning", "A formatter (oxfmt or Prettier) is set up, and templates keep their whitespace as written, so reformatting a template can change the spacing on the page. Fix: generate with whitespace \"collapse\" (ts-dom gen --whitespace=collapse, or tsDom({ whitespace: \"collapse\" })).");
|
|
882
|
+
if (formatter && !collapsed && dirs.some(hasTemplates)) add("formatter-whitespace", "warning", "A formatter (oxfmt or Prettier) is set up, and templates keep their whitespace as written, so reformatting a template can change the spacing on the page. Fix: generate with whitespace \"collapse\" (ts-dom gen --whitespace=collapse, or tsDom({ whitespace: \"collapse\" })).", { file: rel(formatter) });
|
|
678
883
|
for (const skill of listSkills(options.skillsDir ?? skillsRoot())) {
|
|
679
|
-
const copies = installedCopies(
|
|
884
|
+
const copies = workspace.flatMap((d) => installedCopies(d, skill.name));
|
|
680
885
|
const shipped = readFileSync(join(skill.dir, "SKILL.md"), "utf8");
|
|
681
886
|
for (const copy of copies) if (readFileSync(join(copy, "SKILL.md"), "utf8") !== shipped) add("skill-outdated", "info", `${rel(copy)} differs from the ${skill.name} Skill shipped with the installed Nativele. Fix: run ts-dom skills add ${skill.name} to replace it with the shipped version.`, { file: rel(join(copy, "SKILL.md")) });
|
|
682
887
|
const found = skill.detects.filter((d) => d in deps);
|
|
@@ -887,7 +1092,7 @@ connected() { /* put the behavior in the class */ }`
|
|
|
887
1092
|
good: `<button data-on-click="more">more</button>`
|
|
888
1093
|
},
|
|
889
1094
|
"duplicate-define": {
|
|
890
|
-
why: "A custom element tag can be registered only once, so two define()
|
|
1095
|
+
why: "A custom element tag can be registered only once. define() skips a tag that is already registered, so with two classes for one tag the class whose define() runs first wins and the other is dropped without an error, and the tag map cannot say which class the tag stands for. ts-dom check also reports it when tsc finds two tag maps in one program that declare the tag with different classes (TS2717 in ts-dom-tags.ts), for example a folder scanned separately or a package that ships its own tag map.",
|
|
891
1096
|
bad: `// a.ts
|
|
892
1097
|
define("user-card", CardA);
|
|
893
1098
|
// b.ts
|
|
@@ -1186,7 +1391,9 @@ Component(tpl, { props: { task: {} as Task } })`
|
|
|
1186
1391
|
why: "tsc learns which class a tag stands for from ts-dom-tags.ts. If the item component is missing there, its items cannot be typed.",
|
|
1187
1392
|
bad: `// task-row.ts defines the tag, but ts-dom-tags.ts is out of date
|
|
1188
1393
|
define("task-row", TaskRow);`,
|
|
1189
|
-
good: `npx ts-dom gen # regenerates ts-dom-tags.ts
|
|
1394
|
+
good: `npx ts-dom gen # regenerates ts-dom-tags.ts
|
|
1395
|
+
// and keep ts-dom-tags.ts in the program: tsconfig.json
|
|
1396
|
+
{ "include": ["src"] } // not ["src/**/*.tsx"], and no exclude that matches it`
|
|
1190
1397
|
},
|
|
1191
1398
|
"item-class-not-exported": {
|
|
1192
1399
|
why: "The tag map imports the class from the file that calls define(), so the class must be exported or tsc cannot see its props.",
|
|
@@ -1215,7 +1422,7 @@ const n: number = 1;`
|
|
|
1215
1422
|
// vite.config.ts with root: "web": tsDom({ dirs: ["src"] })`
|
|
1216
1423
|
},
|
|
1217
1424
|
"tsc-not-found": {
|
|
1218
|
-
why: "ts-dom check runs the TypeScript compiler of the project. Without it installed, nothing can be type-checked.",
|
|
1425
|
+
why: "ts-dom check runs the TypeScript compiler of the project. Without it installed, nothing can be type-checked, so this is an error: otherwise a CI job that omits the dev dependencies would pass without any type check.",
|
|
1219
1426
|
bad: `npx ts-dom check # typescript is not installed`,
|
|
1220
1427
|
good: `npm i -D typescript
|
|
1221
1428
|
npx ts-dom check`
|
|
@@ -1227,9 +1434,10 @@ npx ts-dom check`
|
|
|
1227
1434
|
{ "compilerOptions": { "strict": true, "lib": ["ES2022", "DOM"] }, "include": ["src"] }`
|
|
1228
1435
|
},
|
|
1229
1436
|
"tsconfig-not-strict": {
|
|
1230
|
-
why: "
|
|
1437
|
+
why: "The names in the templates are checked with or without strict, but without it tsc does not report values that may be null (a querySelector result, an optional ref) or parameters that are implicitly any (a handler parameter without a type). TypeScript 6 and later turn strict on when it is left out; TypeScript 5 leaves it off, so there it has to be set. When the project's TypeScript cannot be resolved, its version is unknown, and a missing strict is reported as on TypeScript 5.",
|
|
1231
1438
|
bad: `// tsconfig.json
|
|
1232
|
-
{ "compilerOptions": { "strict": false } }
|
|
1439
|
+
{ "compilerOptions": { "strict": false } }
|
|
1440
|
+
// or, with TypeScript 5, no "strict" at all`,
|
|
1233
1441
|
good: `// tsconfig.json
|
|
1234
1442
|
{ "compilerOptions": { "strict": true } }`
|
|
1235
1443
|
},
|
|
@@ -1241,9 +1449,11 @@ npx ts-dom check`
|
|
|
1241
1449
|
{ "compilerOptions": { "lib": ["ES2022", "DOM"] } }`
|
|
1242
1450
|
},
|
|
1243
1451
|
"tsconfig-classic-resolution": {
|
|
1244
|
-
why: "
|
|
1452
|
+
why: "Every generated x.html.ts imports the runtime package (@nativele/ts-dom), and the classic resolution does not look in node_modules, so tsc reports TS2792 (Cannot find module) and no component gets its types. TypeScript 5 also resolves the classic way when moduleResolution is left out and module is ES2015 to ESNext (or target is ES2015 or later without module). TypeScript 6 deprecates classic (TS5107) and 7 removes it (TS5108); both stop before checking anything.",
|
|
1245
1453
|
bad: `// tsconfig.json
|
|
1246
|
-
{ "compilerOptions": { "moduleResolution": "classic" } }
|
|
1454
|
+
{ "compilerOptions": { "moduleResolution": "classic" } }
|
|
1455
|
+
// or, with TypeScript 5
|
|
1456
|
+
{ "compilerOptions": { "module": "ESNext" } }`,
|
|
1247
1457
|
good: `// tsconfig.json
|
|
1248
1458
|
{ "compilerOptions": { "moduleResolution": "bundler" } }`
|
|
1249
1459
|
},
|
|
@@ -1253,6 +1463,15 @@ npx ts-dom check`
|
|
|
1253
1463
|
{ "include": ["src/server"] } // templates are in src/ui`,
|
|
1254
1464
|
good: `// tsconfig.json
|
|
1255
1465
|
{ "include": ["src"] }`
|
|
1466
|
+
},
|
|
1467
|
+
"tsconfig-misses-tag-map": {
|
|
1468
|
+
why: "ts-dom-tags.ts registers the components in HTMLElementTagNameMap. Usually no file imports it, so it is in the program only when include matches it and exclude does not remove it; a file in the program that imports or re-exports it (a package entry with export type {} from \"./ts-dom-tags.js\") brings it in as well, and then nothing is reported. Left out, tags are typed as plain HTMLElement and lists of item components fail with item-not-in-tag-map. The x.html.ts files do not have this problem: their components import them. include, exclude and files are read through extends as tsc reads them: each from the nearest config that sets it.",
|
|
1469
|
+
bad: `// tsconfig.json
|
|
1470
|
+
{ "include": ["src/**/*.tsx"] }
|
|
1471
|
+
{ "include": ["src"], "exclude": ["**/ts-dom-tags.ts"] }`,
|
|
1472
|
+
good: `// tsconfig.json
|
|
1473
|
+
{ "include": ["src"] }
|
|
1474
|
+
{ "include": ["src/**/*.tsx", "src/ts-dom-tags.ts"] }`
|
|
1256
1475
|
},
|
|
1257
1476
|
"runtime-not-installed": {
|
|
1258
1477
|
why: "Generated files import the runtime package. If it cannot be resolved from the project, they fail to compile and to run.",
|
|
@@ -1295,7 +1514,7 @@ import viteConfig from "./vite.config";
|
|
|
1295
1514
|
export default mergeConfig(viteConfig, defineConfig({ test: { environment: "happy-dom" } }));`
|
|
1296
1515
|
},
|
|
1297
1516
|
"oxlint-plugin-missing": {
|
|
1298
|
-
why: "The oxlint plugin runs the template rules (unused markup, data flow, accessibility) in the editor. Without it they only run from the CLI.",
|
|
1517
|
+
why: "The oxlint plugin runs the template rules (unused markup, data flow, accessibility) in the editor. Without it they only run from the CLI. In a package of a workspace, the nearest lint config up to the workspace root is read.",
|
|
1299
1518
|
bad: `// .oxlintrc.json
|
|
1300
1519
|
{ "rules": {} }`,
|
|
1301
1520
|
good: `// .oxlintrc.json
|
|
@@ -1305,7 +1524,7 @@ export default mergeConfig(viteConfig, defineConfig({ test: { environment: "happ
|
|
|
1305
1524
|
}`
|
|
1306
1525
|
},
|
|
1307
1526
|
"formatter-whitespace": {
|
|
1308
|
-
why: "Text whitespace in a template is kept as written. A formatter that re-indents the HTML can then change the spaces rendered on the page. The same warning is given when the scripts or the Vite config ask for collapse but the generated files were written with preserve: a run without --whitespace keeps the mode a file was last written with, so one run without the flag leaves the project on preserve.",
|
|
1527
|
+
why: "Text whitespace in a template is kept as written. A formatter that re-indents the HTML can then change the spaces rendered on the page. The same warning is given when the scripts or the Vite config ask for collapse but the generated files were written with preserve: a run without --whitespace keeps the mode a file was last written with, so one run without the flag leaves the project on preserve. In a package of a workspace, the formatter's config files, the prettier key, dependencies and scripts in package.json, and .prettierignore are looked up to the workspace root, nearest first.",
|
|
1309
1528
|
bad: `# formatter is configured, templates keep their whitespace
|
|
1310
1529
|
npx ts-dom gen
|
|
1311
1530
|
# or: package.json has "gen": "ts-dom gen src --whitespace=collapse", but the files came from a plain run
|
|
@@ -1353,8 +1572,10 @@ Commands
|
|
|
1353
1572
|
--json print the issues as JSON instead of text
|
|
1354
1573
|
doctor [dirs...] Check the project set-up: tsconfig, template imports, generated files, the runtime package,
|
|
1355
1574
|
and the Vite, oxlint and formatter configuration (default dir: src)
|
|
1575
|
+
--project=<path> tsconfig to check (default: tsconfig.json)
|
|
1356
1576
|
--include=<glob>, --exclude=<glob>, --whitespace=collapse|preserve as for gen (they decide which
|
|
1357
1577
|
generated files are stale)
|
|
1578
|
+
--strict exit with code 1 on warnings too (errors always do; info never does)
|
|
1358
1579
|
--json print the issues as JSON instead of text
|
|
1359
1580
|
skills List the Nativele Skills shipped with this version: task procedures for coding agents
|
|
1360
1581
|
(set-up, a complete example, known limitations, verification), with the versions verified
|
|
@@ -1389,7 +1610,7 @@ function ruleGroups() {
|
|
|
1389
1610
|
],
|
|
1390
1611
|
[
|
|
1391
1612
|
"lint: unused-markup",
|
|
1392
|
-
"errors from `ts-dom lint` / oxlint rule ts-dom/unused-markup (or ts-dom/<id>)",
|
|
1613
|
+
"errors from `ts-dom lint` / oxlint rule ts-dom/unused-markup (or ts-dom/<id>); no-importer only from `ts-dom lint`, as the oxlint plugin sees one file at a time and cannot tell that no file imports a template",
|
|
1393
1614
|
UNUSED_MARKUP_RULES
|
|
1394
1615
|
],
|
|
1395
1616
|
[
|
|
@@ -1549,13 +1770,15 @@ try {
|
|
|
1549
1770
|
if (issues.filter((i) => i.severity === "error").length || flags.has("--strict") && issues.length) process.exitCode = 1;
|
|
1550
1771
|
if (!flags.has("--json") && !issues.length) console.log("ts-dom check: no issues");
|
|
1551
1772
|
} else if (cmd === "doctor") {
|
|
1773
|
+
const project = args.find((a) => a.startsWith("--project="))?.slice(10);
|
|
1552
1774
|
const issues = runDoctor({
|
|
1553
1775
|
dirs,
|
|
1554
|
-
generate
|
|
1776
|
+
generate,
|
|
1777
|
+
...project ? { project } : {}
|
|
1555
1778
|
});
|
|
1556
1779
|
if (flags.has("--json")) console.log(JSON.stringify({ issues }, null, 2));
|
|
1557
1780
|
else for (const i of issues) (i.severity === "error" ? console.error : i.severity === "warning" ? console.warn : console.log)(formatDoctorIssue(i));
|
|
1558
|
-
if (issues.some((i) => i.severity === "error")) process.exitCode = 1;
|
|
1781
|
+
if (issues.some((i) => i.severity === "error" || flags.has("--strict") && i.severity === "warning")) process.exitCode = 1;
|
|
1559
1782
|
if (!flags.has("--json") && !issues.some((i) => i.severity !== "info")) console.log("ts-dom doctor: the set-up looks right");
|
|
1560
1783
|
} else if (cmd === "lint") {
|
|
1561
1784
|
const files = dirs.flatMap(listTs);
|