@astrojs/ts-plugin 0.3.0 → 0.4.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.
Files changed (52) hide show
  1. package/.turbo/turbo-build.log +5 -2
  2. package/CHANGELOG.md +10 -0
  3. package/LICENSE +36 -0
  4. package/dist/astro-snapshots.js +48 -47
  5. package/dist/astro-sys.js +22 -35
  6. package/dist/astro2tsx.js +22 -26
  7. package/dist/index.js +38 -39
  8. package/dist/language-service/completions.js +42 -42
  9. package/dist/language-service/definition.js +22 -40
  10. package/dist/language-service/diagnostics.js +15 -18
  11. package/dist/language-service/file-references.js +52 -0
  12. package/dist/language-service/find-references.js +20 -38
  13. package/dist/language-service/implementation.js +18 -37
  14. package/dist/language-service/index.js +26 -42
  15. package/dist/language-service/line-column-offset.js +44 -0
  16. package/dist/language-service/rename.js +24 -37
  17. package/dist/logger.js +18 -5
  18. package/dist/module-loader.js +28 -22
  19. package/dist/project-astro-files.js +125 -0
  20. package/dist/utils.js +34 -19
  21. package/dist/workers/TSXService.js +39 -0
  22. package/dist/workers/TSXWorker.js +9 -0
  23. package/package.json +21 -11
  24. package/src/astro-snapshots.ts +17 -26
  25. package/src/astro-sys.ts +1 -1
  26. package/src/astro2tsx.ts +5 -46
  27. package/src/index.ts +34 -34
  28. package/src/language-service/completions.ts +15 -4
  29. package/src/language-service/definition.ts +2 -7
  30. package/src/language-service/diagnostics.ts +1 -2
  31. package/src/language-service/file-references.ts +31 -0
  32. package/src/language-service/find-references.ts +2 -2
  33. package/src/language-service/implementation.ts +2 -7
  34. package/src/language-service/index.ts +10 -25
  35. package/src/language-service/line-column-offset.ts +21 -0
  36. package/src/language-service/rename.ts +2 -2
  37. package/src/module-loader.ts +12 -4
  38. package/src/project-astro-files.ts +130 -0
  39. package/src/utils.ts +29 -20
  40. package/src/workers/TSXService.ts +11 -0
  41. package/src/workers/TSXWorker.ts +8 -0
  42. package/test/fixtures/MyAstroComponent.astro +5 -0
  43. package/test/fixtures/script.ts +9 -0
  44. package/test/fixtures/tsconfig.json +3 -0
  45. package/test/runTest.js +41 -0
  46. package/test/suite/extension.test.js +68 -0
  47. package/test/suite/index.js +38 -0
  48. package/test/tsconfig.json +14 -0
  49. package/test/utils.js +65 -0
  50. package/tsconfig.json +2 -1
  51. package/dist/source-mapper.js +0 -110
  52. package/src/source-mapper.ts +0 -107
@@ -1,8 +1,8 @@
1
+ import { EncodedSourceMap, originalPositionFor, TraceMap } from '@jridgewell/trace-mapping';
1
2
  import type ts from 'typescript/lib/tsserverlibrary';
2
3
  import { astro2tsx } from './astro2tsx.js';
3
- import { Logger } from './logger.js';
4
- import { SourceMapper } from './source-mapper.js';
5
- import { isAstroFilePath, isNoTextSpanInGeneratedCode } from './utils.js';
4
+ import type { Logger } from './logger.js';
5
+ import { isAstroFilePath } from './utils.js';
6
6
 
7
7
  export class AstroSnapshot {
8
8
  private scriptInfo?: ts.server.ScriptInfo;
@@ -13,23 +13,18 @@ export class AstroSnapshot {
13
13
  private typescript: typeof ts,
14
14
  private fileName: string,
15
15
  private astroCode: string,
16
- private mapper: SourceMapper,
17
- private logger: Logger,
18
- public readonly isTsFile: boolean
16
+ private traceMap: TraceMap,
17
+ private logger: Logger
19
18
  ) {}
20
19
 
21
- update(astroCode: string, mapper: SourceMapper) {
20
+ update(astroCode: string, traceMap: TraceMap) {
22
21
  this.astroCode = astroCode;
23
- this.mapper = mapper;
22
+ this.traceMap = traceMap;
24
23
  this.lineOffsets = undefined;
25
24
  this.log('Updated Snapshot');
26
25
  }
27
26
 
28
27
  getOriginalTextSpan(textSpan: ts.TextSpan): ts.TextSpan | null {
29
- if (!isNoTextSpanInGeneratedCode(this.getText(), textSpan)) {
30
- return null;
31
- }
32
-
33
28
  const start = this.getOriginalOffset(textSpan.start);
34
29
  if (start === -1) {
35
30
  return null;
@@ -50,16 +45,17 @@ export class AstroSnapshot {
50
45
  this.toggleMappingMode(true);
51
46
  const lineOffset = this.scriptInfo.positionToLineOffset(generatedOffset);
52
47
  this.debug('try convert offset', generatedOffset, '/', lineOffset);
53
- const original = this.mapper.getOriginalPosition({
54
- line: lineOffset.line - 1,
55
- character: lineOffset.offset - 1,
48
+ const original = originalPositionFor(this.traceMap, {
49
+ line: lineOffset.line,
50
+ column: lineOffset.offset,
56
51
  });
57
52
  this.toggleMappingMode(false);
58
- if (original.line === -1) {
53
+
54
+ if (!original.line) {
59
55
  return -1;
60
56
  }
61
57
 
62
- const originalOffset = this.scriptInfo.lineOffsetToPosition(original.line + 1, original.character + 1);
58
+ const originalOffset = this.scriptInfo.lineOffsetToPosition(original.line, original.column);
63
59
  this.debug('converted offset to', original, '/', originalOffset);
64
60
  return originalOffset;
65
61
  }
@@ -250,14 +246,10 @@ export class AstroSnapshotManager {
250
246
  this.logger.debug('Read Astro file:', path);
251
247
  const astroCode = readFile(path) || '';
252
248
  try {
253
- const isTsFile = true;
254
- const result = astro2tsx(astroCode, {
255
- filename: path.split('/').pop(),
256
- isTsFile,
257
- });
249
+ const result = astro2tsx(astroCode, path);
258
250
  const existingSnapshot = this.snapshots.get(path);
259
251
  if (existingSnapshot) {
260
- existingSnapshot.update(astroCode, new SourceMapper(result.map.mappings));
252
+ existingSnapshot.update(astroCode, new TraceMap(result.map as EncodedSourceMap));
261
253
  } else {
262
254
  this.snapshots.set(
263
255
  path,
@@ -265,9 +257,8 @@ export class AstroSnapshotManager {
265
257
  this.typescript,
266
258
  path,
267
259
  astroCode,
268
- new SourceMapper(result.map.mappings),
269
- this.logger,
270
- isTsFile
260
+ new TraceMap(result.map as EncodedSourceMap),
261
+ this.logger
271
262
  )
272
263
  );
273
264
  }
package/src/astro-sys.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import ts from 'typescript';
2
- import { Logger } from './logger.js';
2
+ import type { Logger } from './logger.js';
3
3
  import { ensureRealAstroFilePath, isVirtualAstroFilePath, toRealAstroFilePath } from './utils.js';
4
4
 
5
5
  /**
package/src/astro2tsx.ts CHANGED
@@ -1,49 +1,8 @@
1
- import type { FileMapping } from './source-mapper';
1
+ import type { TSXResult } from '@astrojs/compiler/shared/types';
2
+ import { convertToTSX } from './workers/TSXService';
2
3
 
3
- // Note that this is a bit of a hack until the new compiler with proper
4
- // source map support.
4
+ export function astro2tsx(content: string, fileName: string): TSXResult {
5
+ const tsx = convertToTSX(content, { sourcefile: fileName });
5
6
 
6
- interface Astro2TSXOptions {
7
- filename: string | undefined;
8
- isTsFile: boolean;
9
- }
10
-
11
- interface Astro2TSXResult {
12
- code: string;
13
- map: {
14
- mappings: FileMapping;
15
- };
16
- }
17
-
18
- export function astro2tsx(code: string, options: Astro2TSXOptions): Astro2TSXResult {
19
- const compiled = transformContent(code);
20
-
21
- const result: Astro2TSXResult = {
22
- code: compiled,
23
- map: {
24
- mappings: [],
25
- },
26
- };
27
-
28
- return result;
29
- }
30
-
31
- // This is hacky but it works for now
32
- function addProps(content: string): string {
33
- let defaultExportType = 'Record<string, any>';
34
-
35
- if (/(interface|type) Props/.test(content)) {
36
- defaultExportType = 'Props';
37
- }
38
-
39
- return '\n' + `export default function (props: ${defaultExportType}): string;`;
40
- }
41
-
42
- function transformContent(content: string) {
43
- const ts = content.replace(/---/g, '///');
44
- return (
45
- ts +
46
- // Add TypeScript definitions
47
- addProps(ts)
48
- );
7
+ return tsx;
49
8
  }
package/src/index.ts CHANGED
@@ -3,12 +3,17 @@ import { AstroSnapshotManager } from './astro-snapshots.js';
3
3
  import { decorateLanguageService } from './language-service/index.js';
4
4
  import { Logger } from './logger.js';
5
5
  import { patchModuleLoader } from './module-loader.js';
6
+ import { ProjectAstroFilesManager } from './project-astro-files.js';
7
+ import { getConfigPathForProject, readProjectAstroFilesFromFs } from './utils.js';
6
8
 
7
- function init(modules: { typescript: typeof ts }) {
8
- function create(info: ts.server.PluginCreateInfo) {
9
+ function init(modules: { typescript: typeof import('typescript/lib/tsserverlibrary') }) {
10
+ const ts = modules.typescript;
11
+
12
+ function create(info: ts.server.PluginCreateInfo): ts.LanguageService {
9
13
  const logger = new Logger(info.project.projectService.logger);
14
+ const parsedCommandLine = info.languageServiceHost.getParsedCommandLine?.(getConfigPathForProject(info.project));
10
15
 
11
- if (!isAstroProject(info)) {
16
+ if (!isAstroProject(info.project, parsedCommandLine)) {
12
17
  logger.log('Detected that this is not an Astro project, abort patching TypeScript');
13
18
  return info.languageService;
14
19
  }
@@ -16,47 +21,42 @@ function init(modules: { typescript: typeof ts }) {
16
21
  logger.log('Starting Astro plugin');
17
22
 
18
23
  const snapshotManager = new AstroSnapshotManager(modules.typescript, info.project.projectService, logger);
24
+ if (parsedCommandLine) {
25
+ new ProjectAstroFilesManager(
26
+ modules.typescript,
27
+ info.project,
28
+ info.serverHost,
29
+ snapshotManager,
30
+ parsedCommandLine
31
+ );
32
+ }
19
33
 
20
- patchCompilerOptions(info.project);
21
34
  patchModuleLoader(logger, snapshotManager, modules.typescript, info.languageServiceHost, info.project);
22
35
  return decorateLanguageService(info.languageService, snapshotManager, logger);
23
36
  }
24
37
 
25
- function getExternalFiles(_project: ts.server.ConfiguredProject) {
26
- // Needed so the ambient definitions are known inside the tsx files
27
- /*const astroTsPath = dirname(require.resolve('astro2tsx'));
28
- const astroTsxFiles = [
29
- './astro-shims.d.ts',
30
- './astro-jsx.d.ts',
31
- './astro-native-jsx.d.ts'
32
- ].map((f) => modules.typescript.sys.resolvePath(resolve(astroTsPath, f)));
33
- return astroTsxFiles;*/
34
- return [];
38
+ function getExternalFiles(project: ts.server.ConfiguredProject) {
39
+ return ProjectAstroFilesManager.getInstance(project.getProjectName())?.getFiles() ?? [];
35
40
  }
36
41
 
37
- function patchCompilerOptions(project: ts.server.Project) {
38
- const compilerOptions = project.getCompilerOptions();
39
- // Patch needed because astro2tsx creates jsx/tsx files
40
- compilerOptions.jsx = modules.typescript.JsxEmit.Preserve;
42
+ function isAstroProject(project: ts.server.Project, parsedCommandLine: ts.ParsedCommandLine | undefined) {
43
+ if (parsedCommandLine) {
44
+ const astroFiles = readProjectAstroFilesFromFs(ts, project, parsedCommandLine);
41
45
 
42
- // detect which JSX namespace to use (astro | astronative) if not specified or not compatible
43
- if (!compilerOptions.jsxFactory?.startsWith('astro')) {
44
- // Default to regular astro, this causes the usage of the "astro.JSX" namespace
45
- // We don't need to add a switch for astro-native because the jsx is only relevant
46
- // within Astro files, which this plugin does not deal with.
47
- compilerOptions.jsxFactory = 'astro.createElement';
46
+ if (astroFiles.length > 0) return true;
48
47
  }
49
- }
50
48
 
51
- function isAstroProject(info: ts.server.PluginCreateInfo) {
52
- // Add more checks like "no Astro file found" or "no config file found"?
53
- const compilerOptions = info.project.getCompilerOptions();
54
- const isNoJsxProject =
55
- (!compilerOptions.jsx || compilerOptions.jsx === modules.typescript.JsxEmit.Preserve) &&
56
- (!compilerOptions.jsxFactory || compilerOptions.jsxFactory.startsWith('astro')) &&
57
- !compilerOptions.jsxFragmentFactory &&
58
- !compilerOptions.jsxImportSource;
59
- return isNoJsxProject;
49
+ try {
50
+ const compilerOptions = project.getCompilerOptions();
51
+ const hasAstroInstalled =
52
+ typeof compilerOptions.configFilePath !== 'string' ||
53
+ require.resolve('astro', { paths: [compilerOptions.configFilePath] });
54
+
55
+ return hasAstroInstalled;
56
+ } catch (e) {
57
+ project.projectService.logger.info(e as string);
58
+ return false;
59
+ }
60
60
  }
61
61
 
62
62
  return { create, getExternalFiles };
@@ -1,5 +1,5 @@
1
1
  import type ts from 'typescript/lib/tsserverlibrary';
2
- import { Logger } from '../logger.js';
2
+ import type { Logger } from '../logger.js';
3
3
  import { isAstroFilePath, replaceDeep } from '../utils.js';
4
4
 
5
5
  const componentPostfix = '__AstroComponent_';
@@ -27,9 +27,20 @@ export function decorateCompletions(ls: ts.LanguageService, logger: Logger): voi
27
27
 
28
28
  const getCompletionEntryDetails = ls.getCompletionEntryDetails;
29
29
  ls.getCompletionEntryDetails = (fileName, position, entryName, formatOptions, source, preferences, data) => {
30
- const details = getCompletionEntryDetails(fileName, position, entryName, formatOptions, source, preferences, data);
31
- if (details || !isAstroFilePath(source || '')) {
32
- return details;
30
+ if (!isAstroFilePath(source || '')) {
31
+ const details = getCompletionEntryDetails(
32
+ fileName,
33
+ position,
34
+ entryName,
35
+ formatOptions,
36
+ source,
37
+ preferences,
38
+ data
39
+ );
40
+
41
+ if (details) {
42
+ return details;
43
+ }
33
44
  }
34
45
 
35
46
  // In the completion list we removed the component postfix. Internally,
@@ -1,13 +1,8 @@
1
1
  import type ts from 'typescript/lib/tsserverlibrary';
2
- import { AstroSnapshotManager } from '../astro-snapshots';
3
- import { Logger } from '../logger';
2
+ import type { AstroSnapshotManager } from '../astro-snapshots';
4
3
  import { isAstroFilePath, isNotNullOrUndefined } from '../utils';
5
4
 
6
- export function decorateGetDefinition(
7
- ls: ts.LanguageService,
8
- snapshotManager: AstroSnapshotManager,
9
- logger: Logger
10
- ): void {
5
+ export function decorateGetDefinition(ls: ts.LanguageService, snapshotManager: AstroSnapshotManager): void {
11
6
  const getDefinitionAndBoundSpan = ls.getDefinitionAndBoundSpan;
12
7
  ls.getDefinitionAndBoundSpan = (fileName, position) => {
13
8
  const definition = getDefinitionAndBoundSpan(fileName, position);
@@ -1,8 +1,7 @@
1
1
  import type ts from 'typescript/lib/tsserverlibrary';
2
- import { Logger } from '../logger.js';
3
2
  import { isAstroFilePath } from '../utils.js';
4
3
 
5
- export function decorateDiagnostics(ls: ts.LanguageService, logger: Logger): void {
4
+ export function decorateDiagnostics(ls: ts.LanguageService): void {
6
5
  decorateSyntacticDiagnostics(ls);
7
6
  decorateSemanticDiagnostics(ls);
8
7
  decorateSuggestionDiagnostics(ls);
@@ -0,0 +1,31 @@
1
+ import type ts from 'typescript/lib/tsserverlibrary';
2
+ import type { AstroSnapshotManager } from '../astro-snapshots.js';
3
+ import { isAstroFilePath, isNotNullOrUndefined } from '../utils.js';
4
+
5
+ export function decorateGetFileReferences(ls: ts.LanguageService, snapshotManager: AstroSnapshotManager): void {
6
+ const getFileReferences = ls.getFileReferences;
7
+ ls.getFileReferences = (fileName) => {
8
+ const references = getFileReferences(fileName);
9
+ return references
10
+ ?.map((ref) => {
11
+ if (!isAstroFilePath(ref.fileName)) {
12
+ return ref;
13
+ }
14
+
15
+ const textSpan = snapshotManager.get(ref.fileName)?.getOriginalTextSpan(ref.textSpan);
16
+ if (!textSpan) {
17
+ return undefined;
18
+ }
19
+
20
+ return {
21
+ ...ref,
22
+ textSpan,
23
+ // Spare the work for now
24
+ contextSpan: undefined,
25
+ originalTextSpan: undefined,
26
+ originalContextSpan: undefined,
27
+ };
28
+ })
29
+ .filter(isNotNullOrUndefined);
30
+ };
31
+ }
@@ -1,6 +1,6 @@
1
1
  import type ts from 'typescript/lib/tsserverlibrary';
2
- import { AstroSnapshotManager } from '../astro-snapshots.js';
3
- import { Logger } from '../logger';
2
+ import type { AstroSnapshotManager } from '../astro-snapshots.js';
3
+ import type { Logger } from '../logger';
4
4
  import { isAstroFilePath, isNotNullOrUndefined } from '../utils.js';
5
5
 
6
6
  export function decorateFindReferences(
@@ -1,13 +1,8 @@
1
1
  import type ts from 'typescript/lib/tsserverlibrary';
2
- import { AstroSnapshotManager } from '../astro-snapshots.js';
3
- import { Logger } from '../logger.js';
2
+ import type { AstroSnapshotManager } from '../astro-snapshots.js';
4
3
  import { isAstroFilePath, isNotNullOrUndefined } from '../utils.js';
5
4
 
6
- export function decorateGetImplementation(
7
- ls: ts.LanguageService,
8
- snapshotManager: AstroSnapshotManager,
9
- logger: Logger
10
- ): void {
5
+ export function decorateGetImplementation(ls: ts.LanguageService, snapshotManager: AstroSnapshotManager): void {
11
6
  const getImplementationAtPosition = ls.getImplementationAtPosition;
12
7
  ls.getImplementationAtPosition = (fileName, position) => {
13
8
  const implementation = getImplementationAtPosition(fileName, position);
@@ -1,12 +1,13 @@
1
1
  import type ts from 'typescript/lib/tsserverlibrary';
2
- import { AstroSnapshotManager } from '../astro-snapshots.js';
3
- import { Logger } from '../logger';
4
- import { isAstroFilePath } from '../utils.js';
2
+ import type { AstroSnapshotManager } from '../astro-snapshots.js';
3
+ import type { Logger } from '../logger';
5
4
  import { decorateCompletions } from './completions.js';
6
5
  import { decorateGetDefinition } from './definition.js';
7
6
  import { decorateDiagnostics } from './diagnostics.js';
7
+ import { decorateGetFileReferences } from './file-references.js';
8
8
  import { decorateFindReferences } from './find-references.js';
9
9
  import { decorateGetImplementation } from './implementation.js';
10
+ import { decorateLineColumnOffset } from './line-column-offset.js';
10
11
  import { decorateRename } from './rename.js';
11
12
 
12
13
  export function decorateLanguageService(
@@ -14,30 +15,14 @@ export function decorateLanguageService(
14
15
  snapshotManager: AstroSnapshotManager,
15
16
  logger: Logger
16
17
  ): ts.LanguageService {
17
- patchLineColumnOffset(ls, snapshotManager);
18
+ decorateLineColumnOffset(ls, snapshotManager);
18
19
  decorateRename(ls, snapshotManager, logger);
19
- decorateDiagnostics(ls, logger);
20
+ decorateDiagnostics(ls);
20
21
  decorateFindReferences(ls, snapshotManager, logger);
21
22
  decorateCompletions(ls, logger);
22
- decorateGetDefinition(ls, snapshotManager, logger);
23
- decorateGetImplementation(ls, snapshotManager, logger);
24
- return ls;
25
- }
23
+ decorateGetDefinition(ls, snapshotManager);
24
+ decorateGetImplementation(ls, snapshotManager);
25
+ decorateGetFileReferences(ls, snapshotManager);
26
26
 
27
- function patchLineColumnOffset(ls: ts.LanguageService, snapshotManager: AstroSnapshotManager) {
28
- if (!ls.toLineColumnOffset) {
29
- return;
30
- }
31
-
32
- // We need to patch this because (according to source, only) getDefinition uses this
33
- const toLineColumnOffset = ls.toLineColumnOffset;
34
- ls.toLineColumnOffset = (fileName, position) => {
35
- if (isAstroFilePath(fileName)) {
36
- const snapshot = snapshotManager.get(fileName);
37
- if (snapshot) {
38
- return snapshot.positionAt(position);
39
- }
40
- }
41
- return toLineColumnOffset(fileName, position);
42
- };
27
+ return ls;
43
28
  }
@@ -0,0 +1,21 @@
1
+ import type ts from 'typescript/lib/tsserverlibrary';
2
+ import type { AstroSnapshotManager } from '../astro-snapshots';
3
+ import { isAstroFilePath } from '../utils';
4
+
5
+ export function decorateLineColumnOffset(ls: ts.LanguageService, snapshotManager: AstroSnapshotManager) {
6
+ if (!ls.toLineColumnOffset) {
7
+ return;
8
+ }
9
+
10
+ // We need to patch this because (according to source, only) getDefinition uses this
11
+ const toLineColumnOffset = ls.toLineColumnOffset;
12
+ ls.toLineColumnOffset = (fileName, position) => {
13
+ if (isAstroFilePath(fileName)) {
14
+ const snapshot = snapshotManager.get(fileName);
15
+ if (snapshot) {
16
+ return snapshot.positionAt(position);
17
+ }
18
+ }
19
+ return toLineColumnOffset(fileName, position);
20
+ };
21
+ }
@@ -1,6 +1,6 @@
1
1
  import type ts from 'typescript/lib/tsserverlibrary';
2
- import { AstroSnapshotManager } from '../astro-snapshots.js';
3
- import { Logger } from '../logger.js';
2
+ import type { AstroSnapshotManager } from '../astro-snapshots.js';
3
+ import type { Logger } from '../logger.js';
4
4
  import { isAstroFilePath, isNotNullOrUndefined } from '../utils.js';
5
5
 
6
6
  export function decorateRename(ls: ts.LanguageService, snapshotManager: AstroSnapshotManager, logger: Logger): void {
@@ -1,7 +1,7 @@
1
1
  import type ts from 'typescript/lib/tsserverlibrary';
2
- import { AstroSnapshotManager } from './astro-snapshots.js';
2
+ import type { AstroSnapshotManager } from './astro-snapshots.js';
3
3
  import { createAstroSys } from './astro-sys.js';
4
- import { Logger } from './logger.js';
4
+ import type { Logger } from './logger.js';
5
5
  import { ensureRealAstroFilePath, isVirtualAstroFilePath } from './utils.js';
6
6
 
7
7
  /**
@@ -73,6 +73,13 @@ export function patchModuleLoader(
73
73
  return origRemoveFile(info, fileExists, detachFromProject);
74
74
  };
75
75
 
76
+ // Patch readDirectory so we get completions for .astro files
77
+ const origReadDirectory = project.readDirectory.bind(project);
78
+ project.readDirectory = (path, extensions, exclude, include, depth) => {
79
+ const extensionsWithAstro = (extensions ?? []).concat('.astro', '.md', '.mdx');
80
+ return origReadDirectory(path, extensionsWithAstro, exclude, include, depth);
81
+ };
82
+
76
83
  function resolveModuleNames(
77
84
  moduleNames: string[],
78
85
  containingFile: string,
@@ -80,7 +87,8 @@ export function patchModuleLoader(
80
87
  redirectedReference: ts.ResolvedProjectReference | undefined,
81
88
  compilerOptions: ts.CompilerOptions
82
89
  ): Array<ts.ResolvedModule | undefined> {
83
- logger.log('Resolving modules names for ' + containingFile);
90
+ // logger.log('Resolving modules names for ' + containingFile);
91
+
84
92
  // Try resolving all module names with the original method first.
85
93
  // The ones that are undefined will be re-checked if they are a
86
94
  // astro file and if so, are resolved, too. This way we can defer
@@ -129,7 +137,7 @@ export function patchModuleLoader(
129
137
  }
130
138
 
131
139
  const resolvedAstroModule: ts.ResolvedModuleFull = {
132
- extension: snapshot.isTsFile ? typescript.Extension.Tsx : typescript.Extension.Jsx,
140
+ extension: typescript.Extension.Tsx,
133
141
  resolvedFileName,
134
142
  };
135
143
  return resolvedAstroModule;
@@ -0,0 +1,130 @@
1
+ import type ts from 'typescript/lib/tsserverlibrary';
2
+ import type { AstroSnapshotManager } from './astro-snapshots';
3
+ import { getConfigPathForProject, isAstroFilePath, readProjectAstroFilesFromFs } from './utils';
4
+
5
+ export class ProjectAstroFilesManager {
6
+ private files = new Set<string>();
7
+ private directoryWatchers = new Set<ts.FileWatcher>();
8
+
9
+ private static instances = new Map<string, ProjectAstroFilesManager>();
10
+
11
+ static getInstance(projectName: string) {
12
+ return this.instances.get(projectName);
13
+ }
14
+
15
+ constructor(
16
+ private readonly typescript: typeof ts,
17
+ private readonly project: ts.server.Project,
18
+ private readonly serverHost: ts.server.ServerHost,
19
+ private readonly snapshotManager: AstroSnapshotManager,
20
+ private parsedCommandLine: ts.ParsedCommandLine
21
+ ) {
22
+ this.setupWatchers();
23
+ this.updateProjectAstroFiles();
24
+
25
+ ProjectAstroFilesManager.instances.set(project.getProjectName(), this);
26
+ }
27
+
28
+ updateProjectConfig(serviceHost: ts.LanguageServiceHost) {
29
+ const parsedCommandLine = serviceHost.getParsedCommandLine?.(getConfigPathForProject(this.project));
30
+
31
+ if (!parsedCommandLine) {
32
+ return;
33
+ }
34
+
35
+ this.disposeWatchersAndFiles();
36
+ this.parsedCommandLine = parsedCommandLine;
37
+ this.setupWatchers();
38
+ this.updateProjectAstroFiles();
39
+ }
40
+
41
+ getFiles() {
42
+ return Array.from(this.files);
43
+ }
44
+
45
+ /**
46
+ * Create directory watcher for include and exclude
47
+ * The watcher in tsserver doesn't support astro file
48
+ * It won't add new created astro file to root
49
+ */
50
+ private setupWatchers() {
51
+ for (const directory in this.parsedCommandLine.wildcardDirectories) {
52
+ if (!Object.prototype.hasOwnProperty.call(this.parsedCommandLine.wildcardDirectories, directory)) {
53
+ continue;
54
+ }
55
+
56
+ const watchDirectoryFlags = this.parsedCommandLine.wildcardDirectories[directory];
57
+ const watcher = this.serverHost.watchDirectory(
58
+ directory,
59
+ this.watcherCallback.bind(this),
60
+ watchDirectoryFlags === this.typescript.WatchDirectoryFlags.Recursive,
61
+ this.parsedCommandLine.watchOptions
62
+ );
63
+
64
+ this.directoryWatchers.add(watcher);
65
+ }
66
+ }
67
+
68
+ private watcherCallback(fileName: string) {
69
+ if (!isAstroFilePath(fileName)) {
70
+ return;
71
+ }
72
+
73
+ // We can't just add the file to the project directly, because
74
+ // - the casing of fileName is different
75
+ // - we don't know whether the file was added or deleted
76
+ this.updateProjectAstroFiles();
77
+ }
78
+
79
+ private updateProjectAstroFiles() {
80
+ const fileNamesAfter = readProjectAstroFilesFromFs(this.typescript, this.project, this.parsedCommandLine);
81
+ const removedFiles = new Set(...this.files);
82
+ const newFiles = fileNamesAfter.filter((fileName) => {
83
+ const has = this.files.has(fileName);
84
+ if (has) {
85
+ removedFiles.delete(fileName);
86
+ }
87
+ return !has;
88
+ });
89
+
90
+ for (const newFile of newFiles) {
91
+ this.addFileToProject(newFile);
92
+ this.files.add(newFile);
93
+ }
94
+ for (const removedFile of removedFiles) {
95
+ this.removeFileFromProject(removedFile, false);
96
+ this.files.delete(removedFile);
97
+ }
98
+ }
99
+
100
+ private addFileToProject(newFile: string) {
101
+ this.snapshotManager.create(newFile);
102
+ const snapshot = this.project.projectService.getScriptInfo(newFile);
103
+
104
+ if (snapshot) {
105
+ this.project.addRoot(snapshot);
106
+ }
107
+ }
108
+
109
+ private removeFileFromProject(file: string, exists = true) {
110
+ const info = this.project.getScriptInfo(file);
111
+
112
+ if (info) {
113
+ this.project.removeFile(info, exists, true);
114
+ }
115
+ }
116
+
117
+ private disposeWatchersAndFiles() {
118
+ this.directoryWatchers.forEach((watcher) => watcher.close());
119
+ this.directoryWatchers.clear();
120
+
121
+ this.files.forEach((file) => this.removeFileFromProject(file));
122
+ this.files.clear();
123
+ }
124
+
125
+ dispose() {
126
+ this.disposeWatchersAndFiles();
127
+
128
+ ProjectAstroFilesManager.instances.delete(this.project.getProjectName());
129
+ }
130
+ }