Skip to content

Commit

Permalink
Merge pull request #251 from mizdra/fix-composes-token
Browse files Browse the repository at this point in the history
Tokens loaded by `composes` are not exported
  • Loading branch information
mizdra authored May 22, 2024
2 parents b308f2b + c919d01 commit 6e41547
Show file tree
Hide file tree
Showing 17 changed files with 18 additions and 198 deletions.
1 change: 0 additions & 1 deletion packages/example/03-composes/1.css.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
declare const styles:
& Readonly<{ "a": string }>
& Readonly<Pick<(typeof import("./2.css"))["default"], "b">>
;
export default styles;
//# sourceMappingURL=./1.css.d.ts.map
2 changes: 1 addition & 1 deletion packages/example/03-composes/1.css.d.ts.map

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

1 change: 0 additions & 1 deletion packages/example/03-composes/2.css.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
declare const styles:
& Readonly<{ "b": string }>
& Readonly<Pick<(typeof import("./3.css"))["default"], "c">>
;
export default styles;
//# sourceMappingURL=./2.css.d.ts.map
2 changes: 1 addition & 1 deletion packages/example/03-composes/2.css.d.ts.map

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

1 change: 0 additions & 1 deletion packages/example/04-sass/1.scss.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ declare const styles:
& Readonly<{ "a_2": string }>
& Readonly<{ "a_2_1": string }>
& Readonly<{ "a_2_2": string }>
& Readonly<Pick<(typeof import("./4.scss"))["default"], "d">>
;
export default styles;
//# sourceMappingURL=./1.scss.d.ts.map
2 changes: 1 addition & 1 deletion packages/example/04-sass/1.scss.d.ts.map

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

1 change: 0 additions & 1 deletion packages/example/05-less/1.less.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ declare const styles:
& Readonly<{ "a_2": string }>
& Readonly<{ "a_2_1": string }>
& Readonly<{ "a_2_2": string }>
& Readonly<Pick<(typeof import("./3.less"))["default"], "c">>
;
export default styles;
//# sourceMappingURL=./1.less.d.ts.map
2 changes: 1 addition & 1 deletion packages/example/05-less/1.less.d.ts.map

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

4 changes: 0 additions & 4 deletions packages/example/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,19 @@ console.log(styles2.a);
console.log(styles2.b);

console.log(styles3.a);
console.log(styles3.b);
// console.log(styles3.c); // Maybe it needs fixing?

console.log(styles4.a_1);
console.log(styles4.a_2);
console.log(styles4.a_2_1);
console.log(styles4.a_2_2);
console.log(styles4.b_1);
console.log(styles4.c);
console.log(styles4.d);

console.log(styles5.a_1);
console.log(styles5.a_2);
console.log(styles5.a_2_1);
console.log(styles5.a_2_2);
console.log(styles5.b_1);
console.log(styles5.c);

console.log(styles6.a_1);
console.log(styles6.a_2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,9 +365,7 @@ test('with transformer', async () => {
"identifier": "c",
},
{
"definitions": [
{ file: "<fixtures>/test/4.scss", text: ".d ", start: { line: 1, offset: 1 }, end: { line: 1, offset: 4 } },
],
"definitions": [],
"identifier": "d",
},
]
Expand Down
56 changes: 4 additions & 52 deletions packages/happy-css-modules/src/locator/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto';
import { jest } from '@jest/globals';
import dedent from 'dedent';
import { createDefaultTransformer } from '../index.js';
import { createFixtures, FIXTURE_DIR_PATH, getFixturePath } from '../test-util/util.js';
import { createFixtures, getFixturePath } from '../test-util/util.js';
import { sleepSync } from '../util.js';

const readFileSpy = jest.spyOn(fs, 'readFile');
Expand Down Expand Up @@ -140,38 +140,14 @@ test('tracks other files when `composes` is present', async () => {
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"],
dependencies: [],
tokens: [
{
name: "a",
originalLocations: [
{ filePath: "<fixtures>/test/1.css", start: { line: 1, column: 1 }, end: { line: 1, column: 2 } },
],
},
{
name: "b",
originalLocations: [
{ filePath: "<fixtures>/test/2.css", start: { line: 1, column: 1 }, end: { line: 1, column: 2 } },
],
},
{
name: "c",
originalLocations: [
{ filePath: "<fixtures>/test/3.css", start: { line: 1, column: 1 }, end: { line: 1, column: 2 } },
],
},
{
name: "d",
originalLocations: [
{ filePath: "<fixtures>/test/3.css", start: { line: 2, column: 1 }, end: { line: 2, column: 2 } },
],
},
{
name: "e",
originalLocations: [
{ filePath: "<fixtures>/test/4.css", start: { line: 1, column: 1 }, end: { line: 1, column: 2 } },
],
},
],
}
`);
Expand Down Expand Up @@ -204,7 +180,7 @@ test('normalizes tokens', async () => {
const result = await locator.load(getFixturePath('/test/1.css'));
expect(result).toMatchInlineSnapshot(`
{
dependencies: ["<fixtures>/test/2.css", "<fixtures>/test/3.css"],
dependencies: ["<fixtures>/test/2.css"],
tokens: [
{
name: "a",
Expand All @@ -220,12 +196,6 @@ test('normalizes tokens', async () => {
{ filePath: "<fixtures>/test/2.css", start: { line: 2, column: 1 }, end: { line: 2, column: 2 } },
],
},
{
name: "c",
originalLocations: [
{ filePath: "<fixtures>/test/3.css", start: { line: 1, column: 1 }, end: { line: 1, column: 2 } },
],
},
],
}
`);
Expand Down Expand Up @@ -292,25 +262,7 @@ test('ignores the composition of non-existent tokens', async () => {
`,
});
const result = await locator.load(getFixturePath('/test/1.css'));
expect(result.tokens.map((t) => t.name)).toStrictEqual(['a', 'b']);
});

test('throws error the composition of non-existent file', async () => {
// In postcss-modules, compositions of non-existent file are causes an error.
// Therefore, happy-css-modules follows suit.
createFixtures({
'/test/1.css': dedent`
.a {
composes: a from './2.css';
}
`,
});
await expect(async () => {
await locator.load(getFixturePath('/test/1.css')).catch((e) => {
e.message = e.message.replace(FIXTURE_DIR_PATH, '<fixtures>');
throw e;
});
}).rejects.toThrowError(`Could not resolve './2.css' in '<fixtures>/test/1.css'`);
expect(result.tokens.map((t) => t.name)).toStrictEqual(['a']);
});

describe('supports sourcemap', () => {
Expand Down
25 changes: 2 additions & 23 deletions packages/happy-css-modules/src/locator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,7 @@ 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,
parseComposesDeclarationWithFromUrl,
collectNodes,
} from './postcss.js';
import { getOriginalLocation, generateLocalTokenNames, parseAtImport, type Location, collectNodes } from './postcss.js';

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

Expand Down Expand Up @@ -165,7 +158,7 @@ export class Locator {

const tokens: Token[] = [];

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

// Load imported sheets recursively.
for (const atImport of atImports) {
Expand Down Expand Up @@ -195,20 +188,6 @@ export class Locator {
});
}

// Load imported tokens by the names recursively.
for (const composesDeclaration of composesDeclarations) {
const declarationDetail = parseComposesDeclarationWithFromUrl(composesDeclaration);
if (!declarationDetail) continue;
if (isIgnoredSpecifier(declarationDetail.from)) continue;
// eslint-disable-next-line no-await-in-loop
const from = await this.resolver(declarationDetail.from, { request: filePath });
// eslint-disable-next-line no-await-in-loop
const result = await this._load(from);
const externalTokens = result.tokens.filter((token) => declarationDetail.tokenNames.includes(token.name));
dependencies.push(from, ...result.dependencies);
tokens.push(...externalTokens);
}

const result: LoadResult = {
dependencies: unique(dependencies).filter((dep) => dep !== filePath),
tokens: normalizeTokens(tokens),
Expand Down
50 changes: 3 additions & 47 deletions packages/happy-css-modules/src/locator/postcss.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,6 @@
import dedent from 'dedent';
import {
createRoot,
createClassSelectors,
createAtImports,
createComposesDeclarations,
createFixtures,
} from '../test-util/util.js';
import {
generateLocalTokenNames,
getOriginalLocation,
parseAtImport,
parseComposesDeclarationWithFromUrl,
collectNodes,
} from './postcss.js';
import { createRoot, createClassSelectors, createAtImports, createFixtures } from '../test-util/util.js';
import { generateLocalTokenNames, getOriginalLocation, parseAtImport, collectNodes } from './postcss.js';

describe('generateLocalTokenNames', () => {
test('basic', async () => {
Expand Down Expand Up @@ -284,7 +272,7 @@ test('collectNodes', () => {
.b { ignored: "ignored"; composes: b; }
`);

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

expect(atImports).toHaveLength(2);
expect(atImports[0]!.toString()).toEqual('@import');
Expand All @@ -294,9 +282,6 @@ test('collectNodes', () => {
expect(classSelectors[0]!.classSelector.toString()).toEqual('.a');
expect(classSelectors[1]!.rule.toString()).toEqual('.b { ignored: "ignored"; composes: b; }');
expect(classSelectors[1]!.classSelector.toString()).toEqual('.b');
expect(composesDeclarations).toHaveLength(2);
expect(composesDeclarations[0]!.toString()).toEqual('composes: a');
expect(composesDeclarations[1]!.toString()).toEqual('composes: b');
});

test('parseAtImport', () => {
Expand All @@ -315,32 +300,3 @@ test('parseAtImport', () => {
expect(parseAtImport(atImports[3]!)).toBe('test.css');
expect(parseAtImport(atImports[4]!)).toBe('test.css');
});

test('parseComposesDeclarationWithFromUrl', () => {
const composesDeclarations = createComposesDeclarations(
createRoot(dedent`
.a {
composes: a;
composes: a b c;
composes: a from "test.css";
composes: a b c from "test.css";
composes: from from from from "test.css";
/* NOTE: CSS Modules do not support '... from url("test.css")'. */
}
`),
);
expect(parseComposesDeclarationWithFromUrl(composesDeclarations[0]!)).toStrictEqual(undefined);
expect(parseComposesDeclarationWithFromUrl(composesDeclarations[1]!)).toStrictEqual(undefined);
expect(parseComposesDeclarationWithFromUrl(composesDeclarations[2]!)).toStrictEqual({
from: 'test.css',
tokenNames: ['a'],
});
expect(parseComposesDeclarationWithFromUrl(composesDeclarations[3]!)).toStrictEqual({
from: 'test.css',
tokenNames: ['a', 'b', 'c'],
});
expect(parseComposesDeclarationWithFromUrl(composesDeclarations[4]!)).toStrictEqual({
from: 'test.css',
tokenNames: ['from', 'from', 'from'], // do not deduplicate.
});
});
43 changes: 1 addition & 42 deletions packages/happy-css-modules/src/locator/postcss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ function isComposesDeclaration(node: Node): node is Declaration {
type CollectNodesResult = {
atImports: AtRule[];
classSelectors: { rule: Rule; classSelector: ClassName }[];
composesDeclarations: Declaration[];
};

/**
Expand All @@ -164,7 +163,6 @@ type CollectNodesResult = {
export function collectNodes(ast: Root): CollectNodesResult {
const atImports: AtRule[] = [];
const classSelectors: { rule: Rule; classSelector: ClassName }[] = [];
const composesDeclarations: Declaration[] = [];
ast.walk((node) => {
if (isAtImportNode(node)) {
atImports.push(node);
Expand All @@ -183,11 +181,9 @@ export function collectNodes(ast: Root): CollectNodesResult {
}
});
}).processSync(node);
} else if (isComposesDeclaration(node)) {
composesDeclarations.push(node);
}
});
return { atImports, classSelectors, composesDeclarations };
return { atImports, classSelectors };
}

/**
Expand All @@ -206,40 +202,3 @@ export function parseAtImport(atImport: AtRule): string | undefined {
}
return undefined;
}

/**
* Parse `composes` declaration with `from <url>`.
* If the declaration is not found or do not have `from <url>`, return `undefined`.
* @param composesDeclaration The `composes` declaration to parse.
* @returns The information of the declaration.
*/
export function parseComposesDeclarationWithFromUrl(
composesDeclaration: Declaration,
): { from: string; tokenNames: string[] } | undefined {
// NOTE: `composes` property syntax is...
// - syntax: `composes: <class-name> [...<class-name>] [from <url>];`
// - variables:
// - `<class-name>`: `<sting>`
// - `<url>`: `<string>`
// - ref:
// - https://github.com/css-modules/css-modules#composition
// - https://github.com/css-modules/css-modules#composing-from-other-files
// - https://github.com/css-modules/postcss-modules-extract-imports#specification

const nodes = valueParser(composesDeclaration.value).nodes;
if (nodes.length < 5) return undefined;

const classNamesOrSpaces = nodes.slice(0, -3);
const [from, , url] = nodes.slice(-3);

const classNames = classNamesOrSpaces.filter((node) => node.type === 'word');

// validate nodes
if (from === undefined) return undefined;
if (from.type !== 'word' || from.value !== 'from') return undefined;
if (url === undefined) return undefined;
if (url.type !== 'string') return undefined;
if (classNames.length === 0) return undefined;

return { from: url.value, tokenNames: classNames.map((node) => node.value) };
}
6 changes: 1 addition & 5 deletions packages/happy-css-modules/src/test-util/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { constants, mkdirSync, realpathSync, rmSync, writeFileSync } from 'fs';
import { access } from 'fs/promises';
import { tmpdir } from 'os';
import { dirname, join, resolve } from 'path';
import postcss, { type Root, type Rule, type AtRule, type Declaration } from 'postcss';
import postcss, { type Root, type Rule, type AtRule } from 'postcss';
import { type ClassName } from 'postcss-selector-parser';
import { type Token, collectNodes, type Location } from '../locator/index.js';
import { sleepSync } from '../util.js';
Expand All @@ -25,10 +25,6 @@ export function createClassSelectors(root: Root): { rule: Rule; classSelector: C
return collectNodes(root).classSelectors;
}

export function createComposesDeclarations(root: Root): Declaration[] {
return collectNodes(root).composesDeclarations;
}

export function fakeToken(args: {
name: Token['name'];
originalLocations: { filePath?: Location['filePath']; start?: Location['start'] }[];
Expand Down
Loading

0 comments on commit 6e41547

Please sign in to comment.