Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor codes #80

Merged
merged 9 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions docs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ The internal name of the item being exported from `*.module.css`.
For example, consider the following CSS file:

```css
.a {
@value a_1: red;
@import b_1, b_2 as b_2_alias from './b.module.css';
.a_2 {
color: red;
}
.b,
.c {
.a_3,
.a_4 {
color: red;
}
:root {
--a: red;
--a-5: red;
}
```

In this case, `a`, `b`, and `c` are tokens. If `dashedIdents` option is `true`, `--a` is also a token.
In this case, `a_1`, `a_2`, `a_3`, `a_4`, `b_1` and `b_2_alias` are tokens. If `dashedIdents` option is `true`, `--a-5` is also a token.
12 changes: 6 additions & 6 deletions packages/codegen/src/dts-writer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import { WriteDtsFileError } from './error.js';
import { createIFF } from './test/fixture.js';

describe('getDtsFilePath', () => {
const options = { cwd: '/app', outDir: 'dist', arbitraryExtensions: false };
const options = { rootDir: '/app', outDir: '/app/dist', arbitraryExtensions: false };
test('cwd', () => {
expect(getDtsFilePath('/app1/src/dir/a.module.css', { ...options, cwd: '/app1' })).toBe(
resolve('/app1/dist/src/dir/a.module.css.d.ts'),
expect(getDtsFilePath('/app1/src/dir/a.module.css', { ...options, rootDir: '/app1' })).toBe(
resolve('/app/dist/src/dir/a.module.css.d.ts'),
);
});
test('outDir', () => {
expect(getDtsFilePath('/app/src/dir/a.module.css', { ...options, outDir: 'dist/dir' })).toBe(
expect(getDtsFilePath('/app/src/dir/a.module.css', { ...options, outDir: '/app/dist/dir' })).toBe(
resolve('/app/dist/dir/src/dir/a.module.css.d.ts'),
);
});
Expand All @@ -36,7 +36,7 @@ describe('writeDtsFile', () => {
iff.join('src/a.module.css'),
{
outDir: iff.join('generated'),
cwd: iff.rootDir,
rootDir: iff.rootDir,
arbitraryExtensions: false,
},
);
Expand All @@ -59,7 +59,7 @@ describe('writeDtsFile', () => {
iff.join('src/a.module.css'),
{
outDir: iff.join('generated'),
cwd: iff.rootDir,
rootDir: iff.rootDir,
arbitraryExtensions: false,
},
),
Expand Down
9 changes: 4 additions & 5 deletions packages/codegen/src/dts-writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { WriteDtsFileError } from './error.js';
* @returns The path to the .d.ts file. It is absolute.
*/
export function getDtsFilePath(cssModuleFileName: string, options: WriteDtsFileOption): string {
const relativePath = relative(options.cwd, cssModuleFileName);
const outputFilePath = resolve(options.cwd, options.outDir, relativePath);
const relativePath = relative(options.rootDir, cssModuleFileName);
const outputFilePath = resolve(options.outDir, relativePath);

if (options.arbitraryExtensions) {
const { dir, name, ext } = parse(outputFilePath);
Expand All @@ -21,10 +21,9 @@ export function getDtsFilePath(cssModuleFileName: string, options: WriteDtsFileO
}

export interface WriteDtsFileOption {
/** Directory to write the d.ts file. This is relative to {@link cwd}. */
/** Directory to write the d.ts file. This is an absolute path. */
outDir: string;
/** Current working directory. */
cwd: string;
rootDir: string;
/** Generate `.d.css.ts` instead of `.css.d.ts`. */
arbitraryExtensions: boolean;
}
Expand Down
32 changes: 12 additions & 20 deletions packages/codegen/src/runner.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
// eslint-disable-next-line n/no-unsupported-features/node-builtins -- TODO: Require Node.js version which have stable glob API
import { glob, readFile } from 'node:fs/promises';
import { join } from 'node:path';
import type { Diagnostic, HCMConfig, IsExternalFile, ResolvedHCMConfig, Resolver } from 'honey-css-modules-core';
import { createDts, createIsExternalFile, createResolver, parseCSSModule, resolveConfig } from 'honey-css-modules-core';
import type { Diagnostic, HCMConfig, IsProjectFile, ResolvedHCMConfig, Resolver } from 'honey-css-modules-core';
import { createDts, createIsProjectFile, createResolver, parseCSSModule, resolveConfig } from 'honey-css-modules-core';
import { writeDtsFile } from './dts-writer.js';
import { ReadCSSModuleFileError } from './error.js';
import type { Logger } from './logger/logger.js';
Expand All @@ -13,9 +12,9 @@ import type { Logger } from './logger/logger.js';
*/
async function processFile(
fileName: string,
{ dashedIdents, dtsOutDir, cwd, arbitraryExtensions }: ResolvedHCMConfig,
{ dashedIdents, dtsOutDir, rootDir, arbitraryExtensions }: ResolvedHCMConfig,
resolver: Resolver,
isExternalFile: IsExternalFile,
isProjectFile: IsProjectFile,
): Promise<Diagnostic[]> {
let text: string;
try {
Expand All @@ -27,10 +26,10 @@ async function processFile(
if (diagnostics.length > 0) {
return diagnostics;
}
const dts = createDts(cssModule, { resolver, isExternalFile });
const dts = createDts(cssModule, { resolver, isProjectFile });
await writeDtsFile(dts.text, fileName, {
outDir: dtsOutDir,
cwd,
rootDir,
arbitraryExtensions,
});
return [];
Expand All @@ -41,22 +40,15 @@ async function processFile(
* @throws {ReadCSSModuleFileError} When failed to read CSS Module file.
* @throws {WriteDtsFileError}
*/
export async function runHCM(config: HCMConfig, cwd: string, logger: Logger): Promise<void> {
const resolvedConfig = resolveConfig(config, cwd);
export async function runHCM(config: HCMConfig, rootDir: string, logger: Logger): Promise<void> {
const resolvedConfig = resolveConfig(config, rootDir);
const { pattern, alias } = resolvedConfig;
const resolver = createResolver(alias, cwd);
const isExternalFile = createIsExternalFile(resolvedConfig);
const resolver = createResolver(alias);
const isProjectFile = createIsProjectFile(resolvedConfig);

const promises: Promise<Diagnostic[]>[] = [];
for await (const fileName of glob(pattern, { cwd })) {
promises.push(
processFile(
join(cwd, fileName), // `fileName` is 'src/a.module.css', so convert it to '/project/src/a.module.css'
resolvedConfig,
resolver,
isExternalFile,
),
);
for await (const fileName of glob(pattern)) {
promises.push(processFile(fileName, resolvedConfig, resolver, isProjectFile));
}
const diagnostics = (await Promise.all(promises)).flat();
if (diagnostics.length > 0) {
Expand Down
59 changes: 52 additions & 7 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface HCMConfig {
dtsOutDir: string;
alias?: Record<string, string> | undefined;
arbitraryExtensions?: boolean | undefined;
dashedIdents?: boolean | undefined;
// dashedIdents?: boolean | undefined; // TODO: Support dashedIdents
}

export function defineConfig(config: HCMConfig): HCMConfig {
Expand Down Expand Up @@ -100,15 +100,60 @@ export interface ResolvedHCMConfig {
alias: Record<string, string>;
arbitraryExtensions: boolean;
dashedIdents: boolean;
cwd: string;
/**
* The root directory of the project. This is used to determine the output directory of the d.ts file.
*
* For example, let’s say you have some input files:
* ```
* .
* ├── hcm.config.js
* ├── src
* │ ├── a.module.css
* │ ├── b.module.css
* │ ├── sub
* │ │ ├── c.module.css
* ```
*
* If you set `rootDir` to `src`, the output files will be:
* ```
* .
* ├── dist
* │ ├── a.module.css.d.ts
* │ ├── b.module.css.d.ts
* │ ├── sub
* │ │ ├── c.module.css.d.ts
* ```
*
* If you set `rootDir` to `.` (the project root), the output files will be:
* ```
* .
* ├── dist
* │ ├── src
* │ │ ├── a.module.css.d.ts
* │ │ ├── b.module.css.d.ts
* │ │ ├── sub
* │ │ │ ├── c.module.css.d.ts
* ```
*/
rootDir: string;
}

export function resolveConfig(config: HCMConfig, cwd: string): ResolvedHCMConfig {
function resolveAlias(alias: Record<string, string> | undefined, cwd: string): Record<string, string> {
if (alias === undefined) return {};
const resolvedAlias: Record<string, string> = {};
for (const [key, value] of Object.entries(alias)) {
resolvedAlias[key] = join(cwd, value);
}
return resolvedAlias;
}

export function resolveConfig(config: HCMConfig, rootDir: string): ResolvedHCMConfig {
return {
...config,
alias: config.alias ?? {},
pattern: join(rootDir, config.pattern),
dtsOutDir: join(rootDir, config.dtsOutDir),
alias: resolveAlias(config.alias, rootDir),
arbitraryExtensions: config.arbitraryExtensions ?? false,
dashedIdents: config.dashedIdents ?? false,
cwd,
dashedIdents: false, // TODO: Support dashedIdents
rootDir,
};
}
4 changes: 2 additions & 2 deletions packages/core/src/dts-creator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createDts, type CreateDtsOptions } from './dts-creator.js';

const options: CreateDtsOptions = {
resolver: (specifier, { request }) => resolve(dirname(request), specifier),
isExternalFile: () => false,
isProjectFile: () => true,
};

function fakeLoc(offset: number) {
Expand Down Expand Up @@ -333,7 +333,7 @@ describe('createDts', () => {
},
],
},
{ ...options, isExternalFile: () => true },
{ ...options, isProjectFile: () => false },
),
).toMatchInlineSnapshot(`
{
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/dts-creator.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { IsExternalFile } from './external-file.js';
import type { IsProjectFile } from './external-file.js';
import type { CSSModuleFile } from './parser/css-module-parser.js';
import type { Resolver } from './resolver.js';

export const STYLES_EXPORT_NAME = 'styles';

export interface CreateDtsOptions {
resolver: Resolver;
isExternalFile: IsExternalFile;
isProjectFile: IsProjectFile;
}

interface CodeMapping {
Expand Down Expand Up @@ -68,7 +68,7 @@ export function createDts(
// Filter external files
const tokenImporters = _tokenImporters.filter((tokenImporter) => {
const resolved = options.resolver(tokenImporter.from, { request: fileName });
return resolved !== undefined && !options.isExternalFile(resolved);
return resolved !== undefined && options.isProjectFile(resolved);
});

// If the CSS module file has no tokens, return an .d.ts file with an empty object.
Expand Down
9 changes: 3 additions & 6 deletions packages/core/src/external-file.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import path from 'node:path';
import type { ResolvedHCMConfig } from './config.js';

export type IsExternalFile = (fileName: string) => boolean;
export type IsProjectFile = (fileName: string) => boolean;

export function createIsExternalFile(config: ResolvedHCMConfig): IsExternalFile {
export function createIsProjectFile(config: ResolvedHCMConfig): IsProjectFile {
return (fileName: string) =>
// eslint-disable-next-line n/no-unsupported-features/node-builtins
!path.matchesGlob(
fileName,
path.join(config.cwd, config.pattern), // `pattern` is 'src/**/*.module.css', so convert it to '/project/src/**/*.module.css'
);
path.matchesGlob(fileName, config.pattern);
}
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ export {
} from './diagnostic.js';
export { type CreateDtsOptions, createDts, STYLES_EXPORT_NAME } from './dts-creator.js';
export { createResolver, type Resolver } from './resolver.js';
export { type IsExternalFile, createIsExternalFile } from './external-file.js';
export { type IsProjectFile, createIsProjectFile } from './external-file.js';
export { getCssModuleFileName, isComponentFileName, isCSSModuleFile, findComponentFile } from './file.js';
76 changes: 43 additions & 33 deletions packages/core/src/resolver.test.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,56 @@
import path from 'node:path';
import { resolve } from 'node:path';
import { describe, expect, test } from 'vitest';
import { createResolver } from './resolver.js';

describe('createResolver', () => {
test('resolves relative path', () => {
const resolve = createResolver({}, '/app');
expect(resolve('./a.module.css', { request: '/app/request.module.css' })).toBe(path.resolve('/app/a.module.css'));
expect(resolve('./dir/a.module.css', { request: '/app/request.module.css' })).toBe(
path.resolve('/app/dir/a.module.css'),
);
describe('resolves relative path', () => {
const resolver = createResolver({});
test.each([
['./a.module.css', resolve('/app/request.module.css'), resolve('/app/a.module.css')],
['./dir/a.module.css', resolve('/app/request.module.css'), resolve('/app/dir/a.module.css')],
])('resolves %s from %s', (specifier, request, expected) => {
expect(resolver(specifier, { request })).toBe(expected);
});
});
describe('resolves absolute path', () => {
const resolver = createResolver({});
test.each([
[resolve('/app/a.module.css'), resolve('/app/request.module.css'), resolve('/app/a.module.css')],
[resolve('/app/dir/a.module.css'), resolve('/app/request.module.css'), resolve('/app/dir/a.module.css')],
])('resolves %s from %s', (specifier, request, expected) => {
expect(resolver(specifier, { request })).toBe(expected);
});
});
describe('resolves alias', () => {
test('alias is used if import specifiers start with alias', () => {
const resolve = createResolver({ '@': './alias', '#': 'alias' }, '/app');
expect(resolve('@/a.module.css', { request: '/app/request.module.css' })).toBe(
path.resolve('/app/alias/a.module.css'),
);
expect(resolve('./@/a.module.css', { request: '/app/request.module.css' })).toBe(
path.resolve('/app/@/a.module.css'),
);
expect(resolve('@/dir/a.module.css', { request: '/app/request.module.css' })).toBe(
path.resolve('/app/alias/dir/a.module.css'),
);
expect(resolve('@/a.module.css', { request: '/app/dir/request.module.css' })).toBe(
path.resolve('/app/alias/a.module.css'),
);
expect(resolve('#/a.module.css', { request: '/app/request.module.css' })).toBe(
path.resolve('/app/alias/a.module.css'),
);
describe('alias is used if import specifiers start with alias', () => {
const resolver = createResolver({ '@': resolve('/app/alias'), '#': resolve('/app/alias') });
test.each([
['@/a.module.css', resolve('/app/request.module.css'), resolve('/app/alias/a.module.css')],
['./@/a.module.css', resolve('/app/request.module.css'), resolve('/app/@/a.module.css')],
['@/dir/a.module.css', resolve('/app/request.module.css'), resolve('/app/alias/dir/a.module.css')],
['@/a.module.css', resolve('/app/dir/request.module.css'), resolve('/app/alias/a.module.css')],
['#/a.module.css', resolve('/app/request.module.css'), resolve('/app/alias/a.module.css')],
])('resolves %s from %s', (specifier, request, expected) => {
expect(resolver(specifier, { request })).toBe(expected);
});
});
test('the first alias is used if multiple aliases match', () => {
const resolve = createResolver({ '@': './alias1', '@@': './alias2' }, '/app');
expect(resolve('@/a.module.css', { request: '/app/request.module.css' })).toBe(
path.resolve('/app/alias1/a.module.css'),
const resolver = createResolver({ '@': resolve('/app/alias1'), '@@': resolve('/app/alias2') });
expect(resolver('@/a.module.css', { request: resolve('/app/request.module.css') })).toBe(
resolve('/app/alias1/a.module.css'),
);
});
});
test('does not resolve invalid path', () => {
const resolve = createResolver({}, '/app');
expect(resolve('http://example.com', { request: '/app/request.module.css' })).toBe(undefined);
expect(resolve('package', { request: '/app/request.module.css' })).toBe(undefined);
expect(resolve('@scope/package', { request: '/app/request.module.css' })).toBe(undefined);
expect(resolve('~package', { request: '/app/request.module.css' })).toBe(undefined);
describe('does not resolve invalid path', () => {
const resolver = createResolver({});
test.each([
['http://example.com', resolve('/app/request.module.css')],
['package', resolve('/app/request.module.css')],
['@scope/package', resolve('/app/request.module.css')],
['~package', resolve('/app/request.module.css')],
['file:///app/a.module.css', resolve('/app/request.module.css')],
])('does not resolve %s from %s', (specifier, request) => {
expect(resolver(specifier, { request })).toBe(undefined);
});
});
});
Loading