@memoized-dom/language-service 0.0.2

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,33 @@
1
+ # @memoized-dom/language-service
2
+
3
+ TypeScript editor diagnostics and code fixes for memoized-dom. Compiler errors
4
+ come from `@memoized-dom/compiler`'s structured graph diagnostic API, so the
5
+ editor, Vite overlay, and direct compiler report the same restrictions.
6
+
7
+ Add the plugin to the application `tsconfig.json`:
8
+
9
+ ```json
10
+ {
11
+ "compilerOptions": {
12
+ "plugins": [
13
+ {
14
+ "name": "@memoized-dom/language-service"
15
+ }
16
+ ]
17
+ }
18
+ }
19
+ ```
20
+
21
+ TypeScript language-service plugins run in editors powered by tsserver. They
22
+ do not add diagnostics to standalone `tsc`; the analysis core is kept separate
23
+ so a future CLI can report the same diagnostics in CI.
24
+
25
+ The first diagnostic suggests `const` for initialized `let` bindings that are
26
+ never reassigned. Its message explains that memoized-dom reactivity follows
27
+ reads and writes rather than declaration keywords; `const` arrays and objects
28
+ can still mutate their contents. The editor fix changes only the declaration
29
+ keyword.
30
+
31
+ Set `"preferConst": false` on the plugin entry to disable this diagnostic.
32
+ Set `"compilerDiagnostics": false` only when compiler errors should be hidden
33
+ from tsserver.
@@ -0,0 +1,8 @@
1
+ import { compilerDiagnosticCode, compilerDiagnosticSource as diagnosticSource } from '@memoized-dom/compiler';
2
+ export { compilerDiagnosticCode, diagnosticSource };
3
+ export declare const diagnosticCodes: {
4
+ readonly compiler: 98000;
5
+ readonly preferConst: 98001;
6
+ };
7
+ export declare function preferConstMessage(names: readonly string[]): string;
8
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/diagnostics/catalog.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,wBAAwB,IAAI,gBAAgB,EAC7C,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,CAAC;AAEpD,eAAO,MAAM,eAAe;;;CAGlB,CAAC;AAEX,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAUnE"}
@@ -0,0 +1,6 @@
1
+ import type * as ts from 'typescript';
2
+ type TypeScript = typeof ts;
3
+ export declare function collectPreferConstDiagnostics(typescript: TypeScript, program: ts.Program, sourceFile: ts.SourceFile): ts.DiagnosticWithLocation[];
4
+ export declare function createPreferConstCodeFix(typescript: TypeScript, sourceFile: ts.SourceFile, diagnosticStart: number): ts.CodeFixAction | null;
5
+ export {};
6
+ //# sourceMappingURL=prefer-const.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-const.d.ts","sourceRoot":"","sources":["../../src/diagnostics/prefer-const.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AAOtC,KAAK,UAAU,GAAG,OAAO,EAAE,CAAC;AAQ5B,wBAAgB,6BAA6B,CAC3C,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,EAAE,CAAC,OAAO,EACnB,UAAU,EAAE,EAAE,CAAC,UAAU,GACxB,EAAE,CAAC,sBAAsB,EAAE,CA6B7B;AAED,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,UAAU,EACtB,UAAU,EAAE,EAAE,CAAC,UAAU,EACzB,eAAe,EAAE,MAAM,GACtB,EAAE,CAAC,aAAa,GAAG,IAAI,CAyBzB"}
package/dist/index.cjs ADDED
@@ -0,0 +1,353 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ default: () => initialize
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/plugin.ts
28
+ var import_compiler2 = require("@memoized-dom/compiler");
29
+
30
+ // src/diagnostics/catalog.ts
31
+ var import_compiler = require("@memoized-dom/compiler");
32
+ var diagnosticCodes = {
33
+ compiler: import_compiler.compilerDiagnosticCode,
34
+ preferConst: 98001
35
+ };
36
+ function preferConstMessage(names) {
37
+ const binding = names.length === 1 ? `'${names[0]}' is` : `Bindings ${names.map((name) => `'${name}'`).join(", ")} are`;
38
+ return `${binding} never reassigned. Use const; memoized-dom reactivity follows state reads and assignments, not let declarations. const arrays and objects may still mutate their contents.`;
39
+ }
40
+
41
+ // src/diagnostics/prefer-const.ts
42
+ function collectPreferConstDiagnostics(typescript, program, sourceFile) {
43
+ if (sourceFile.isDeclarationFile) return [];
44
+ const checker = program.getTypeChecker();
45
+ const candidates = collectCandidates(typescript, checker, sourceFile);
46
+ if (candidates.length === 0) return [];
47
+ const candidateSymbols = new Set(
48
+ candidates.flatMap((candidate) => candidate.symbols)
49
+ );
50
+ const reassigned = collectReassignedSymbols(
51
+ typescript,
52
+ checker,
53
+ sourceFile,
54
+ candidateSymbols
55
+ );
56
+ return candidates.filter(
57
+ (candidate) => candidate.symbols.every((symbol) => !reassigned.has(symbol))
58
+ ).map((candidate) => ({
59
+ file: sourceFile,
60
+ start: candidate.declarationList.getStart(sourceFile),
61
+ length: "let".length,
62
+ category: typescript.DiagnosticCategory.Suggestion,
63
+ code: diagnosticCodes.preferConst,
64
+ source: import_compiler.compilerDiagnosticSource,
65
+ messageText: preferConstMessage(candidate.names)
66
+ }));
67
+ }
68
+ function createPreferConstCodeFix(typescript, sourceFile, diagnosticStart2) {
69
+ const declarationList = findDeclarationList(
70
+ typescript,
71
+ sourceFile,
72
+ diagnosticStart2
73
+ );
74
+ if (declarationList === null) return null;
75
+ return {
76
+ fixName: "memoizedDomPreferConst",
77
+ description: "Change let to const",
78
+ changes: [
79
+ {
80
+ fileName: sourceFile.fileName,
81
+ textChanges: [
82
+ {
83
+ span: {
84
+ start: declarationList.getStart(sourceFile),
85
+ length: "let".length
86
+ },
87
+ newText: "const"
88
+ }
89
+ ]
90
+ }
91
+ ]
92
+ };
93
+ }
94
+ function collectCandidates(typescript, checker, sourceFile) {
95
+ const candidates = [];
96
+ const visit = (node) => {
97
+ if (typescript.isVariableStatement(node) && (node.declarationList.flags & typescript.NodeFlags.Let) !== 0 && node.declarationList.declarations.every(
98
+ (declaration) => declaration.initializer !== void 0
99
+ )) {
100
+ const identifiers = node.declarationList.declarations.flatMap(
101
+ (declaration) => bindingIdentifiers(typescript, declaration.name)
102
+ );
103
+ const bindings = identifiers.map((identifier) => ({
104
+ name: identifier.text,
105
+ symbol: checker.getSymbolAtLocation(identifier)
106
+ })).filter(
107
+ (binding) => binding.symbol !== void 0
108
+ );
109
+ if (bindings.length === identifiers.length && bindings.length > 0) {
110
+ candidates.push({
111
+ declarationList: node.declarationList,
112
+ names: bindings.map((binding) => binding.name),
113
+ symbols: bindings.map((binding) => binding.symbol)
114
+ });
115
+ }
116
+ }
117
+ typescript.forEachChild(node, visit);
118
+ };
119
+ visit(sourceFile);
120
+ return candidates;
121
+ }
122
+ function collectReassignedSymbols(typescript, checker, sourceFile, candidates) {
123
+ const reassigned = /* @__PURE__ */ new Set();
124
+ const markTarget = (node) => {
125
+ const target = unwrapTarget(typescript, node);
126
+ if (typescript.isIdentifier(target)) {
127
+ const symbol = checker.getSymbolAtLocation(target);
128
+ if (symbol !== void 0 && candidates.has(symbol)) {
129
+ reassigned.add(symbol);
130
+ }
131
+ return;
132
+ }
133
+ if (typescript.isArrayLiteralExpression(target)) {
134
+ for (const element of target.elements) {
135
+ if (!typescript.isOmittedExpression(element)) markTarget(element);
136
+ }
137
+ return;
138
+ }
139
+ if (typescript.isObjectLiteralExpression(target)) {
140
+ for (const property of target.properties) {
141
+ if (typescript.isShorthandPropertyAssignment(property)) {
142
+ markTarget(property.name);
143
+ } else if (typescript.isPropertyAssignment(property)) {
144
+ markTarget(property.initializer);
145
+ } else if (typescript.isSpreadAssignment(property)) {
146
+ markTarget(property.expression);
147
+ }
148
+ }
149
+ }
150
+ };
151
+ const visit = (node) => {
152
+ if (typescript.isBinaryExpression(node) && node.operatorToken.kind >= typescript.SyntaxKind.FirstAssignment && node.operatorToken.kind <= typescript.SyntaxKind.LastAssignment) {
153
+ markTarget(node.left);
154
+ } else if ((typescript.isPrefixUnaryExpression(node) || typescript.isPostfixUnaryExpression(node)) && (node.operator === typescript.SyntaxKind.PlusPlusToken || node.operator === typescript.SyntaxKind.MinusMinusToken)) {
155
+ markTarget(node.operand);
156
+ } else if ((typescript.isForInStatement(node) || typescript.isForOfStatement(node)) && !typescript.isVariableDeclarationList(node.initializer)) {
157
+ markTarget(node.initializer);
158
+ }
159
+ typescript.forEachChild(node, visit);
160
+ };
161
+ visit(sourceFile);
162
+ return reassigned;
163
+ }
164
+ function bindingIdentifiers(typescript, name) {
165
+ if (typescript.isIdentifier(name)) return [name];
166
+ return name.elements.flatMap(
167
+ (element) => typescript.isOmittedExpression(element) ? [] : bindingIdentifiers(typescript, element.name)
168
+ );
169
+ }
170
+ function unwrapTarget(typescript, node) {
171
+ while (typescript.isParenthesizedExpression(node) || typescript.isAsExpression(node) || typescript.isTypeAssertionExpression(node) || typescript.isNonNullExpression(node) || typescript.isSatisfiesExpression(node)) {
172
+ node = node.expression;
173
+ }
174
+ return node;
175
+ }
176
+ function findDeclarationList(typescript, sourceFile, start) {
177
+ let found = null;
178
+ const visit = (node) => {
179
+ if (found !== null || start < node.getFullStart() || start > node.end) {
180
+ return;
181
+ }
182
+ if (typescript.isVariableDeclarationList(node) && (node.flags & typescript.NodeFlags.Let) !== 0 && node.getStart(sourceFile) === start) {
183
+ found = node;
184
+ return;
185
+ }
186
+ typescript.forEachChild(node, visit);
187
+ };
188
+ visit(sourceFile);
189
+ return found;
190
+ }
191
+
192
+ // src/plugin.ts
193
+ function createLanguageService(typescript, info) {
194
+ const original = info.languageService;
195
+ const proxy = bindLanguageService(original);
196
+ const config = info.config;
197
+ const preferConstCache = /* @__PURE__ */ new WeakMap();
198
+ const compilerCache = /* @__PURE__ */ new WeakMap();
199
+ proxy.getSemanticDiagnostics = (fileName) => {
200
+ const diagnostics = original.getSemanticDiagnostics(fileName);
201
+ const program = original.getProgram();
202
+ const sourceFile = program?.getSourceFile(fileName);
203
+ if (program === void 0 || sourceFile === void 0) {
204
+ return diagnostics;
205
+ }
206
+ const customDiagnostics = [];
207
+ if (config.preferConst !== false) {
208
+ let preferConst = preferConstCache.get(sourceFile);
209
+ if (preferConst === void 0) {
210
+ preferConst = collectPreferConstDiagnostics(
211
+ typescript,
212
+ program,
213
+ sourceFile
214
+ );
215
+ preferConstCache.set(sourceFile, preferConst);
216
+ }
217
+ customDiagnostics.push(...preferConst);
218
+ }
219
+ if (config.compilerDiagnostics !== false) {
220
+ let byFile = compilerCache.get(program);
221
+ if (byFile === void 0) {
222
+ byFile = collectCompilerDiagnostics(
223
+ typescript,
224
+ program
225
+ );
226
+ compilerCache.set(program, byFile);
227
+ }
228
+ customDiagnostics.push(...byFile.get(normalizePath(fileName)) ?? []);
229
+ }
230
+ return [...diagnostics, ...customDiagnostics];
231
+ };
232
+ proxy.getCodeFixesAtPosition = (fileName, start, end, errorCodes, formatOptions, preferences) => {
233
+ const fixes = original.getCodeFixesAtPosition(
234
+ fileName,
235
+ start,
236
+ end,
237
+ errorCodes,
238
+ formatOptions,
239
+ preferences
240
+ );
241
+ if (config.preferConst === false || !errorCodes.includes(diagnosticCodes.preferConst)) {
242
+ return fixes;
243
+ }
244
+ const sourceFile = original.getProgram()?.getSourceFile(fileName);
245
+ if (sourceFile === void 0) return fixes;
246
+ const fix = createPreferConstCodeFix(
247
+ typescript,
248
+ sourceFile,
249
+ start
250
+ );
251
+ return fix === null ? fixes : [...fixes, fix];
252
+ };
253
+ return proxy;
254
+ }
255
+ function collectCompilerDiagnostics(typescript, program) {
256
+ const sourceFiles = program.getSourceFiles().filter(
257
+ (sourceFile) => !sourceFile.isDeclarationFile && !normalizePath(sourceFile.fileName).includes("/node_modules/") && /\.[jt]sx?$/.test(sourceFile.fileName)
258
+ );
259
+ const modules = Object.fromEntries(
260
+ sourceFiles.map((sourceFile) => [
261
+ normalizePath(sourceFile.fileName),
262
+ sourceFile.text
263
+ ])
264
+ );
265
+ const diagnostics = (0, import_compiler2.diagnoseModules)(modules, {
266
+ resolveImport(specifier, importer) {
267
+ const resolution = typescript.resolveModuleName(
268
+ specifier,
269
+ importer,
270
+ program.getCompilerOptions(),
271
+ typescript.sys
272
+ ).resolvedModule;
273
+ if (resolution === void 0) return void 0;
274
+ const resolved = normalizePath(resolution.resolvedFileName);
275
+ return Object.hasOwn(modules, resolved) ? resolved : void 0;
276
+ }
277
+ });
278
+ const byFile = /* @__PURE__ */ new Map();
279
+ for (const diagnostic of diagnostics) {
280
+ const moduleId = diagnostic.moduleId;
281
+ const sourceFile = moduleId === void 0 ? sourceFiles[0] : sourceFiles.find(
282
+ (candidate) => normalizePath(candidate.fileName) === moduleId
283
+ );
284
+ if (sourceFile === void 0) continue;
285
+ const converted = compilerDiagnostic(
286
+ typescript,
287
+ sourceFile,
288
+ diagnostic
289
+ );
290
+ const key = normalizePath(sourceFile.fileName);
291
+ const list = byFile.get(key) ?? [];
292
+ list.push(converted);
293
+ byFile.set(key, list);
294
+ }
295
+ return byFile;
296
+ }
297
+ function compilerDiagnostic(typescript, sourceFile, diagnostic) {
298
+ const start = diagnosticStart(sourceFile, diagnostic);
299
+ return {
300
+ file: sourceFile,
301
+ start,
302
+ length: Math.min(1, Math.max(0, sourceFile.text.length - start)),
303
+ category: typescript.DiagnosticCategory.Error,
304
+ code: diagnosticCodes.compiler,
305
+ source: import_compiler.compilerDiagnosticSource,
306
+ messageText: diagnostic.message
307
+ };
308
+ }
309
+ function diagnosticStart(sourceFile, diagnostic) {
310
+ if (diagnostic.line !== void 0 && diagnostic.column !== void 0) {
311
+ const line = Math.max(0, diagnostic.line - 1);
312
+ if (line < sourceFile.getLineStarts().length) {
313
+ return sourceFile.getPositionOfLineAndCharacter(line, diagnostic.column);
314
+ }
315
+ }
316
+ const binding = diagnostic.message.match(/'([^']+)'/)?.[1];
317
+ if (binding !== void 0) {
318
+ const position = sourceFile.text.indexOf(binding);
319
+ if (position !== -1) return position;
320
+ }
321
+ return 0;
322
+ }
323
+ function normalizePath(fileName) {
324
+ return fileName.replaceAll("\\", "/");
325
+ }
326
+ function bindLanguageService(languageService) {
327
+ const proxy = /* @__PURE__ */ Object.create(null);
328
+ const writable = proxy;
329
+ for (const key of Object.keys(languageService)) {
330
+ const member = languageService[key];
331
+ if (typeof member === "function") {
332
+ writable[key] = (...args) => member.apply(
333
+ languageService,
334
+ args
335
+ );
336
+ } else {
337
+ writable[key] = member;
338
+ }
339
+ }
340
+ return proxy;
341
+ }
342
+
343
+ // src/index.ts
344
+ function initialize({
345
+ typescript
346
+ }) {
347
+ return {
348
+ create(info) {
349
+ return createLanguageService(typescript, info);
350
+ }
351
+ };
352
+ }
353
+ module.exports = module.exports.default;
@@ -0,0 +1,11 @@
1
+ import type * as ts from 'typescript';
2
+ type TypeScript = typeof ts;
3
+ /**
4
+ * tsserver requires the package's CommonJS value to be this factory function.
5
+ * The package build footer unwraps this default export after bundling.
6
+ */
7
+ export default function initialize({ typescript, }: {
8
+ typescript: TypeScript;
9
+ }): ts.server.PluginModule;
10
+ export {};
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AAGtC,KAAK,UAAU,GAAG,OAAO,EAAE,CAAC;AAE5B;;;GAGG;AACH,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,EACjC,UAAU,GACX,EAAE;IACD,UAAU,EAAE,UAAU,CAAC;CACxB,GAAG,EAAE,CAAC,MAAM,CAAC,YAAY,CAMzB"}
@@ -0,0 +1,5 @@
1
+ import type * as ts from 'typescript';
2
+ type TypeScript = typeof ts;
3
+ export declare function createLanguageService(typescript: TypeScript, info: ts.server.PluginCreateInfo): ts.LanguageService;
4
+ export {};
5
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AActC,KAAK,UAAU,GAAG,OAAO,EAAE,CAAC;AAO5B,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,UAAU,EACtB,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,gBAAgB,GAC/B,EAAE,CAAC,eAAe,CAgFpB"}
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@memoized-dom/language-service",
3
+ "version": "0.0.2",
4
+ "description": "TypeScript language-service diagnostics and code fixes for memoized-dom",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "engines": {
8
+ "node": ">=24.11.0"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "main": "./dist/index.cjs",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "require": "./dist/index.cjs",
20
+ "default": "./dist/index.cjs"
21
+ }
22
+ },
23
+ "scripts": {
24
+ "build": "bun run ./scripts/clean.ts && esbuild ./src/index.ts --bundle --outfile=./dist/index.cjs --platform=node --format=cjs --target=node24 --packages=external --footer:js=\"module.exports = module.exports.default;\" && tsc -p tsconfig.build.json",
25
+ "test": "vitest run --config vitest.config.ts"
26
+ },
27
+ "dependencies": {
28
+ "@memoized-dom/compiler": "^0.0.6"
29
+ },
30
+ "peerDependencies": {
31
+ "typescript": ">=5.7 <7"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ }
36
+ }