@fluixi/ts-plugin 0.1.0-alpha.10 → 0.1.0-alpha.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -14
- package/dist/index.js +66 -73
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,34 +1,49 @@
|
|
|
1
1
|
# @fluixi/ts-plugin
|
|
2
2
|
|
|
3
|
-
A TypeScript language-service plugin that
|
|
4
|
-
|
|
3
|
+
A TypeScript language-service plugin that gives `` html`...` `` templates the same editor
|
|
4
|
+
support JSX gets.
|
|
5
5
|
|
|
6
6
|
## The problem
|
|
7
7
|
|
|
8
|
-
TypeScript sees a tagged template as an opaque string.
|
|
9
|
-
only by tag
|
|
8
|
+
TypeScript sees a tagged template as an opaque string. Everything inside it falls off the
|
|
9
|
+
map: a component referenced only by tag is reported "declared but never used", a callback
|
|
10
|
+
param is an implicit `any`, nothing completes, hover says nothing, and go-to-definition
|
|
11
|
+
lands on the template literal instead of the component.
|
|
10
12
|
|
|
11
13
|
```ts
|
|
12
14
|
function UserCard() { /* … */ }
|
|
13
15
|
|
|
14
16
|
const app = html`<main><UserCard /></main>`;
|
|
17
|
+
// ^ greyed out — TypeScript never counted this as a reference
|
|
15
18
|
```
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
never used"** and greyed out, even though it's used.
|
|
20
|
+
Authoring in `` html`` `` shouldn't cost you the editor.
|
|
19
21
|
|
|
20
|
-
##
|
|
22
|
+
## What you get
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
Reparse the template with [`@fluixi/template-parser`](../template-parser), map every tag and
|
|
25
|
+
hole back to a real source position, and ask the program's own checker about it:
|
|
26
|
+
|
|
27
|
+
- **Hover** — a component tag shows its signature, a prop shows its declared type, an
|
|
28
|
+
element attribute shows what it takes, and an inferred callback param (`each`, `event`,
|
|
29
|
+
`ref`, `use`, `Show`) shows the type it was inferred to.
|
|
30
|
+
- **Completion** — component prop names inside a tag, element attributes on an intrinsic
|
|
31
|
+
tag, and members of an inferred param (`it.`, `el.`, `e.`).
|
|
32
|
+
- **Navigation** — go-to-definition, find-all-references and rename work through tags.
|
|
33
|
+
Rename only adds tags when TypeScript's own rename succeeded, so you never get a
|
|
34
|
+
rewritten `<Tag>` with a stale `</Tag>`.
|
|
35
|
+
- **Diagnostics** — a bare accessor in a template (`` html`<p>${count}</p>` ``) is flagged,
|
|
36
|
+
since it renders the function once and never updates; a property that doesn't exist on an
|
|
37
|
+
inferred param is reported.
|
|
38
|
+
- **Quiet where it should be** — unused-component reports and implicit-`any` noise on params
|
|
39
|
+
the template can infer are dropped.
|
|
40
|
+
- **Quick fixes** — "Call `count`" for a bare accessor, with a fix-all for the file.
|
|
26
41
|
|
|
27
42
|
## Enable it
|
|
28
43
|
|
|
29
|
-
Add it to your project's `tsconfig.json` and use the **workspace** TypeScript
|
|
30
|
-
|
|
31
|
-
|
|
44
|
+
Add it to your project's `tsconfig.json` and use the **workspace** TypeScript version
|
|
45
|
+
(VS Code: `TypeScript: Select TypeScript Version → Use Workspace Version` — tsconfig plugins
|
|
46
|
+
only load under the workspace TS):
|
|
32
47
|
|
|
33
48
|
```jsonc
|
|
34
49
|
{
|
|
@@ -38,6 +53,22 @@ Version`, since tsconfig plugins only load under the workspace TS):
|
|
|
38
53
|
}
|
|
39
54
|
```
|
|
40
55
|
|
|
56
|
+
Nothing else. JSX needs no plugin, and neither do the `$signal`-style intrinsics — those are
|
|
57
|
+
typed by the `fluixi.d.ts` the compiler generates.
|
|
58
|
+
|
|
59
|
+
## How it stays out of the way
|
|
60
|
+
|
|
61
|
+
Everything is computed on the **existing** program's checker. The plugin creates no language
|
|
62
|
+
service, adds no files, and never touches the host — it wraps the service it's handed, and
|
|
63
|
+
every addition is wrapped so a failure costs the extra information, never the real answer.
|
|
64
|
+
|
|
65
|
+
That restraint is deliberate. Two earlier designs for deep type-checking inside `${…}` holes
|
|
66
|
+
both had to go: a second `ts.LanguageService` meant a second document registry, so lib.d.ts
|
|
67
|
+
and node_modules were parsed twice; and shadow files added via `getScriptFileNames` re-entered
|
|
68
|
+
host machinery on every project sync and crashed the language service outright. Hole
|
|
69
|
+
diagnostics weren't worth either — a `${…}` hole is a real expression, so TypeScript already
|
|
70
|
+
checks it.
|
|
71
|
+
|
|
41
72
|
## License
|
|
42
73
|
|
|
43
74
|
MIT
|
package/dist/index.js
CHANGED
|
@@ -376,79 +376,6 @@ var N = class {
|
|
|
376
376
|
function b(e, t = {}) {
|
|
377
377
|
return new N(e, t).parse();
|
|
378
378
|
}
|
|
379
|
-
function I(e) {
|
|
380
|
-
let t = [], i2 = 0;
|
|
381
|
-
return e.forEach((n, r) => {
|
|
382
|
-
t.push({ kind: "static", text: n, start: i2 }), i2 += n.length, r < e.length - 1 && t.push({ kind: "hole", index: r, start: i2, end: i2 });
|
|
383
|
-
}), t;
|
|
384
|
-
}
|
|
385
|
-
function Q(e, t = {}) {
|
|
386
|
-
return b(I(e), t);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// src/collect.ts
|
|
390
|
-
var UNUSED_CODES = /* @__PURE__ */ new Set([6133, 6196, 6198, 6199, 6205, 6138, 6192]);
|
|
391
|
-
function isUnusedCode(code) {
|
|
392
|
-
return UNUSED_CODES.has(code);
|
|
393
|
-
}
|
|
394
|
-
function usedComponentTags(ts, sourceFile) {
|
|
395
|
-
const tags = /* @__PURE__ */ new Set();
|
|
396
|
-
const visit = (node) => {
|
|
397
|
-
if (ts.isTaggedTemplateExpression(node) && ts.isIdentifier(node.tag) && (node.tag.text === "html" || node.tag.text === "svg")) {
|
|
398
|
-
collectTags(Q(templateStatics(ts, node.template)).root.children, tags);
|
|
399
|
-
}
|
|
400
|
-
ts.forEachChild(node, visit);
|
|
401
|
-
};
|
|
402
|
-
visit(sourceFile);
|
|
403
|
-
return tags;
|
|
404
|
-
}
|
|
405
|
-
function templateStatics(ts, tpl) {
|
|
406
|
-
if (ts.isNoSubstitutionTemplateLiteral(tpl)) return [tpl.text];
|
|
407
|
-
const out = [tpl.head.text];
|
|
408
|
-
for (const span of tpl.templateSpans) out.push(span.literal.text);
|
|
409
|
-
return out;
|
|
410
|
-
}
|
|
411
|
-
function collectTags(nodes, out) {
|
|
412
|
-
for (const node of nodes) {
|
|
413
|
-
if (node.kind === "Component" && node.tag) out.add(node.tag.split(".")[0]);
|
|
414
|
-
if ("children" in node && node.children) collectTags(node.children, out);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
function shouldSuppress(ts, sourceFile, diagnostic, used) {
|
|
418
|
-
if (diagnostic.start === void 0 || !isUnusedCode(diagnostic.code)) return false;
|
|
419
|
-
const spanText = sourceFile.text.substr(diagnostic.start, diagnostic.length ?? 0);
|
|
420
|
-
const decl = importDeclarationAt(ts, sourceFile, diagnostic.start);
|
|
421
|
-
if (decl) {
|
|
422
|
-
const names = importedNames(ts, decl);
|
|
423
|
-
if (names.includes(spanText)) return used.has(spanText);
|
|
424
|
-
return names.some((n) => used.has(n));
|
|
425
|
-
}
|
|
426
|
-
return used.has(spanText);
|
|
427
|
-
}
|
|
428
|
-
function importDeclarationAt(ts, sourceFile, pos) {
|
|
429
|
-
let found;
|
|
430
|
-
const visit = (node) => {
|
|
431
|
-
if (found) return;
|
|
432
|
-
if (ts.isImportDeclaration(node) && node.getStart(sourceFile) <= pos && node.getEnd() >= pos) {
|
|
433
|
-
found = node;
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
ts.forEachChild(node, visit);
|
|
437
|
-
};
|
|
438
|
-
visit(sourceFile);
|
|
439
|
-
return found;
|
|
440
|
-
}
|
|
441
|
-
function importedNames(ts, decl) {
|
|
442
|
-
const bindings = decl.importClause?.namedBindings;
|
|
443
|
-
const names = [];
|
|
444
|
-
if (decl.importClause?.name) names.push(decl.importClause.name.text);
|
|
445
|
-
if (bindings && ts.isNamedImports(bindings)) {
|
|
446
|
-
for (const el of bindings.elements) names.push(el.name.text);
|
|
447
|
-
} else if (bindings && ts.isNamespaceImport(bindings)) {
|
|
448
|
-
names.push(bindings.name.text);
|
|
449
|
-
}
|
|
450
|
-
return names;
|
|
451
|
-
}
|
|
452
379
|
|
|
453
380
|
// src/pieces.ts
|
|
454
381
|
function rawContent(ts, node, sf) {
|
|
@@ -491,9 +418,17 @@ function parseCached(ts, sf, tpl) {
|
|
|
491
418
|
return result;
|
|
492
419
|
}
|
|
493
420
|
var templates = /* @__PURE__ */ new WeakMap();
|
|
421
|
+
function mayHaveTemplate(text) {
|
|
422
|
+
return text.includes("html`") || text.includes("svg`");
|
|
423
|
+
}
|
|
424
|
+
var NONE = [];
|
|
494
425
|
function templatesIn(ts, sf) {
|
|
495
426
|
const hit = templates.get(sf);
|
|
496
427
|
if (hit) return hit;
|
|
428
|
+
if (!mayHaveTemplate(sf.text)) {
|
|
429
|
+
templates.set(sf, NONE);
|
|
430
|
+
return NONE;
|
|
431
|
+
}
|
|
497
432
|
const found = [];
|
|
498
433
|
const visit = (node) => {
|
|
499
434
|
if (ts.isTaggedTemplateExpression(node) && ts.isIdentifier(node.tag) && (node.tag.text === "html" || node.tag.text === "svg")) {
|
|
@@ -516,6 +451,64 @@ function templateAtCached(ts, sf, pos) {
|
|
|
516
451
|
return match;
|
|
517
452
|
}
|
|
518
453
|
|
|
454
|
+
// src/collect.ts
|
|
455
|
+
var UNUSED_CODES = /* @__PURE__ */ new Set([6133, 6196, 6198, 6199, 6205, 6138, 6192]);
|
|
456
|
+
function isUnusedCode(code) {
|
|
457
|
+
return UNUSED_CODES.has(code);
|
|
458
|
+
}
|
|
459
|
+
var tagCache = /* @__PURE__ */ new WeakMap();
|
|
460
|
+
function usedComponentTags(ts, sourceFile) {
|
|
461
|
+
const hit = tagCache.get(sourceFile);
|
|
462
|
+
if (hit) return hit;
|
|
463
|
+
const tags = /* @__PURE__ */ new Set();
|
|
464
|
+
for (const template of templatesIn(ts, sourceFile)) {
|
|
465
|
+
collectTags(parseCached(ts, sourceFile, template.template).root.children, tags);
|
|
466
|
+
}
|
|
467
|
+
tagCache.set(sourceFile, tags);
|
|
468
|
+
return tags;
|
|
469
|
+
}
|
|
470
|
+
function collectTags(nodes, out) {
|
|
471
|
+
for (const node of nodes) {
|
|
472
|
+
if (node.kind === "Component" && node.tag) out.add(node.tag.split(".")[0]);
|
|
473
|
+
if ("children" in node && node.children) collectTags(node.children, out);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function shouldSuppress(ts, sourceFile, diagnostic, used) {
|
|
477
|
+
if (diagnostic.start === void 0 || !isUnusedCode(diagnostic.code)) return false;
|
|
478
|
+
const spanText = sourceFile.text.substr(diagnostic.start, diagnostic.length ?? 0);
|
|
479
|
+
const decl = importDeclarationAt(ts, sourceFile, diagnostic.start);
|
|
480
|
+
if (decl) {
|
|
481
|
+
const names = importedNames(ts, decl);
|
|
482
|
+
if (names.includes(spanText)) return used.has(spanText);
|
|
483
|
+
return names.some((n) => used.has(n));
|
|
484
|
+
}
|
|
485
|
+
return used.has(spanText);
|
|
486
|
+
}
|
|
487
|
+
function importDeclarationAt(ts, sourceFile, pos) {
|
|
488
|
+
let found;
|
|
489
|
+
const visit = (node) => {
|
|
490
|
+
if (found) return;
|
|
491
|
+
if (ts.isImportDeclaration(node) && node.getStart(sourceFile) <= pos && node.getEnd() >= pos) {
|
|
492
|
+
found = node;
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
ts.forEachChild(node, visit);
|
|
496
|
+
};
|
|
497
|
+
visit(sourceFile);
|
|
498
|
+
return found;
|
|
499
|
+
}
|
|
500
|
+
function importedNames(ts, decl) {
|
|
501
|
+
const bindings = decl.importClause?.namedBindings;
|
|
502
|
+
const names = [];
|
|
503
|
+
if (decl.importClause?.name) names.push(decl.importClause.name.text);
|
|
504
|
+
if (bindings && ts.isNamedImports(bindings)) {
|
|
505
|
+
for (const el of bindings.elements) names.push(el.name.text);
|
|
506
|
+
} else if (bindings && ts.isNamespaceImport(bindings)) {
|
|
507
|
+
names.push(bindings.name.text);
|
|
508
|
+
}
|
|
509
|
+
return names;
|
|
510
|
+
}
|
|
511
|
+
|
|
519
512
|
// ../compiler/dist/resolve/builtins.mjs
|
|
520
513
|
var s = ["Show", "For", "Index", "Switch", "Match", "Portal", "Dynamic", "ErrorBoundary"];
|
|
521
514
|
var i = ["Suspense", "SuspenseList", "Await"];
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluixi/ts-plugin",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.12",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"registry": "https://registry.npmjs.org/",
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
10
|
-
"description": "
|
|
10
|
+
"description": "gives html`` templates the editor support JSX already gets; a component used only as a tag is no longer reported as unused",
|
|
11
11
|
"type": "commonjs",
|
|
12
12
|
"files": [
|
|
13
13
|
"dist",
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
],
|
|
17
17
|
"main": "./dist/index.js",
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@fluixi/template-parser": "0.1.0-alpha.
|
|
20
|
-
"@fluixi/compiler": "1.0.0-alpha.
|
|
19
|
+
"@fluixi/template-parser": "0.1.0-alpha.4",
|
|
20
|
+
"@fluixi/compiler": "1.0.0-alpha.78"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"esbuild": "^0.24.0",
|