Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
6 changes: 6 additions & 0 deletions extensions/vscode/schemas/vue-tsconfig.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@
"default": false,
"markdownDescription": "Strict type checking of CSS modules."
},
"cssModulesLocalsConvention": {
"type": ["string", "null"],
"default": null,
"enum": ["camelCase", "camelCaseOnly", "dashes", "dashesOnly", null],
"markdownDescription": "Style of exported class names for CSS modules."
},
"checkUnknownProps": {
"type": "boolean",
"default": false,
Expand Down
43 changes: 35 additions & 8 deletions packages/language-core/lib/codegen/style/modules.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,35 @@
import type { Code } from '../../types';
import camelCase from "lodash.camelcase";
import type { Code, LocalsConvention } from '../../types';
import { codeFeatures } from '../codeFeatures';
import * as names from '../names';
import type { TemplateCodegenContext } from '../template/context';
import { endOfLine, newLine } from '../utils';
import type { StyleCodegenOptions } from '.';
import { generateClassProperty, generateStyleImports } from './common';

// See https://github.com/madyankin/postcss-modules/blob/master/src/localsConvention.js

function dashesCamelCase(string: string) {
return string.replace(/-+(\w)/g, (_, firstLetter) => firstLetter.toUpperCase());
}

function generateClasses(classNameWithoutDot: string, localsConvention: LocalsConvention): string[] {
switch (localsConvention) {
case "camelCase":
return [classNameWithoutDot, camelCase(classNameWithoutDot)];

case "camelCaseOnly":
return [camelCase(classNameWithoutDot)];

case "dashes":
return [classNameWithoutDot, dashesCamelCase(classNameWithoutDot)];

case "dashesOnly":
return [dashesCamelCase(classNameWithoutDot)];
}
return [classNameWithoutDot];
}

export function* generateStyleModules(
{ styles, vueCompilerOptions }: StyleCodegenOptions,
ctx: TemplateCodegenContext,
Expand Down Expand Up @@ -38,13 +62,16 @@ export function* generateStyleModules(
if (vueCompilerOptions.resolveStyleImports) {
yield* generateStyleImports(style);
}
for (const className of style.classNames) {
yield* generateClassProperty(
style.name,
className.text,
className.offset,
'string',
);
for (const classNameWithDot of style.classNames) {
const moduleClassNamesWithoutDot = generateClasses(classNameWithDot.text.slice(1), vueCompilerOptions.cssModulesLocalsConvention);
for (const moduleClassNameWithoutDot of moduleClassNamesWithoutDot) {
yield* generateClassProperty(
style.name,
`.${moduleClassNameWithoutDot}`,
classNameWithDot.offset,
'string',
);
}
}
yield `>${endOfLine}`;
}
Expand Down
1 change: 1 addition & 0 deletions packages/language-core/lib/compilerOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ export function getDefaultCompilerOptions(
petiteVueExtensions: [],
jsxSlots: false,
strictCssModules: false,
cssModulesLocalsConvention: null,
strictVModel: strictTemplates,
checkUnknownProps: strictTemplates,
checkUnknownEvents: strictTemplates,
Expand Down
3 changes: 3 additions & 0 deletions packages/language-core/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type { Segment } from 'muggle-string';
import type * as ts from 'typescript';
import type { VueEmbeddedCode } from './virtualCode/embeddedCodes';

export type LocalsConvention = 'camelCase' | 'camelCaseOnly' | 'dashes' | 'dashesOnly' | null; // Note that postcss-modules localsConvention also has a function type which we don't want. Aligned with Vite localsConvention option.

export type { SFCParseResult } from '@vue/compiler-sfc';

export { VueEmbeddedCode };
Expand Down Expand Up @@ -33,6 +35,7 @@ export interface VueCompilerOptions {
jsxSlots: boolean;
strictVModel: boolean;
strictCssModules: boolean;
cssModulesLocalsConvention: LocalsConvention;
checkUnknownProps: boolean;
checkUnknownEvents: boolean;
checkUnknownDirectives: boolean;
Expand Down
2 changes: 2 additions & 0 deletions packages/language-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
"@vue/compiler-dom": "^3.5.0",
"@vue/shared": "^3.5.0",
"alien-signals": "^3.0.0",
"lodash.camelcase": "^4.3.0",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use import { camelize } from "@vue/shared"; instead.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized that @vue/shared camelize works a bit different from lodash.camelcase. My tests using underscores failed. While I like to reuse deps, in this case it should work the same way as postcss-modules, which uses lodash.camelcase 🙂 🐫🐫🐫.

"muggle-string": "^0.4.1",
"path-browserify": "^1.0.1",
"picomatch": "^4.0.2"
},
"devDependencies": {
"@types/lodash.camelcase": "^4.3.9",
"@types/node": "^22.10.4",
"@types/path-browserify": "^1.0.1",
"@types/picomatch": "^4.0.0",
Expand Down
Loading
Loading