Skip to content

Commit 6c18abf

Browse files
committed
support namedExports options
1 parent b7a7d97 commit 6c18abf

9 files changed

Lines changed: 251 additions & 129 deletions

File tree

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,23 @@ Determines whether to generate `*.module.d.css.ts` instead of `*.module.css.d.ts
148148
}
149149
```
150150

151+
### `cmkOptions.namedExports`
152+
153+
Type: `boolean`, Default: `false`
154+
155+
Determines whether to generate named exports in the d.ts file instead of a default export.
156+
157+
```jsonc
158+
{
159+
"compilerOptions": {
160+
// ...
161+
},
162+
"cmkOptions": {
163+
"namedExports": true,
164+
},
165+
}
166+
```
167+
151168
## Limitations
152169

153170
- Sass/Less are not supported to simplify the implementation

packages/codegen/src/runner.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,11 @@ async function parseCSSModuleByFileName(fileName: string): Promise<ParseCSSModul
4141
*/
4242
async function writeDtsByCSSModule(
4343
cssModule: CSSModule,
44-
{ dtsOutDir, basePath, arbitraryExtensions }: CMKConfig,
44+
{ dtsOutDir, basePath, arbitraryExtensions, namedExports }: CMKConfig,
4545
resolver: Resolver,
4646
matchesPattern: MatchesPattern,
4747
): Promise<void> {
48-
const dts = createDts(cssModule, { resolver, matchesPattern });
48+
const dts = createDts(cssModule, { resolver, matchesPattern, namedExports });
4949
await writeDtsFile(dts.text, cssModule.fileName, {
5050
outDir: dtsOutDir,
5151
basePath,

packages/core/src/config.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ describe('readTsConfigFile', () => {
2727
"module": "esnext"
2828
},
2929
"cmkOptions": {
30-
"dtsOutDir": "generated/cmk"
30+
"dtsOutDir": "generated/cmk",
31+
"arbitraryExtensions": false,
32+
"namedExports": true
3133
}
3234
}
3335
`,
@@ -38,6 +40,8 @@ describe('readTsConfigFile', () => {
3840
includes: ['src'],
3941
excludes: ['src/test'],
4042
dtsOutDir: 'generated/cmk',
43+
arbitraryExtensions: false,
44+
namedExports: true,
4145
},
4246
compilerOptions: expect.objectContaining({
4347
module: ts.ModuleKind.ESNext,

packages/core/src/config.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export interface CMKConfig {
1515
excludes: string[];
1616
dtsOutDir: string;
1717
arbitraryExtensions: boolean;
18+
/** Whether to generate named exports in the d.ts file instead of a default export. */
19+
namedExports: boolean;
1820
/**
1921
* A root directory to resolve relative path entries in the config file to.
2022
* This is an absolute path.
@@ -68,6 +70,7 @@ interface UnnormalizedRawConfig {
6870
excludes?: string[];
6971
dtsOutDir?: string;
7072
arbitraryExtensions?: boolean;
73+
namedExports?: boolean;
7174
}
7275

7376
/**
@@ -134,6 +137,17 @@ function parseRawData(raw: unknown, tsConfigSourceFile: ts.TsConfigSourceFile):
134137
});
135138
}
136139
}
140+
if ('namedExports' in raw.cmkOptions) {
141+
if (typeof raw.cmkOptions.namedExports === 'boolean') {
142+
result.config.namedExports = raw.cmkOptions.namedExports;
143+
} else {
144+
result.diagnostics.push({
145+
category: 'error',
146+
text: `\`namedExports\` in ${tsConfigSourceFile.fileName} must be a boolean.`,
147+
// MEMO: Location information can be obtained from `tsConfigSourceFile.statements`, but this is complicated and will be omitted.
148+
});
149+
}
150+
}
137151
}
138152
return result;
139153
}
@@ -146,6 +160,7 @@ function mergeParsedRawData(base: ParsedRawData, overrides: ParsedRawData): Pars
146160
if (overrides.config.dtsOutDir !== undefined) result.config.dtsOutDir = overrides.config.dtsOutDir;
147161
if (overrides.config.arbitraryExtensions !== undefined)
148162
result.config.arbitraryExtensions = overrides.config.arbitraryExtensions;
163+
if (overrides.config.namedExports !== undefined) result.config.namedExports = overrides.config.namedExports;
149164
result.diagnostics.push(...overrides.diagnostics);
150165
return result;
151166
}
@@ -226,6 +241,7 @@ export function readConfigFile(project: string): CMKConfig {
226241
excludes: (config.excludes ?? []).map((e) => join(basePath, e)),
227242
dtsOutDir: join(basePath, config.dtsOutDir ?? 'generated'),
228243
arbitraryExtensions: config.arbitraryExtensions ?? false,
244+
namedExports: config.namedExports ?? false,
229245
basePath,
230246
configFileName,
231247
compilerOptions,

packages/core/src/dts-creator.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { fakeCSSModule } from './test/css-module.js';
66
const options: CreateDtsOptions = {
77
resolver: (specifier, { request }) => join(dirname(request), specifier),
88
matchesPattern: () => true,
9+
namedExports: false,
910
};
1011

1112
function fakeLoc(offset: number) {
@@ -377,4 +378,84 @@ describe('createDts', () => {
377378
}
378379
`);
379380
});
381+
test('creates d.ts file with named exports', () => {
382+
expect(
383+
createDts(
384+
fakeCSSModule({
385+
localTokens: [
386+
{ name: 'local1', loc: fakeLoc(0) },
387+
{ name: 'local2', loc: fakeLoc(1) },
388+
],
389+
tokenImporters: [
390+
{ type: 'import', from: './a.module.css', fromLoc: fakeLoc(2) },
391+
{
392+
type: 'value',
393+
values: [
394+
{ name: 'imported1', loc: fakeLoc(3) },
395+
{ name: 'imported2', loc: fakeLoc(4), localName: 'aliasedImported2', localLoc: fakeLoc(5) },
396+
],
397+
from: './b.module.css',
398+
fromLoc: fakeLoc(6),
399+
},
400+
],
401+
}),
402+
{ ...options, namedExports: true },
403+
),
404+
).toMatchInlineSnapshot(`
405+
{
406+
"linkedCodeMapping": {
407+
"generatedLengths": [
408+
9,
409+
],
410+
"generatedOffsets": [
411+
125,
412+
],
413+
"lengths": [
414+
16,
415+
],
416+
"sourceOffsets": [
417+
138,
418+
],
419+
},
420+
"mapping": {
421+
"generatedOffsets": [
422+
26,
423+
53,
424+
83,
425+
112,
426+
125,
427+
138,
428+
163,
429+
],
430+
"lengths": [
431+
6,
432+
6,
433+
16,
434+
9,
435+
9,
436+
16,
437+
16,
438+
],
439+
"sourceOffsets": [
440+
0,
441+
1,
442+
1,
443+
3,
444+
4,
445+
5,
446+
5,
447+
],
448+
},
449+
"text": "// @ts-nocheck
450+
export var local1: string;
451+
export var local2: string;
452+
export * from './a.module.css';
453+
export {
454+
imported1,
455+
imported2 as aliasedImported2,
456+
} from './b.module.css';
457+
",
458+
}
459+
`);
460+
});
380461
});

packages/core/src/dts-creator.ts

Lines changed: 120 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import type { CSSModule, MatchesPattern, Resolver } from './type.js';
1+
import type { CSSModule, MatchesPattern, Resolver, Token, TokenImporter } from './type.js';
22

33
export const STYLES_EXPORT_NAME = 'styles';
44

55
export interface CreateDtsOptions {
66
resolver: Resolver;
77
matchesPattern: MatchesPattern;
8+
namedExports: boolean;
89
}
910

1011
interface CodeMapping {
@@ -29,8 +30,122 @@ interface LinkedCodeMapping extends CodeMapping {
2930
generatedLengths: number[];
3031
}
3132

33+
interface CreateDtsResult {
34+
text: string;
35+
mapping: CodeMapping;
36+
linkedCodeMapping: LinkedCodeMapping;
37+
}
38+
39+
/**
40+
* Create a d.ts file.
41+
*/
42+
export function createDts(cssModules: CSSModule, options: CreateDtsOptions): CreateDtsResult {
43+
// Filter external files
44+
const tokenImporters = cssModules.tokenImporters.filter((tokenImporter) => {
45+
const resolved = options.resolver(tokenImporter.from, { request: cssModules.fileName });
46+
return resolved !== undefined && options.matchesPattern(resolved);
47+
});
48+
if (options.namedExports) {
49+
return createNamedExportsDts(cssModules.localTokens, tokenImporters);
50+
} else {
51+
return createDefaultExportDts(cssModules.localTokens, tokenImporters);
52+
}
53+
}
54+
3255
/**
33-
* Create a d.ts file from a CSS module file.
56+
* Create a d.ts file with named exports.
57+
* @example
58+
* If the CSS module file is:
59+
* ```css
60+
* @import './a.module.css';
61+
* @value local1: string;
62+
* @value imported1, imported2 as aliasedImported2 from './b.module.css';
63+
* .local2 { color: red }
64+
* ```
65+
* The d.ts file would be:
66+
* ```ts
67+
* // @ts-nocheck
68+
* export var local1: string;
69+
* export var local2: string;
70+
* export * from './a.module.css';
71+
* export {
72+
* imported1,
73+
* imported2 as aliasedImported2,
74+
* } from './b.module.css';
75+
* ```
76+
*/
77+
function createNamedExportsDts(
78+
localTokens: Token[],
79+
tokenImporters: TokenImporter[],
80+
): { text: string; mapping: CodeMapping; linkedCodeMapping: LinkedCodeMapping } {
81+
const mapping: CodeMapping = { sourceOffsets: [], lengths: [], generatedOffsets: [] };
82+
const linkedCodeMapping: LinkedCodeMapping = {
83+
sourceOffsets: [],
84+
lengths: [],
85+
generatedOffsets: [],
86+
generatedLengths: [],
87+
};
88+
89+
// MEMO: Depending on the TypeScript compilation options, the generated type definition file contains compile errors.
90+
// For example, it contains `Top-level 'await' expressions are only allowed when the 'module' option is set to ...` error.
91+
//
92+
// If `--skipLibCheck` is false, those errors will be reported by `tsc`. However, these are negligible errors.
93+
// Therefore, `@ts-nocheck` is added to the generated type definition file.
94+
let text = `// @ts-nocheck\n`;
95+
96+
for (const token of localTokens) {
97+
text += `export var `;
98+
mapping.sourceOffsets.push(token.loc.start.offset);
99+
mapping.generatedOffsets.push(text.length);
100+
mapping.lengths.push(token.name.length);
101+
text += `${token.name}: string;\n`;
102+
}
103+
for (const tokenImporter of tokenImporters) {
104+
if (tokenImporter.type === 'import') {
105+
text += `export * from `;
106+
mapping.sourceOffsets.push(tokenImporter.fromLoc.start.offset - 1);
107+
mapping.lengths.push(tokenImporter.from.length + 2);
108+
mapping.generatedOffsets.push(text.length);
109+
text += `'${tokenImporter.from}';\n`;
110+
} else {
111+
text += `export {\n`;
112+
// eslint-disable-next-line no-loop-func
113+
tokenImporter.values.forEach((value) => {
114+
const localName = value.localName ?? value.name;
115+
const localLoc = value.localLoc ?? value.loc;
116+
text += ` `;
117+
if ('localName' in value) {
118+
mapping.sourceOffsets.push(value.loc.start.offset);
119+
mapping.lengths.push(value.name.length);
120+
mapping.generatedOffsets.push(text.length);
121+
linkedCodeMapping.generatedOffsets.push(text.length);
122+
linkedCodeMapping.generatedLengths.push(value.name.length);
123+
text += `${value.name} as `;
124+
mapping.sourceOffsets.push(localLoc.start.offset);
125+
mapping.lengths.push(localName.length);
126+
mapping.generatedOffsets.push(text.length);
127+
linkedCodeMapping.sourceOffsets.push(text.length);
128+
linkedCodeMapping.lengths.push(localName.length);
129+
text += `${localName},\n`;
130+
} else {
131+
mapping.sourceOffsets.push(value.loc.start.offset);
132+
mapping.lengths.push(value.name.length);
133+
mapping.generatedOffsets.push(text.length);
134+
text += `${value.name},\n`;
135+
}
136+
});
137+
text += `} from `;
138+
mapping.sourceOffsets.push(tokenImporter.fromLoc.start.offset - 1);
139+
mapping.lengths.push(tokenImporter.from.length + 2);
140+
mapping.generatedOffsets.push(text.length);
141+
text += `'${tokenImporter.from}';\n`;
142+
}
143+
}
144+
return { text, mapping, linkedCodeMapping };
145+
}
146+
147+
/**
148+
* Create a d.ts file with a default export.
34149
* @example
35150
* If the CSS module file is:
36151
* ```css
@@ -52,9 +167,9 @@ interface LinkedCodeMapping extends CodeMapping {
52167
* export default styles;
53168
* ```
54169
*/
55-
export function createDts(
56-
{ fileName, localTokens, tokenImporters: _tokenImporters }: CSSModule,
57-
options: CreateDtsOptions,
170+
function createDefaultExportDts(
171+
localTokens: Token[],
172+
tokenImporters: TokenImporter[],
58173
): { text: string; mapping: CodeMapping; linkedCodeMapping: LinkedCodeMapping } {
59174
const mapping: CodeMapping = { sourceOffsets: [], lengths: [], generatedOffsets: [] };
60175
const linkedCodeMapping: LinkedCodeMapping = {
@@ -64,12 +179,6 @@ export function createDts(
64179
generatedLengths: [],
65180
};
66181

67-
// Filter external files
68-
const tokenImporters = _tokenImporters.filter((tokenImporter) => {
69-
const resolved = options.resolver(tokenImporter.from, { request: fileName });
70-
return resolved !== undefined && options.matchesPattern(resolved);
71-
});
72-
73182
// MEMO: Depending on the TypeScript compilation options, the generated type definition file contains compile errors.
74183
// For example, it contains `Top-level 'await' expressions are only allowed when the 'module' option is set to ...` error.
75184
//

0 commit comments

Comments
 (0)