@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.
- package/LICENSE +21 -0
- package/README.md +95 -0
- package/dist/applicable-refactor.d.ts +3 -0
- package/dist/applicable-refactor.d.ts.map +1 -0
- package/dist/applicable-refactor.js +9 -0
- package/dist/applicable-refactor.js.map +1 -0
- package/dist/ast.d.ts +6 -0
- package/dist/ast.d.ts.map +1 -0
- package/dist/ast.js +27 -0
- package/dist/ast.js.map +1 -0
- package/dist/file-text-change.d.ts +4 -0
- package/dist/file-text-change.d.ts.map +1 -0
- package/dist/file-text-change.js +29 -0
- package/dist/file-text-change.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/language-plugin.d.ts +19 -0
- package/dist/language-plugin.d.ts.map +1 -0
- package/dist/language-plugin.js +72 -0
- package/dist/language-plugin.js.map +1 -0
- package/dist/language-service/feature/code-fix.d.ts +5 -0
- package/dist/language-service/feature/code-fix.d.ts.map +1 -0
- package/dist/language-service/feature/code-fix.js +83 -0
- package/dist/language-service/feature/code-fix.js.map +1 -0
- package/dist/language-service/feature/completion.d.ts +3 -0
- package/dist/language-service/feature/completion.d.ts.map +1 -0
- package/dist/language-service/feature/completion.js +46 -0
- package/dist/language-service/feature/completion.js.map +1 -0
- package/dist/language-service/feature/refactor.d.ts +12 -0
- package/dist/language-service/feature/refactor.d.ts.map +1 -0
- package/dist/language-service/feature/refactor.js +43 -0
- package/dist/language-service/feature/refactor.js.map +1 -0
- package/dist/language-service/feature/semantic-diagnostic.d.ts +5 -0
- package/dist/language-service/feature/semantic-diagnostic.d.ts.map +1 -0
- package/dist/language-service/feature/semantic-diagnostic.js +43 -0
- package/dist/language-service/feature/semantic-diagnostic.js.map +1 -0
- package/dist/language-service/feature/syntactic-diagnostic.d.ts +4 -0
- package/dist/language-service/feature/syntactic-diagnostic.d.ts.map +1 -0
- package/dist/language-service/feature/syntactic-diagnostic.js +38 -0
- package/dist/language-service/feature/syntactic-diagnostic.js.map +1 -0
- package/dist/language-service/proxy.d.ts +5 -0
- package/dist/language-service/proxy.d.ts.map +1 -0
- package/dist/language-service/proxy.js +35 -0
- package/dist/language-service/proxy.js.map +1 -0
- package/dist/language-service.d.ts +4 -0
- package/dist/language-service.d.ts.map +1 -0
- package/dist/language-service.js +125 -0
- package/dist/language-service.js.map +1 -0
- package/dist/util.d.ts +5 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +25 -0
- package/dist/util.js.map +1 -0
- package/package.json +47 -0
- package/src/index.ts +53 -0
- package/src/language-plugin.ts +88 -0
- package/src/language-service/feature/code-fix.ts +109 -0
- package/src/language-service/feature/completion.ts +48 -0
- package/src/language-service/feature/refactor.ts +52 -0
- package/src/language-service/feature/semantic-diagnostic.ts +49 -0
- package/src/language-service/feature/syntactic-diagnostic.ts +39 -0
- package/src/language-service/proxy.ts +51 -0
- 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
|
+
}
|