Skip to content
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
8 changes: 8 additions & 0 deletions .changeset/container-option.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@css-modules-kit/core': minor
'@css-modules-kit/ts-plugin': minor
'@css-modules-kit/codegen': minor
'css-modules-kit-vscode': minor
---

feat(core, ts-plugin, codegen, vscode): support scoping container names via `cmkOptions.container`
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ function myFunction() {

## Glossary

- **Token**: A generic term for things exported by CSS Modules, such as class names, `@value` definitions, `@keyframes` names, and `<dashed-ident>`s (custom properties, when `cmkOptions.dashedIdents` is enabled).
- **Token reference**: A usage of a token elsewhere in the CSS. Currently produced for `animation-name: <name>` and `composes: <name>`. There are two kinds:
- **Token**: A generic term for things exported by CSS Modules, such as class names, `@value` definitions, `@keyframes` names, container names (when `cmkOptions.container` is enabled), and `<dashed-ident>`s (custom properties, when `cmkOptions.dashedIdents` is enabled).
- **Token reference**: A usage of a token elsewhere in the CSS. Currently produced for `animation-name: <name>`, `composes: <name>`, and `@container <name> (...)`. There are two kinds:
- **Local token reference**: References a token available in the current file. The token may be defined in the same file (e.g. `@keyframes`) or imported via `@import` / `@value ... from`.
- **External token reference**: References tokens exported by another file, like `composes: <name> ... from '<specifier>'`. One reference corresponds to one `from` clause and holds an entry per referenced token.
- **Diagnostic**: An object representing errors or warnings
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,23 @@ Determines whether to generate the [token](docs/glossary.md#token) of `<dashed-i
}
```

### `cmkOptions.container`

Type: `boolean`, Default: `false`

Determines whether to generate the [token](docs/glossary.md#token) of container names (used in `container-name`, the `container` shorthand, and `@container`) in the d.ts file.

```jsonc
{
"compilerOptions": {
// ...
},
"cmkOptions": {
"container": true,
},
}
```

## Limitations

Due to implementation constraints and technical reasons, css-modules-kit has various limitations.
Expand Down
18 changes: 18 additions & 0 deletions docs/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Design Decisions

This document records design decisions that are not obvious from the code, along with their rationale.

## `cmkOptions.container` does not support `local(...)` / `global(...)`

css-modules-kit does not interpret `local(...)` / `global(...)` in `container-name`, the `container` shorthand, or the `@container` prelude (e.g. `container-name: local(foo)`). Function nodes in these positions are ignored: they produce neither a token nor a token reference.

Rationale:

- The `cmkOptions.container` option follows the [`container` option of lightningcss's CSS Modules support](https://lightningcss.dev/css-modules.html#container-queries). lightningcss parses container names as plain `<custom-ident>`s and provides no `local(...)` / `global(...)` escape hatch.
- css-loader does not scope container names at all. Therefore, no CSS Modules implementation gives `local(...)` in these positions any semantics.
- Per [css-conditional-5](https://drafts.csswg.org/css-conditional-5/#container-name), the grammar of `container-name` is `none | <custom-ident>+`. Function forms are invalid CSS.
- Extracting tokens from `local(...)` would emit names into the `.d.ts` that no bundler actually scopes.

This is in contrast to constructs such as `animation-name`, the `animation` shorthand, and `@keyframes`, where css-modules-kit interprets escape hatches like `local(...)` / `global(...)` and `:local(...)` / `:global(...)` because css-loader implements that syntax for `<keyframes-name>`s.

If lightningcss or css-loader adds an escape hatch for container names in the future, css-modules-kit should follow it.
2 changes: 1 addition & 1 deletion docs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ For example, consider the following CSS file:
}
```

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.
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. If `container` option is `true`, container names used in `container-name`, the `container` shorthand, and `@container` are also tokens.

## Corresponding Component File

Expand Down
1 change: 1 addition & 0 deletions examples/1-basic/generated/src/a.module.css.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ declare const styles = {
'--anchor': '' as string,
'--view-timeline': '' as string,
'--scroll-timeline': '' as string,
'container': '' as string,
...blockErrorType((await import('./b.module.css')).default),
'c_1': (await import('./c.module.css')).default['c_1'],
'c_alias': (await import('./c.module.css')).default['c_2'],
Expand Down
6 changes: 6 additions & 0 deletions examples/1-basic/src/a.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,9 @@
scroll-timeline-name: --scroll-timeline;
animation-timeline: --scroll-timeline;
}

/* container-name */
:root {
container-name: container;
}
@container container (width > 400px) {}
1 change: 1 addition & 0 deletions examples/1-basic/src/a.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ styles['--position-try'];
styles['--anchor'];
styles['--view-timeline'];
styles['--scroll-timeline'];
styles['container'];
styles.b_1;
styles.b_2;
styles.c_1;
Expand Down
3 changes: 2 additions & 1 deletion examples/1-basic/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
},
"cmkOptions": {
"enabled": true,
"dashedIdents": true
"dashedIdents": true,
"container": true
}
}
1 change: 1 addition & 0 deletions packages/codegen/src/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ export function createProject(args: ProjectArgs): Project {
includeSyntaxError: true,
animation: config.animation,
dashedIdents: config.dashedIdents,
container: config.container,
});
}

Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ describe('readConfigFile', () => {
prioritizeNamedImports: false,
animation: true,
dashedIdents: false,
container: false,
enabled: false,
compilerOptions: expect.any(Object),
wildcardDirectories: [{ fileName: iff.rootDir, recursive: true }],
Expand All @@ -50,6 +51,7 @@ describe('readConfigFile', () => {
"prioritizeNamedImports": true,
"animation": false,
"dashedIdents": true,
"container": true,
"enabled": true
}
}
Expand All @@ -65,6 +67,7 @@ describe('readConfigFile', () => {
prioritizeNamedImports: true,
animation: false,
dashedIdents: true,
container: true,
enabled: true,
compilerOptions: expect.objectContaining({
module: ts.ModuleKind.ESNext,
Expand All @@ -90,6 +93,7 @@ describe('readConfigFile', () => {
"prioritizeNamedImports": true,
"animation": false,
"dashedIdents": true,
"container": true,
"enabled": true
}
}
Expand All @@ -106,6 +110,7 @@ describe('readConfigFile', () => {
prioritizeNamedImports: true,
animation: false,
dashedIdents: true,
container: true,
enabled: true,
compilerOptions: expect.objectContaining({
module: ts.ModuleKind.ESNext,
Expand Down Expand Up @@ -393,6 +398,17 @@ describe('readConfigFile', () => {
},
]);
});
test('reports an error if `container` is not a boolean', async () => {
const iff = await createIFF({
'tsconfig.json': '{ "cmkOptions": { "container": 1 } }',
});
expect(readConfigFile(iff.rootDir).diagnostics).toStrictEqual([
{
category: 'error',
text: `\`container\` in ${iff.paths['tsconfig.json']} must be a boolean.`,
},
]);
});
});
describe('deprecated `keyframes` option', () => {
test('adopts the `keyframes` value as `animation` and reports a deprecation warning', async () => {
Expand Down
13 changes: 13 additions & 0 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface CMKConfig {
prioritizeNamedImports: boolean;
animation: boolean;
dashedIdents: boolean;
container: boolean;
/**
* A root directory to resolve relative path entries in the config file to.
* This is an absolute path.
Expand Down Expand Up @@ -81,6 +82,7 @@ interface UnnormalizedRawConfig {
animation?: boolean;
keyframes?: boolean;
dashedIdents?: boolean;
container?: boolean;
}

/**
Expand Down Expand Up @@ -196,6 +198,16 @@ function parseRawData(raw: unknown, tsConfigSourceFile: ts.TsConfigSourceFile, b
});
}
}
if ('container' in raw.cmkOptions) {
if (typeof raw.cmkOptions.container === 'boolean') {
result.config.container = raw.cmkOptions.container;
} else {
result.diagnostics.push({
category: 'error',
text: `\`container\` in ${tsConfigSourceFile.fileName} must be a boolean.`,
});
}
}
if ('namedExports' in raw.cmkOptions) {
if (typeof raw.cmkOptions.namedExports === 'boolean') {
result.config.namedExports = raw.cmkOptions.namedExports;
Expand Down Expand Up @@ -316,6 +328,7 @@ export function readConfigFile(project: string): CMKConfig {
prioritizeNamedImports: parsedTsConfig.config.prioritizeNamedImports ?? false,
animation: parsedTsConfig.config.animation ?? parsedTsConfig.config.keyframes ?? true,
dashedIdents: parsedTsConfig.config.dashedIdents ?? false,
container: parsedTsConfig.config.container ?? false,
enabled: parsedTsConfig.config.enabled ?? false,
basePath,
configFileName,
Expand Down
8 changes: 2 additions & 6 deletions packages/core/src/parser/animation-parser.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { Declaration } from 'postcss';
import postcssValueParser from 'postcss-value-parser';
import type { DiagnosticWithDetachedLocation, TokenReference } from '../type.js';
import { calcDeclValueLoc } from './decl-value-location.js';
import { calcDeclValueLoc } from './util.js';
import { VALID_IDENT_RE } from './util.js';

const ANIMATION_NAME_PROP_RE = /^(?:-(?:webkit|moz|o|ms)-)?animation-name$/iu;
const ANIMATION_PROP_RE = /^(?:-(?:webkit|moz|o|ms)-)?animation$/iu;
Expand Down Expand Up @@ -60,11 +61,6 @@ const ANIMATION_SHORTHAND_RESERVED_KEYWORDS = new Set([
'paused',
]);

// A valid CSS identifier, including `<dashed-ident>`. Excludes `<time>` (`3s`), `<number>` (`2`),
// and other non-ident words. We don't validate `hex digits`, because it is the job of linters.
const VALID_IDENT_RE =
/^-?([a-zA-Z\u0080-\uFFFF_]|(\\[^\r\n\f])|-(?![0-9]))((\\[^\r\n\f])|[a-zA-Z\u0080-\uFFFF_0-9-])*$/u;

interface ParseAnimationResult {
references: TokenReference[];
diagnostics: DiagnosticWithDetachedLocation[];
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/parser/composes-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
LocalTokenReference,
TokenReference,
} from '../type.js';
import { calcDeclValueLoc } from './decl-value-location.js';
import { calcDeclValueLoc } from './util.js';

const COMPOSES_PROP_RE = /^composes$/iu;

Expand Down
Loading
Loading