@tsrx/typescript-plugin 0.3.33 → 0.3.35

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/src/language.js DELETED
@@ -1,926 +0,0 @@
1
- /** @import { CodeMapping } from '@tsrx/ripple' */
2
- /** @import {TSRXCompileError, VolarMappingsResult} from '@tsrx/ripple' */
3
-
4
- /** @typedef {{ compile_to_volar_mappings(source: string, filename: string, options?: { loose?: boolean }): VolarMappingsResult }} TSRXCompilerModule */
5
-
6
- /** @typedef {Map<string, CodeMapping>} CachedMappings */
7
- /** @typedef {import('typescript').CompilerOptions} CompilerOptions */
8
- /** @typedef {import('@volar/language-core').IScriptSnapshot} IScriptSnapshot */
9
- /** @typedef {import('@volar/language-core').VirtualCode} VirtualCode */
10
- /** @typedef {string | { fsPath: string }} ScriptId */
11
- // Side-effect import: augments @volar/language-core's LanguagePlugin with the `typescript` field.
12
- /** @typedef {typeof import('@volar/typescript')} _VolarTypeScriptAugmentation */
13
- /** @typedef {import('@volar/language-core').LanguagePlugin<ScriptId, VirtualCode>} RippleLanguagePlugin */
14
-
15
- /** @typedef {InstanceType<typeof import('./language.js')["TSRXVirtualCode"]>} TSRXVirtualCodeInstance */
16
-
17
- import ts from 'typescript';
18
- import { forEachEmbeddedCode } from '@volar/language-core';
19
- import fs from 'fs';
20
- import path from 'path';
21
- import { createRequire } from 'module';
22
- import { fileURLToPath } from 'url';
23
- import { createLogging, DEBUG } from './utils.js';
24
-
25
- const require = createRequire(import.meta.url);
26
- const root_dirname = path.dirname(fileURLToPath(import.meta.url));
27
-
28
- const { log, logWarning, logError } = createLogging('[Ripple Language]');
29
- export const RIPPLE_EXTENSIONS = ['.tsrx'];
30
- /** @typedef {[string, string[], string[], string[]]} CompilerCandidate */
31
- /** @type {CompilerCandidate[]} */
32
- export const COMPILER_CANDIDATES = [
33
- [
34
- '@tsrx/ripple',
35
- ['node_modules', '@tsrx', 'ripple'],
36
- ['.tsrx'],
37
- ['@tsrx/ripple', 'ripple', '@ripple-ts/vite-plugin', '@ripple-ts/compat-react'],
38
- ],
39
- [
40
- '@tsrx/react',
41
- ['node_modules', '@tsrx', 'react'],
42
- ['.tsrx'],
43
- ['@tsrx/react', '@tsrx/vite-plugin-react'],
44
- ],
45
- [
46
- '@tsrx/solid',
47
- ['node_modules', '@tsrx', 'solid'],
48
- ['.tsrx'],
49
- ['@tsrx/solid', '@tsrx/vite-plugin-solid'],
50
- ],
51
- [
52
- '@tsrx/preact',
53
- ['node_modules', '@tsrx', 'preact'],
54
- ['.tsrx'],
55
- ['@tsrx/preact', '@tsrx/vite-plugin-preact'],
56
- ],
57
- ];
58
-
59
- /**
60
- * @param {string} file_name
61
- * @returns {boolean}
62
- */
63
- export function is_ripple_file(file_name) {
64
- return RIPPLE_EXTENSIONS.some((extension) => file_name.endsWith(extension));
65
- }
66
-
67
- /**
68
- * @returns {RippleLanguagePlugin}
69
- */
70
- export function getRippleLanguagePlugin() {
71
- log('Creating Ripple language plugin...');
72
-
73
- return {
74
- getLanguageId(fileNameOrUri) {
75
- const file_name =
76
- typeof fileNameOrUri === 'string'
77
- ? fileNameOrUri
78
- : fileNameOrUri.fsPath.replace(/\\/g, '/');
79
- if (is_ripple_file(file_name)) {
80
- log('Identified Ripple file:', file_name);
81
- return 'ripple';
82
- }
83
- },
84
- createVirtualCode(fileNameOrUri, languageId, snapshot) {
85
- if (languageId === 'ripple') {
86
- const file_name = normalizeFileNameOrUri(fileNameOrUri);
87
- const ripple = get_tsrx_compiler(file_name);
88
- if (!ripple) {
89
- logError(`Ripple compiler not found for file: ${file_name}`);
90
- return undefined;
91
- }
92
- log('Creating virtual code for:', file_name);
93
- try {
94
- return new TSRXVirtualCode(file_name, snapshot, ripple);
95
- } catch (err) {
96
- logError('Failed to create virtual code for:', file_name, ':', err);
97
- throw err;
98
- }
99
- }
100
- return undefined;
101
- },
102
- updateVirtualCode(fileNameOrUri, virtualCode, snapshot) {
103
- if (virtualCode instanceof TSRXVirtualCode) {
104
- log('Updating existing virtual code for:', virtualCode.fileName);
105
- virtualCode.update(snapshot);
106
- return virtualCode;
107
- }
108
- return undefined;
109
- },
110
-
111
- typescript: {
112
- extraFileExtensions: RIPPLE_EXTENSIONS.map((extension) => ({
113
- extension: extension.slice(1),
114
- isMixedContent: false,
115
- scriptKind: 7,
116
- })),
117
- /**
118
- * @param {VirtualCode} ripple_code
119
- */
120
- getServiceScript(ripple_code) {
121
- for (const code of forEachEmbeddedCode(ripple_code)) {
122
- if (code.languageId === 'ripple') {
123
- return {
124
- code,
125
- extension: '.tsx',
126
- scriptKind: 4,
127
- };
128
- }
129
- }
130
- return undefined;
131
- },
132
- },
133
- };
134
- }
135
-
136
- /**
137
- * @implements {VirtualCode}
138
- */
139
- export class TSRXVirtualCode {
140
- /** @type {string} */
141
- id = 'root';
142
- /** @type {string} */
143
- languageId = 'ripple';
144
- /** @type {unknown[]} */
145
- codegenStacks = [];
146
- /** @type {TSRXCompilerModule} */
147
- tsrx;
148
- /** @type {string} */
149
- generatedCode = '';
150
- /** @type {VirtualCode['embeddedCodes']} */
151
- embeddedCodes = [];
152
- /** @type {CodeMapping[]} */
153
- mappings = [];
154
- /** @type {TSRXCompileError[]} */
155
- fatalErrors = [];
156
- /** @type {TSRXCompileError[]} */
157
- usageErrors = [];
158
- /** @type {IScriptSnapshot} */
159
- snapshot;
160
- /** @type {IScriptSnapshot} */
161
- sourceSnapshot;
162
- /** @type {string} */
163
- originalCode = '';
164
- /** @type {unknown[]} */
165
- diagnostics = [];
166
- /** @type {CachedMappings | null} */
167
- #mappingGenToSource = null;
168
- /** @type {CachedMappings | null} */
169
- #mappingSourceToGen = null;
170
-
171
- /**
172
- * @param {string} file_name
173
- * @param {IScriptSnapshot} snapshot
174
- * @param {TSRXCompilerModule} tsrx
175
- */
176
- constructor(file_name, snapshot, tsrx) {
177
- log('Initializing TSRXVirtualCode for:', file_name);
178
-
179
- this.fileName = file_name;
180
- this.tsrx = tsrx;
181
- this.snapshot = snapshot;
182
- this.sourceSnapshot = snapshot;
183
- this.originalCode = snapshot.getText(0, snapshot.getLength());
184
-
185
- // Validate ripple compiler
186
- if (!tsrx || typeof tsrx.compile_to_volar_mappings !== 'function') {
187
- logError('Invalid ripple compiler - missing compile_to_volar_mappings method');
188
- throw new Error('Invalid ripple compiler');
189
- }
190
-
191
- this.update(snapshot);
192
- }
193
-
194
- /**
195
- * @param {IScriptSnapshot} snapshot
196
- * @returns {void}
197
- */
198
- update(snapshot) {
199
- log('Updating virtual code for:', this.fileName);
200
-
201
- const newCode = snapshot.getText(0, snapshot.getLength());
202
- const changeRange = snapshot.getChangeRange(this.sourceSnapshot);
203
- this.sourceSnapshot = snapshot;
204
-
205
- // Only clear mapping index - don't update snapshot/originalCode yet
206
- this.#mappingGenToSource = null;
207
- this.#mappingSourceToGen = null;
208
-
209
- this.fatalErrors = [];
210
- this.usageErrors = [];
211
-
212
- /** @type {VolarMappingsResult | undefined} */
213
- let transpiled;
214
-
215
- // Check if a single "." was typed using changeRange
216
- let isDotTyped = false;
217
- let dotPosition = -1;
218
-
219
- log('changeRange:', JSON.stringify(changeRange));
220
-
221
- if (changeRange) {
222
- const changeStart = changeRange.span.start;
223
- const changeEnd = changeStart + changeRange.span.length;
224
- const newEnd = changeStart + changeRange.newLength;
225
-
226
- // Get the old text (what was replaced) from originalCode
227
- const oldText = this.originalCode.substring(changeStart, changeEnd);
228
- // Get the new text (what replaced it) from newCode
229
- const newText = newCode.substring(changeStart, newEnd);
230
-
231
- log('Change details:');
232
- log(' Position:', changeStart, '-', changeEnd, '(length:', changeRange.span.length, ')');
233
- log(' Old text:', JSON.stringify(oldText));
234
- log(' New text:', JSON.stringify(newText), '(length:', changeRange.newLength, ')');
235
-
236
- // Check if a dot was added at the end of the new text
237
- if (newText.endsWith('.')) {
238
- // The dot is at position newEnd - 1
239
- // We need to check the character BEFORE the dot (inside the new text)
240
- const charBeforeDot = newEnd > 1 ? newCode[newEnd - 2] : '';
241
- log(' Char before dot:', JSON.stringify(charBeforeDot));
242
-
243
- if (/[$#_\u200C\u200D\p{ID_Continue}\)\]\}]/u.test(charBeforeDot)) {
244
- isDotTyped = true;
245
- dotPosition = newEnd - 1; // Position of the dot
246
- log('ChangeRange detected dot typed at position', dotPosition);
247
- }
248
- }
249
- }
250
-
251
- try {
252
- // If user typed a ".", compile without it and then stitch it back into
253
- // the generated output so completions can still resolve.
254
- if (isDotTyped && dotPosition >= 0) {
255
- const codeWithoutDot =
256
- newCode.substring(0, dotPosition) + newCode.substring(dotPosition + 1);
257
-
258
- log('Compiling without typed dot at position', dotPosition);
259
- transpiled = this.tsrx.compile_to_volar_mappings(codeWithoutDot, this.fileName, {
260
- loose: true,
261
- });
262
- log('Compilation without dot successful');
263
-
264
- if (transpiled && transpiled.code && transpiled.mappings.length > 0) {
265
- const insertedDotPosition = restore_typed_dot_in_transpiled_code(transpiled, dotPosition);
266
-
267
- if (insertedDotPosition === null) {
268
- logWarning('Failed to restore typed dot into transpiled output');
269
- } else {
270
- log('Inserted typed dot at generated position', insertedDotPosition);
271
- }
272
- }
273
- } else {
274
- // Normal compilation
275
- log('Compiling Ripple code...');
276
- transpiled = this.tsrx.compile_to_volar_mappings(newCode, this.fileName, {
277
- loose: true,
278
- });
279
- log('Compilation successful, generated code length:', transpiled?.code?.length || 0);
280
- }
281
- } catch (e) {
282
- const error = /** @type {TSRXCompileError} */ (e);
283
- logError('Ripple compilation failed for', this.fileName, ':', error);
284
- error.type = 'fatal';
285
- this.fatalErrors.push(error);
286
- }
287
-
288
- if (transpiled && transpiled.code) {
289
- // Successful compilation - update everything
290
- this.originalCode = newCode;
291
- this.generatedCode = transpiled.code;
292
- this.mappings = transpiled.mappings ?? [];
293
- this.usageErrors = transpiled.errors;
294
-
295
- const cssMappings = transpiled.cssMappings;
296
- if (cssMappings.length > 0) {
297
- log('Creating', cssMappings.length, 'CSS embedded codes');
298
-
299
- this.embeddedCodes = cssMappings.map((mapping, index) => {
300
- const cssContent = /** @type {string} */ (mapping.data?.customData?.content);
301
- log(
302
- `CSS region ${index}: \
303
- offset ${mapping.sourceOffsets[0]}-${mapping.sourceOffsets[0] + mapping.lengths[0]}, \
304
- length ${mapping.lengths[0]}`,
305
- );
306
-
307
- return {
308
- id: /** @type {string} */ (mapping.data?.customData?.embeddedId),
309
- languageId: 'css',
310
- snapshot: {
311
- getText: (/** @type {number} */ start, /** @type {number} */ end) =>
312
- cssContent.substring(start, end),
313
- getLength: () => mapping.lengths[0],
314
- getChangeRange: () => undefined,
315
- },
316
- mappings: [mapping],
317
- embeddedCodes: [],
318
- };
319
- });
320
- } else {
321
- this.embeddedCodes = [];
322
- }
323
-
324
- if (DEBUG) {
325
- log('CSS embedded codes:', (this.embeddedCodes || []).length);
326
- log('Using transpiled code, mapping count:', this.mappings.length);
327
- log('Original code length:', newCode.length);
328
- log('Generated code length:', this.generatedCode.length);
329
- log('Last 100 chars of original:', JSON.stringify(newCode.slice(-100)));
330
- log('Last 200 chars of generated:', JSON.stringify(this.generatedCode.slice(-200)));
331
- log('Last few mappings:');
332
- const startIdx = Math.max(0, this.mappings.length - 5);
333
- for (let i = startIdx; i < this.mappings.length; i++) {
334
- const m = this.mappings[i];
335
- log(
336
- ` Mapping ${i}: source[${m.sourceOffsets[0]}:${m.sourceOffsets[0] + m.lengths[0]}] -> gen[${m.generatedOffsets[0]}:${m.generatedOffsets[0] + m.lengths[0]}], len=${m.lengths[0]}, completion=${m.data?.completion}`,
337
- );
338
- }
339
- }
340
-
341
- this.snapshot = /** @type {IScriptSnapshot} */ ({
342
- getText: (start, end) => this.generatedCode.substring(start, end),
343
- getLength: () => this.generatedCode.length,
344
- getChangeRange: () => undefined,
345
- });
346
- } else {
347
- // When compilation fails, show where it failed and disable all
348
- // TypeScript diagnostics until the compilation error is fixed
349
- log('Compilation failed, only display where the compilation error occurred.');
350
-
351
- this.originalCode = newCode;
352
- this.generatedCode = newCode;
353
-
354
- // Create 1:1 mappings for the entire content
355
- this.mappings = [
356
- {
357
- sourceOffsets: [0],
358
- generatedOffsets: [0],
359
- lengths: [newCode.length],
360
- generatedLengths: [newCode.length],
361
- data: {
362
- verification: true, // disable TS since we're using source code as generated code
363
- customData: {},
364
- },
365
- },
366
- ];
367
-
368
- // Extract CSS from <style>...</style> tags for embedded codes
369
- this.embeddedCodes = extractCssFromSource(newCode);
370
-
371
- this.snapshot = /** @type {IScriptSnapshot} */ ({
372
- getText: (start, end) => this.generatedCode.substring(start, end),
373
- getLength: () => this.generatedCode.length,
374
- getChangeRange: () => undefined,
375
- });
376
- }
377
- }
378
-
379
- #buildMappingCache() {
380
- if (this.#mappingGenToSource || this.#mappingSourceToGen) {
381
- return;
382
- }
383
-
384
- this.#mappingGenToSource = new Map();
385
- this.#mappingSourceToGen = new Map();
386
-
387
- var mapping, genStart, genLength, genEnd, genKey;
388
- var sourceStart, sourceLength, sourceEnd, sourceKey;
389
- for (var i = 0; i < this.mappings.length; i++) {
390
- mapping = this.mappings[i];
391
-
392
- genStart = mapping.generatedOffsets[0];
393
- genLength = mapping.generatedLengths[0];
394
- genEnd = genStart + genLength;
395
- genKey = `${genStart}-${genEnd}`;
396
- this.#mappingGenToSource.set(genKey, mapping);
397
-
398
- sourceStart = mapping.sourceOffsets[0];
399
- sourceLength = mapping.lengths[0];
400
- sourceEnd = sourceStart + sourceLength;
401
- sourceKey = `${sourceStart}-${sourceEnd}`;
402
- this.#mappingSourceToGen.set(sourceKey, mapping);
403
- }
404
- }
405
-
406
- /**
407
- * Find mapping by generated range
408
- * @param {number} start - The start offset of the range
409
- * @param {number} end - The end offset of the range
410
- * @returns {CodeMapping | null} The mapping for this range, or null if not found
411
- */
412
- findMappingByGeneratedRange(start, end) {
413
- this.#buildMappingCache();
414
- return /** @type {CachedMappings} */ (this.#mappingGenToSource).get(`${start}-${end}`) ?? null;
415
- }
416
-
417
- /**
418
- * Find mapping by source range
419
- * @param {number} start - The start offset of the range
420
- * @param {number} end - The end offset of the range
421
- * @returns {CodeMapping | null} The mapping for this range, or null if not found
422
- */
423
- findMappingBySourceRange(start, end) {
424
- this.#buildMappingCache();
425
- return /** @type {CachedMappings} */ (this.#mappingSourceToGen).get(`${start}-${end}`) ?? null;
426
- }
427
- }
428
-
429
- /**
430
- * Extract CSS content from <style>...</style> tags in source code
431
- * @param {string} code - The source code to extract CSS from
432
- * @returns {VirtualCode[]} Array of embedded CSS virtual codes
433
- */
434
- function extractCssFromSource(code) {
435
- /** @type {VirtualCode[]} */
436
- const embeddedCodes = [];
437
- const styleRegex = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
438
- let match;
439
- let index = 0;
440
-
441
- while ((match = styleRegex.exec(code)) !== null) {
442
- const fullMatch = match[0];
443
- const cssContent = match[1];
444
- const styleTagStart = match.index;
445
- const openTagEnd = fullMatch.indexOf('>') + 1;
446
- const cssStart = styleTagStart + openTagEnd;
447
- const cssLength = cssContent.length;
448
-
449
- log(`Extracted CSS region ${index}: offset ${cssStart}, length ${cssLength}`);
450
-
451
- /** @type {CodeMapping} */
452
- const mapping = {
453
- sourceOffsets: [cssStart],
454
- generatedOffsets: [0],
455
- lengths: [cssLength],
456
- generatedLengths: [cssLength],
457
- data: {
458
- verification: true,
459
- completion: true,
460
- semantic: true,
461
- navigation: true,
462
- structure: true,
463
- format: false,
464
- customData: {
465
- content: cssContent,
466
- embeddedId: `style_${index}`,
467
- },
468
- },
469
- };
470
-
471
- embeddedCodes.push({
472
- id: `style_${index}`,
473
- languageId: 'css',
474
- snapshot: {
475
- getText: (start, end) => cssContent.substring(start, end),
476
- getLength: () => cssLength,
477
- getChangeRange: () => undefined,
478
- },
479
- mappings: [mapping],
480
- embeddedCodes: [],
481
- });
482
-
483
- index++;
484
- }
485
-
486
- if (embeddedCodes.length > 0) {
487
- log(`Extracted ${embeddedCodes.length} CSS embedded codes from style tags`);
488
- }
489
-
490
- return embeddedCodes;
491
- }
492
-
493
- /**
494
- * Insert a typed dot back into the transpiled code and update mappings so the
495
- * source and generated offsets stay aligned for completion requests.
496
- * @param {VolarMappingsResult} transpiled
497
- * @param {number} dotPosition
498
- * @returns {number | null}
499
- */
500
- function restore_typed_dot_in_transpiled_code(transpiled, dotPosition) {
501
- let dot_mapping = null;
502
-
503
- for (const mapping of transpiled.mappings) {
504
- const source_end = mapping.sourceOffsets[0] + mapping.lengths[0];
505
- if (source_end === dotPosition) {
506
- dot_mapping = mapping;
507
- break;
508
- }
509
- }
510
-
511
- if (!dot_mapping) {
512
- return null;
513
- }
514
-
515
- const generated_length = dot_mapping.generatedLengths[0];
516
- const insertedDotPosition = dot_mapping.generatedOffsets[0] + generated_length;
517
-
518
- transpiled.code =
519
- transpiled.code.substring(0, insertedDotPosition) +
520
- '.' +
521
- transpiled.code.substring(insertedDotPosition);
522
-
523
- // Create a separate 1:1 mapping for the dot character instead of extending
524
- // the existing mapping. When source and generated lengths differ (e.g.
525
- // #ripple → _$__u0023_ripple), Volar's translateOffset uses
526
- // Math.min(relativePos, toLength) which would map the cursor after the dot
527
- // to the middle of the generated identifier instead of after it.
528
-
529
- /** @type {CodeMapping} */
530
- const new_dot_mapping = {
531
- sourceOffsets: [dotPosition],
532
- generatedOffsets: [insertedDotPosition],
533
- lengths: [1],
534
- generatedLengths: [1],
535
- data: { ...dot_mapping.data },
536
- };
537
-
538
- // Find the index to insert after dot_mapping
539
- const dot_mapping_index = transpiled.mappings.indexOf(dot_mapping);
540
- transpiled.mappings.splice(dot_mapping_index + 1, 0, new_dot_mapping);
541
-
542
- for (const mapping of transpiled.mappings) {
543
- if (
544
- mapping !== dot_mapping &&
545
- mapping !== new_dot_mapping &&
546
- mapping.generatedOffsets[0] >= insertedDotPosition
547
- ) {
548
- mapping.generatedOffsets[0] += 1;
549
- }
550
- if (
551
- mapping !== dot_mapping &&
552
- mapping !== new_dot_mapping &&
553
- mapping.sourceOffsets[0] >= dotPosition
554
- ) {
555
- mapping.sourceOffsets[0] += 1;
556
- }
557
- }
558
-
559
- return insertedDotPosition;
560
- }
561
-
562
- /**
563
- * @template T
564
- * @param {{ options?: CompilerOptions } & T} config
565
- * @returns {{ options: CompilerOptions } & T}
566
- */
567
- export const resolveConfig = (config) => {
568
- const baseOptions = config.options ?? /** @type {CompilerOptions} */ ({});
569
- /** @type {CompilerOptions} */
570
- const options = { ...baseOptions };
571
-
572
- // Default target: align with modern bundlers while staying configurable.
573
- if (options.target === undefined) {
574
- options.target = ts.ScriptTarget.ESNext;
575
- }
576
-
577
- /** @param {string} libName */
578
- const normalizeLibName = (libName) => {
579
- if (typeof libName !== 'string' || libName.length === 0) {
580
- return undefined;
581
- }
582
- const trimmed = libName.trim();
583
- if (trimmed.startsWith('lib.')) {
584
- return trimmed.toLowerCase();
585
- }
586
- return `lib.${trimmed.toLowerCase().replace(/\s+/g, '').replace(/_/g, '.')}\.d.ts`;
587
- };
588
-
589
- const normalizedLibs = new Set(
590
- (options.lib ?? []).map(normalizeLibName).filter((lib) => typeof lib === 'string'),
591
- );
592
-
593
- if (normalizedLibs.size === 0) {
594
- const host = ts.createCompilerHost(options);
595
- const defaultLibFileName = host.getDefaultLibFileName(options).toLowerCase();
596
- normalizedLibs.add(defaultLibFileName);
597
- normalizedLibs.add('lib.dom.d.ts');
598
- normalizedLibs.add('lib.dom.iterable.d.ts');
599
- }
600
-
601
- options.lib = [...normalizedLibs];
602
-
603
- // Default typeRoots: automatically discover @types like tsserver.
604
- if (!options.types) {
605
- const host = ts.createCompilerHost(options);
606
- const typeRoots = ts.getEffectiveTypeRoots(options, host);
607
- if (typeRoots && typeRoots.length > 0) {
608
- options.typeRoots = typeRoots;
609
- }
610
- }
611
-
612
- return {
613
- ...config,
614
- options,
615
- };
616
- };
617
-
618
- /** @type {Map<string, string | null>} */
619
- export const path2RipplePathMap = new Map();
620
- /** @type {Map<string, string>} */
621
- const pathToTypesCache = new Map();
622
- /** @type {Map<string, RegExpMatchArray>} */
623
- const typeNameMatchCache = new Map();
624
- /** @type {Map<string, { name: string | null, dependencies: Set<string> } | null>} */
625
- const pathToPackageManifestCache = new Map();
626
-
627
- /**
628
- * @param {ScriptId} fileNameOrUri
629
- * @returns {string}
630
- */
631
- export function normalizeFileNameOrUri(fileNameOrUri) {
632
- return typeof fileNameOrUri === 'string'
633
- ? fileNameOrUri
634
- : fileNameOrUri.fsPath.replace(/\\/g, '/');
635
- }
636
-
637
- /**
638
- * @param {string} start_dir
639
- * @param {(file_path: import('fs').PathLike) => boolean} [exists_sync]
640
- * @returns {{ name: string | null, dependencies: Set<string> } | null}
641
- */
642
- function get_nearest_package_manifest(start_dir, exists_sync = fs.existsSync) {
643
- let current_dir = start_dir;
644
- /** @type {string[]} */
645
- const visited_dirs = [];
646
-
647
- while (current_dir) {
648
- if (pathToPackageManifestCache.has(current_dir)) {
649
- const cached_manifest = pathToPackageManifestCache.get(current_dir) ?? null;
650
- for (const visited_dir of visited_dirs) {
651
- pathToPackageManifestCache.set(visited_dir, cached_manifest);
652
- }
653
- return cached_manifest;
654
- }
655
-
656
- visited_dirs.push(current_dir);
657
-
658
- const package_json_path = path.join(current_dir, 'package.json');
659
- if (exists_sync(package_json_path)) {
660
- try {
661
- const package_json = JSON.parse(fs.readFileSync(package_json_path, 'utf8'));
662
- const dependencies = new Set([
663
- ...Object.keys(package_json.dependencies ?? {}),
664
- ...Object.keys(package_json.devDependencies ?? {}),
665
- ...Object.keys(package_json.peerDependencies ?? {}),
666
- ...Object.keys(package_json.optionalDependencies ?? {}),
667
- ]);
668
- const package_manifest = {
669
- name: typeof package_json.name === 'string' ? package_json.name : null,
670
- dependencies,
671
- };
672
-
673
- for (const visited_dir of visited_dirs) {
674
- pathToPackageManifestCache.set(visited_dir, package_manifest);
675
- }
676
-
677
- return package_manifest;
678
- } catch {
679
- for (const visited_dir of visited_dirs) {
680
- pathToPackageManifestCache.set(visited_dir, null);
681
- }
682
- return null;
683
- }
684
- }
685
-
686
- const parent_dir = path.dirname(current_dir);
687
- if (parent_dir === current_dir) {
688
- break;
689
- }
690
- current_dir = parent_dir;
691
- }
692
-
693
- for (const visited_dir of visited_dirs) {
694
- pathToPackageManifestCache.set(visited_dir, null);
695
- }
696
-
697
- return null;
698
- }
699
-
700
- /**
701
- * @param {{ name: string | null, dependencies: Set<string> } | null} package_manifest
702
- * @param {string} compiler_name
703
- * @param {string[]} package_hints
704
- * @returns {boolean}
705
- */
706
- function package_manifest_matches_compiler(package_manifest, compiler_name, package_hints) {
707
- if (!package_manifest) {
708
- return false;
709
- }
710
-
711
- if (
712
- package_manifest.name === compiler_name ||
713
- package_hints.includes(package_manifest.name ?? '')
714
- ) {
715
- return true;
716
- }
717
-
718
- if (package_manifest.dependencies.has(compiler_name)) {
719
- return true;
720
- }
721
-
722
- for (const package_hint of package_hints) {
723
- if (package_manifest.dependencies.has(package_hint)) {
724
- return true;
725
- }
726
- }
727
-
728
- return false;
729
- }
730
-
731
- /**
732
- * @param {string} normalized_file_name
733
- * @returns {TSRXCompilerModule | undefined}
734
- */
735
- function get_tsrx_compiler(normalized_file_name) {
736
- const compiler_path = get_compiler_entry_for_file(normalized_file_name);
737
- if (compiler_path) {
738
- return require(compiler_path);
739
- }
740
- }
741
-
742
- /**
743
- * @param {string} normalized_file_name
744
- * @param {(file_path: import('fs').PathLike) => boolean} [exists_sync]
745
- * @param {Map<string, string | null>} [compiler_path_map]
746
- * @returns {string | undefined}
747
- */
748
- export function find_workspace_compiler_entry_for_file(
749
- normalized_file_name,
750
- exists_sync = fs.existsSync,
751
- compiler_path_map = path2RipplePathMap,
752
- ) {
753
- const parts = normalized_file_name.split('/');
754
- const ext = path.extname(normalized_file_name);
755
-
756
- for (let i = parts.length - 2; i >= 0; i--) {
757
- const dir = parts.slice(0, i + 1).join('/');
758
- const cache_key = dir + '\0' + ext;
759
-
760
- if (!compiler_path_map.has(cache_key)) {
761
- /** @type {Array<[string, string, string[]]>} */
762
- const available_candidates = [];
763
- for (const [
764
- compiler_name,
765
- compiler_dir_parts,
766
- supported_extensions,
767
- package_hints,
768
- ] of COMPILER_CANDIDATES) {
769
- if (!supported_extensions.includes(ext)) {
770
- continue;
771
- }
772
- const full_path = [dir, ...compiler_dir_parts, 'src', 'index.js'].join('/');
773
- if (exists_sync(full_path)) {
774
- available_candidates.push([compiler_name, full_path, package_hints]);
775
- }
776
- }
777
-
778
- let found_path = null;
779
- if (available_candidates.length > 0) {
780
- const package_manifest = get_nearest_package_manifest(dir, exists_sync);
781
- const preferred_candidate = available_candidates.find(([compiler_name, , package_hints]) =>
782
- package_manifest_matches_compiler(package_manifest, compiler_name, package_hints),
783
- );
784
- found_path = preferred_candidate?.[1] ?? available_candidates[0][1];
785
- log('Found tsrx compiler at:', found_path, 'for extension:', ext);
786
- }
787
-
788
- compiler_path_map.set(cache_key, found_path);
789
- }
790
-
791
- const compiler_path = compiler_path_map.get(cache_key);
792
- if (compiler_path) {
793
- return compiler_path;
794
- }
795
- }
796
- }
797
-
798
- /**
799
- * @param {string} normalized_file_name
800
- * @returns {string | undefined}
801
- */
802
- export function get_compiler_entry_for_file(normalized_file_name) {
803
- const ext = path.extname(normalized_file_name);
804
- const package_manifest = get_nearest_package_manifest(path.dirname(normalized_file_name));
805
-
806
- const workspace_compiler_path = find_workspace_compiler_entry_for_file(normalized_file_name);
807
- if (workspace_compiler_path) {
808
- return workspace_compiler_path;
809
- }
810
-
811
- const warn_message = `No supported tsrx compiler found in workspace for ${normalized_file_name}.`;
812
-
813
- // Fallback: look for a packaged compiler.
814
- let current_dir = root_dirname;
815
-
816
- while (current_dir) {
817
- /** @type {Array<[string, string, string[]]>} */
818
- const available_candidates = [];
819
- for (const [
820
- compiler_name,
821
- compiler_dir_parts,
822
- supported_extensions,
823
- package_hints,
824
- ] of COMPILER_CANDIDATES) {
825
- if (!supported_extensions.includes(ext)) {
826
- continue;
827
- }
828
- const full_path = path.join(current_dir, ...compiler_dir_parts);
829
- const entry_path = path.join(full_path, 'src', 'index.js');
830
- if (fs.existsSync(entry_path)) {
831
- available_candidates.push([compiler_name, entry_path, package_hints]);
832
- }
833
- }
834
-
835
- if (available_candidates.length > 0) {
836
- const preferred_candidate = available_candidates.find(([compiler_name, , package_hints]) =>
837
- package_manifest_matches_compiler(package_manifest, compiler_name, package_hints),
838
- );
839
- const entry_path = preferred_candidate?.[1] ?? available_candidates[0][1];
840
- logWarning(`${warn_message} Using packaged version at ${entry_path}`);
841
- return entry_path;
842
- }
843
-
844
- const parent_dir = path.dirname(current_dir);
845
- if (parent_dir === current_dir) {
846
- break;
847
- }
848
- current_dir = parent_dir;
849
- }
850
-
851
- return undefined;
852
- }
853
-
854
- /**
855
- * @param {string} typesFilePath
856
- * @returns {string | undefined}
857
- */
858
- export function getCachedTypeDefinitionFile(typesFilePath) {
859
- const cached = pathToTypesCache.get(typesFilePath);
860
- if (cached) {
861
- return cached;
862
- }
863
-
864
- if (!fs.existsSync(typesFilePath)) {
865
- logWarning(`Types file does not exist at path: ${typesFilePath}`);
866
- return;
867
- }
868
-
869
- log(`Found ripple types at: ${typesFilePath}`);
870
-
871
- // Read the file to find the class definition offset
872
- const fileContent = fs.readFileSync(typesFilePath, 'utf8');
873
-
874
- if (!fileContent) {
875
- logWarning(`Failed to read content of types file at: ${typesFilePath}`);
876
- return;
877
- }
878
-
879
- pathToTypesCache.set(typesFilePath, fileContent);
880
- return fileContent;
881
- }
882
-
883
- /**
884
- * @param {string} typeName
885
- * @param {string} text
886
- * @returns {RegExpMatchArray | undefined}
887
- */
888
- export function getCachedTypeMatches(typeName, text) {
889
- const cached = typeNameMatchCache.get(typeName);
890
- if (cached) {
891
- return cached;
892
- }
893
-
894
- const searchPattern = new RegExp(
895
- `(?:export\\s+(?:declare\\s+)?|declare\\s+)(class|function)\\s+${typeName}`,
896
- );
897
-
898
- const match = text.match(searchPattern);
899
-
900
- if (match && match.index !== undefined) {
901
- typeNameMatchCache.set(typeName, match);
902
- return match;
903
- }
904
-
905
- return;
906
- }
907
-
908
- /**
909
- * @param {string} normalized_file_name
910
- * @returns {string | undefined}
911
- */
912
- export function get_compiler_dir_for_file(normalized_file_name) {
913
- const entry = get_compiler_entry_for_file(normalized_file_name);
914
- if (entry) {
915
- // Walk up from .../src/index.js to the package root
916
- return path.dirname(path.dirname(entry));
917
- }
918
- }
919
-
920
- export { get_compiler_dir_for_file as getRippleDirForFile };
921
-
922
- /** Reset module-level state used in tests. */
923
- export function _reset_for_test() {
924
- path2RipplePathMap.clear();
925
- pathToPackageManifestCache.clear();
926
- }