Skip to content

Commit

Permalink
Merge pull request #213 from mizdra/support-at-value
Browse files Browse the repository at this point in the history
Support for `@value`
  • Loading branch information
mizdra committed Jun 16, 2024
2 parents 85ecbb5 + fcfca7a commit fa9d107
Show file tree
Hide file tree
Showing 20 changed files with 434 additions and 73 deletions.
1 change: 1 addition & 0 deletions packages/example/01-basic/1.css
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@
}
:local(.local_class_name_4) {
}
@value value: #BF4040;
1 change: 1 addition & 0 deletions packages/example/01-basic/1.css.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ declare const styles:
& Readonly<{ "local_class_name_2": string }>
& Readonly<{ "local_class_name_3": string }>
& Readonly<{ "local_class_name_4": string }>
& Readonly<{ "value": string }>
;
export default styles;
//# sourceMappingURL=./1.css.d.ts.map
2 changes: 1 addition & 1 deletion packages/example/01-basic/1.css.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions packages/example/08-value-from/1.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@value value1 from './2.css'; /* single token */
@value value2, value3 from './2.css'; /* multiple tokens */
@value value4 as alias from './2.css'; /* alias */
@value value5 from './2.css'; /* re-exported token */
9 changes: 9 additions & 0 deletions packages/example/08-value-from/1.css.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
declare const styles:
& Readonly<Pick<(typeof import("./2.css"))["default"], "value1">>
& Readonly<Pick<(typeof import("./2.css"))["default"], "value2">>
& Readonly<Pick<(typeof import("./2.css"))["default"], "value3">>
& Readonly<{ "alias": (typeof import("./2.css"))["default"]["value4"] }>
& Readonly<Pick<(typeof import("./3.css"))["default"], "value5">>
;
export default styles;
//# sourceMappingURL=./1.css.d.ts.map
1 change: 1 addition & 0 deletions packages/example/08-value-from/1.css.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions packages/example/08-value-from/2.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@value value1: '';
@value value2: '';
@value value3: '';
@value value4: '';
@value value5 from './3.css';
9 changes: 9 additions & 0 deletions packages/example/08-value-from/2.css.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
declare const styles:
& Readonly<{ "value1": string }>
& Readonly<{ "value2": string }>
& Readonly<{ "value3": string }>
& Readonly<{ "value4": string }>
& Readonly<Pick<(typeof import("./3.css"))["default"], "value5">>
;
export default styles;
//# sourceMappingURL=./2.css.d.ts.map
1 change: 1 addition & 0 deletions packages/example/08-value-from/2.css.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/example/08-value-from/3.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@value value5: '';
5 changes: 5 additions & 0 deletions packages/example/08-value-from/3.css.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
declare const styles:
& Readonly<{ "value5": string }>
;
export default styles;
//# sourceMappingURL=./3.css.d.ts.map
1 change: 1 addition & 0 deletions packages/example/08-value-from/3.css.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions packages/example/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import styles3 from './03-composes/1.css';
import styles4 from './04-sass/1.scss';
import styles5 from './05-less/1.less';
import styles6 from './06-postcss/1.css';
import styles8 from './08-value-from/1.css';

console.log(styles1.basic);
console.log(styles1.cascading);
Expand All @@ -22,6 +23,7 @@ console.log(styles1.local_class_name_1);
console.log(styles1.local_class_name_2);
console.log(styles1.local_class_name_3);
console.log(styles1.local_class_name_4);
console.log(styles1.value);

console.log(styles2.a);
console.log(styles2.b);
Expand All @@ -44,3 +46,9 @@ console.log(styles5.b_1);
console.log(styles6.a_1);
console.log(styles6.a_2);
console.log(styles6.b);

console.log(styles8.value1);
console.log(styles8.value2);
console.log(styles8.value3);
console.log(styles8.alias);
console.log(styles8.value5);
6 changes: 3 additions & 3 deletions packages/happy-css-modules/src/emitter/dts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,9 @@ describe('generateDtsContentWithSourceMap', () => {
});
test('emit other directory', async () => {
createFixtures({
'/test/1.css': `.a {}`,
'/test/src/1.css': `.a {}`,
});
const result = await locator.load(filePath);
const result = await locator.load(getFixturePath('/test/src/1.css'));
const { dtsContent, sourceMap } = generateDtsContentWithSourceMap(
getFixturePath('/test/src/1.css'),
getFixturePath('/test/dist/1.css.d.ts'),
Expand All @@ -224,7 +224,7 @@ describe('generateDtsContentWithSourceMap', () => {
);
expect(dtsContent).toMatchInlineSnapshot(`
"declare const styles:
& Readonly<Pick<(typeof import("../1.css"))["default"], "a">>
& Readonly<{ "a": string }>
;
export default styles;
"
Expand Down
31 changes: 27 additions & 4 deletions packages/happy-css-modules/src/emitter/dts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,26 @@ function dashesCamelCase(str: string): string {
}

function formatTokens(tokens: Token[], localsConvention: LocalsConvention): Token[] {
function formatToken(token: Token, formatter: (str: string) => string): Token {
if ('importedName' in token && typeof token.importedName === 'string') {
return { ...token, name: formatter(token.name), importedName: formatter(token.importedName) };
} else {
return { ...token, name: formatter(token.name) };
}
}

const result: Token[] = [];
for (const token of tokens) {
if (localsConvention === 'camelCaseOnly') {
result.push({ ...token, name: camelcase(token.name) });
result.push(formatToken(token, camelcase));
} else if (localsConvention === 'camelCase') {
result.push(token);
result.push({ ...token, name: camelcase(token.name) });
result.push(formatToken(token, camelcase));
} else if (localsConvention === 'dashesOnly') {
result.push({ ...token, name: dashesCamelCase(token.name) });
result.push(formatToken(token, dashesCamelCase));
} else if (localsConvention === 'dashes') {
result.push(token);
result.push({ ...token, name: dashesCamelCase(token.name) });
result.push(formatToken(token, dashesCamelCase));
} else {
result.push(token); // asIs
}
Expand Down Expand Up @@ -86,6 +94,21 @@ function generateTokenDeclarations(
),
': string }>',
])
: typeof token.importedName === 'string'
? new SourceNode(null, null, null, [
`& Readonly<{ `,
new SourceNode(
originalLocation.start.line ?? null,
// The SourceNode's column is 0-based, but the originalLocation's column is 1-based.
originalLocation.start.column - 1 ?? null,
getRelativePath(sourceMapFilePath, originalLocation.filePath),
`"${token.name}"`,
token.name,
),
`: (typeof import(`,
`"${getRelativePath(filePath, originalLocation.filePath)}"`,
`))["default"]["${token.importedName}"] }>`,
])
: // Imported tokens in non-external files are typed by dynamic import.
// See https://github.com/mizdra/happy-css-modules/issues/106.
new SourceNode(null, null, null, [
Expand Down
51 changes: 51 additions & 0 deletions packages/happy-css-modules/src/locator/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,57 @@ test('does not track other files by `composes`', async () => {
`);
});

test('tracks other files when `@value` is present', async () => {
createFixtures({
'/test/1.css': dedent`
@value a from './2.css';
@value b from '3.css';
@value c from '${getFixturePath('/test/4.css')}';
`,
'/test/2.css': dedent`
@value a: 1;
`,
'/test/3.css': dedent`
@value b: 2;
`,
'/test/4.css': dedent`
@value c: 3;
`,
});
const result = await locator.load(getFixturePath('/test/1.css'));
expect(result).toMatchInlineSnapshot(`
{
dependencies: ["<fixtures>/test/2.css", "<fixtures>/test/3.css", "<fixtures>/test/4.css"],
tokens: [
{
name: "a",
originalLocation: {
filePath: "<fixtures>/test/2.css",
start: { line: 1, column: 8 },
end: { line: 1, column: 9 },
},
},
{
name: "b",
originalLocation: {
filePath: "<fixtures>/test/3.css",
start: { line: 1, column: 8 },
end: { line: 1, column: 9 },
},
},
{
name: "c",
originalLocation: {
filePath: "<fixtures>/test/4.css",
start: { line: 1, column: 8 },
end: { line: 1, column: 9 },
},
},
],
}
`);
});

test('unique tokens', async () => {
createFixtures({
'/test/1.css': dedent`
Expand Down
50 changes: 47 additions & 3 deletions packages/happy-css-modules/src/locator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ import type { Resolver } from '../resolver/index.js';
import { createDefaultResolver } from '../resolver/index.js';
import { createDefaultTransformer, type Transformer } from '../transformer/index.js';
import { unique, uniqueBy } from '../util.js';
import { getOriginalLocation, generateLocalTokenNames, parseAtImport, type Location, collectNodes } from './postcss.js';
import {
getOriginalLocationOfClassSelector,
getOriginalLocationOfAtValue,
generateLocalTokenNames,
parseAtImport,
type Location,
collectNodes,
parseAtValue,
} from './postcss.js';

export { collectNodes, type Location } from './postcss.js';

Expand All @@ -20,6 +28,8 @@ function isIgnoredSpecifier(specifier: string): boolean {
export type Token = {
/** The token name. */
name: string;
/** The name of the imported token. */
importedName?: string;
/** The original location of the token in the source file. */
originalLocation: Location;
};
Expand Down Expand Up @@ -142,7 +152,7 @@ export class Locator {

const tokens: Token[] = [];

const { atImports, classSelectors } = collectNodes(ast);
const { atImports, atValues, classSelectors } = collectNodes(ast);

// Load imported sheets recursively.
for (const atImport of atImports) {
Expand All @@ -164,14 +174,48 @@ export class Locator {
// NOTE: This method has false positives. However, it works as expected in many cases.
if (!localTokenNames.includes(classSelector.value)) continue;

const originalLocation = getOriginalLocation(rule, classSelector);
const originalLocation = getOriginalLocationOfClassSelector(rule, classSelector);

tokens.push({
name: classSelector.value,
originalLocation,
});
}

for (const atValue of atValues) {
const parsedAtValue = parseAtValue(atValue);

if (parsedAtValue.type === 'valueDeclaration') {
tokens.push({
name: parsedAtValue.tokenName,
originalLocation: getOriginalLocationOfAtValue(atValue, parsedAtValue),
});
} else if (parsedAtValue.type === 'valueImportDeclaration') {
if (isIgnoredSpecifier(parsedAtValue.from)) continue;
// eslint-disable-next-line no-await-in-loop
const from = await this.resolver(parsedAtValue.from, { request: filePath });
// eslint-disable-next-line no-await-in-loop
const result = await this._load(from);
dependencies.push(from, ...result.dependencies);
for (const token of result.tokens) {
const matchedImport = parsedAtValue.imports.find((i) => i.importedTokenName === token.name);
if (!matchedImport) continue;
if (matchedImport.localTokenName === matchedImport.importedTokenName) {
tokens.push({
name: matchedImport.localTokenName,
originalLocation: token.originalLocation,
});
} else {
tokens.push({
name: matchedImport.localTokenName,
importedName: matchedImport.importedTokenName,
originalLocation: token.originalLocation,
});
}
}
}
}

const result: LoadResult = {
dependencies: unique(dependencies).filter((dep) => dep !== filePath),
tokens: uniqueBy(tokens, (token) => JSON.stringify(token)),
Expand Down
Loading

0 comments on commit fa9d107

Please sign in to comment.