@astrojs/ts-plugin 0.2.1 → 0.3.0

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.
@@ -1,297 +1,285 @@
1
- import { astro2tsx } from './astro2tsx.js';
2
1
  import type ts from 'typescript/lib/tsserverlibrary';
2
+ import { astro2tsx } from './astro2tsx.js';
3
3
  import { Logger } from './logger.js';
4
4
  import { SourceMapper } from './source-mapper.js';
5
- import { isNoTextSpanInGeneratedCode, isAstroFilePath } from './utils.js';
5
+ import { isAstroFilePath, isNoTextSpanInGeneratedCode } from './utils.js';
6
6
 
7
7
  export class AstroSnapshot {
8
- private scriptInfo?: ts.server.ScriptInfo;
9
- private lineOffsets?: number[];
10
- private convertInternalCodePositions = false;
11
-
12
- constructor(
13
- private typescript: typeof ts,
14
- private fileName: string,
15
- private astroCode: string,
16
- private mapper: SourceMapper,
17
- private logger: Logger,
18
- public readonly isTsFile: boolean
19
- ) {}
20
-
21
- update(astroCode: string, mapper: SourceMapper) {
22
- this.astroCode = astroCode;
23
- this.mapper = mapper;
24
- this.lineOffsets = undefined;
25
- this.log('Updated Snapshot');
26
- }
27
-
28
- getOriginalTextSpan(textSpan: ts.TextSpan): ts.TextSpan | null {
29
- if (!isNoTextSpanInGeneratedCode(this.getText(), textSpan)) {
30
- return null;
31
- }
32
-
33
- const start = this.getOriginalOffset(textSpan.start);
34
- if (start === -1) {
35
- return null;
36
- }
37
-
38
- // Assumption: We don't change identifiers itself, so we don't change ranges.
39
- return {
40
- start,
41
- length: textSpan.length
42
- };
43
- }
44
-
45
- getOriginalOffset(generatedOffset: number) {
46
- if (!this.scriptInfo) {
47
- return generatedOffset;
48
- }
49
-
50
- this.toggleMappingMode(true);
51
- const lineOffset = this.scriptInfo.positionToLineOffset(generatedOffset);
52
- this.debug('try convert offset', generatedOffset, '/', lineOffset);
53
- const original = this.mapper.getOriginalPosition({
54
- line: lineOffset.line - 1,
55
- character: lineOffset.offset - 1
56
- });
57
- this.toggleMappingMode(false);
58
- if (original.line === -1) {
59
- return -1;
60
- }
61
-
62
- const originalOffset = this.scriptInfo.lineOffsetToPosition(
63
- original.line + 1,
64
- original.character + 1
65
- );
66
- this.debug('converted offset to', original, '/', originalOffset);
67
- return originalOffset;
68
- }
69
-
70
- setAndPatchScriptInfo(scriptInfo: ts.server.ScriptInfo) {
71
- // @ts-expect-error
72
- scriptInfo.scriptKind = this.typescript.ScriptKind.TSX;
73
-
74
- const positionToLineOffset = scriptInfo.positionToLineOffset.bind(scriptInfo);
75
- scriptInfo.positionToLineOffset = (position) => {
76
- if (this.convertInternalCodePositions) {
77
- const lineOffset = positionToLineOffset(position);
78
- this.debug('positionToLineOffset for generated code', position, lineOffset);
79
- return lineOffset;
80
- }
81
-
82
- const lineOffset = this.positionAt(position);
83
- this.debug('positionToLineOffset for original code', position, lineOffset);
84
- return { line: lineOffset.line + 1, offset: lineOffset.character + 1 };
85
- };
86
-
87
- const lineOffsetToPosition = scriptInfo.lineOffsetToPosition.bind(scriptInfo);
88
- scriptInfo.lineOffsetToPosition = (line, offset) => {
89
- if (this.convertInternalCodePositions) {
90
- const position = lineOffsetToPosition(line, offset);
91
- this.debug('lineOffsetToPosition for generated code', { line, offset }, position);
92
- return position;
93
- }
94
-
95
- const position = this.offsetAt({ line: line - 1, character: offset - 1 });
96
- this.debug('lineOffsetToPosition for original code', { line, offset }, position);
97
- return position;
98
- };
99
-
100
- this.scriptInfo = scriptInfo;
101
- this.log('patched scriptInfo');
102
- }
103
-
104
- /**
105
- * Get the line and character based on the offset
106
- * @param offset The index of the position
107
- */
108
- positionAt(offset: number): ts.LineAndCharacter {
109
- offset = this.clamp(offset, 0, this.astroCode.length);
110
-
111
- const lineOffsets = this.getLineOffsets();
112
- let low = 0;
113
- let high = lineOffsets.length;
114
- if (high === 0) {
115
- return { line: 0, character: offset };
116
- }
117
-
118
- while (low < high) {
119
- const mid = Math.floor((low + high) / 2);
120
- if (lineOffsets[mid] > offset) {
121
- high = mid;
122
- } else {
123
- low = mid + 1;
124
- }
125
- }
126
-
127
- // low is the least x for which the line offset is larger than the current offset
128
- // or array.length if no line offset is larger than the current offset
129
- const line = low - 1;
130
-
131
- return { line, character: offset - lineOffsets[line] };
132
- }
133
-
134
- /**
135
- * Get the index of the line and character position
136
- * @param position Line and character position
137
- */
138
- offsetAt(position: ts.LineAndCharacter): number {
139
- const lineOffsets = this.getLineOffsets();
140
-
141
- if (position.line >= lineOffsets.length) {
142
- return this.astroCode.length;
143
- } else if (position.line < 0) {
144
- return 0;
145
- }
146
-
147
- const lineOffset = lineOffsets[position.line];
148
- const nextLineOffset =
149
- position.line + 1 < lineOffsets.length
150
- ? lineOffsets[position.line + 1]
151
- : this.astroCode.length;
152
-
153
- return this.clamp(nextLineOffset, lineOffset, lineOffset + position.character);
154
- }
155
-
156
- private getLineOffsets() {
157
- if (this.lineOffsets) {
158
- return this.lineOffsets;
159
- }
160
-
161
- const lineOffsets = [];
162
- const text = this.astroCode;
163
- let isLineStart = true;
164
-
165
- for (let i = 0; i < text.length; i++) {
166
- if (isLineStart) {
167
- lineOffsets.push(i);
168
- isLineStart = false;
169
- }
170
- const ch = text.charAt(i);
171
- isLineStart = ch === '\r' || ch === '\n';
172
- if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') {
173
- i++;
174
- }
175
- }
176
-
177
- if (isLineStart && text.length > 0) {
178
- lineOffsets.push(text.length);
179
- }
180
-
181
- this.lineOffsets = lineOffsets;
182
- return lineOffsets;
183
- }
184
-
185
- private clamp(num: number, min: number, max: number): number {
186
- return Math.max(min, Math.min(max, num));
187
- }
188
-
189
- private log(...args: any[]) {
190
- this.logger.log('AstroSnapshot:', this.fileName, '-', ...args);
191
- }
192
-
193
- private debug(...args: any[]) {
194
- this.logger.debug('AstroSnapshot:', this.fileName, '-', ...args);
195
- }
196
-
197
- private toggleMappingMode(convertInternalCodePositions: boolean) {
198
- this.convertInternalCodePositions = convertInternalCodePositions;
199
- }
200
-
201
- private getText() {
202
- const snapshot = this.scriptInfo?.getSnapshot();
203
- if (!snapshot) {
204
- return '';
205
- }
206
- return snapshot.getText(0, snapshot.getLength());
207
- }
8
+ private scriptInfo?: ts.server.ScriptInfo;
9
+ private lineOffsets?: number[];
10
+ private convertInternalCodePositions = false;
11
+
12
+ constructor(
13
+ private typescript: typeof ts,
14
+ private fileName: string,
15
+ private astroCode: string,
16
+ private mapper: SourceMapper,
17
+ private logger: Logger,
18
+ public readonly isTsFile: boolean
19
+ ) {}
20
+
21
+ update(astroCode: string, mapper: SourceMapper) {
22
+ this.astroCode = astroCode;
23
+ this.mapper = mapper;
24
+ this.lineOffsets = undefined;
25
+ this.log('Updated Snapshot');
26
+ }
27
+
28
+ getOriginalTextSpan(textSpan: ts.TextSpan): ts.TextSpan | null {
29
+ if (!isNoTextSpanInGeneratedCode(this.getText(), textSpan)) {
30
+ return null;
31
+ }
32
+
33
+ const start = this.getOriginalOffset(textSpan.start);
34
+ if (start === -1) {
35
+ return null;
36
+ }
37
+
38
+ // Assumption: We don't change identifiers itself, so we don't change ranges.
39
+ return {
40
+ start,
41
+ length: textSpan.length,
42
+ };
43
+ }
44
+
45
+ getOriginalOffset(generatedOffset: number) {
46
+ if (!this.scriptInfo) {
47
+ return generatedOffset;
48
+ }
49
+
50
+ this.toggleMappingMode(true);
51
+ const lineOffset = this.scriptInfo.positionToLineOffset(generatedOffset);
52
+ this.debug('try convert offset', generatedOffset, '/', lineOffset);
53
+ const original = this.mapper.getOriginalPosition({
54
+ line: lineOffset.line - 1,
55
+ character: lineOffset.offset - 1,
56
+ });
57
+ this.toggleMappingMode(false);
58
+ if (original.line === -1) {
59
+ return -1;
60
+ }
61
+
62
+ const originalOffset = this.scriptInfo.lineOffsetToPosition(original.line + 1, original.character + 1);
63
+ this.debug('converted offset to', original, '/', originalOffset);
64
+ return originalOffset;
65
+ }
66
+
67
+ setAndPatchScriptInfo(scriptInfo: ts.server.ScriptInfo) {
68
+ // @ts-expect-error
69
+ scriptInfo.scriptKind = this.typescript.ScriptKind.TSX;
70
+
71
+ const positionToLineOffset = scriptInfo.positionToLineOffset.bind(scriptInfo);
72
+ scriptInfo.positionToLineOffset = (position) => {
73
+ if (this.convertInternalCodePositions) {
74
+ const lineOffset = positionToLineOffset(position);
75
+ this.debug('positionToLineOffset for generated code', position, lineOffset);
76
+ return lineOffset;
77
+ }
78
+
79
+ const lineOffset = this.positionAt(position);
80
+ this.debug('positionToLineOffset for original code', position, lineOffset);
81
+ return { line: lineOffset.line + 1, offset: lineOffset.character + 1 };
82
+ };
83
+
84
+ const lineOffsetToPosition = scriptInfo.lineOffsetToPosition.bind(scriptInfo);
85
+ scriptInfo.lineOffsetToPosition = (line, offset) => {
86
+ if (this.convertInternalCodePositions) {
87
+ const position = lineOffsetToPosition(line, offset);
88
+ this.debug('lineOffsetToPosition for generated code', { line, offset }, position);
89
+ return position;
90
+ }
91
+
92
+ const position = this.offsetAt({ line: line - 1, character: offset - 1 });
93
+ this.debug('lineOffsetToPosition for original code', { line, offset }, position);
94
+ return position;
95
+ };
96
+
97
+ this.scriptInfo = scriptInfo;
98
+ this.log('patched scriptInfo');
99
+ }
100
+
101
+ /**
102
+ * Get the line and character based on the offset
103
+ * @param offset The index of the position
104
+ */
105
+ positionAt(offset: number): ts.LineAndCharacter {
106
+ offset = this.clamp(offset, 0, this.astroCode.length);
107
+
108
+ const lineOffsets = this.getLineOffsets();
109
+ let low = 0;
110
+ let high = lineOffsets.length;
111
+ if (high === 0) {
112
+ return { line: 0, character: offset };
113
+ }
114
+
115
+ while (low < high) {
116
+ const mid = Math.floor((low + high) / 2);
117
+ if (lineOffsets[mid] > offset) {
118
+ high = mid;
119
+ } else {
120
+ low = mid + 1;
121
+ }
122
+ }
123
+
124
+ // low is the least x for which the line offset is larger than the current offset
125
+ // or array.length if no line offset is larger than the current offset
126
+ const line = low - 1;
127
+
128
+ return { line, character: offset - lineOffsets[line] };
129
+ }
130
+
131
+ /**
132
+ * Get the index of the line and character position
133
+ * @param position Line and character position
134
+ */
135
+ offsetAt(position: ts.LineAndCharacter): number {
136
+ const lineOffsets = this.getLineOffsets();
137
+
138
+ if (position.line >= lineOffsets.length) {
139
+ return this.astroCode.length;
140
+ } else if (position.line < 0) {
141
+ return 0;
142
+ }
143
+
144
+ const lineOffset = lineOffsets[position.line];
145
+ const nextLineOffset =
146
+ position.line + 1 < lineOffsets.length ? lineOffsets[position.line + 1] : this.astroCode.length;
147
+
148
+ return this.clamp(nextLineOffset, lineOffset, lineOffset + position.character);
149
+ }
150
+
151
+ private getLineOffsets() {
152
+ if (this.lineOffsets) {
153
+ return this.lineOffsets;
154
+ }
155
+
156
+ const lineOffsets = [];
157
+ const text = this.astroCode;
158
+ let isLineStart = true;
159
+
160
+ for (let i = 0; i < text.length; i++) {
161
+ if (isLineStart) {
162
+ lineOffsets.push(i);
163
+ isLineStart = false;
164
+ }
165
+ const ch = text.charAt(i);
166
+ isLineStart = ch === '\r' || ch === '\n';
167
+ if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') {
168
+ i++;
169
+ }
170
+ }
171
+
172
+ if (isLineStart && text.length > 0) {
173
+ lineOffsets.push(text.length);
174
+ }
175
+
176
+ this.lineOffsets = lineOffsets;
177
+ return lineOffsets;
178
+ }
179
+
180
+ private clamp(num: number, min: number, max: number): number {
181
+ return Math.max(min, Math.min(max, num));
182
+ }
183
+
184
+ private log(...args: any[]) {
185
+ this.logger.log('AstroSnapshot:', this.fileName, '-', ...args);
186
+ }
187
+
188
+ private debug(...args: any[]) {
189
+ this.logger.debug('AstroSnapshot:', this.fileName, '-', ...args);
190
+ }
191
+
192
+ private toggleMappingMode(convertInternalCodePositions: boolean) {
193
+ this.convertInternalCodePositions = convertInternalCodePositions;
194
+ }
195
+
196
+ private getText() {
197
+ const snapshot = this.scriptInfo?.getSnapshot();
198
+ if (!snapshot) {
199
+ return '';
200
+ }
201
+ return snapshot.getText(0, snapshot.getLength());
202
+ }
208
203
  }
209
204
 
210
205
  export class AstroSnapshotManager {
211
- private snapshots = new Map<string, AstroSnapshot>();
212
-
213
- constructor(
214
- private typescript: typeof ts,
215
- private projectService: ts.server.ProjectService,
216
- private logger: Logger
217
- ) {
218
- this.patchProjectServiceReadFile();
219
- }
220
-
221
- get(fileName: string) {
222
- return this.snapshots.get(fileName);
223
- }
224
-
225
- create(fileName: string): AstroSnapshot | undefined {
226
- if (this.snapshots.has(fileName)) {
227
- return this.snapshots.get(fileName)!;
228
- }
229
-
230
- // This will trigger projectService.host.readFile which is patched below
231
- const scriptInfo = this.projectService.getOrCreateScriptInfoForNormalizedPath(
232
- this.typescript.server.toNormalizedPath(fileName),
233
- false
234
- );
235
- if (!scriptInfo) {
236
- this.logger.log('Was not able get snapshot for', fileName);
237
- return;
238
- }
239
-
240
- try {
241
- scriptInfo.getSnapshot(); // needed to trigger readFile
242
- } catch (e) {
243
- this.logger.log('Loading Snapshot failed', fileName);
244
- }
245
- const snapshot = this.snapshots.get(fileName);
246
- if (!snapshot) {
247
- this.logger.log(
248
- 'Astro snapshot was not found after trying to load script snapshot for',
249
- fileName
250
- );
251
- return; // should never get here
252
- }
253
- snapshot.setAndPatchScriptInfo(scriptInfo);
254
- this.snapshots.set(fileName, snapshot);
255
- return snapshot;
256
- }
257
-
258
- private patchProjectServiceReadFile() {
259
- const readFile = this.projectService.host.readFile;
260
- this.projectService.host.readFile = (path: string) => {
261
- if (isAstroFilePath(path)) {
262
- this.logger.debug('Read Astro file:', path);
263
- const astroCode = readFile(path) || '';
264
- try {
265
- const isTsFile = true;
266
- const result = astro2tsx(astroCode, {
267
- filename: path.split('/').pop(),
268
- isTsFile
269
- });
270
- const existingSnapshot = this.snapshots.get(path);
271
- if (existingSnapshot) {
272
- existingSnapshot.update(astroCode, new SourceMapper(result.map.mappings));
273
- } else {
274
- this.snapshots.set(
275
- path,
276
- new AstroSnapshot(
277
- this.typescript,
278
- path,
279
- astroCode,
280
- new SourceMapper(result.map.mappings),
281
- this.logger,
282
- isTsFile
283
- )
284
- );
285
- }
286
- this.logger.log('Successfully read Astro file contents of', path);
287
- return result.code;
288
- } catch (e) {
289
- this.logger.log('Error loading Astro file:', path);
290
- this.logger.debug('Error:', e);
291
- }
292
- } else {
293
- return readFile(path);
294
- }
295
- };
296
- }
297
- }
206
+ private snapshots = new Map<string, AstroSnapshot>();
207
+
208
+ constructor(private typescript: typeof ts, private projectService: ts.server.ProjectService, private logger: Logger) {
209
+ this.patchProjectServiceReadFile();
210
+ }
211
+
212
+ get(fileName: string) {
213
+ return this.snapshots.get(fileName);
214
+ }
215
+
216
+ create(fileName: string): AstroSnapshot | undefined {
217
+ if (this.snapshots.has(fileName)) {
218
+ return this.snapshots.get(fileName)!;
219
+ }
220
+
221
+ // This will trigger projectService.host.readFile which is patched below
222
+ const scriptInfo = this.projectService.getOrCreateScriptInfoForNormalizedPath(
223
+ this.typescript.server.toNormalizedPath(fileName),
224
+ false
225
+ );
226
+ if (!scriptInfo) {
227
+ this.logger.log('Was not able get snapshot for', fileName);
228
+ return;
229
+ }
230
+
231
+ try {
232
+ scriptInfo.getSnapshot(); // needed to trigger readFile
233
+ } catch (e) {
234
+ this.logger.log('Loading Snapshot failed', fileName);
235
+ }
236
+ const snapshot = this.snapshots.get(fileName);
237
+ if (!snapshot) {
238
+ this.logger.log('Astro snapshot was not found after trying to load script snapshot for', fileName);
239
+ return; // should never get here
240
+ }
241
+ snapshot.setAndPatchScriptInfo(scriptInfo);
242
+ this.snapshots.set(fileName, snapshot);
243
+ return snapshot;
244
+ }
245
+
246
+ private patchProjectServiceReadFile() {
247
+ const readFile = this.projectService.host.readFile;
248
+ this.projectService.host.readFile = (path: string) => {
249
+ if (isAstroFilePath(path)) {
250
+ this.logger.debug('Read Astro file:', path);
251
+ const astroCode = readFile(path) || '';
252
+ try {
253
+ const isTsFile = true;
254
+ const result = astro2tsx(astroCode, {
255
+ filename: path.split('/').pop(),
256
+ isTsFile,
257
+ });
258
+ const existingSnapshot = this.snapshots.get(path);
259
+ if (existingSnapshot) {
260
+ existingSnapshot.update(astroCode, new SourceMapper(result.map.mappings));
261
+ } else {
262
+ this.snapshots.set(
263
+ path,
264
+ new AstroSnapshot(
265
+ this.typescript,
266
+ path,
267
+ astroCode,
268
+ new SourceMapper(result.map.mappings),
269
+ this.logger,
270
+ isTsFile
271
+ )
272
+ );
273
+ }
274
+ this.logger.log('Successfully read Astro file contents of', path);
275
+ return result.code;
276
+ } catch (e) {
277
+ this.logger.log('Error loading Astro file:', path);
278
+ this.logger.debug('Error:', e);
279
+ }
280
+ } else {
281
+ return readFile(path);
282
+ }
283
+ };
284
+ }
285
+ }
package/src/astro-sys.ts CHANGED
@@ -6,27 +6,27 @@ import { ensureRealAstroFilePath, isVirtualAstroFilePath, toRealAstroFilePath }
6
6
  * This should only be accessed by TS astro module resolution.
7
7
  */
8
8
  export function createAstroSys(logger: Logger) {
9
- const astroSys: ts.System = {
10
- ...ts.sys,
11
- fileExists(path: string) {
12
- return ts.sys.fileExists(ensureRealAstroFilePath(path));
13
- },
14
- readDirectory(path, extensions, exclude, include, depth) {
15
- const extensionsWithAstro = (extensions ?? []).concat('.astro');
9
+ const astroSys: ts.System = {
10
+ ...ts.sys,
11
+ fileExists(path: string) {
12
+ return ts.sys.fileExists(ensureRealAstroFilePath(path));
13
+ },
14
+ readDirectory(path, extensions, exclude, include, depth) {
15
+ const extensionsWithAstro = (extensions ?? []).concat('.astro');
16
16
 
17
- return ts.sys.readDirectory(path, extensionsWithAstro, exclude, include, depth);
18
- }
19
- };
17
+ return ts.sys.readDirectory(path, extensionsWithAstro, exclude, include, depth);
18
+ },
19
+ };
20
20
 
21
- if (ts.sys.realpath) {
22
- const realpath = ts.sys.realpath;
23
- astroSys.realpath = function (path) {
24
- if (isVirtualAstroFilePath(path)) {
25
- return realpath(toRealAstroFilePath(path)) + '.ts';
26
- }
27
- return realpath(path);
28
- };
29
- }
21
+ if (ts.sys.realpath) {
22
+ const realpath = ts.sys.realpath;
23
+ astroSys.realpath = function (path) {
24
+ if (isVirtualAstroFilePath(path)) {
25
+ return realpath(toRealAstroFilePath(path)) + '.ts';
26
+ }
27
+ return realpath(path);
28
+ };
29
+ }
30
30
 
31
- return astroSys;
32
- }
31
+ return astroSys;
32
+ }