@astrojs/ts-plugin 0.2.1 → 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 (54) hide show
  1. package/.turbo/turbo-build.log +5 -2
  2. package/CHANGELOG.md +16 -0
  3. package/LICENSE +36 -0
  4. package/README.md +1 -1
  5. package/dist/astro-snapshots.js +48 -47
  6. package/dist/astro-sys.js +22 -35
  7. package/dist/astro2tsx.js +17 -41
  8. package/dist/index.js +38 -39
  9. package/dist/language-service/completions.js +42 -42
  10. package/dist/language-service/definition.js +22 -40
  11. package/dist/language-service/diagnostics.js +15 -18
  12. package/dist/language-service/file-references.js +52 -0
  13. package/dist/language-service/find-references.js +20 -38
  14. package/dist/language-service/implementation.js +18 -37
  15. package/dist/language-service/index.js +26 -42
  16. package/dist/language-service/line-column-offset.js +44 -0
  17. package/dist/language-service/rename.js +24 -37
  18. package/dist/logger.js +18 -5
  19. package/dist/module-loader.js +28 -22
  20. package/dist/project-astro-files.js +125 -0
  21. package/dist/utils.js +34 -19
  22. package/dist/workers/TSXService.js +39 -0
  23. package/dist/workers/TSXWorker.js +9 -0
  24. package/package.json +36 -27
  25. package/src/astro-snapshots.ts +270 -291
  26. package/src/astro-sys.ts +22 -22
  27. package/src/astro2tsx.ts +5 -50
  28. package/src/index.ts +49 -60
  29. package/src/language-service/completions.ts +56 -64
  30. package/src/language-service/definition.ts +35 -42
  31. package/src/language-service/diagnostics.ts +32 -33
  32. package/src/language-service/file-references.ts +31 -0
  33. package/src/language-service/find-references.ts +67 -76
  34. package/src/language-service/implementation.ts +27 -34
  35. package/src/language-service/index.ts +17 -32
  36. package/src/language-service/line-column-offset.ts +21 -0
  37. package/src/language-service/rename.ts +38 -48
  38. package/src/logger.ts +32 -36
  39. package/src/module-loader.ts +124 -124
  40. package/src/project-astro-files.ts +130 -0
  41. package/src/utils.ts +54 -45
  42. package/src/workers/TSXService.ts +11 -0
  43. package/src/workers/TSXWorker.ts +8 -0
  44. package/test/fixtures/MyAstroComponent.astro +5 -0
  45. package/test/fixtures/script.ts +9 -0
  46. package/test/fixtures/tsconfig.json +3 -0
  47. package/test/runTest.js +41 -0
  48. package/test/suite/extension.test.js +68 -0
  49. package/test/suite/index.js +38 -0
  50. package/test/tsconfig.json +14 -0
  51. package/test/utils.js +65 -0
  52. package/tsconfig.json +2 -1
  53. package/dist/source-mapper.js +0 -114
  54. package/src/source-mapper.ts +0 -123
package/src/astro2tsx.ts CHANGED
@@ -1,53 +1,8 @@
1
- import type { FileMapping } from './source-mapper';
2
- import { Project } from 'ts-morph';
1
+ import type { TSXResult } from '@astrojs/compiler/shared/types';
2
+ import { convertToTSX } from './workers/TSXService';
3
3
 
4
- // Note that this is a bit of a hack until the new compiler with proper
5
- // source map support.
4
+ export function astro2tsx(content: string, fileName: string): TSXResult {
5
+ const tsx = convertToTSX(content, { sourcefile: fileName });
6
6
 
7
- interface Astro2TSXOptions {
8
- filename: string | undefined;
9
- isTsFile: boolean;
10
- }
11
-
12
- interface Astro2TSXResult {
13
- code: string;
14
- map: {
15
- mappings: FileMapping
16
- }
17
- }
18
-
19
- export function astro2tsx(code: string, options: Astro2TSXOptions): Astro2TSXResult {
20
- const compiled = transformContent(code);
21
-
22
- const result: Astro2TSXResult = {
23
- code: compiled,
24
- map: {
25
- mappings: []
26
- }
27
- };
28
-
29
- return result;
30
- }
31
-
32
- // This is hacky but it works for now
33
- function addProps(content: string): string {
34
- let defaultExportType = 'Record<string, any>';
35
-
36
- // See if this has an interface already
37
- const project = new Project({});
38
- const sourceFile = project.createSourceFile("testing.ts", content);
39
- const declarations = sourceFile.getExportedDeclarations();
40
- if(declarations.has('Props')) {
41
- defaultExportType = 'Props';
42
- }
43
- return '\n' + `export default function (props: ${defaultExportType}): string;`
44
- }
45
-
46
- function transformContent(content: string) {
47
- const ts = content.replace(/---/g, '///');
48
- return (
49
- ts +
50
- // Add TypeScript definitions
51
- addProps(ts)
52
- );
7
+ return tsx;
53
8
  }
package/src/index.ts CHANGED
@@ -1,76 +1,65 @@
1
1
  import type ts from 'typescript/lib/tsserverlibrary';
2
- import { dirname, resolve } from 'path';
2
+ 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 { AstroSnapshotManager } from './astro-snapshots.js';
6
+ import { ProjectAstroFilesManager } from './project-astro-files.js';
7
+ import { getConfigPathForProject, readProjectAstroFilesFromFs } from './utils.js';
8
+
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 {
13
+ const logger = new Logger(info.project.projectService.logger);
14
+ const parsedCommandLine = info.languageServiceHost.getParsedCommandLine?.(getConfigPathForProject(info.project));
7
15
 
8
- function init(modules: { typescript: typeof ts }) {
9
- function create(info: ts.server.PluginCreateInfo) {
10
- const logger = new Logger(info.project.projectService.logger);
16
+ if (!isAstroProject(info.project, parsedCommandLine)) {
17
+ logger.log('Detected that this is not an Astro project, abort patching TypeScript');
18
+ return info.languageService;
19
+ }
11
20
 
12
- if (!isAstroProject(info)) {
13
- logger.log('Detected that this is not an Astro project, abort patching TypeScript');
14
- return info.languageService;
15
- }
21
+ logger.log('Starting Astro plugin');
16
22
 
17
- logger.log('Starting Astro plugin');
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
+ }
18
33
 
19
- const snapshotManager = new AstroSnapshotManager(
20
- modules.typescript,
21
- info.project.projectService,
22
- logger
23
- );
34
+ patchModuleLoader(logger, snapshotManager, modules.typescript, info.languageServiceHost, info.project);
35
+ return decorateLanguageService(info.languageService, snapshotManager, logger);
36
+ }
24
37
 
25
- patchCompilerOptions(info.project);
26
- patchModuleLoader(
27
- logger,
28
- snapshotManager,
29
- modules.typescript,
30
- info.languageServiceHost,
31
- info.project
32
- );
33
- return decorateLanguageService(info.languageService, snapshotManager, logger);
34
- }
38
+ function getExternalFiles(project: ts.server.ConfiguredProject) {
39
+ return ProjectAstroFilesManager.getInstance(project.getProjectName())?.getFiles() ?? [];
40
+ }
35
41
 
36
- function getExternalFiles(_project: ts.server.ConfiguredProject) {
37
- // Needed so the ambient definitions are known inside the tsx files
38
- /*const astroTsPath = dirname(require.resolve('astro2tsx'));
39
- const astroTsxFiles = [
40
- './astro-shims.d.ts',
41
- './astro-jsx.d.ts',
42
- './astro-native-jsx.d.ts'
43
- ].map((f) => modules.typescript.sys.resolvePath(resolve(astroTsPath, f)));
44
- return astroTsxFiles;*/
45
- return [];
46
- }
42
+ function isAstroProject(project: ts.server.Project, parsedCommandLine: ts.ParsedCommandLine | undefined) {
43
+ if (parsedCommandLine) {
44
+ const astroFiles = readProjectAstroFilesFromFs(ts, project, parsedCommandLine);
47
45
 
48
- function patchCompilerOptions(project: ts.server.Project) {
49
- const compilerOptions = project.getCompilerOptions();
50
- // Patch needed because astro2tsx creates jsx/tsx files
51
- compilerOptions.jsx = modules.typescript.JsxEmit.Preserve;
46
+ if (astroFiles.length > 0) return true;
47
+ }
52
48
 
53
- // detect which JSX namespace to use (astro | astronative) if not specified or not compatible
54
- if (!compilerOptions.jsxFactory?.startsWith('astro')) {
55
- // Default to regular astro, this causes the usage of the "astro.JSX" namespace
56
- // We don't need to add a switch for astro-native because the jsx is only relevant
57
- // within Astro files, which this plugin does not deal with.
58
- compilerOptions.jsxFactory = 'astro.createElement';
59
- }
60
- }
49
+ try {
50
+ const compilerOptions = project.getCompilerOptions();
51
+ const hasAstroInstalled =
52
+ typeof compilerOptions.configFilePath !== 'string' ||
53
+ require.resolve('astro', { paths: [compilerOptions.configFilePath] });
61
54
 
62
- function isAstroProject(info: ts.server.PluginCreateInfo) {
63
- // Add more checks like "no Astro file found" or "no config file found"?
64
- const compilerOptions = info.project.getCompilerOptions();
65
- const isNoJsxProject =
66
- (!compilerOptions.jsx || compilerOptions.jsx === modules.typescript.JsxEmit.Preserve) &&
67
- (!compilerOptions.jsxFactory || compilerOptions.jsxFactory.startsWith('astro')) &&
68
- !compilerOptions.jsxFragmentFactory &&
69
- !compilerOptions.jsxImportSource;
70
- return isNoJsxProject;
71
- }
55
+ return hasAstroInstalled;
56
+ } catch (e) {
57
+ project.projectService.logger.info(e as string);
58
+ return false;
59
+ }
60
+ }
72
61
 
73
- return { create, getExternalFiles };
62
+ return { create, getExternalFiles };
74
63
  }
75
64
 
76
- export = init;
65
+ export = init;
@@ -1,73 +1,65 @@
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_';
6
6
 
7
7
  export function decorateCompletions(ls: ts.LanguageService, logger: Logger): void {
8
- const getCompletionsAtPosition = ls.getCompletionsAtPosition;
9
- ls.getCompletionsAtPosition = (fileName, position, options) => {
10
- const completions = getCompletionsAtPosition(fileName, position, options);
11
- if (!completions) {
12
- return completions;
13
- }
14
- return {
15
- ...completions,
16
- entries: completions.entries.map((entry) => {
17
- if (
18
- !isAstroFilePath(entry.source || '') ||
19
- !entry.name.endsWith(componentPostfix)
20
- ) {
21
- return entry;
22
- }
23
- return {
24
- ...entry,
25
- name: entry.name.slice(0, -componentPostfix.length)
26
- };
27
- })
28
- };
29
- };
8
+ const getCompletionsAtPosition = ls.getCompletionsAtPosition;
9
+ ls.getCompletionsAtPosition = (fileName, position, options) => {
10
+ const completions = getCompletionsAtPosition(fileName, position, options);
11
+ if (!completions) {
12
+ return completions;
13
+ }
14
+ return {
15
+ ...completions,
16
+ entries: completions.entries.map((entry) => {
17
+ if (!isAstroFilePath(entry.source || '') || !entry.name.endsWith(componentPostfix)) {
18
+ return entry;
19
+ }
20
+ return {
21
+ ...entry,
22
+ name: entry.name.slice(0, -componentPostfix.length),
23
+ };
24
+ }),
25
+ };
26
+ };
30
27
 
31
- const getCompletionEntryDetails = ls.getCompletionEntryDetails;
32
- ls.getCompletionEntryDetails = (
33
- fileName,
34
- position,
35
- entryName,
36
- formatOptions,
37
- source,
38
- preferences,
39
- data
40
- ) => {
41
- const details = getCompletionEntryDetails(
42
- fileName,
43
- position,
44
- entryName,
45
- formatOptions,
46
- source,
47
- preferences,
48
- data
49
- );
50
- if (details || !isAstroFilePath(source || '')) {
51
- return details;
52
- }
28
+ const getCompletionEntryDetails = ls.getCompletionEntryDetails;
29
+ ls.getCompletionEntryDetails = (fileName, position, entryName, formatOptions, source, preferences, data) => {
30
+ if (!isAstroFilePath(source || '')) {
31
+ const details = getCompletionEntryDetails(
32
+ fileName,
33
+ position,
34
+ entryName,
35
+ formatOptions,
36
+ source,
37
+ preferences,
38
+ data
39
+ );
53
40
 
54
- // In the completion list we removed the component postfix. Internally,
55
- // the language service saved the list with the postfix, so details
56
- // won't match anything. Therefore add it back and remove it afterwards again.
57
- const astroDetails = getCompletionEntryDetails(
58
- fileName,
59
- position,
60
- entryName + componentPostfix,
61
- formatOptions,
62
- source,
63
- preferences,
64
- data
65
- );
66
- if (!astroDetails) {
67
- return undefined;
68
- }
69
- logger.debug('Found Astro Component import completion details');
41
+ if (details) {
42
+ return details;
43
+ }
44
+ }
70
45
 
71
- return replaceDeep(astroDetails, componentPostfix, '');
72
- };
73
- }
46
+ // In the completion list we removed the component postfix. Internally,
47
+ // the language service saved the list with the postfix, so details
48
+ // won't match anything. Therefore add it back and remove it afterwards again.
49
+ const astroDetails = getCompletionEntryDetails(
50
+ fileName,
51
+ position,
52
+ entryName + componentPostfix,
53
+ formatOptions,
54
+ source,
55
+ preferences,
56
+ data
57
+ );
58
+ if (!astroDetails) {
59
+ return undefined;
60
+ }
61
+ logger.debug('Found Astro Component import completion details');
62
+
63
+ return replaceDeep(astroDetails, componentPostfix, '');
64
+ };
65
+ }
@@ -1,46 +1,39 @@
1
1
  import type ts from 'typescript/lib/tsserverlibrary';
2
- import { Logger } from '../logger';
3
- import { AstroSnapshotManager } from '../astro-snapshots';
4
- import { isNotNullOrUndefined, isAstroFilePath } from '../utils';
2
+ import type { AstroSnapshotManager } from '../astro-snapshots';
3
+ import { isAstroFilePath, isNotNullOrUndefined } from '../utils';
5
4
 
6
- export function decorateGetDefinition(
7
- ls: ts.LanguageService,
8
- snapshotManager: AstroSnapshotManager,
9
- logger: Logger
10
- ): void {
11
- const getDefinitionAndBoundSpan = ls.getDefinitionAndBoundSpan;
12
- ls.getDefinitionAndBoundSpan = (fileName, position) => {
13
- const definition = getDefinitionAndBoundSpan(fileName, position);
14
- if (!definition?.definitions) {
15
- return definition;
16
- }
5
+ export function decorateGetDefinition(ls: ts.LanguageService, snapshotManager: AstroSnapshotManager): void {
6
+ const getDefinitionAndBoundSpan = ls.getDefinitionAndBoundSpan;
7
+ ls.getDefinitionAndBoundSpan = (fileName, position) => {
8
+ const definition = getDefinitionAndBoundSpan(fileName, position);
9
+ if (!definition?.definitions) {
10
+ return definition;
11
+ }
17
12
 
18
- return {
19
- ...definition,
20
- definitions: definition.definitions
21
- .map((def) => {
22
- if (!isAstroFilePath(def.fileName)) {
23
- return def;
24
- }
13
+ return {
14
+ ...definition,
15
+ definitions: definition.definitions
16
+ .map((def) => {
17
+ if (!isAstroFilePath(def.fileName)) {
18
+ return def;
19
+ }
25
20
 
26
- let textSpan = snapshotManager
27
- .get(def.fileName)
28
- ?.getOriginalTextSpan(def.textSpan);
29
- if (!textSpan) {
30
- // Unmapped positions are for example the default export.
31
- // Fall back to the start of the file to at least go to the correct file.
32
- textSpan = { start: 0, length: 1 };
33
- }
34
- return {
35
- ...def,
36
- textSpan,
37
- // Spare the work for now
38
- originalTextSpan: undefined,
39
- contextSpan: undefined,
40
- originalContextSpan: undefined
41
- };
42
- })
43
- .filter(isNotNullOrUndefined)
44
- };
45
- };
46
- }
21
+ let textSpan = snapshotManager.get(def.fileName)?.getOriginalTextSpan(def.textSpan);
22
+ if (!textSpan) {
23
+ // Unmapped positions are for example the default export.
24
+ // Fall back to the start of the file to at least go to the correct file.
25
+ textSpan = { start: 0, length: 1 };
26
+ }
27
+ return {
28
+ ...def,
29
+ textSpan,
30
+ // Spare the work for now
31
+ originalTextSpan: undefined,
32
+ contextSpan: undefined,
33
+ originalContextSpan: undefined,
34
+ };
35
+ })
36
+ .filter(isNotNullOrUndefined),
37
+ };
38
+ };
39
+ }
@@ -1,45 +1,44 @@
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 {
6
- decorateSyntacticDiagnostics(ls);
7
- decorateSemanticDiagnostics(ls);
8
- decorateSuggestionDiagnostics(ls);
4
+ export function decorateDiagnostics(ls: ts.LanguageService): void {
5
+ decorateSyntacticDiagnostics(ls);
6
+ decorateSemanticDiagnostics(ls);
7
+ decorateSuggestionDiagnostics(ls);
9
8
  }
10
9
 
11
10
  function decorateSyntacticDiagnostics(ls: ts.LanguageService): void {
12
- const getSyntacticDiagnostics = ls.getSyntacticDiagnostics;
13
- ls.getSyntacticDiagnostics = (fileName: string) => {
14
- // Diagnostics inside Astro files are done
15
- // by the @astrojs/language-server / Astro for VS Code extension
16
- if (isAstroFilePath(fileName)) {
17
- return [];
18
- }
19
- return getSyntacticDiagnostics(fileName);
20
- };
11
+ const getSyntacticDiagnostics = ls.getSyntacticDiagnostics;
12
+ ls.getSyntacticDiagnostics = (fileName: string) => {
13
+ // Diagnostics inside Astro files are done
14
+ // by the @astrojs/language-server / Astro for VS Code extension
15
+ if (isAstroFilePath(fileName)) {
16
+ return [];
17
+ }
18
+ return getSyntacticDiagnostics(fileName);
19
+ };
21
20
  }
22
21
 
23
22
  function decorateSemanticDiagnostics(ls: ts.LanguageService): void {
24
- const getSemanticDiagnostics = ls.getSemanticDiagnostics;
25
- ls.getSemanticDiagnostics = (fileName: string) => {
26
- // Diagnostics inside Astro files are done
27
- // by the @astrojs/language-server / Astro for VS Code extension
28
- if (isAstroFilePath(fileName)) {
29
- return [];
30
- }
31
- return getSemanticDiagnostics(fileName);
32
- };
23
+ const getSemanticDiagnostics = ls.getSemanticDiagnostics;
24
+ ls.getSemanticDiagnostics = (fileName: string) => {
25
+ // Diagnostics inside Astro files are done
26
+ // by the @astrojs/language-server / Astro for VS Code extension
27
+ if (isAstroFilePath(fileName)) {
28
+ return [];
29
+ }
30
+ return getSemanticDiagnostics(fileName);
31
+ };
33
32
  }
34
33
 
35
34
  function decorateSuggestionDiagnostics(ls: ts.LanguageService): void {
36
- const getSuggestionDiagnostics = ls.getSuggestionDiagnostics;
37
- ls.getSuggestionDiagnostics = (fileName: string) => {
38
- // Diagnostics inside Astro files are done
39
- // by the @astrojs/language-server / Astro for VS Code extension
40
- if (isAstroFilePath(fileName)) {
41
- return [];
42
- }
43
- return getSuggestionDiagnostics(fileName);
44
- };
45
- }
35
+ const getSuggestionDiagnostics = ls.getSuggestionDiagnostics;
36
+ ls.getSuggestionDiagnostics = (fileName: string) => {
37
+ // Diagnostics inside Astro files are done
38
+ // by the @astrojs/language-server / Astro for VS Code extension
39
+ if (isAstroFilePath(fileName)) {
40
+ return [];
41
+ }
42
+ return getSuggestionDiagnostics(fileName);
43
+ };
44
+ }
@@ -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
+ }