Skip to content
Open
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
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,56 @@ With this `{rootDir}/src/ui/tsconfig.json`:
- `lightningCssMinimizerOptions` (`(options: LightningCssMinimizerRspackPluginOptions) => LightningCssMinimizerRspackPluginOptions`) - modify or return a custom [LightningCssMinimizerRspackPlugin](https://rspack.dev/plugins/rspack/lightning-css-minimizer-rspack-plugin)
- `terser` (`(options: TerserOptions) => TerserOptions`) - modify or return a custom [Terser options](https://github.com/terser/terser#minify-options).

##### CSS Loader configuration

- `cssLoader` (`Partial<CssLoaderOptions>`) — allows to override default css-loader settings. All options are optional and will be merged with default values.

**CssLoaderOptions interface:**

- `url` (`boolean | {filter: (url: string, resourcePath: string) => boolean}`) — enables/disables `url()`/`image-set()` functions handling. Default: `{filter: (url: string) => !url.startsWith('data:')}` (ignores data URIs)
- `import` (`boolean | {filter: (url: string, media: string, resourcePath: string) => boolean}`) — enables/disables `@import` at-rules handling
- `modules` (`boolean | 'local' | 'global' | 'pure' | 'icss' | CssLoaderModulesOptions`) — enables/disables CSS Modules or ICSS and setup configuration. Default: `{auto: true, localIdentName: '[name]__[local]--[hash:base64:5]', exportLocalsConvention: 'camelCase'}`
- `sourceMap` (`boolean`) — enables/disables source maps. Default: `!disableSourceMapGeneration`
- `esModule` (`boolean`) — use the ES modules syntax
- `exportType` (`'array' | 'string' | 'css-style-sheet'`) — allows exporting styles as array with modules, string or constructable stylesheet

**CssLoaderModulesOptions interface:**

- `auto` (`RegExp | ((resourcePath: string) => boolean) | boolean`) — allows auto enable CSS modules based on filename
- `mode` (`'local' | 'global' | 'pure' | 'icss' | ((resourcePath: string) => 'local' | 'global' | 'pure' | 'icss')`) — setup mode option
- `localIdentName` (`string`) — allows to configure the generated local ident name
- `localIdentContext` (`string`) — allows to redefine basic loader context for local ident name
- `localIdentHashSalt` (`string`) — allows to add custom hash to generate more unique classes
- `localIdentHashFunction` (`string`) — allows to specify hash function to generate classes
- `localIdentHashDigest` (`string`) — allows to specify hash digest to generate classes
- `localIdentHashDigestLength` (`number`) — allows to specify hash digest length to generate classes
- `hashStrategy` (`'resource-path-and-local-name' | 'minimal-subset'`) — allows to specify should localName be used when computing the hash
- `localIdentRegExp` (`string | RegExp`) — allows to specify custom RegExp for local ident name
- `getLocalIdent` (`(context: {resourcePath: string, resourceQuery: string}, localIdentName: string, localName: string, options: CssLoaderModulesOptions) => string`) — allows to specify a function to generate the classname
- `namedExport` (`boolean`) — enables/disables ES modules named export for locals
- `exportGlobals` (`boolean`) — allows to export names from global class or id, so you can use that as local name
- `exportLocalsConvention` (`'asIs' | 'as-is' | 'camelCase' | 'camel-case' | 'camelCaseOnly' | 'camel-case-only' | 'dashes' | 'dashesOnly' | 'dashes-only' | ((className: string) => string)`) — style of exported classnames
- `exportOnlyLocals` (`boolean`) — export only locals
- `getJSON` (`(cssModules: Record<string, string>) => void`) — allows outputting of CSS modules mapping through a callback

For more details, see [css-loader documentation](https://github.com/webpack/css-loader#options).

**Default configuration:**

```ts
{
url: {
filter: (url: string) => !url.startsWith('data:'),
},
sourceMap: !disableSourceMapGeneration,
modules: {
auto: true,
localIdentName: '[name]__[local]--[hash:base64:5]',
exportLocalsConvention: 'camelCase',
},
}
```

##### Monaco editor support

- `monaco` (`object`) — use [monaco-editor-webpack-plugin](https://github.com/microsoft/monaco-editor/tree/main/webpack-plugin#monaco-editor-webpack-loader-plugin)
Expand Down
258 changes: 258 additions & 0 deletions src/common/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
import {normalizeConfig} from './config';
import type {ClientConfig, CssLoaderOptions} from './models';

// Type guard for url filter
function isUrlFilterObject(
url: CssLoaderOptions['url'],
): url is {filter: (url: string, resourcePath: string) => boolean} {
return typeof url === 'object' && url !== null && 'filter' in url;
}

describe('cssLoader configuration', () => {
it('should apply default cssLoader config when not specified', async () => {
const clientConfig: ClientConfig = {};
const normalized = await normalizeConfig({client: clientConfig});

expect(normalized.client.cssLoaderConfig).toEqual({
url: {
filter: expect.any(Function),
},
sourceMap: true,
modules: {
auto: true,
localIdentName: '[name]__[local]--[hash:base64:5]',
exportLocalsConvention: 'camelCase',
},
});

// Check that url filter works correctly
const url = normalized.client.cssLoaderConfig.url;
if (isUrlFilterObject(url)) {
expect(url.filter('data:image/png;base64,abc', '/path/to/file.css')).toBe(false);
expect(url.filter('./image.png', '/path/to/file.css')).toBe(true);
}
});

it('should merge user cssLoader config with defaults', async () => {
const clientConfig: ClientConfig = {
cssLoader: {
modules: {
localIdentName: '[local]--[hash:base64:8]',
exportLocalsConvention: 'camelCaseOnly',
},
sourceMap: false,
},
};
const normalized = await normalizeConfig({client: clientConfig});

expect(normalized.client.cssLoaderConfig).toEqual({
url: {
filter: expect.any(Function),
},
sourceMap: false,
modules: {
auto: true,
localIdentName: '[local]--[hash:base64:8]',
exportLocalsConvention: 'camelCaseOnly',
},
});
});

it('should allow complete override of cssLoader config', async () => {
const clientConfig: ClientConfig = {
cssLoader: {
url: false,
import: false,
modules: false,
sourceMap: false,
esModule: false,
exportType: 'array',
},
};
const normalized = await normalizeConfig({client: clientConfig});

expect(normalized.client.cssLoaderConfig).toEqual({
url: false,
import: false,
modules: false,
sourceMap: false,
esModule: false,
exportType: 'array',
});
});

it('should allow partial override of modules config', async () => {
const clientConfig: ClientConfig = {
cssLoader: {
modules: {
localIdentName: 'custom-[local]',
},
},
};
const normalized = await normalizeConfig({client: clientConfig});

expect(normalized.client.cssLoaderConfig.modules).toEqual({
auto: true,
localIdentName: 'custom-[local]',
exportLocalsConvention: 'camelCase',
});
});

it('should allow modules to be a boolean', async () => {
const clientConfig: ClientConfig = {
cssLoader: {
modules: true,
},
};
const normalized = await normalizeConfig({client: clientConfig});

expect(normalized.client.cssLoaderConfig.modules).toBe(true);
});

it('should allow modules to be a string', async () => {
const clientConfig: ClientConfig = {
cssLoader: {
modules: 'local',
},
};
const normalized = await normalizeConfig({client: clientConfig});

expect(normalized.client.cssLoaderConfig.modules).toBe('local');
});

it('should allow url to be a boolean', async () => {
const clientConfig: ClientConfig = {
cssLoader: {
url: false,
},
};
const normalized = await normalizeConfig({client: clientConfig});

expect(normalized.client.cssLoaderConfig.url).toBe(false);
});

it('should allow url to be an object with filter', async () => {
const customFilter = (url: string) => url.endsWith('.png');
const clientConfig: ClientConfig = {
cssLoader: {
url: {
filter: customFilter,
},
},
};
const normalized = await normalizeConfig({client: clientConfig});

expect(normalized.client.cssLoaderConfig.url).toEqual({
filter: customFilter,
});
});

it('should respect disableSourceMapGeneration for sourceMap', async () => {
const clientConfig: ClientConfig = {
disableSourceMapGeneration: true,
};
const normalized = await normalizeConfig({client: clientConfig});

expect(normalized.client.cssLoaderConfig.sourceMap).toBe(false);
});

it('should allow user to override sourceMap even with disableSourceMapGeneration', async () => {
const clientConfig: ClientConfig = {
disableSourceMapGeneration: true,
cssLoader: {
sourceMap: true,
},
};
const normalized = await normalizeConfig({client: clientConfig});

expect(normalized.client.cssLoaderConfig.sourceMap).toBe(true);
});

it('should allow import to be a boolean', async () => {
const clientConfig: ClientConfig = {
cssLoader: {
import: false,
},
};
const normalized = await normalizeConfig({client: clientConfig});

expect(normalized.client.cssLoaderConfig.import).toBe(false);
});

it('should allow import to be an object with filter', async () => {
const customFilter = (_url: string, media: string) => media === 'screen';
const clientConfig: ClientConfig = {
cssLoader: {
import: {
filter: customFilter,
},
},
};
const normalized = await normalizeConfig({client: clientConfig});

expect(normalized.client.cssLoaderConfig.import).toEqual({
filter: customFilter,
});
});

it('should allow setting exportType', async () => {
const clientConfig: ClientConfig = {
cssLoader: {
exportType: 'css-style-sheet',
},
};
const normalized = await normalizeConfig({client: clientConfig});

expect(normalized.client.cssLoaderConfig.exportType).toBe('css-style-sheet');
});

it('should allow setting esModule', async () => {
const clientConfig: ClientConfig = {
cssLoader: {
esModule: false,
},
};
const normalized = await normalizeConfig({client: clientConfig});

expect(normalized.client.cssLoaderConfig.esModule).toBe(false);
});

it('should allow complex modules configuration', async () => {
const clientConfig: ClientConfig = {
cssLoader: {
modules: {
auto: /\.module\.css$/,
mode: 'local',
localIdentName: '[path][name]__[local]--[hash:base64:5]',
localIdentContext: 'src',
localIdentHashSalt: 'custom-salt',
localIdentHashFunction: 'sha256',
localIdentHashDigest: 'hex',
localIdentHashDigestLength: 10,
hashStrategy: 'resource-path-and-local-name',
namedExport: true,
exportGlobals: true,
exportLocalsConvention: 'dashes',
exportOnlyLocals: false,
},
},
};
const normalized = await normalizeConfig({client: clientConfig});

expect(normalized.client.cssLoaderConfig.modules).toEqual({
auto: /\.module\.css$/,
mode: 'local',
localIdentName: '[path][name]__[local]--[hash:base64:5]',
localIdentContext: 'src',
localIdentHashSalt: 'custom-salt',
localIdentHashFunction: 'sha256',
localIdentHashDigest: 'hex',
localIdentHashDigestLength: 10,
hashStrategy: 'resource-path-and-local-name',
namedExport: true,
exportGlobals: true,
exportLocalsConvention: 'dashes',
exportOnlyLocals: false,
});
});
});
22 changes: 22 additions & 0 deletions src/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,16 @@
import {getPort, hasMFAssetsIsolation} from './utils';
import logger from './logger';

function splitPaths(paths: string | string[]) {

Check warning on line 27 in src/common/config.ts

View workflow job for this annotation

GitHub Actions / Verify Files

'paths' is already declared in the upper scope on line 8 column 8
return (Array.isArray(paths) ? paths : [paths]).flatMap((p) => p.split(','));
}

function remapPaths(paths: string | string[]) {

Check warning on line 31 in src/common/config.ts

View workflow job for this annotation

GitHub Actions / Verify Files

'paths' is already declared in the upper scope on line 8 column 8
return splitPaths(paths).map((p) => path.resolve(process.cwd(), p));
}

function omitUndefined<T extends object>(obj: T) {
const newObj: Record<string, any> = {};

Check warning on line 36 in src/common/config.ts

View workflow job for this annotation

GitHub Actions / Verify Files

Unexpected any. Specify a different type
for (const [key, value] of Object.entries(obj)) {
if (value !== undefined) {
newObj[key] = value;
Expand Down Expand Up @@ -266,6 +266,28 @@
lazyCompilation: undefined,
bundler: client.bundler || 'webpack',
javaScriptLoader: client.javaScriptLoader || 'babel',
cssLoaderConfig: {
url: client.cssLoader?.url ?? {
filter: (url: string) => !url.startsWith('data:'),
},
sourceMap: client.cssLoader?.sourceMap ?? !client.disableSourceMapGeneration,
modules:
typeof client.cssLoader?.modules === 'object'
? {
auto: true,
localIdentName: '[name]__[local]--[hash:base64:5]',
exportLocalsConvention: 'camelCase',
...client.cssLoader.modules,
}
: (client.cssLoader?.modules ?? {
auto: true,
localIdentName: '[name]__[local]--[hash:base64:5]',
exportLocalsConvention: 'camelCase',
}),
import: client.cssLoader?.import,
esModule: client.cssLoader?.esModule,
exportType: client.cssLoader?.exportType,
},
};

if (mode === 'dev') {
Expand Down
Loading
Loading