@madenowhere/phaze-tsplugin 0.0.1
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 +47 -0
- package/dist/index.cjs +104 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +104 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# @madenowhere/phaze-tsplugin
|
|
2
|
+
|
|
3
|
+
TypeScript language-service plugin that teaches editors (VS Code, etc.) about Phaze JSX `use:directive` attributes — silences false-positive "unused binding" diagnostics when an import is consumed only via a `use:NAME` JSX attribute.
|
|
4
|
+
|
|
5
|
+
## The problem
|
|
6
|
+
|
|
7
|
+
phaze-compile rewrites this:
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
<input use:autofocus={true} />
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
into this (post-AST):
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
((__el) => (autofocus(__el, () => true), __el))(<input />)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The rewritten code references `autofocus` like any other function call. But TypeScript's source-level analysis only sees the `use:autofocus` JSX namespaced attribute — which it doesn't count as a variable reference. With `noUnusedLocals: true` (or VS Code's "show unused" hints), the editor flags the import as unused even though it's the canonical consumer for the directive.
|
|
20
|
+
|
|
21
|
+
## What this plugin does
|
|
22
|
+
|
|
23
|
+
Filters out `TS6133` / `TS6192` / `TS6196` diagnostics whose unused name matches a binding consumed via a `use:NAME` JSX attribute somewhere in the same file. Other unused-binding warnings still fire — only the directive false-positive is suppressed.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
pnpm add -D @madenowhere/phaze-tsplugin
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then add to your `tsconfig.json`:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"compilerOptions": {
|
|
36
|
+
"plugins": [{ "name": "@madenowhere/phaze-tsplugin" }]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
VS Code picks it up automatically via its bundled TypeScript server. Restart the TS server (`Cmd+Shift+P → "TypeScript: Restart TS Server"`) after first install, then any time you change tsconfig plugins.
|
|
42
|
+
|
|
43
|
+
## Limitations
|
|
44
|
+
|
|
45
|
+
- **IDE only.** TypeScript language-service plugins run inside the language server (editor flow). They do **not** affect `tsc` CLI behavior. If your CI runs `tsc --noEmit` with `noUnusedLocals: true`, those checks will still flag directive imports. Workarounds: scope `noUnusedLocals` to source files only, or use eslint-based unused-imports checking with a Phaze-aware rule.
|
|
46
|
+
- **Per-file scope.** The plugin scans only the source file the diagnostic is emitted in. If a directive import is in `a.ts` and the `use:NAME` reference is in `b.ts`, the import in `a.ts` would still appear unused — which is correct (re-exporting through an unused intermediate file isn't a Phaze idiom).
|
|
47
|
+
- **Name-based matching.** The plugin matches on the local binding name as it appears in TS's diagnostic message. Aliasing imports (`import { autofocus as _af }`) means the JSX has to use `use:_af` to match — TS still tracks the local name, so this works as expected.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @madenowhere/phaze-tsplugin
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
// TypeScript language-service plugin that teaches the IDE about Phaze
|
|
5
|
+
// JSX `use:directive` attributes.
|
|
6
|
+
//
|
|
7
|
+
// Problem: phaze-compile rewrites `<el use:foo={value}>` to
|
|
8
|
+
// ((__el) => (foo(__el, () => value), __el))(<el />)
|
|
9
|
+
// post-AST. The rewritten code references `foo` as a regular call, but
|
|
10
|
+
// TypeScript's source-level analysis only sees the `use:foo` JSX
|
|
11
|
+
// namespace attribute — which TS doesn't count as a variable
|
|
12
|
+
// reference. Result: when a file imports a directive only to use it
|
|
13
|
+
// via `use:`, TypeScript flags the import as TS6133 ("'foo' is
|
|
14
|
+
// declared but its value is never read") and the editor shows it
|
|
15
|
+
// fading / red.
|
|
16
|
+
//
|
|
17
|
+
// Fix: this plugin wraps the language service's `getSemanticDiagnostics`
|
|
18
|
+
// and filters out TS6133 / TS6192 diagnostics whose unused name
|
|
19
|
+
// matches a binding that the file consumes via a `use:NAME` JSX
|
|
20
|
+
// attribute. Other unused-binding warnings still surface — only the
|
|
21
|
+
// false-positive directive case is suppressed.
|
|
22
|
+
//
|
|
23
|
+
// Wiring:
|
|
24
|
+
// tsconfig.json:
|
|
25
|
+
// {
|
|
26
|
+
// "compilerOptions": {
|
|
27
|
+
// "plugins": [{ "name": "@madenowhere/phaze-tsplugin" }]
|
|
28
|
+
// }
|
|
29
|
+
// }
|
|
30
|
+
//
|
|
31
|
+
// VS Code picks up the plugin automatically via its bundled TypeScript
|
|
32
|
+
// language server. No restart required for the plugin itself when
|
|
33
|
+
// rebuilt — VS Code reloads when its TS server restarts.
|
|
34
|
+
// TS error codes for "declared but never used":
|
|
35
|
+
// 6133 — local declaration unused
|
|
36
|
+
// 6192 — all imports unused
|
|
37
|
+
// 6196 — declared but only used in type position (rarer)
|
|
38
|
+
const UNUSED_DIAGNOSTIC_CODES = new Set([6133, 6192, 6196]);
|
|
39
|
+
const init = (arg) => ({
|
|
40
|
+
create(info) {
|
|
41
|
+
const ts = arg.typescript;
|
|
42
|
+
const ls = info.languageService;
|
|
43
|
+
// Walk a SourceFile and collect every local name referenced via a
|
|
44
|
+
// `use:NAME` JSX namespaced attribute. The set is computed per
|
|
45
|
+
// semantic-diagnostics call — small files, fast walk, no caching
|
|
46
|
+
// concerns.
|
|
47
|
+
const collectUseDirectiveNames = (sf) => {
|
|
48
|
+
const names = new Set();
|
|
49
|
+
const visit = (node) => {
|
|
50
|
+
if (ts.isJsxAttribute(node) &&
|
|
51
|
+
node.name &&
|
|
52
|
+
ts.isJsxNamespacedName(node.name) &&
|
|
53
|
+
node.name.namespace.text === 'use') {
|
|
54
|
+
names.add(node.name.name.text);
|
|
55
|
+
}
|
|
56
|
+
ts.forEachChild(node, visit);
|
|
57
|
+
};
|
|
58
|
+
visit(sf);
|
|
59
|
+
return names;
|
|
60
|
+
};
|
|
61
|
+
const extractUnusedName = (d) => {
|
|
62
|
+
// The diagnostic's primary text contains the unused identifier's
|
|
63
|
+
// name in single quotes. Pull it out via regex — TS doesn't expose
|
|
64
|
+
// the binding directly on the Diagnostic object, but the message
|
|
65
|
+
// shape is stable across TS versions:
|
|
66
|
+
// "'foo' is declared but its value is never read."
|
|
67
|
+
// "'foo' is declared but never used."
|
|
68
|
+
const text = typeof d.messageText === 'string'
|
|
69
|
+
? d.messageText
|
|
70
|
+
: d.messageText.messageText;
|
|
71
|
+
const m = text.match(/^'([^']+)' is declared/);
|
|
72
|
+
return m?.[1];
|
|
73
|
+
};
|
|
74
|
+
return new Proxy(ls, {
|
|
75
|
+
get(target, key) {
|
|
76
|
+
if (key !== 'getSemanticDiagnostics') {
|
|
77
|
+
return Reflect.get(target, key);
|
|
78
|
+
}
|
|
79
|
+
return (fileName) => {
|
|
80
|
+
const diags = target.getSemanticDiagnostics(fileName);
|
|
81
|
+
if (diags.length === 0)
|
|
82
|
+
return diags;
|
|
83
|
+
if (!diags.some((d) => UNUSED_DIAGNOSTIC_CODES.has(d.code)))
|
|
84
|
+
return diags;
|
|
85
|
+
const sf = target.getProgram()?.getSourceFile(fileName);
|
|
86
|
+
if (!sf)
|
|
87
|
+
return diags;
|
|
88
|
+
const useNames = collectUseDirectiveNames(sf);
|
|
89
|
+
if (useNames.size === 0)
|
|
90
|
+
return diags;
|
|
91
|
+
return diags.filter((d) => {
|
|
92
|
+
if (!UNUSED_DIAGNOSTIC_CODES.has(d.code))
|
|
93
|
+
return true;
|
|
94
|
+
const name = extractUnusedName(d);
|
|
95
|
+
// Suppress only when the unused binding name matches a
|
|
96
|
+
// directive consumed via use:NAME in this file.
|
|
97
|
+
return !name || !useNames.has(name);
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
module.exports = init;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type tsTypes from 'typescript/lib/tsserverlibrary';
|
|
2
|
+
interface PluginCreateInfo {
|
|
3
|
+
languageService: tsTypes.LanguageService;
|
|
4
|
+
serverHost: tsTypes.server.ServerHost;
|
|
5
|
+
project: tsTypes.server.Project;
|
|
6
|
+
config: {
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
interface PluginInitArg {
|
|
11
|
+
typescript: typeof tsTypes;
|
|
12
|
+
}
|
|
13
|
+
interface PluginModuleFactory {
|
|
14
|
+
(arg: PluginInitArg): {
|
|
15
|
+
create(info: PluginCreateInfo): tsTypes.LanguageService;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
declare const init: PluginModuleFactory;
|
|
19
|
+
export = init;
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,OAAO,MAAM,gCAAgC,CAAA;AAEzD,UAAU,gBAAgB;IACxB,eAAe,EAAE,OAAO,CAAC,eAAe,CAAA;IACxC,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,CAAA;IACrC,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO,CAAA;IAC/B,MAAM,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAA;CACnC;AAED,UAAU,aAAa;IACrB,UAAU,EAAE,OAAO,OAAO,CAAA;CAC3B;AAED,UAAU,mBAAmB;IAC3B,CAAC,GAAG,EAAE,aAAa,GAAG;QACpB,MAAM,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAA;KACxD,CAAA;CACF;AAQD,QAAA,MAAM,IAAI,EAAE,mBAgEV,CAAA;AAEF,SAAS,IAAI,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @madenowhere/phaze-tsplugin
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
// TypeScript language-service plugin that teaches the IDE about Phaze
|
|
5
|
+
// JSX `use:directive` attributes.
|
|
6
|
+
//
|
|
7
|
+
// Problem: phaze-compile rewrites `<el use:foo={value}>` to
|
|
8
|
+
// ((__el) => (foo(__el, () => value), __el))(<el />)
|
|
9
|
+
// post-AST. The rewritten code references `foo` as a regular call, but
|
|
10
|
+
// TypeScript's source-level analysis only sees the `use:foo` JSX
|
|
11
|
+
// namespace attribute — which TS doesn't count as a variable
|
|
12
|
+
// reference. Result: when a file imports a directive only to use it
|
|
13
|
+
// via `use:`, TypeScript flags the import as TS6133 ("'foo' is
|
|
14
|
+
// declared but its value is never read") and the editor shows it
|
|
15
|
+
// fading / red.
|
|
16
|
+
//
|
|
17
|
+
// Fix: this plugin wraps the language service's `getSemanticDiagnostics`
|
|
18
|
+
// and filters out TS6133 / TS6192 diagnostics whose unused name
|
|
19
|
+
// matches a binding that the file consumes via a `use:NAME` JSX
|
|
20
|
+
// attribute. Other unused-binding warnings still surface — only the
|
|
21
|
+
// false-positive directive case is suppressed.
|
|
22
|
+
//
|
|
23
|
+
// Wiring:
|
|
24
|
+
// tsconfig.json:
|
|
25
|
+
// {
|
|
26
|
+
// "compilerOptions": {
|
|
27
|
+
// "plugins": [{ "name": "@madenowhere/phaze-tsplugin" }]
|
|
28
|
+
// }
|
|
29
|
+
// }
|
|
30
|
+
//
|
|
31
|
+
// VS Code picks up the plugin automatically via its bundled TypeScript
|
|
32
|
+
// language server. No restart required for the plugin itself when
|
|
33
|
+
// rebuilt — VS Code reloads when its TS server restarts.
|
|
34
|
+
// TS error codes for "declared but never used":
|
|
35
|
+
// 6133 — local declaration unused
|
|
36
|
+
// 6192 — all imports unused
|
|
37
|
+
// 6196 — declared but only used in type position (rarer)
|
|
38
|
+
const UNUSED_DIAGNOSTIC_CODES = new Set([6133, 6192, 6196]);
|
|
39
|
+
const init = (arg) => ({
|
|
40
|
+
create(info) {
|
|
41
|
+
const ts = arg.typescript;
|
|
42
|
+
const ls = info.languageService;
|
|
43
|
+
// Walk a SourceFile and collect every local name referenced via a
|
|
44
|
+
// `use:NAME` JSX namespaced attribute. The set is computed per
|
|
45
|
+
// semantic-diagnostics call — small files, fast walk, no caching
|
|
46
|
+
// concerns.
|
|
47
|
+
const collectUseDirectiveNames = (sf) => {
|
|
48
|
+
const names = new Set();
|
|
49
|
+
const visit = (node) => {
|
|
50
|
+
if (ts.isJsxAttribute(node) &&
|
|
51
|
+
node.name &&
|
|
52
|
+
ts.isJsxNamespacedName(node.name) &&
|
|
53
|
+
node.name.namespace.text === 'use') {
|
|
54
|
+
names.add(node.name.name.text);
|
|
55
|
+
}
|
|
56
|
+
ts.forEachChild(node, visit);
|
|
57
|
+
};
|
|
58
|
+
visit(sf);
|
|
59
|
+
return names;
|
|
60
|
+
};
|
|
61
|
+
const extractUnusedName = (d) => {
|
|
62
|
+
// The diagnostic's primary text contains the unused identifier's
|
|
63
|
+
// name in single quotes. Pull it out via regex — TS doesn't expose
|
|
64
|
+
// the binding directly on the Diagnostic object, but the message
|
|
65
|
+
// shape is stable across TS versions:
|
|
66
|
+
// "'foo' is declared but its value is never read."
|
|
67
|
+
// "'foo' is declared but never used."
|
|
68
|
+
const text = typeof d.messageText === 'string'
|
|
69
|
+
? d.messageText
|
|
70
|
+
: d.messageText.messageText;
|
|
71
|
+
const m = text.match(/^'([^']+)' is declared/);
|
|
72
|
+
return m?.[1];
|
|
73
|
+
};
|
|
74
|
+
return new Proxy(ls, {
|
|
75
|
+
get(target, key) {
|
|
76
|
+
if (key !== 'getSemanticDiagnostics') {
|
|
77
|
+
return Reflect.get(target, key);
|
|
78
|
+
}
|
|
79
|
+
return (fileName) => {
|
|
80
|
+
const diags = target.getSemanticDiagnostics(fileName);
|
|
81
|
+
if (diags.length === 0)
|
|
82
|
+
return diags;
|
|
83
|
+
if (!diags.some((d) => UNUSED_DIAGNOSTIC_CODES.has(d.code)))
|
|
84
|
+
return diags;
|
|
85
|
+
const sf = target.getProgram()?.getSourceFile(fileName);
|
|
86
|
+
if (!sf)
|
|
87
|
+
return diags;
|
|
88
|
+
const useNames = collectUseDirectiveNames(sf);
|
|
89
|
+
if (useNames.size === 0)
|
|
90
|
+
return diags;
|
|
91
|
+
return diags.filter((d) => {
|
|
92
|
+
if (!UNUSED_DIAGNOSTIC_CODES.has(d.code))
|
|
93
|
+
return true;
|
|
94
|
+
const name = extractUnusedName(d);
|
|
95
|
+
// Suppress only when the unused binding name matches a
|
|
96
|
+
// directive consumed via use:NAME in this file.
|
|
97
|
+
return !name || !useNames.has(name);
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
module.exports = init;
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@madenowhere/phaze-tsplugin",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "TypeScript language-service plugin that teaches the IDE about Phaze JSX `use:` directives — suppresses TS6133 'unused binding' diagnostics when the binding is consumed by a `use:X` JSX attribute.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "madenowhere",
|
|
7
|
+
"homepage": "https://phaze.build",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/madenowhere/phaze.git",
|
|
11
|
+
"directory": "packages/phaze-tsplugin"
|
|
12
|
+
},
|
|
13
|
+
"bugs": "https://github.com/madenowhere/phaze/issues",
|
|
14
|
+
"main": "dist/index.cjs",
|
|
15
|
+
"types": "dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js",
|
|
20
|
+
"require": "./dist/index.cjs"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc -p tsconfig.json && node -e \"const fs=require('fs');const c=fs.readFileSync('dist/index.js','utf8').replace(/export\\s*=\\s*(\\w+);?/,'module.exports=$1;');fs.writeFileSync('dist/index.cjs',c);\"",
|
|
29
|
+
"dev": "tsc -p tsconfig.json --watch"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"typescript": ">=5.0.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependenciesMeta": {
|
|
35
|
+
"typescript": { "optional": false }
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"typescript": "^6.0.0"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"phaze",
|
|
42
|
+
"typescript",
|
|
43
|
+
"language-service",
|
|
44
|
+
"plugin",
|
|
45
|
+
"jsx",
|
|
46
|
+
"directives"
|
|
47
|
+
]
|
|
48
|
+
}
|