Skip to content

Commit

Permalink
feat: add getJSON option to output CSS modules mapping
Browse files Browse the repository at this point in the history
This changeset adds a `getJSON` option to output CSS modules mappings to JSON.
This value can be a boolean or a function, and it employs similar logic to
[postcss-modules#getJSON](https://github.com/madyankin/postcss-modules?tab=readme-ov-file#saving-exported-classes) as a function.
This is particularly useful for SSR/SSG/templating languages when CSS modules mappings need to be present at build time.

Addresses [#988](#988).
  • Loading branch information
stephenkao committed Mar 10, 2024
1 parent 24e114a commit e97b4c3
Show file tree
Hide file tree
Showing 8 changed files with 334 additions and 1 deletion.
73 changes: 73 additions & 0 deletions README.md
Expand Up @@ -327,6 +327,9 @@ type modules =
| "dashesOnly"
| ((name: string) => string);
exportOnlyLocals: boolean;
getJSON:
| string
| ((resourcePath: string, json: object, outputPath: string) => any);
};
```

Expand Down Expand Up @@ -592,6 +595,7 @@ module.exports = {
namedExport: true,
exportLocalsConvention: "camelCase",
exportOnlyLocals: false,
getJSON: false,
},
},
},
Expand Down Expand Up @@ -1372,6 +1376,75 @@ module.exports = {
};
```

##### `getJSON`

Type:

```ts
type getJSON =
| boolean
| ((resourcePath: string, json: object, outputPath: string) => any);
```

Default: `undefined`

Enables the outputting of the CSS modules mapping JSON. This can be omitted or set to a falsy value to disable any output.

###### `boolean`

Possible values:

- `true` - writes a JSON file next located in the same directory as the loaded resource file. For example, given a resource file located at /foo/bar/baz.css, this would write the CSS modules mapping JSON to /foo/bar/baz.css.json
- `false` - disables CSS modules mapping JSON output

**webpack.config.js**

```js
module.exports = {
module: {
rules: [
{
test: /\.css$/i,
loader: "css-loader",
options: {
modules: {
getJSON: true,
},
},
},
],
},
};
```

###### `function`

Enables custom handling of the CSS modules mapping JSON output. The return value of the function is not used for anything internally and is only intended to customize output.

**webpack.config.js**

```js
module.exports = {
module: {
rules: [
{
test: /\.css$/i,
loader: "css-loader",
options: {
modules: {
getJSON: (resourcePath, json, outputPath) => {
// `resourcePath` is the original resource file path, e.g., /foo/bar/baz.css
// `json` is the CSS modules map
// `outputPath` is the expected output file path, e.g., /foo/bar/baz.css.json
},
},
},
},
],
},
};
```

### `importLoaders`

Type:
Expand Down
12 changes: 12 additions & 0 deletions src/index.js
Expand Up @@ -27,6 +27,7 @@ import {
stringifyRequest,
warningFactory,
syntaxErrorFactory,
writeModulesMap,
} from "./utils";

export default async function loader(content, map, meta) {
Expand Down Expand Up @@ -274,5 +275,16 @@ export default async function loader(content, map, meta) {
isTemplateLiteralSupported
);

try {
const { getJSON } = options.modules;
if (getJSON) {
await writeModulesMap(getJSON, resourcePath, exports);
}
} catch (error) {
callback(error);

return;
}

callback(null, `${importCode}${moduleCode}${exportCode}`);
}
12 changes: 12 additions & 0 deletions src/options.json
Expand Up @@ -169,6 +169,18 @@
"description": "Export only locals.",
"link": "https://github.com/webpack-contrib/css-loader#exportonlylocals",
"type": "boolean"
},
"getJSON": {
"description": "Output CSS modules mapping to a JSON file or through a callback.",
"link": "https://github.com/webpack-contrib/css-loader#getJSON",
"anyOf": [
{
"type": "boolean"
},
{
"instanceof": "Function"
}
]
}
}
}
Expand Down
22 changes: 22 additions & 0 deletions src/utils.js
Expand Up @@ -4,6 +4,7 @@
*/
import { fileURLToPath } from "url";
import path from "path";
import fsp from "fs/promises";

import modulesValues from "postcss-modules-values";
import localByDefault from "postcss-modules-local-by-default";
Expand Down Expand Up @@ -1412,6 +1413,26 @@ function syntaxErrorFactory(error) {
return obj;
}

async function writeModulesMap(getJSON, resourcePath, exports) {
const json = exports.reduce((acc, { name, value }) => {
return { ...acc, [name]: value };
}, {});

const outputPath = path.resolve(
path.dirname(resourcePath),
`${path.basename(resourcePath)}.json`
);

if (getJSON === true) {
// If true, output a JSON CSS modules mapping file in the same directory as the resource
await fsp.writeFile(outputPath, JSON.stringify(json));
} else if (typeof getJSON === "function") {
// If function, call function with call getJSON with similar args as postcss-modules#getJSON
// https://github.com/madyankin/postcss-modules/tree/master?tab=readme-ov-file#saving-exported-classes
getJSON(resourcePath, json, outputPath);
}
}

export {
normalizeOptions,
shouldUseModulesPlugins,
Expand Down Expand Up @@ -1439,4 +1460,5 @@ export {
defaultGetLocalIdent,
warningFactory,
syntaxErrorFactory,
writeModulesMap,
};
132 changes: 132 additions & 0 deletions test/__snapshots__/modules-option.test.js.snap
Expand Up @@ -1122,6 +1122,75 @@ exports[`"modules" option should emit warning when localIdentName is emoji: erro

exports[`"modules" option should emit warning when localIdentName is emoji: warnings 1`] = `Array []`;

exports[`"modules" option should invoke the custom getJSON function with getJSON as a function: errors 1`] = `Array []`;

exports[`"modules" option should invoke the custom getJSON function with getJSON as a function: mapping 1`] = `
Object {
"a": "RT7ktT7mB7tfBR25sJDZ",
"b": "IZmhTnK9CIeu6ww6Zjbv",
"c": "PV11nPFlF7mzEgCXkQw4",
}
`;

exports[`"modules" option should invoke the custom getJSON function with getJSON as a function: module 1`] = `
"// Imports
import ___CSS_LOADER_API_NO_SOURCEMAP_IMPORT___ from \\"../../../../src/runtime/noSourceMaps.js\\";
import ___CSS_LOADER_API_IMPORT___ from \\"../../../../src/runtime/api.js\\";
var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_NO_SOURCEMAP_IMPORT___);
// Module
___CSS_LOADER_EXPORT___.push([module.id, \`.RT7ktT7mB7tfBR25sJDZ {
background-color: aliceblue;
}

.IZmhTnK9CIeu6ww6Zjbv {
background-color: burlywood;
}

.PV11nPFlF7mzEgCXkQw4 {
background-color: chartreuse;
}

.d {
background-color: darkgoldenrod
}
\`, \\"\\"]);
// Exports
___CSS_LOADER_EXPORT___.locals = {
\\"a\\": \`RT7ktT7mB7tfBR25sJDZ\`,
\\"b\\": \`IZmhTnK9CIeu6ww6Zjbv\`,
\\"c\\": \`PV11nPFlF7mzEgCXkQw4\`
};
export default ___CSS_LOADER_EXPORT___;
"
`;

exports[`"modules" option should invoke the custom getJSON function with getJSON as a function: result 1`] = `
Array [
Array [
"./modules/getJSON/source.css",
".RT7ktT7mB7tfBR25sJDZ {
background-color: aliceblue;
}

.IZmhTnK9CIeu6ww6Zjbv {
background-color: burlywood;
}

.PV11nPFlF7mzEgCXkQw4 {
background-color: chartreuse;
}

.d {
background-color: darkgoldenrod
}
",
"",
],
]
`;

exports[`"modules" option should invoke the custom getJSON function with getJSON as a function: warnings 1`] = `Array []`;

exports[`"modules" option should keep order: errors 1`] = `Array []`;

exports[`"modules" option should keep order: module 1`] = `
Expand Down Expand Up @@ -1194,6 +1263,69 @@ Array [

exports[`"modules" option should keep order: warnings 1`] = `Array []`;

exports[`"modules" option should output a co-located CSS modules map file with getJSON as true: errors 1`] = `Array []`;

exports[`"modules" option should output a co-located CSS modules map file with getJSON as true: mapping 1`] = `"{\\"a\\":\\"RT7ktT7mB7tfBR25sJDZ\\",\\"b\\":\\"IZmhTnK9CIeu6ww6Zjbv\\",\\"c\\":\\"PV11nPFlF7mzEgCXkQw4\\"}"`;

exports[`"modules" option should output a co-located CSS modules map file with getJSON as true: module 1`] = `
"// Imports
import ___CSS_LOADER_API_NO_SOURCEMAP_IMPORT___ from \\"../../../../src/runtime/noSourceMaps.js\\";
import ___CSS_LOADER_API_IMPORT___ from \\"../../../../src/runtime/api.js\\";
var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_NO_SOURCEMAP_IMPORT___);
// Module
___CSS_LOADER_EXPORT___.push([module.id, \`.RT7ktT7mB7tfBR25sJDZ {
background-color: aliceblue;
}

.IZmhTnK9CIeu6ww6Zjbv {
background-color: burlywood;
}

.PV11nPFlF7mzEgCXkQw4 {
background-color: chartreuse;
}

.d {
background-color: darkgoldenrod
}
\`, \\"\\"]);
// Exports
___CSS_LOADER_EXPORT___.locals = {
\\"a\\": \`RT7ktT7mB7tfBR25sJDZ\`,
\\"b\\": \`IZmhTnK9CIeu6ww6Zjbv\`,
\\"c\\": \`PV11nPFlF7mzEgCXkQw4\`
};
export default ___CSS_LOADER_EXPORT___;
"
`;

exports[`"modules" option should output a co-located CSS modules map file with getJSON as true: result 1`] = `
Array [
Array [
"./modules/getJSON/source.css",
".RT7ktT7mB7tfBR25sJDZ {
background-color: aliceblue;
}

.IZmhTnK9CIeu6ww6Zjbv {
background-color: burlywood;
}

.PV11nPFlF7mzEgCXkQw4 {
background-color: chartreuse;
}

.d {
background-color: darkgoldenrod
}
",
"",
],
]
`;

exports[`"modules" option should output a co-located CSS modules map file with getJSON as true: warnings 1`] = `Array []`;

exports[`"modules" option should resolve absolute path in composes: errors 1`] = `Array []`;

exports[`"modules" option should resolve absolute path in composes: module 1`] = `
Expand Down
15 changes: 15 additions & 0 deletions test/fixtures/modules/getJSON/source.css
@@ -0,0 +1,15 @@
.a {
background-color: aliceblue;
}

.b {
background-color: burlywood;
}

.c {
background-color: chartreuse;
}

:global(.d) {
background-color: darkgoldenrod
}
5 changes: 5 additions & 0 deletions test/fixtures/modules/getJSON/source.js
@@ -0,0 +1,5 @@
import css from './source.css';

__export__ = css;

export default css;

0 comments on commit e97b4c3

Please sign in to comment.