@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 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;
@@ -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
+ }