tsifdef 1.1.8

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.
Files changed (71) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/CHANGELOG.zh-CN.md +22 -0
  3. package/CONTRIBUTING.md +52 -0
  4. package/INTEGRATION.md +166 -0
  5. package/INTEGRATION.zh-CN.md +159 -0
  6. package/LICENSE +210 -0
  7. package/README.md +150 -0
  8. package/README.zh-CN.md +141 -0
  9. package/SECURITY.md +15 -0
  10. package/assets/icon-mono.svg +15 -0
  11. package/assets/icon.png +0 -0
  12. package/assets/icon.svg +16 -0
  13. package/dist/cli/build.d.ts +70 -0
  14. package/dist/cli/build.js +298 -0
  15. package/dist/cli/config.d.ts +30 -0
  16. package/dist/cli/config.js +135 -0
  17. package/dist/cli/diagnostics.d.ts +3 -0
  18. package/dist/cli/diagnostics.js +31 -0
  19. package/dist/cli/index.d.ts +6 -0
  20. package/dist/cli/index.js +35 -0
  21. package/dist/cli/main.d.ts +9 -0
  22. package/dist/cli/main.js +165 -0
  23. package/dist/cli/precompile.d.ts +38 -0
  24. package/dist/cli/precompile.js +192 -0
  25. package/dist/cli/source-files.d.ts +10 -0
  26. package/dist/cli/source-files.js +75 -0
  27. package/dist/cli/watch.d.ts +34 -0
  28. package/dist/cli/watch.js +183 -0
  29. package/dist/core/conditional.d.ts +18 -0
  30. package/dist/core/conditional.js +126 -0
  31. package/dist/core/expression.d.ts +44 -0
  32. package/dist/core/expression.js +224 -0
  33. package/dist/core/index.d.ts +7 -0
  34. package/dist/core/index.js +37 -0
  35. package/dist/core/projection.d.ts +15 -0
  36. package/dist/core/projection.js +99 -0
  37. package/dist/core/scanner.d.ts +33 -0
  38. package/dist/core/scanner.js +344 -0
  39. package/dist/eslint/package.d.ts +10 -0
  40. package/dist/eslint/package.js +55 -0
  41. package/dist/eslint/parser.d.ts +35 -0
  42. package/dist/eslint/parser.js +59 -0
  43. package/dist/eslint/plugin.d.ts +78 -0
  44. package/dist/eslint/plugin.js +229 -0
  45. package/dist/eslint/projection.d.ts +11 -0
  46. package/dist/eslint/projection.js +116 -0
  47. package/dist/tsserver/host-projection.d.ts +45 -0
  48. package/dist/tsserver/host-projection.js +160 -0
  49. package/dist/tsserver/index.d.ts +3 -0
  50. package/dist/tsserver/index.js +32 -0
  51. package/dist/tsserver/plugin.d.ts +15 -0
  52. package/dist/tsserver/plugin.js +129 -0
  53. package/dist/tsserver/project-controller.d.ts +37 -0
  54. package/dist/tsserver/project-controller.js +60 -0
  55. package/dist/version.d.ts +3 -0
  56. package/dist/version.js +20 -0
  57. package/dist/vscode/document-analysis.d.ts +62 -0
  58. package/dist/vscode/document-analysis.js +111 -0
  59. package/dist/vscode/extension.d.ts +133 -0
  60. package/dist/vscode/extension.js +241 -0
  61. package/dist/vscode/host.d.ts +79 -0
  62. package/dist/vscode/host.js +16 -0
  63. package/dist/vscode/index.d.ts +7 -0
  64. package/dist/vscode/index.js +36 -0
  65. package/dist/vscode/macro-presentation.d.ts +43 -0
  66. package/dist/vscode/macro-presentation.js +130 -0
  67. package/dist/vscode/package-profile.d.ts +27 -0
  68. package/dist/vscode/package-profile.js +142 -0
  69. package/dist/vscode/profile-state.d.ts +22 -0
  70. package/dist/vscode/profile-state.js +81 -0
  71. package/package.json +129 -0
@@ -0,0 +1,78 @@
1
+ interface LintFix {
2
+ readonly range: readonly [number, number];
3
+ readonly text: string;
4
+ }
5
+ interface LintSuggestion {
6
+ readonly fix?: LintFix;
7
+ [key: string]: unknown;
8
+ }
9
+ interface LintMessage {
10
+ readonly ruleId?: string | null;
11
+ readonly fatal?: boolean;
12
+ readonly line?: number;
13
+ readonly column?: number;
14
+ readonly endLine?: number;
15
+ readonly endColumn?: number;
16
+ readonly fix?: LintFix;
17
+ readonly suggestions?: readonly LintSuggestion[];
18
+ [key: string]: unknown;
19
+ }
20
+ export declare const processors: {
21
+ macros: {
22
+ meta: {
23
+ name: string;
24
+ version: string;
25
+ };
26
+ supportsAutofix: boolean;
27
+ preprocess(text: string, filename: string): string[];
28
+ postprocess(messages: LintMessage[][], filename: string): LintMessage[];
29
+ };
30
+ };
31
+ export interface FlatConfig {
32
+ readonly files: readonly string[];
33
+ readonly plugins: {
34
+ readonly tsifdef: EslintPlugin;
35
+ };
36
+ readonly processor: string;
37
+ readonly settings: {
38
+ readonly "import/parsers": {
39
+ readonly "tsifdef/parser": readonly string[];
40
+ };
41
+ };
42
+ }
43
+ /**
44
+ * The legacy `.eslintrc` schema rejects a top-level `files` key, so the flat
45
+ * config cannot be reused for `extends: ["plugin:tsifdef/recommended"]`. The
46
+ * same targeting is expressed through `overrides` instead. Legacy resolves the
47
+ * plugin as `eslint-plugin-tsifdef`, so `import/parsers` must be keyed by that
48
+ * full package name rather than the flat-config short name.
49
+ */
50
+ export interface LegacyConfig {
51
+ readonly plugins: readonly string[];
52
+ readonly overrides: readonly {
53
+ readonly files: readonly string[];
54
+ readonly processor: string;
55
+ }[];
56
+ readonly settings: {
57
+ readonly "import/parsers": {
58
+ readonly "eslint-plugin-tsifdef/parser": readonly string[];
59
+ };
60
+ };
61
+ }
62
+ interface EslintPlugin {
63
+ readonly meta: {
64
+ readonly name: string;
65
+ readonly version: string;
66
+ };
67
+ readonly processors: typeof processors;
68
+ readonly configs: Record<string, FlatConfig | LegacyConfig>;
69
+ }
70
+ export declare const meta: {
71
+ name: string;
72
+ version: string;
73
+ };
74
+ export declare const configs: Record<string, FlatConfig | LegacyConfig>;
75
+ declare const plugin: EslintPlugin;
76
+ export declare const legacyRecommended: LegacyConfig;
77
+ export default plugin;
78
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1,229 @@
1
+ "use strict";
2
+ // Copyright (C) 2026 Tencent. All rights reserved.
3
+ //
4
+ // Licensed under the Apache License, Version 2.0 (the "License");
5
+ // you may not use this file except in compliance with the License.
6
+ // You may obtain a copy of the License at
7
+ //
8
+ // http://www.apache.org/licenses/LICENSE-2.0
9
+ //
10
+ // Unless required by applicable law or agreed to in writing, software
11
+ // distributed under the License is distributed on an "AS IS" BASIS,
12
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ // See the License for the specific language governing permissions and
14
+ // limitations under the License.
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.legacyRecommended = exports.configs = exports.meta = exports.processors = void 0;
17
+ const node_path_1 = require("node:path");
18
+ const version_js_1 = require("../version.js");
19
+ const projection_js_1 = require("./projection.js");
20
+ const projectionContexts = new Map();
21
+ function rememberProjection(filename, source, maskedRanges) {
22
+ projectionContexts.set((0, node_path_1.resolve)(filename), {
23
+ source,
24
+ lineStarts: collectLineStarts(source),
25
+ maskedRanges,
26
+ });
27
+ }
28
+ function takeProjection(filename) {
29
+ const key = (0, node_path_1.resolve)(filename);
30
+ const context = projectionContexts.get(key);
31
+ projectionContexts.delete(key);
32
+ return context;
33
+ }
34
+ function collectLineStarts(source) {
35
+ const starts = [0];
36
+ for (let index = 0; index < source.length; index += 1) {
37
+ if (source[index] === "\n") {
38
+ starts.push(index + 1);
39
+ }
40
+ }
41
+ return starts;
42
+ }
43
+ function locationToOffset(context, line, column) {
44
+ if (!Number.isInteger(line) || !Number.isInteger(column) || line < 1 || column < 1) {
45
+ return undefined;
46
+ }
47
+ const lineStart = context.lineStarts[line - 1];
48
+ if (lineStart === undefined) {
49
+ return undefined;
50
+ }
51
+ const nextLineStart = context.lineStarts[line] ?? context.source.length;
52
+ let lineEnd = nextLineStart;
53
+ if (lineEnd > lineStart && context.source[lineEnd - 1] === "\n") {
54
+ lineEnd -= 1;
55
+ }
56
+ if (lineEnd > lineStart && context.source[lineEnd - 1] === "\r") {
57
+ lineEnd -= 1;
58
+ }
59
+ const offset = lineStart + column - 1;
60
+ return offset <= lineEnd ? offset : undefined;
61
+ }
62
+ /**
63
+ * Diagnostics caused solely by projected text do not describe the user's source.
64
+ * Keep diagnostics that touch any unmasked, non-newline source character so
65
+ * active code or real whitespace is never hidden merely because a range crosses a macro.
66
+ */
67
+ function isSyntheticDiagnostic(message, context) {
68
+ if (message.fatal === true
69
+ || message.ruleId == null
70
+ || message.line === undefined
71
+ || message.column === undefined) {
72
+ return false;
73
+ }
74
+ const start = locationToOffset(context, message.line, message.column);
75
+ if (start === undefined) {
76
+ return false;
77
+ }
78
+ const hasEndLine = message.endLine !== undefined;
79
+ const hasEndColumn = message.endColumn !== undefined;
80
+ if (hasEndLine !== hasEndColumn) {
81
+ return false;
82
+ }
83
+ const explicitEnd = hasEndLine && hasEndColumn
84
+ ? locationToOffset(context, message.endLine, message.endColumn)
85
+ : undefined;
86
+ if (hasEndLine && (explicitEnd === undefined || explicitEnd < start)) {
87
+ return false;
88
+ }
89
+ const end = explicitEnd ?? Math.min(start + 1, context.source.length);
90
+ if (end === start) {
91
+ return context.maskedRanges.some((range) => start >= range.start && start < range.end);
92
+ }
93
+ let cursor = start;
94
+ let overlapsMaskedRange = false;
95
+ for (const range of context.maskedRanges) {
96
+ if (range.end <= start) {
97
+ continue;
98
+ }
99
+ if (range.start >= end) {
100
+ break;
101
+ }
102
+ const maskedStart = Math.max(start, range.start);
103
+ const maskedEnd = Math.min(end, range.end);
104
+ if (/[^\r\n]/u.test(context.source.slice(cursor, maskedStart))) {
105
+ return false;
106
+ }
107
+ overlapsMaskedRange = true;
108
+ cursor = Math.max(cursor, maskedEnd);
109
+ }
110
+ if (!overlapsMaskedRange) {
111
+ return false;
112
+ }
113
+ return !/[^\r\n]/u.test(context.source.slice(cursor, end));
114
+ }
115
+ /**
116
+ * A fix is safe only when its replacement range touches no masked character.
117
+ *
118
+ * Equal-length masking keeps projected offsets identical to source offsets, so a
119
+ * fix range that avoids every masked range rewrites exactly the active text the
120
+ * rule saw. A fix that overlaps masking would splice the user's `#if` directives
121
+ * or inactive branches into the replacement text and silently destroy them, so
122
+ * it is dropped. Zero-length insertions are treated as a single point: they are
123
+ * unsafe only when the insertion point falls strictly inside a masked range.
124
+ */
125
+ function isSafeFix(fix, context) {
126
+ const range = fix.range;
127
+ if (!Array.isArray(range) || range.length !== 2) {
128
+ return false;
129
+ }
130
+ const [start, end] = range;
131
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start) {
132
+ return false;
133
+ }
134
+ return !context.maskedRanges.some((masked) => start === end
135
+ ? start > masked.start && start < masked.end
136
+ : start < masked.end && end > masked.start);
137
+ }
138
+ /**
139
+ * Strip only the unsafe fixes, keeping the diagnostic itself so the problem is
140
+ * still reported even when TSIfDef cannot offer a safe automatic repair.
141
+ */
142
+ function withSafeFixes(message, context) {
143
+ const fixIsUnsafe = message.fix !== undefined && !isSafeFix(message.fix, context);
144
+ const suggestions = message.suggestions;
145
+ const safeSuggestions = suggestions === undefined
146
+ ? undefined
147
+ : suggestions.filter((suggestion) => suggestion.fix === undefined || isSafeFix(suggestion.fix, context));
148
+ const suggestionsChanged = suggestions !== undefined && safeSuggestions.length !== suggestions.length;
149
+ if (!fixIsUnsafe && !suggestionsChanged) {
150
+ return message;
151
+ }
152
+ const next = { ...message };
153
+ if (fixIsUnsafe) {
154
+ delete next.fix;
155
+ }
156
+ if (suggestionsChanged) {
157
+ next.suggestions = safeSuggestions;
158
+ }
159
+ return next;
160
+ }
161
+ exports.processors = {
162
+ macros: {
163
+ meta: { name: "tsifdef/macros", version: version_js_1.VERSION },
164
+ // Autofix stays enabled so unrelated rules keep their quick fixes; ESLint
165
+ // disables fixes for every rule in the file when this is false. Fixes that
166
+ // would overwrite masked directives or inactive branches are removed
167
+ // individually in postprocess instead.
168
+ supportsAutofix: true,
169
+ preprocess(text, filename) {
170
+ // Equal-length masking replaces inactive branches and directive lines
171
+ // with spaces while preserving CR/LF characters and total length.
172
+ const projection = (0, projection_js_1.projectSourceForEslint)(text, filename);
173
+ rememberProjection(filename, text, projection.maskedRanges);
174
+ return [projection.projectedText];
175
+ },
176
+ postprocess(messages, filename) {
177
+ // The equal-length projection preserves diagnostic line and column positions.
178
+ // Discard only diagnostics whose reported range contains no visible source
179
+ // outside TSIfDef's synthetic masking; all active-source diagnostics survive.
180
+ // Surviving diagnostics keep only the fixes that stay clear of masked text.
181
+ const flattened = messages.flat();
182
+ const context = takeProjection(filename);
183
+ return context === undefined
184
+ ? flattened
185
+ : flattened
186
+ .filter((message) => !isSyntheticDiagnostic(message, context))
187
+ .map((message) => withSafeFixes(message, context));
188
+ },
189
+ },
190
+ };
191
+ const MACRO_FILE_GLOBS = ["**/*.{ts,tsx,mts,cts}"];
192
+ const MACRO_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts"];
193
+ exports.meta = { name: "tsifdef", version: version_js_1.VERSION };
194
+ exports.configs = {};
195
+ const plugin = { meta: exports.meta, processors: exports.processors, configs: exports.configs };
196
+ const flatRecommended = {
197
+ files: MACRO_FILE_GLOBS,
198
+ plugins: { tsifdef: plugin },
199
+ processor: "tsifdef/macros",
200
+ settings: {
201
+ "import/parsers": {
202
+ "tsifdef/parser": MACRO_EXTENSIONS,
203
+ },
204
+ },
205
+ };
206
+ exports.legacyRecommended = {
207
+ plugins: ["tsifdef"],
208
+ overrides: [
209
+ {
210
+ files: ["*.ts", "*.tsx", "*.mts", "*.cts"],
211
+ processor: "tsifdef/macros",
212
+ },
213
+ ],
214
+ settings: {
215
+ "import/parsers": {
216
+ "eslint-plugin-tsifdef/parser": MACRO_EXTENSIONS,
217
+ },
218
+ },
219
+ };
220
+ // This entry is what flat configs import as `tsifdef/eslint-plugin`, where
221
+ // `recommended` has always meant the flat config. Keep it that way: changing
222
+ // its shape makes ESLint 9 reject the config outright. The legacy shape is
223
+ // reachable here under an explicit name, and is what `configs.recommended`
224
+ // resolves to on the package entry that `.eslintrc` loads.
225
+ exports.configs.recommended = flatRecommended;
226
+ exports.configs["flat/recommended"] = flatRecommended;
227
+ exports.configs["legacy/recommended"] = exports.legacyRecommended;
228
+ exports.default = plugin;
229
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1,11 @@
1
+ import { type SourceRange } from "../core/index.js";
2
+ export interface EslintProjection {
3
+ readonly projectedText: string;
4
+ readonly maskedRanges: readonly SourceRange[];
5
+ }
6
+ /**
7
+ * Resolve the nearest TSIfDef Profile and return an equal-length projection.
8
+ * Any missing or transiently invalid configuration fails open to the raw text.
9
+ */
10
+ export declare function projectSourceForEslint(source: string, filename: string | undefined): EslintProjection;
11
+ //# sourceMappingURL=projection.d.ts.map
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ // Copyright (C) 2026 Tencent. All rights reserved.
3
+ //
4
+ // Licensed under the Apache License, Version 2.0 (the "License");
5
+ // you may not use this file except in compliance with the License.
6
+ // You may obtain a copy of the License at
7
+ //
8
+ // http://www.apache.org/licenses/LICENSE-2.0
9
+ //
10
+ // Unless required by applicable law or agreed to in writing, software
11
+ // distributed under the License is distributed on an "AS IS" BASIS,
12
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ // See the License for the specific language governing permissions and
14
+ // limitations under the License.
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.projectSourceForEslint = projectSourceForEslint;
17
+ const node_fs_1 = require("node:fs");
18
+ const node_path_1 = require("node:path");
19
+ const config_js_1 = require("../cli/config.js");
20
+ const index_js_1 = require("../core/index.js");
21
+ const profileCache = new Map();
22
+ const packagePointerCache = new Map();
23
+ const macroFilePattern = /\.(?:d\.)?(?:ts|tsx|mts|cts)$/i;
24
+ /**
25
+ * Resolve the nearest TSIfDef Profile and return an equal-length projection.
26
+ * Any missing or transiently invalid configuration fails open to the raw text.
27
+ */
28
+ function projectSourceForEslint(source, filename) {
29
+ if (filename === undefined || !macroFilePattern.test(filename)) {
30
+ return { projectedText: source, maskedRanges: [] };
31
+ }
32
+ const profilePath = resolveProfilePath(filename);
33
+ if (profilePath === undefined) {
34
+ return { projectedText: source, maskedRanges: [] };
35
+ }
36
+ const definitions = loadDefinitions(profilePath);
37
+ if (definitions === undefined) {
38
+ return { projectedText: source, maskedRanges: [] };
39
+ }
40
+ const projection = (0, index_js_1.projectSource)(source, definitions);
41
+ return {
42
+ projectedText: projection.projectedText,
43
+ maskedRanges: projection.maskedRanges,
44
+ };
45
+ }
46
+ function resolveProfilePath(filename) {
47
+ let dir = (0, node_path_1.dirname)((0, node_path_1.resolve)(filename));
48
+ for (;;) {
49
+ const lookup = readPackagePointer((0, node_path_1.join)(dir, "package.json"), dir);
50
+ if (lookup.kind === "resolved") {
51
+ return lookup.profilePath ?? undefined;
52
+ }
53
+ const parent = (0, node_path_1.dirname)(dir);
54
+ if (parent === dir)
55
+ return undefined;
56
+ dir = parent;
57
+ }
58
+ }
59
+ function readPackagePointer(packagePath, packageDir) {
60
+ let stat;
61
+ try {
62
+ stat = (0, node_fs_1.statSync)(packagePath);
63
+ }
64
+ catch {
65
+ return { kind: "continue" };
66
+ }
67
+ const cached = packagePointerCache.get(packagePath);
68
+ if (cached !== undefined && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
69
+ return { kind: "resolved", profilePath: cached.profilePath };
70
+ }
71
+ try {
72
+ const raw = JSON.parse((0, node_fs_1.readFileSync)(packagePath, "utf8").replace(/^\uFEFF/, ""));
73
+ if (raw !== null && typeof raw === "object") {
74
+ const value = raw.tsifdef;
75
+ const profilePath = typeof value === "string" && value.trim() !== ""
76
+ ? (0, node_path_1.resolve)(packageDir, value)
77
+ : null;
78
+ packagePointerCache.set(packagePath, {
79
+ mtimeMs: stat.mtimeMs,
80
+ size: stat.size,
81
+ profilePath,
82
+ });
83
+ return { kind: "resolved", profilePath };
84
+ }
85
+ }
86
+ catch {
87
+ // Keep lint permissive when a package.json is temporarily unreadable.
88
+ }
89
+ return { kind: "continue" };
90
+ }
91
+ function loadDefinitions(profilePath) {
92
+ let mtimeMs;
93
+ let size;
94
+ try {
95
+ const stat = (0, node_fs_1.statSync)(profilePath);
96
+ mtimeMs = stat.mtimeMs;
97
+ size = stat.size;
98
+ }
99
+ catch {
100
+ return undefined;
101
+ }
102
+ const cached = profileCache.get(profilePath);
103
+ if (cached !== undefined && cached.mtimeMs === mtimeMs && cached.size === size) {
104
+ return cached.definitions;
105
+ }
106
+ try {
107
+ const text = (0, node_fs_1.readFileSync)(profilePath, "utf8");
108
+ const definitions = (0, config_js_1.parseProfileFile)(text, profilePath).definitions;
109
+ profileCache.set(profilePath, { mtimeMs, size, definitions });
110
+ return definitions;
111
+ }
112
+ catch {
113
+ return undefined;
114
+ }
115
+ }
116
+ //# sourceMappingURL=projection.js.map
@@ -0,0 +1,45 @@
1
+ import type * as ts from "typescript";
2
+ import type { MacroDefinitions } from "../core/expression.js";
3
+ /** The active Profile applied to macro files, or `undefined` when none is selected. */
4
+ export interface ActiveProfile {
5
+ /** The externally selected, read-only macro definitions. */
6
+ readonly definitions: MacroDefinitions;
7
+ /** A version tag identifying the Profile, appended to each script version. */
8
+ readonly version: string;
9
+ }
10
+ export interface HostProjectionOptions {
11
+ /**
12
+ * Return the currently active Profile, read fresh on every snapshot and
13
+ * version request. Returning `undefined` disables projection so raw snapshots
14
+ * and versions pass through. Reading live (rather than capturing once) lets a
15
+ * later Profile change take effect without recreating the language service.
16
+ */
17
+ readonly getProfile: () => ActiveProfile | undefined;
18
+ /** Decide whether a file participates in macro projection. */
19
+ readonly isMacroFile?: (fileName: string) => boolean;
20
+ }
21
+ /**
22
+ * Wrap a `LanguageServiceHost` so the language service sees macro-projected
23
+ * source.
24
+ *
25
+ * `getScriptSnapshot` reads the complete current snapshot via
26
+ * `getText(0, getLength())`, analyzes every directive in that whole file, and
27
+ * returns one equal-length projected snapshot. tsserver may then read any slice
28
+ * of that projection; macro state is never inferred from a requested slice.
29
+ * Unsaved edits are honored because the wrapper reads the host's in-memory
30
+ * snapshot rather than the file on disk.
31
+ *
32
+ * Projections are memoized per file against the host snapshot identity and the
33
+ * Profile version, so repeated reads of an unchanged file do not re-scan it.
34
+ * Files containing no directives are passed through untouched, which keeps
35
+ * large generated `.d.ts` files on TypeScript's own fast paths.
36
+ *
37
+ * `getScriptVersion` gains the Profile version so tsserver never reuses an AST
38
+ * built for a different Profile. Non-macro files, and all files while no Profile
39
+ * is selected, pass through unchanged.
40
+ *
41
+ * The `ts` module is injected (tsserver supplies it to the plugin) so this
42
+ * wrapper is testable without a global TypeScript dependency.
43
+ */
44
+ export declare function wrapHostWithProjection<THost extends ts.LanguageServiceHost>(_typescript: typeof ts, host: THost, options: HostProjectionOptions): THost;
45
+ //# sourceMappingURL=host-projection.d.ts.map
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ // Copyright (C) 2026 Tencent. All rights reserved.
3
+ //
4
+ // Licensed under the Apache License, Version 2.0 (the "License");
5
+ // you may not use this file except in compliance with the License.
6
+ // You may obtain a copy of the License at
7
+ //
8
+ // http://www.apache.org/licenses/LICENSE-2.0
9
+ //
10
+ // Unless required by applicable law or agreed to in writing, software
11
+ // distributed under the License is distributed on an "AS IS" BASIS,
12
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ // See the License for the specific language governing permissions and
14
+ // limitations under the License.
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.wrapHostWithProjection = wrapHostWithProjection;
17
+ const index_js_1 = require("../core/index.js");
18
+ /** Default macro-file matcher: TypeScript-family extensions. */
19
+ const macroExtensionPattern = /\.(?:d\.)?(?:ts|tsx|mts|cts)$/i;
20
+ /**
21
+ * A projected snapshot that keeps incremental reparsing working.
22
+ *
23
+ * tsserver asks a snapshot how it differs from the previous one via
24
+ * `getChangeRange`. When that returns `undefined`, TypeScript cannot reuse any
25
+ * of the previous AST and reparses the whole file on every keystroke, which is
26
+ * catastrophic on large files. `ts.ScriptSnapshot.fromString` always returns
27
+ * `undefined`, so wrapping the host with it silently disabled incremental
28
+ * reparsing for every macro file.
29
+ *
30
+ * Equal-length masking is what makes delegation sound: a projected document has
31
+ * exactly the same length and offsets as its source, so a change range computed
32
+ * over the source is valid for the projection verbatim. The one exception is an
33
+ * edit that changes macro structure (adding, removing, or flipping a directive),
34
+ * which can rewrite text far from the edit. That case is detected by comparing
35
+ * masked ranges and reported as a full change so TypeScript reparses.
36
+ */
37
+ class ProjectedSnapshot {
38
+ projectedText;
39
+ maskedRanges;
40
+ sourceSnapshot;
41
+ constructor(projectedText,
42
+ /** Masked ranges of this projection, used to detect macro-structure edits. */
43
+ maskedRanges,
44
+ /** The host snapshot this projection was derived from. */
45
+ sourceSnapshot) {
46
+ this.projectedText = projectedText;
47
+ this.maskedRanges = maskedRanges;
48
+ this.sourceSnapshot = sourceSnapshot;
49
+ }
50
+ getText(start, end) {
51
+ return this.projectedText.slice(start, end);
52
+ }
53
+ getLength() {
54
+ return this.projectedText.length;
55
+ }
56
+ getChangeRange(oldSnapshot) {
57
+ if (!(oldSnapshot instanceof ProjectedSnapshot)) {
58
+ return undefined;
59
+ }
60
+ // A macro-structure edit can alter text anywhere, so the source delta no
61
+ // longer describes the projection. Force a full reparse.
62
+ if (!sameMaskedRanges(oldSnapshot.maskedRanges, this.maskedRanges)) {
63
+ return undefined;
64
+ }
65
+ // Offsets are identical between source and projection, so the host's own
66
+ // change range applies to the projected text unchanged.
67
+ return this.sourceSnapshot.getChangeRange(oldSnapshot.sourceSnapshot);
68
+ }
69
+ }
70
+ function sameMaskedRanges(left, right) {
71
+ if (left.length !== right.length) {
72
+ return false;
73
+ }
74
+ for (let index = 0; index < left.length; index += 1) {
75
+ if (left[index].start !== right[index].start || left[index].end !== right[index].end) {
76
+ return false;
77
+ }
78
+ }
79
+ return true;
80
+ }
81
+ /**
82
+ * Wrap a `LanguageServiceHost` so the language service sees macro-projected
83
+ * source.
84
+ *
85
+ * `getScriptSnapshot` reads the complete current snapshot via
86
+ * `getText(0, getLength())`, analyzes every directive in that whole file, and
87
+ * returns one equal-length projected snapshot. tsserver may then read any slice
88
+ * of that projection; macro state is never inferred from a requested slice.
89
+ * Unsaved edits are honored because the wrapper reads the host's in-memory
90
+ * snapshot rather than the file on disk.
91
+ *
92
+ * Projections are memoized per file against the host snapshot identity and the
93
+ * Profile version, so repeated reads of an unchanged file do not re-scan it.
94
+ * Files containing no directives are passed through untouched, which keeps
95
+ * large generated `.d.ts` files on TypeScript's own fast paths.
96
+ *
97
+ * `getScriptVersion` gains the Profile version so tsserver never reuses an AST
98
+ * built for a different Profile. Non-macro files, and all files while no Profile
99
+ * is selected, pass through unchanged.
100
+ *
101
+ * The `ts` module is injected (tsserver supplies it to the plugin) so this
102
+ * wrapper is testable without a global TypeScript dependency.
103
+ */
104
+ function wrapHostWithProjection(
105
+ // Retained for API compatibility and future use; projection no longer needs
106
+ // to build snapshots through the TypeScript module.
107
+ _typescript, host, options) {
108
+ const isMacroFile = options.isMacroFile ?? ((fileName) => macroExtensionPattern.test(fileName));
109
+ const originalGetScriptSnapshot = host.getScriptSnapshot.bind(host);
110
+ const originalGetScriptVersion = host.getScriptVersion.bind(host);
111
+ const projectionCache = new Map();
112
+ host.getScriptSnapshot = (fileName) => {
113
+ const snapshot = originalGetScriptSnapshot(fileName);
114
+ const profile = options.getProfile();
115
+ if (snapshot === undefined || profile === undefined || !isMacroFile(fileName)) {
116
+ return snapshot;
117
+ }
118
+ // Reuse the previous projection when neither the file content nor the
119
+ // Profile changed. tsserver requests the same snapshot repeatedly while
120
+ // serving one request, and re-scanning a multi-megabyte file each time is
121
+ // pure overhead. The snapshot identity check is the cheap path; hosts that
122
+ // rebuild snapshot objects per call still hit the cache via content.
123
+ const cached = projectionCache.get(fileName);
124
+ if (cached !== undefined && cached.profileVersion === profile.version) {
125
+ if (cached.sourceSnapshot === snapshot) {
126
+ return cached.snapshot ?? snapshot;
127
+ }
128
+ if (snapshot.getLength() === cached.sourceText.length
129
+ && snapshot.getText(0, snapshot.getLength()) === cached.sourceText) {
130
+ return cached.snapshot ?? snapshot;
131
+ }
132
+ }
133
+ // Read the entire current file; directive pairing spans the whole file and
134
+ // cannot be evaluated from an isolated slice.
135
+ const source = snapshot.getText(0, snapshot.getLength());
136
+ const projection = (0, index_js_1.projectSource)(source, profile.definitions);
137
+ const projected = projection.maskedRanges.length === 0
138
+ // Nothing is masked, so the projection is the source. Returning the host
139
+ // snapshot preserves its change range and avoids copying the text.
140
+ ? undefined
141
+ : new ProjectedSnapshot(projection.projectedText, projection.maskedRanges, snapshot);
142
+ projectionCache.set(fileName, {
143
+ sourceSnapshot: snapshot,
144
+ sourceText: source,
145
+ profileVersion: profile.version,
146
+ snapshot: projected,
147
+ });
148
+ return projected ?? snapshot;
149
+ };
150
+ host.getScriptVersion = (fileName) => {
151
+ const version = originalGetScriptVersion(fileName);
152
+ const profile = options.getProfile();
153
+ if (profile === undefined || !isMacroFile(fileName)) {
154
+ return version;
155
+ }
156
+ return `${version}|tsifdef:${profile.version}`;
157
+ };
158
+ return host;
159
+ }
160
+ //# sourceMappingURL=host-projection.js.map
@@ -0,0 +1,3 @@
1
+ export * from "./host-projection.js";
2
+ export * from "./project-controller.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ // Copyright (C) 2026 Tencent. All rights reserved.
3
+ //
4
+ // Licensed under the Apache License, Version 2.0 (the "License");
5
+ // you may not use this file except in compliance with the License.
6
+ // You may obtain a copy of the License at
7
+ //
8
+ // http://www.apache.org/licenses/LICENSE-2.0
9
+ //
10
+ // Unless required by applicable law or agreed to in writing, software
11
+ // distributed under the License is distributed on an "AS IS" BASIS,
12
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ // See the License for the specific language governing permissions and
14
+ // limitations under the License.
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
27
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
28
+ };
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ __exportStar(require("./host-projection.js"), exports);
31
+ __exportStar(require("./project-controller.js"), exports);
32
+ //# sourceMappingURL=index.js.map