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.
- package/CHANGELOG.md +25 -0
- package/CHANGELOG.zh-CN.md +22 -0
- package/CONTRIBUTING.md +52 -0
- package/INTEGRATION.md +166 -0
- package/INTEGRATION.zh-CN.md +159 -0
- package/LICENSE +210 -0
- package/README.md +150 -0
- package/README.zh-CN.md +141 -0
- package/SECURITY.md +15 -0
- package/assets/icon-mono.svg +15 -0
- package/assets/icon.png +0 -0
- package/assets/icon.svg +16 -0
- package/dist/cli/build.d.ts +70 -0
- package/dist/cli/build.js +298 -0
- package/dist/cli/config.d.ts +30 -0
- package/dist/cli/config.js +135 -0
- package/dist/cli/diagnostics.d.ts +3 -0
- package/dist/cli/diagnostics.js +31 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.js +35 -0
- package/dist/cli/main.d.ts +9 -0
- package/dist/cli/main.js +165 -0
- package/dist/cli/precompile.d.ts +38 -0
- package/dist/cli/precompile.js +192 -0
- package/dist/cli/source-files.d.ts +10 -0
- package/dist/cli/source-files.js +75 -0
- package/dist/cli/watch.d.ts +34 -0
- package/dist/cli/watch.js +183 -0
- package/dist/core/conditional.d.ts +18 -0
- package/dist/core/conditional.js +126 -0
- package/dist/core/expression.d.ts +44 -0
- package/dist/core/expression.js +224 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.js +37 -0
- package/dist/core/projection.d.ts +15 -0
- package/dist/core/projection.js +99 -0
- package/dist/core/scanner.d.ts +33 -0
- package/dist/core/scanner.js +344 -0
- package/dist/eslint/package.d.ts +10 -0
- package/dist/eslint/package.js +55 -0
- package/dist/eslint/parser.d.ts +35 -0
- package/dist/eslint/parser.js +59 -0
- package/dist/eslint/plugin.d.ts +78 -0
- package/dist/eslint/plugin.js +229 -0
- package/dist/eslint/projection.d.ts +11 -0
- package/dist/eslint/projection.js +116 -0
- package/dist/tsserver/host-projection.d.ts +45 -0
- package/dist/tsserver/host-projection.js +160 -0
- package/dist/tsserver/index.d.ts +3 -0
- package/dist/tsserver/index.js +32 -0
- package/dist/tsserver/plugin.d.ts +15 -0
- package/dist/tsserver/plugin.js +129 -0
- package/dist/tsserver/project-controller.d.ts +37 -0
- package/dist/tsserver/project-controller.js +60 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.js +20 -0
- package/dist/vscode/document-analysis.d.ts +62 -0
- package/dist/vscode/document-analysis.js +111 -0
- package/dist/vscode/extension.d.ts +133 -0
- package/dist/vscode/extension.js +241 -0
- package/dist/vscode/host.d.ts +79 -0
- package/dist/vscode/host.js +16 -0
- package/dist/vscode/index.d.ts +7 -0
- package/dist/vscode/index.js +36 -0
- package/dist/vscode/macro-presentation.d.ts +43 -0
- package/dist/vscode/macro-presentation.js +130 -0
- package/dist/vscode/package-profile.d.ts +27 -0
- package/dist/vscode/package-profile.js +142 -0
- package/dist/vscode/profile-state.d.ts +22 -0
- package/dist/vscode/profile-state.js +81 -0
- 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,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
|