@css-modules-kit/ts-plugin 0.0.1

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 (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +95 -0
  3. package/dist/applicable-refactor.d.ts +3 -0
  4. package/dist/applicable-refactor.d.ts.map +1 -0
  5. package/dist/applicable-refactor.js +9 -0
  6. package/dist/applicable-refactor.js.map +1 -0
  7. package/dist/ast.d.ts +6 -0
  8. package/dist/ast.d.ts.map +1 -0
  9. package/dist/ast.js +27 -0
  10. package/dist/ast.js.map +1 -0
  11. package/dist/file-text-change.d.ts +4 -0
  12. package/dist/file-text-change.d.ts.map +1 -0
  13. package/dist/file-text-change.js +29 -0
  14. package/dist/file-text-change.js.map +1 -0
  15. package/dist/index.d.ts +3 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +43 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/language-plugin.d.ts +19 -0
  20. package/dist/language-plugin.d.ts.map +1 -0
  21. package/dist/language-plugin.js +72 -0
  22. package/dist/language-plugin.js.map +1 -0
  23. package/dist/language-service/feature/code-fix.d.ts +5 -0
  24. package/dist/language-service/feature/code-fix.d.ts.map +1 -0
  25. package/dist/language-service/feature/code-fix.js +83 -0
  26. package/dist/language-service/feature/code-fix.js.map +1 -0
  27. package/dist/language-service/feature/completion.d.ts +3 -0
  28. package/dist/language-service/feature/completion.d.ts.map +1 -0
  29. package/dist/language-service/feature/completion.js +46 -0
  30. package/dist/language-service/feature/completion.js.map +1 -0
  31. package/dist/language-service/feature/refactor.d.ts +12 -0
  32. package/dist/language-service/feature/refactor.d.ts.map +1 -0
  33. package/dist/language-service/feature/refactor.js +43 -0
  34. package/dist/language-service/feature/refactor.js.map +1 -0
  35. package/dist/language-service/feature/semantic-diagnostic.d.ts +5 -0
  36. package/dist/language-service/feature/semantic-diagnostic.d.ts.map +1 -0
  37. package/dist/language-service/feature/semantic-diagnostic.js +43 -0
  38. package/dist/language-service/feature/semantic-diagnostic.js.map +1 -0
  39. package/dist/language-service/feature/syntactic-diagnostic.d.ts +4 -0
  40. package/dist/language-service/feature/syntactic-diagnostic.d.ts.map +1 -0
  41. package/dist/language-service/feature/syntactic-diagnostic.js +38 -0
  42. package/dist/language-service/feature/syntactic-diagnostic.js.map +1 -0
  43. package/dist/language-service/proxy.d.ts +5 -0
  44. package/dist/language-service/proxy.d.ts.map +1 -0
  45. package/dist/language-service/proxy.js +35 -0
  46. package/dist/language-service/proxy.js.map +1 -0
  47. package/dist/language-service.d.ts +4 -0
  48. package/dist/language-service.d.ts.map +1 -0
  49. package/dist/language-service.js +125 -0
  50. package/dist/language-service.js.map +1 -0
  51. package/dist/util.d.ts +5 -0
  52. package/dist/util.d.ts.map +1 -0
  53. package/dist/util.js +25 -0
  54. package/dist/util.js.map +1 -0
  55. package/package.json +47 -0
  56. package/src/index.ts +53 -0
  57. package/src/language-plugin.ts +88 -0
  58. package/src/language-service/feature/code-fix.ts +109 -0
  59. package/src/language-service/feature/completion.ts +48 -0
  60. package/src/language-service/feature/refactor.ts +52 -0
  61. package/src/language-service/feature/semantic-diagnostic.ts +49 -0
  62. package/src/language-service/feature/syntactic-diagnostic.ts +39 -0
  63. package/src/language-service/proxy.ts +51 -0
  64. package/src/util.ts +19 -0
@@ -0,0 +1,48 @@
1
+ import { getCssModuleFileName, isComponentFileName, STYLES_EXPORT_NAME } from '@css-modules-kit/core';
2
+ import ts from 'typescript';
3
+
4
+ export function getCompletionsAtPosition(
5
+ languageService: ts.LanguageService,
6
+ ): ts.LanguageService['getCompletionsAtPosition'] {
7
+ return (fileName, position, options, formattingSettings) => {
8
+ const prior = languageService.getCompletionsAtPosition(fileName, position, options, formattingSettings);
9
+
10
+ if (isComponentFileName(fileName) && prior) {
11
+ const cssModuleFileName = getCssModuleFileName(fileName);
12
+ for (const entry of prior.entries) {
13
+ if (isStylesEntryForCSSModuleFile(entry, cssModuleFileName)) {
14
+ // Prioritize the completion of the `styles' import for the current .ts file for usability.
15
+ // NOTE: This is a hack to make the completion item appear at the top
16
+ entry.sortText = '0';
17
+ } else if (isClassNamePropEntry(entry)) {
18
+ // Complete `className={...}` instead of `className="..."` for usability.
19
+ entry.insertText = 'className={$1}';
20
+ }
21
+ }
22
+ }
23
+ return prior;
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Check if the completion entry is the `styles` entry for the CSS module file.
29
+ */
30
+ function isStylesEntryForCSSModuleFile(entry: ts.CompletionEntry, cssModuleFileName: string) {
31
+ return (
32
+ entry.name === STYLES_EXPORT_NAME &&
33
+ entry.data &&
34
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
35
+ entry.data.exportName === ts.InternalSymbolName.Default &&
36
+ entry.data.fileName &&
37
+ entry.data.fileName === cssModuleFileName
38
+ );
39
+ }
40
+
41
+ function isClassNamePropEntry(entry: ts.CompletionEntry) {
42
+ return (
43
+ entry.name === 'className' &&
44
+ entry.kind === ts.ScriptElementKind.memberVariableElement &&
45
+ entry.insertText === 'className="$1"' &&
46
+ entry.isSnippet
47
+ );
48
+ }
@@ -0,0 +1,52 @@
1
+ import { getCssModuleFileName, isComponentFileName } from '@css-modules-kit/core';
2
+ import type ts from 'typescript';
3
+
4
+ export const createCssModuleFileRefactor = {
5
+ name: 'Create CSS Module file',
6
+ description: 'Create CSS Module file',
7
+ actions: [{ name: 'Create CSS Module file', description: 'Create CSS Module file for current file' }],
8
+ } as const satisfies ts.ApplicableRefactorInfo;
9
+
10
+ export function getApplicableRefactors(
11
+ languageService: ts.LanguageService,
12
+ project: ts.server.Project,
13
+ ): ts.LanguageService['getApplicableRefactors'] {
14
+ return (fileName, positionOrRange, preferences) => {
15
+ const prior = languageService.getApplicableRefactors(fileName, positionOrRange, preferences) ?? [];
16
+ if (isComponentFileName(fileName)) {
17
+ // If the CSS Module file does not exist, provide a refactor to create it.
18
+ if (!project.fileExists(getCssModuleFileName(fileName))) {
19
+ prior.push(createCssModuleFileRefactor);
20
+ }
21
+ }
22
+ return prior;
23
+ };
24
+ }
25
+
26
+ export function getEditsForRefactor(languageService: ts.LanguageService): ts.LanguageService['getEditsForRefactor'] {
27
+ // eslint-disable-next-line max-params
28
+ return (fileName, formatOptions, positionOrRange, refactorName, actionName, preferences) => {
29
+ const prior = languageService.getEditsForRefactor(
30
+ fileName,
31
+ formatOptions,
32
+ positionOrRange,
33
+ refactorName,
34
+ actionName,
35
+ preferences,
36
+ ) ?? { edits: [] };
37
+ if (isComponentFileName(fileName)) {
38
+ if (refactorName === createCssModuleFileRefactor.name) {
39
+ prior.edits.push(createNewCssModuleFileChange(getCssModuleFileName(fileName)));
40
+ }
41
+ }
42
+ return prior;
43
+ };
44
+ }
45
+
46
+ function createNewCssModuleFileChange(cssFilename: string): ts.FileTextChanges {
47
+ return {
48
+ fileName: cssFilename,
49
+ textChanges: [{ span: { start: 0, length: 0 }, newText: '' }],
50
+ isNewFile: true,
51
+ };
52
+ }
@@ -0,0 +1,49 @@
1
+ import type { CSSModule, ExportBuilder, MatchesPattern, Resolver, SemanticDiagnostic } from '@css-modules-kit/core';
2
+ import { checkCSSModule } from '@css-modules-kit/core';
3
+ import type { Language } from '@volar/language-core';
4
+ import ts from 'typescript';
5
+ import { CMK_DATA_KEY, isCSSModuleScript } from '../../language-plugin.js';
6
+ import { convertErrorCategory, TS_ERROR_CODE_FOR_CMK_ERROR } from '../../util.js';
7
+
8
+ // eslint-disable-next-line max-params
9
+ export function getSemanticDiagnostics(
10
+ language: Language<string>,
11
+ languageService: ts.LanguageService,
12
+ exportBuilder: ExportBuilder,
13
+ resolver: Resolver,
14
+ matchesPattern: MatchesPattern,
15
+ getCSSModule: (path: string) => CSSModule | undefined,
16
+ ): ts.LanguageService['getSemanticDiagnostics'] {
17
+ return (fileName: string) => {
18
+ const prior = languageService.getSemanticDiagnostics(fileName);
19
+ const script = language.scripts.get(fileName);
20
+ if (isCSSModuleScript(script)) {
21
+ const virtualCode = script.generated.root;
22
+ const cssModule = virtualCode[CMK_DATA_KEY].cssModule;
23
+ const diagnostics = checkCSSModule(cssModule, exportBuilder, matchesPattern, resolver, getCSSModule);
24
+ const sourceFile = languageService.getProgram()!.getSourceFile(fileName)!;
25
+ const tsDiagnostics = diagnostics.map((diagnostic) => convertDiagnostic(diagnostic, sourceFile));
26
+ prior.push(...tsDiagnostics);
27
+ }
28
+ return prior;
29
+ };
30
+ }
31
+
32
+ function convertDiagnostic(diagnostic: SemanticDiagnostic, sourceFile: ts.SourceFile): ts.Diagnostic {
33
+ const start =
34
+ diagnostic.start ?
35
+ ts.getPositionOfLineAndCharacter(sourceFile, diagnostic.start.line - 1, diagnostic.start.column - 1)
36
+ : undefined;
37
+ const length =
38
+ start !== undefined && diagnostic.end ?
39
+ ts.getPositionOfLineAndCharacter(sourceFile, diagnostic.end.line - 1, diagnostic.end.column - 1) - start
40
+ : undefined;
41
+ return {
42
+ file: sourceFile,
43
+ start,
44
+ category: convertErrorCategory(diagnostic.category),
45
+ length,
46
+ messageText: diagnostic.text,
47
+ code: TS_ERROR_CODE_FOR_CMK_ERROR,
48
+ };
49
+ }
@@ -0,0 +1,39 @@
1
+ import type { SyntacticDiagnostic } from '@css-modules-kit/core';
2
+ import type { Language } from '@volar/language-core';
3
+ import ts from 'typescript';
4
+ import { CMK_DATA_KEY, isCSSModuleScript } from '../../language-plugin.js';
5
+ import { convertErrorCategory, TS_ERROR_CODE_FOR_CMK_ERROR } from '../../util.js';
6
+
7
+ export function getSyntacticDiagnostics(
8
+ language: Language<string>,
9
+ languageService: ts.LanguageService,
10
+ ): ts.LanguageService['getSyntacticDiagnostics'] {
11
+ return (fileName: string) => {
12
+ const prior = languageService.getSyntacticDiagnostics(fileName);
13
+ const script = language.scripts.get(fileName);
14
+ if (isCSSModuleScript(script)) {
15
+ const virtualCode = script.generated.root;
16
+ const diagnostics = virtualCode[CMK_DATA_KEY].diagnostics;
17
+ const sourceFile = languageService.getProgram()!.getSourceFile(fileName)!;
18
+ const tsDiagnostics = diagnostics.map((diagnostic) => convertDiagnostic(diagnostic, sourceFile));
19
+ prior.push(...tsDiagnostics);
20
+ }
21
+ return prior;
22
+ };
23
+ }
24
+
25
+ function convertDiagnostic(diagnostic: SyntacticDiagnostic, sourceFile: ts.SourceFile): ts.DiagnosticWithLocation {
26
+ const start = ts.getPositionOfLineAndCharacter(sourceFile, diagnostic.start.line - 1, diagnostic.start.column - 1);
27
+ const length =
28
+ diagnostic.end ?
29
+ ts.getPositionOfLineAndCharacter(sourceFile, diagnostic.end.line - 1, diagnostic.end.column - 1) - start
30
+ : 1;
31
+ return {
32
+ file: sourceFile,
33
+ start,
34
+ category: convertErrorCategory(diagnostic.category),
35
+ length,
36
+ messageText: diagnostic.text,
37
+ code: TS_ERROR_CODE_FOR_CMK_ERROR,
38
+ };
39
+ }
@@ -0,0 +1,51 @@
1
+ import { createExportBuilder, type MatchesPattern, type Resolver } from '@css-modules-kit/core';
2
+ import type { Language } from '@volar/language-core';
3
+ import type ts from 'typescript';
4
+ import { CMK_DATA_KEY, isCSSModuleScript } from '../language-plugin.js';
5
+ import { getCodeFixesAtPosition } from './feature/code-fix.js';
6
+ import { getCompletionsAtPosition } from './feature/completion.js';
7
+ import { getApplicableRefactors, getEditsForRefactor } from './feature/refactor.js';
8
+ import { getSemanticDiagnostics } from './feature/semantic-diagnostic.js';
9
+ import { getSyntacticDiagnostics } from './feature/syntactic-diagnostic.js';
10
+
11
+ export function proxyLanguageService(
12
+ language: Language<string>,
13
+ languageService: ts.LanguageService,
14
+ project: ts.server.Project,
15
+ resolver: Resolver,
16
+ matchesPattern: MatchesPattern,
17
+ ): ts.LanguageService {
18
+ const proxy: ts.LanguageService = Object.create(null);
19
+
20
+ for (const k of Object.keys(languageService) as (keyof ts.LanguageService)[]) {
21
+ const x = languageService[k]!;
22
+ // @ts-expect-error - JS runtime trickery which is tricky to type tersely
23
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
24
+ proxy[k] = (...args: {}[]) => x.apply(languageService, args);
25
+ }
26
+
27
+ const getCSSModule = (path: string) => {
28
+ const script = language.scripts.get(path);
29
+ if (isCSSModuleScript(script)) {
30
+ return script.generated.root[CMK_DATA_KEY].cssModule;
31
+ }
32
+ return undefined;
33
+ };
34
+ const exportBuilder = createExportBuilder({ getCSSModule, resolver, matchesPattern });
35
+
36
+ proxy.getSyntacticDiagnostics = getSyntacticDiagnostics(language, languageService);
37
+ proxy.getSemanticDiagnostics = getSemanticDiagnostics(
38
+ language,
39
+ languageService,
40
+ exportBuilder,
41
+ resolver,
42
+ matchesPattern,
43
+ getCSSModule,
44
+ );
45
+ proxy.getApplicableRefactors = getApplicableRefactors(languageService, project);
46
+ proxy.getEditsForRefactor = getEditsForRefactor(languageService);
47
+ proxy.getCompletionsAtPosition = getCompletionsAtPosition(languageService);
48
+ proxy.getCodeFixesAtPosition = getCodeFixesAtPosition(language, languageService, project);
49
+
50
+ return proxy;
51
+ }
package/src/util.ts ADDED
@@ -0,0 +1,19 @@
1
+ import ts from 'typescript';
2
+
3
+ /** The error code used by tsserver to display the css-modules-kit error in the editor. */
4
+ // NOTE: Use any other number than 1002 or later, as they are reserved by TypeScript's built-in errors.
5
+ // ref: https://github.com/microsoft/TypeScript/blob/220706eb0320ff46fad8bf80a5e99db624ee7dfb/src/compiler/diagnosticMessages.json
6
+ export const TS_ERROR_CODE_FOR_CMK_ERROR = 0;
7
+
8
+ export function convertErrorCategory(category: 'error' | 'warning' | 'suggestion'): ts.DiagnosticCategory {
9
+ switch (category) {
10
+ case 'error':
11
+ return ts.DiagnosticCategory.Error;
12
+ case 'warning':
13
+ return ts.DiagnosticCategory.Warning;
14
+ case 'suggestion':
15
+ return ts.DiagnosticCategory.Suggestion;
16
+ default:
17
+ throw new Error(`Unknown category: ${String(category)}`);
18
+ }
19
+ }