@css-modules-kit/core 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/dist/checker.d.ts +7 -0
- package/dist/checker.d.ts.map +1 -0
- package/dist/checker.js +46 -0
- package/dist/checker.js.map +1 -0
- package/dist/config.d.ts +101 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +200 -0
- package/dist/config.js.map +1 -0
- package/dist/diagnostic.d.ts +34 -0
- package/dist/diagnostic.d.ts.map +1 -0
- package/dist/diagnostic.js +3 -0
- package/dist/diagnostic.js.map +1 -0
- package/dist/dts-creator.d.ts +56 -0
- package/dist/dts-creator.d.ts.map +1 -0
- package/dist/dts-creator.js +95 -0
- package/dist/dts-creator.js.map +1 -0
- package/dist/error.d.ts +8 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +18 -0
- package/dist/error.js.map +1 -0
- package/dist/export-builder.d.ts +23 -0
- package/dist/export-builder.d.ts.map +1 -0
- package/dist/export-builder.js +34 -0
- package/dist/export-builder.js.map +1 -0
- package/dist/external-file.d.ts +2 -0
- package/dist/external-file.d.ts.map +1 -0
- package/dist/external-file.js +3 -0
- package/dist/external-file.js.map +1 -0
- package/dist/file.d.ts +28 -0
- package/dist/file.d.ts.map +1 -0
- package/dist/file.js +73 -0
- package/dist/file.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/language-plugin.d.ts +23 -0
- package/dist/language-plugin.d.ts.map +1 -0
- package/dist/language-plugin.js +73 -0
- package/dist/language-plugin.js.map +1 -0
- package/dist/parser/at-import-parser.d.ts +14 -0
- package/dist/parser/at-import-parser.d.ts.map +1 -0
- package/dist/parser/at-import-parser.js +47 -0
- package/dist/parser/at-import-parser.js.map +1 -0
- package/dist/parser/at-value-parser.d.ts +44 -0
- package/dist/parser/at-value-parser.d.ts.map +1 -0
- package/dist/parser/at-value-parser.js +147 -0
- package/dist/parser/at-value-parser.js.map +1 -0
- package/dist/parser/css-module-parser.d.ts +93 -0
- package/dist/parser/css-module-parser.d.ts.map +1 -0
- package/dist/parser/css-module-parser.js +96 -0
- package/dist/parser/css-module-parser.js.map +1 -0
- package/dist/parser/diagnostic.d.ts +34 -0
- package/dist/parser/diagnostic.d.ts.map +1 -0
- package/dist/parser/diagnostic.js +3 -0
- package/dist/parser/diagnostic.js.map +1 -0
- package/dist/parser/location.d.ts +34 -0
- package/dist/parser/location.d.ts.map +1 -0
- package/dist/parser/location.js +9 -0
- package/dist/parser/location.js.map +1 -0
- package/dist/parser/rule-parser.d.ts +19 -0
- package/dist/parser/rule-parser.d.ts.map +1 -0
- package/dist/parser/rule-parser.js +122 -0
- package/dist/parser/rule-parser.js.map +1 -0
- package/dist/path.d.ts +11 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +43 -0
- package/dist/path.js.map +1 -0
- package/dist/resolver.d.ts +13 -0
- package/dist/resolver.d.ts.map +1 -0
- package/dist/resolver.js +50 -0
- package/dist/resolver.js.map +1 -0
- package/dist/semantic-builder.d.ts +23 -0
- package/dist/semantic-builder.d.ts.map +1 -0
- package/dist/semantic-builder.js +33 -0
- package/dist/semantic-builder.js.map +1 -0
- package/dist/util.d.ts +2 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +7 -0
- package/dist/util.js.map +1 -0
- package/package.json +54 -0
- package/src/checker.ts +66 -0
- package/src/config.ts +285 -0
- package/src/diagnostic.ts +37 -0
- package/src/dts-creator.ts +128 -0
- package/src/error.ts +13 -0
- package/src/export-builder.ts +51 -0
- package/src/file.ts +83 -0
- package/src/index.ts +38 -0
- package/src/parser/at-import-parser.ts +47 -0
- package/src/parser/at-value-parser.ts +177 -0
- package/src/parser/css-module-parser.ts +193 -0
- package/src/parser/location.ts +43 -0
- package/src/parser/rule-parser.ts +140 -0
- package/src/path.ts +40 -0
- package/src/resolver.ts +57 -0
- package/src/typing/typescript.d.ts +19 -0
- package/src/util.ts +3 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { MatchesPattern } from './file.js';
|
|
2
|
+
import type { CSSModule } from './parser/css-module-parser.js';
|
|
3
|
+
import type { Resolver } from './resolver.js';
|
|
4
|
+
|
|
5
|
+
export const STYLES_EXPORT_NAME = 'styles';
|
|
6
|
+
|
|
7
|
+
export interface CreateDtsOptions {
|
|
8
|
+
resolver: Resolver;
|
|
9
|
+
matchesPattern: MatchesPattern;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface CodeMapping {
|
|
13
|
+
/** The source offsets of the tokens in the *.module.css file. */
|
|
14
|
+
sourceOffsets: number[];
|
|
15
|
+
/** The lengths of the tokens in the *.module.css file. */
|
|
16
|
+
lengths: number[];
|
|
17
|
+
/** The generated offsets of the tokens in the *.d.ts file. */
|
|
18
|
+
generatedOffsets: number[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The map linking the two codes in *.d.ts */
|
|
22
|
+
// NOTE: `sourceOffsets` and `generatedOffsets` are interchangeable. Exchanging code assignments does not change the behavior.
|
|
23
|
+
interface LinkedCodeMapping extends CodeMapping {
|
|
24
|
+
/** The offset of the first code to be linked. */
|
|
25
|
+
sourceOffsets: number[];
|
|
26
|
+
/** The length of the first code to be linked. */
|
|
27
|
+
lengths: number[];
|
|
28
|
+
/** The offset of the second code to be linked. */
|
|
29
|
+
generatedOffsets: number[];
|
|
30
|
+
/** The length of the second code to be linked. */
|
|
31
|
+
generatedLengths: number[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a d.ts file from a CSS module file.
|
|
36
|
+
* @example
|
|
37
|
+
* If the CSS module file is:
|
|
38
|
+
* ```css
|
|
39
|
+
* @import './a.module.css';
|
|
40
|
+
* @value local1: string;
|
|
41
|
+
* @value imported1, imported2 as aliasedImported2 from './b.module.css';
|
|
42
|
+
* .local2 { color: red }
|
|
43
|
+
* ```
|
|
44
|
+
* The d.ts file would be:
|
|
45
|
+
* ```ts
|
|
46
|
+
* const styles = {
|
|
47
|
+
* local1: '' as readonly string,
|
|
48
|
+
* local2: '' as readonly string,
|
|
49
|
+
* ...(await import('./a.module.css')).default,
|
|
50
|
+
* imported1: (await import('./b.module.css')).default.imported1,
|
|
51
|
+
* aliasedImported2: (await import('./b.module.css')).default.imported2,
|
|
52
|
+
* };
|
|
53
|
+
* export default styles;
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export function createDts(
|
|
57
|
+
{ fileName, localTokens, tokenImporters: _tokenImporters }: CSSModule,
|
|
58
|
+
options: CreateDtsOptions,
|
|
59
|
+
): { text: string; mapping: CodeMapping; linkedCodeMapping: LinkedCodeMapping } {
|
|
60
|
+
const mapping: CodeMapping = { sourceOffsets: [], lengths: [], generatedOffsets: [] };
|
|
61
|
+
const linkedCodeMapping: LinkedCodeMapping = {
|
|
62
|
+
sourceOffsets: [],
|
|
63
|
+
lengths: [],
|
|
64
|
+
generatedOffsets: [],
|
|
65
|
+
generatedLengths: [],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Filter external files
|
|
69
|
+
const tokenImporters = _tokenImporters.filter((tokenImporter) => {
|
|
70
|
+
const resolved = options.resolver(tokenImporter.from, { request: fileName });
|
|
71
|
+
return resolved !== undefined && options.matchesPattern(resolved);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// If the CSS module file has no tokens, return an .d.ts file with an empty object.
|
|
75
|
+
if (localTokens.length === 0 && tokenImporters.length === 0) {
|
|
76
|
+
return {
|
|
77
|
+
text: `declare const ${STYLES_EXPORT_NAME} = {};\nexport default ${STYLES_EXPORT_NAME};\n`,
|
|
78
|
+
mapping,
|
|
79
|
+
linkedCodeMapping,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let text = `declare const ${STYLES_EXPORT_NAME} = {\n`;
|
|
84
|
+
for (const token of localTokens) {
|
|
85
|
+
text += ` `;
|
|
86
|
+
mapping.sourceOffsets.push(token.loc.start.offset);
|
|
87
|
+
mapping.generatedOffsets.push(text.length);
|
|
88
|
+
mapping.lengths.push(token.name.length);
|
|
89
|
+
text += `${token.name}: '' as readonly string,\n`;
|
|
90
|
+
}
|
|
91
|
+
for (const tokenImporter of tokenImporters) {
|
|
92
|
+
if (tokenImporter.type === 'import') {
|
|
93
|
+
text += ` ...(await import(`;
|
|
94
|
+
mapping.sourceOffsets.push(tokenImporter.fromLoc.start.offset - 1);
|
|
95
|
+
mapping.lengths.push(tokenImporter.from.length + 2);
|
|
96
|
+
mapping.generatedOffsets.push(text.length);
|
|
97
|
+
text += `'${tokenImporter.from}')).default,\n`;
|
|
98
|
+
} else {
|
|
99
|
+
// eslint-disable-next-line no-loop-func
|
|
100
|
+
tokenImporter.values.forEach((value, i) => {
|
|
101
|
+
const localName = value.localName ?? value.name;
|
|
102
|
+
const localLoc = value.localLoc ?? value.loc;
|
|
103
|
+
|
|
104
|
+
text += ` `;
|
|
105
|
+
mapping.sourceOffsets.push(localLoc.start.offset);
|
|
106
|
+
mapping.lengths.push(localName.length);
|
|
107
|
+
mapping.generatedOffsets.push(text.length);
|
|
108
|
+
linkedCodeMapping.sourceOffsets.push(text.length);
|
|
109
|
+
linkedCodeMapping.lengths.push(localName.length);
|
|
110
|
+
text += `${localName}: (await import(`;
|
|
111
|
+
if (i === 0) {
|
|
112
|
+
mapping.sourceOffsets.push(tokenImporter.fromLoc.start.offset - 1);
|
|
113
|
+
mapping.lengths.push(tokenImporter.from.length + 2);
|
|
114
|
+
mapping.generatedOffsets.push(text.length);
|
|
115
|
+
}
|
|
116
|
+
text += `'${tokenImporter.from}')).default.`;
|
|
117
|
+
mapping.sourceOffsets.push(value.loc.start.offset);
|
|
118
|
+
mapping.lengths.push(value.name.length);
|
|
119
|
+
mapping.generatedOffsets.push(text.length);
|
|
120
|
+
linkedCodeMapping.generatedOffsets.push(text.length);
|
|
121
|
+
linkedCodeMapping.generatedLengths.push(value.name.length);
|
|
122
|
+
text += `${value.name},\n`;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
text += `};\nexport default ${STYLES_EXPORT_NAME};\n`;
|
|
127
|
+
return { text, mapping, linkedCodeMapping };
|
|
128
|
+
}
|
package/src/error.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export class SystemError extends Error {
|
|
2
|
+
code: string;
|
|
3
|
+
constructor(code: string, message: string, cause?: unknown) {
|
|
4
|
+
super(message, { cause });
|
|
5
|
+
this.code = code;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class TsConfigFileNotFoundError extends SystemError {
|
|
10
|
+
constructor() {
|
|
11
|
+
super('TS_CONFIG_NOT_FOUND', 'No tsconfig.json found.');
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { MatchesPattern } from './file.js';
|
|
2
|
+
import type { CSSModule } from './parser/css-module-parser.js';
|
|
3
|
+
import type { Resolver } from './resolver.js';
|
|
4
|
+
|
|
5
|
+
export interface ExportBuilderHost {
|
|
6
|
+
matchesPattern: MatchesPattern;
|
|
7
|
+
getCSSModule: (path: string) => CSSModule | undefined;
|
|
8
|
+
resolver: Resolver;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The export token record of a CSS module.
|
|
13
|
+
*/
|
|
14
|
+
export interface ExportRecord {
|
|
15
|
+
/** The all exported tokens of the CSS module. */
|
|
16
|
+
allTokens: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ExportBuilder {
|
|
20
|
+
build(cssModule: CSSModule): ExportRecord;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A builder for exported token records of CSS modules.
|
|
25
|
+
*/
|
|
26
|
+
// TODO: Add cache
|
|
27
|
+
// TODO: Add support for circular dependencies
|
|
28
|
+
// TODO: Handle same token name from different modules
|
|
29
|
+
export function createExportBuilder(host: ExportBuilderHost): ExportBuilder {
|
|
30
|
+
function build(cssModule: CSSModule): ExportRecord {
|
|
31
|
+
const result: ExportRecord = { allTokens: [...cssModule.localTokens.map((t) => t.name)] };
|
|
32
|
+
|
|
33
|
+
for (const tokenImporter of cssModule.tokenImporters) {
|
|
34
|
+
const from = host.resolver(tokenImporter.from, { request: cssModule.fileName });
|
|
35
|
+
if (!from || !host.matchesPattern(from)) continue;
|
|
36
|
+
const imported = host.getCSSModule(from);
|
|
37
|
+
if (!imported) continue;
|
|
38
|
+
|
|
39
|
+
const importedResult = build(imported);
|
|
40
|
+
if (tokenImporter.type === 'import') {
|
|
41
|
+
result.allTokens.push(...importedResult.allTokens);
|
|
42
|
+
} else {
|
|
43
|
+
for (const value of tokenImporter.values) {
|
|
44
|
+
result.allTokens.push(value.name);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
return { build };
|
|
51
|
+
}
|
package/src/file.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { join, parse } from './path.js';
|
|
3
|
+
|
|
4
|
+
export const CSS_MODULE_EXTENSION = '.module.css';
|
|
5
|
+
const COMPONENT_EXTENSIONS = ['.tsx', '.jsx'];
|
|
6
|
+
|
|
7
|
+
export function isCSSModuleFile(fileName: string): boolean {
|
|
8
|
+
return fileName.endsWith(CSS_MODULE_EXTENSION);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getCssModuleFileName(tsFileName: string): string {
|
|
12
|
+
const { dir, name } = parse(tsFileName);
|
|
13
|
+
return join(dir, `${name}${CSS_MODULE_EXTENSION}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isComponentFileName(fileName: string): boolean {
|
|
17
|
+
// NOTE: Do not check whether it is an upper camel case or not, since lower camel case (e.g. `page.tsx`) is used in Next.js.
|
|
18
|
+
return COMPONENT_EXTENSIONS.some((ext) => fileName.endsWith(ext));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function findComponentFile(
|
|
22
|
+
cssModuleFileName: string,
|
|
23
|
+
readFile: (path: string) => Promise<string>,
|
|
24
|
+
): Promise<{ fileName: string; text: string } | undefined> {
|
|
25
|
+
const pathWithoutExtension = cssModuleFileName.slice(0, -CSS_MODULE_EXTENSION.length);
|
|
26
|
+
for (const path of COMPONENT_EXTENSIONS.map((ext) => pathWithoutExtension + ext)) {
|
|
27
|
+
try {
|
|
28
|
+
// TODO: Cache the result of readFile
|
|
29
|
+
// eslint-disable-next-line no-await-in-loop
|
|
30
|
+
const text = await readFile(path);
|
|
31
|
+
return { fileName: path, text };
|
|
32
|
+
} catch {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type MatchesPattern = (fileName: string) => boolean;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Create a function that checks whether the given file name matches the pattern.
|
|
43
|
+
* This does not access the file system.
|
|
44
|
+
* @param options
|
|
45
|
+
* @returns
|
|
46
|
+
*/
|
|
47
|
+
export function createMatchesPattern(options: { includes: string[]; excludes: string[] }): MatchesPattern {
|
|
48
|
+
// Setup utilities to check for pattern matches without accessing the file system
|
|
49
|
+
const realpath = (path: string) => path;
|
|
50
|
+
const getFileSystemEntries = (path: string): ts.FileSystemEntries => {
|
|
51
|
+
return {
|
|
52
|
+
files: [path],
|
|
53
|
+
directories: [],
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (fileName: string) => {
|
|
58
|
+
const matchedFileNames = ts.matchFiles(
|
|
59
|
+
fileName,
|
|
60
|
+
[CSS_MODULE_EXTENSION],
|
|
61
|
+
options.excludes,
|
|
62
|
+
options.includes,
|
|
63
|
+
ts.sys.useCaseSensitiveFileNames,
|
|
64
|
+
'', // `fileName`, `includes`, and `excludes` are absolute paths, so `currentDirectory` is not needed.
|
|
65
|
+
undefined,
|
|
66
|
+
getFileSystemEntries,
|
|
67
|
+
realpath,
|
|
68
|
+
);
|
|
69
|
+
return matchedFileNames.length > 0;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get files matched by the pattern.
|
|
75
|
+
*/
|
|
76
|
+
export function getFileNamesByPattern(options: { basePath: string; includes: string[]; excludes: string[] }): string[] {
|
|
77
|
+
// ref: https://github.com/microsoft/TypeScript/blob/caf1aee269d1660b4d2a8b555c2d602c97cb28d7/src/compiler/commandLineParser.ts#L3929
|
|
78
|
+
|
|
79
|
+
// MEMO: `ts.sys.readDirectory` catch errors internally. So we don't need to wrap with try-catch.
|
|
80
|
+
// https://github.com/microsoft/TypeScript/blob/caf1aee269d1660b4d2a8b555c2d602c97cb28d7/src/compiler/sys.ts#L1877-L1879
|
|
81
|
+
|
|
82
|
+
return ts.sys.readDirectory(options.basePath, [CSS_MODULE_EXTENSION], options.excludes, options.includes);
|
|
83
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type { CMKConfig } from './config.js';
|
|
2
|
+
export { readConfigFile } from './config.js';
|
|
3
|
+
export { TsConfigFileNotFoundError, SystemError } from './error.js';
|
|
4
|
+
export {
|
|
5
|
+
parseCSSModule,
|
|
6
|
+
type ParseCSSModuleOptions,
|
|
7
|
+
type CSSModule,
|
|
8
|
+
type Token,
|
|
9
|
+
type AtImportTokenImporter,
|
|
10
|
+
type TokenImporter,
|
|
11
|
+
type AtValueTokenImporter,
|
|
12
|
+
type AtValueTokenImporterValue,
|
|
13
|
+
type ParseCSSModuleResult,
|
|
14
|
+
} from './parser/css-module-parser.js';
|
|
15
|
+
export { type Location, type Position } from './parser/location.js';
|
|
16
|
+
export { parseRule } from './parser/rule-parser.js';
|
|
17
|
+
export {
|
|
18
|
+
type Diagnostic,
|
|
19
|
+
type SemanticDiagnostic,
|
|
20
|
+
type SyntacticDiagnostic,
|
|
21
|
+
type DiagnosticCategory,
|
|
22
|
+
type DiagnosticPosition,
|
|
23
|
+
} from './diagnostic.js';
|
|
24
|
+
export { type CreateDtsOptions, createDts, STYLES_EXPORT_NAME } from './dts-creator.js';
|
|
25
|
+
export { createResolver, type Resolver } from './resolver.js';
|
|
26
|
+
export {
|
|
27
|
+
CSS_MODULE_EXTENSION,
|
|
28
|
+
getCssModuleFileName,
|
|
29
|
+
isComponentFileName,
|
|
30
|
+
isCSSModuleFile,
|
|
31
|
+
findComponentFile,
|
|
32
|
+
type MatchesPattern,
|
|
33
|
+
createMatchesPattern,
|
|
34
|
+
getFileNamesByPattern,
|
|
35
|
+
} from './file.js';
|
|
36
|
+
export { checkCSSModule } from './checker.js';
|
|
37
|
+
export { type ExportBuilder, createExportBuilder } from './export-builder.js';
|
|
38
|
+
export { join, resolve, relative, dirname, basename, parse, matchesGlob, isAbsolute } from './path.js';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { AtRule } from 'postcss';
|
|
2
|
+
import postcssValueParser from 'postcss-value-parser';
|
|
3
|
+
import type { Location } from './location.js';
|
|
4
|
+
|
|
5
|
+
interface ParsedAtImport {
|
|
6
|
+
from: string;
|
|
7
|
+
fromLoc: Location;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse the `@import` rule.
|
|
12
|
+
* @param atImport The `@import` rule to parse.
|
|
13
|
+
* @returns The specifier of the imported file.
|
|
14
|
+
*/
|
|
15
|
+
export function parseAtImport(atImport: AtRule): ParsedAtImport | undefined {
|
|
16
|
+
const firstNode = postcssValueParser(atImport.params).nodes[0];
|
|
17
|
+
if (firstNode === undefined) return undefined;
|
|
18
|
+
if (firstNode.type === 'string') return convertParsedAtImport(atImport, firstNode);
|
|
19
|
+
if (firstNode.type === 'function' && firstNode.value === 'url') {
|
|
20
|
+
if (firstNode.nodes[0] === undefined) return undefined;
|
|
21
|
+
if (firstNode.nodes[0].type === 'string') return convertParsedAtImport(atImport, firstNode.nodes[0]);
|
|
22
|
+
if (firstNode.nodes[0].type === 'word') return convertParsedAtImport(atImport, firstNode.nodes[0]);
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function convertParsedAtImport(
|
|
28
|
+
atImport: AtRule,
|
|
29
|
+
node: postcssValueParser.StringNode | postcssValueParser.WordNode,
|
|
30
|
+
): ParsedAtImport {
|
|
31
|
+
// The length of the `@import ` part in `@import '...'`
|
|
32
|
+
const baseLength = 7 + (atImport.raws.afterName?.length ?? 0);
|
|
33
|
+
const start = {
|
|
34
|
+
line: atImport.source!.start!.line,
|
|
35
|
+
column: atImport.source!.start!.column + baseLength + node.sourceIndex + (node.type === 'string' ? 1 : 0),
|
|
36
|
+
offset: atImport.source!.start!.offset + baseLength + node.sourceIndex + (node.type === 'string' ? 1 : 0),
|
|
37
|
+
};
|
|
38
|
+
const end = {
|
|
39
|
+
line: start.line,
|
|
40
|
+
column: start.column + node.value.length,
|
|
41
|
+
offset: start.offset + node.value.length,
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
from: node.value,
|
|
45
|
+
fromLoc: { start, end },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { AtRule } from 'postcss';
|
|
2
|
+
import type { DiagnosticPosition, SyntacticDiagnostic } from '../diagnostic.js';
|
|
3
|
+
import type { Location } from './location.js';
|
|
4
|
+
|
|
5
|
+
interface ValueDeclaration {
|
|
6
|
+
type: 'valueDeclaration';
|
|
7
|
+
name: string;
|
|
8
|
+
// value: string; // unused
|
|
9
|
+
loc: Location;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ValueImportDeclaration {
|
|
13
|
+
type: 'valueImportDeclaration';
|
|
14
|
+
values: {
|
|
15
|
+
name: string;
|
|
16
|
+
loc: Location;
|
|
17
|
+
localName?: string;
|
|
18
|
+
localLoc?: Location;
|
|
19
|
+
}[];
|
|
20
|
+
from: string;
|
|
21
|
+
fromLoc: Location;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type ParsedAtValue = ValueDeclaration | ValueImportDeclaration;
|
|
25
|
+
|
|
26
|
+
interface ParseAtValueResult {
|
|
27
|
+
atValue?: ParsedAtValue;
|
|
28
|
+
diagnostics: SyntacticDiagnostic[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const VALUE_IMPORT_PATTERN = /^(.+?)\s+from\s+("[^"]*"|'[^']*')$/du;
|
|
32
|
+
const VALUE_DEFINITION_PATTERN = /(?:\s+|^)([\w-]+):?(.*?)$/du;
|
|
33
|
+
const IMPORTED_ITEM_PATTERN = /^([\w-]+)(?:\s+as\s+([\w-]+))?/du;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse the `@value` rule.
|
|
37
|
+
* Forked from https://github.com/css-modules/postcss-modules-values/blob/v4.0.0/src/index.js.
|
|
38
|
+
*
|
|
39
|
+
* @license
|
|
40
|
+
* ISC License (ISC)
|
|
41
|
+
* Copyright (c) 2015, Glen Maddern
|
|
42
|
+
*
|
|
43
|
+
* Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted,
|
|
44
|
+
* provided that the above copyright notice and this permission notice appear in all copies.
|
|
45
|
+
*
|
|
46
|
+
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING
|
|
47
|
+
* ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
48
|
+
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
|
49
|
+
* WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH
|
|
50
|
+
* THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
51
|
+
*/
|
|
52
|
+
// MEMO: css-modules-kit does not support `@value` with parentheses (e.g., `@value (a, b) from '...';`) to simplify the implementation.
|
|
53
|
+
// MEMO: css-modules-kit does not support `@value` with variable module name (e.g., `@value a from moduleName;`) to simplify the implementation.
|
|
54
|
+
export function parseAtValue(atValue: AtRule): ParseAtValueResult {
|
|
55
|
+
const matchesForValueImport = atValue.params.match(VALUE_IMPORT_PATTERN);
|
|
56
|
+
const diagnostics: SyntacticDiagnostic[] = [];
|
|
57
|
+
if (matchesForValueImport) {
|
|
58
|
+
const [, importedItems, from] = matchesForValueImport as [string, string, string];
|
|
59
|
+
// The length of the `@value ` part in `@value import1 from '...'`
|
|
60
|
+
const baseLength = 6 + (atValue.raws.afterName?.length ?? 0);
|
|
61
|
+
|
|
62
|
+
let lastItemIndex = 0;
|
|
63
|
+
const values: ValueImportDeclaration['values'] = [];
|
|
64
|
+
for (const alias of importedItems.split(/\s*,\s*/u)) {
|
|
65
|
+
const currentItemIndex = importedItems.indexOf(alias, lastItemIndex);
|
|
66
|
+
lastItemIndex = currentItemIndex;
|
|
67
|
+
const matchesForImportedItem = alias.match(IMPORTED_ITEM_PATTERN);
|
|
68
|
+
|
|
69
|
+
if (matchesForImportedItem) {
|
|
70
|
+
const [, name, localName] = matchesForImportedItem as [string, string, string | undefined];
|
|
71
|
+
const nameIndex = matchesForImportedItem.indices![1]![0];
|
|
72
|
+
const start = {
|
|
73
|
+
line: atValue.source!.start!.line,
|
|
74
|
+
column: atValue.source!.start!.column + baseLength + currentItemIndex + nameIndex,
|
|
75
|
+
offset: atValue.source!.start!.offset + baseLength + currentItemIndex + nameIndex,
|
|
76
|
+
};
|
|
77
|
+
const end = {
|
|
78
|
+
line: start.line,
|
|
79
|
+
column: start.column + name.length,
|
|
80
|
+
offset: start.offset + name.length,
|
|
81
|
+
};
|
|
82
|
+
const result = { name, loc: { start, end } };
|
|
83
|
+
if (localName === undefined) {
|
|
84
|
+
values.push(result);
|
|
85
|
+
} else {
|
|
86
|
+
const localNameIndex = matchesForImportedItem.indices![2]![0];
|
|
87
|
+
const start = {
|
|
88
|
+
line: atValue.source!.start!.line,
|
|
89
|
+
column: atValue.source!.start!.column + baseLength + currentItemIndex + localNameIndex,
|
|
90
|
+
offset: atValue.source!.start!.offset + baseLength + currentItemIndex + localNameIndex,
|
|
91
|
+
};
|
|
92
|
+
const end = {
|
|
93
|
+
line: start.line,
|
|
94
|
+
column: start.column + localName.length,
|
|
95
|
+
offset: start.offset + localName.length,
|
|
96
|
+
};
|
|
97
|
+
values.push({ ...result, localName, localLoc: { start, end } });
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
const start: DiagnosticPosition = {
|
|
101
|
+
line: atValue.source!.start!.line,
|
|
102
|
+
column: atValue.source!.start!.column + baseLength + currentItemIndex,
|
|
103
|
+
};
|
|
104
|
+
const end: DiagnosticPosition = {
|
|
105
|
+
line: start.line,
|
|
106
|
+
column: start.column + alias.length,
|
|
107
|
+
};
|
|
108
|
+
diagnostics.push({
|
|
109
|
+
type: 'syntactic',
|
|
110
|
+
fileName: atValue.source!.input.file!,
|
|
111
|
+
start,
|
|
112
|
+
end,
|
|
113
|
+
text: `\`${alias}\` is invalid syntax.`,
|
|
114
|
+
category: 'error',
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// `from` is surrounded by quotes (e.g., `"./test.module.css"`). So, remove the quotes.
|
|
120
|
+
const normalizedFrom = from.slice(1, -1);
|
|
121
|
+
|
|
122
|
+
const fromIndex = matchesForValueImport.indices![2]![0] + 1;
|
|
123
|
+
const start = {
|
|
124
|
+
line: atValue.source!.start!.line,
|
|
125
|
+
column: atValue.source!.start!.column + baseLength + fromIndex,
|
|
126
|
+
offset: atValue.source!.start!.offset + baseLength + fromIndex,
|
|
127
|
+
};
|
|
128
|
+
const end = {
|
|
129
|
+
line: start.line,
|
|
130
|
+
column: start.column + normalizedFrom.length,
|
|
131
|
+
offset: start.offset + normalizedFrom.length,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const parsedAtValue: ValueImportDeclaration = {
|
|
135
|
+
type: 'valueImportDeclaration',
|
|
136
|
+
values,
|
|
137
|
+
from: normalizedFrom,
|
|
138
|
+
fromLoc: { start, end },
|
|
139
|
+
};
|
|
140
|
+
return { atValue: parsedAtValue, diagnostics };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const matchesForValueDefinition = `${atValue.params}${atValue.raws.between!}`.match(VALUE_DEFINITION_PATTERN);
|
|
144
|
+
if (matchesForValueDefinition) {
|
|
145
|
+
const [, name, _value] = matchesForValueDefinition;
|
|
146
|
+
if (name === undefined) throw new Error(`unreachable`);
|
|
147
|
+
/** The index of the `<name>` in `@value <name>: <value>;`. */
|
|
148
|
+
const nameIndex = 6 + (atValue.raws.afterName?.length ?? 0) + matchesForValueDefinition.indices![1]![0];
|
|
149
|
+
const start = {
|
|
150
|
+
line: atValue.source!.start!.line,
|
|
151
|
+
column: atValue.source!.start!.column + nameIndex,
|
|
152
|
+
offset: atValue.source!.start!.offset + nameIndex,
|
|
153
|
+
};
|
|
154
|
+
const end = {
|
|
155
|
+
line: start.line,
|
|
156
|
+
column: start.column + name.length,
|
|
157
|
+
offset: start.offset + name.length,
|
|
158
|
+
};
|
|
159
|
+
const parsedAtValue = { type: 'valueDeclaration', name, loc: { start, end } } as const;
|
|
160
|
+
return { atValue: parsedAtValue, diagnostics };
|
|
161
|
+
}
|
|
162
|
+
diagnostics.push({
|
|
163
|
+
type: 'syntactic',
|
|
164
|
+
fileName: atValue.source!.input.file!,
|
|
165
|
+
start: {
|
|
166
|
+
line: atValue.source!.start!.line,
|
|
167
|
+
column: atValue.source!.start!.column,
|
|
168
|
+
},
|
|
169
|
+
end: {
|
|
170
|
+
line: atValue.source!.end!.line,
|
|
171
|
+
column: atValue.source!.end!.column + 1,
|
|
172
|
+
},
|
|
173
|
+
text: `\`${atValue.toString()}\` is a invalid syntax.`,
|
|
174
|
+
category: 'error',
|
|
175
|
+
});
|
|
176
|
+
return { diagnostics };
|
|
177
|
+
}
|