Skip to content

Commit

Permalink
feat: .d.ts generation and treeshakeable injector options
Browse files Browse the repository at this point in the history
  • Loading branch information
Anidetrix committed Jun 26, 2020
1 parent 62353b2 commit 2990cb0
Show file tree
Hide file tree
Showing 13 changed files with 129 additions and 28 deletions.
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
/coverage
/docs
/__tests__/fixtures/dist
/__tests__/fixtures/**/*.d.ts
/pnpm-lock.yaml
/CHANGELOG.md
2 changes: 2 additions & 0 deletions __tests__/fixtures/keyword-fail/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import style from "./style.css";
console.log(style.module);
7 changes: 7 additions & 0 deletions __tests__/fixtures/keyword-fail/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.inject {
color: red;
}

.module {
color: royalblue;
}
5 changes: 5 additions & 0 deletions __tests__/fixtures/modules/composed.css.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const css: string;
interface ModulesExports {"primary":"#BF4040","composition":"composed_composition composition2_compositioned"}
interface ModulesExports {inject:()=>void}
declare const modules_2a102cc5: ModulesExports;
export default modules_2a102cc5;
5 changes: 3 additions & 2 deletions __tests__/fixtures/modules/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import style from "./style.css";
import composed from "./composed.css";
import "./composed.css";
import composition2 from "./subdir/composition2.css";

console.log(style.module, composed.composition, composition2.compositioned);
if (composition2.inject) composition2.inject();
else console.log(style.module, composition2.compositioned);
5 changes: 5 additions & 0 deletions __tests__/fixtures/modules/style.css.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const css: string;
interface ModulesExports {"primary":"#BF4040","secondary":"#1F4F7F","module":"style_module","module2":"style_module2 composed_composition composition2_compositioned"}
interface ModulesExports {inject:()=>void}
declare const modules_5a199c00: ModulesExports;
export default modules_5a199c00;
5 changes: 5 additions & 0 deletions __tests__/fixtures/modules/subdir/composition2.css.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const css: string;
interface ModulesExports {"compositioned":"composition2_compositioned"}
interface ModulesExports {inject:()=>void}
declare const modules_354770d7: ModulesExports;
export default modules_354770d7;
7 changes: 5 additions & 2 deletions __tests__/fixtures/named-exports/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import * as style from "./style.css";
import style, { css } from "./style.css";

console.log(style);
console.log(css);
for (const name of Object.values(style)) {
console.log(name);
}
7 changes: 7 additions & 0 deletions __tests__/fixtures/named-exports/style.css.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const css: string;
export const valid: "style_valid";
export const _new: "style_new";
export const _css: "style_css";
interface ModulesExports {"valid":"style_valid","new":"style_new","css":"style_css"}
declare const modules_5a199c00: ModulesExports;
export default modules_5a199c00;
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,20 @@ export default (options: Options = {}): Plugin => {
modules: inferOption(options.modules, false),

to: options.to,
dts: options.dts ?? false,
namedExports: options.namedExports ?? false,
autoModules: options.autoModules ?? false,
extensions: options.extensions ?? [".css", ".pcss", ".postcss", ".sss"],
postcss: {},
};

if (
typeof loaderOpts.inject === "object" &&
loaderOpts.inject.treeshakeable &&
loaderOpts.namedExports
)
throw new Error("`inject.treeshakeable` option is incompatible with `namedExports` option");

if (options.parser) loaderOpts.postcss.parser = ensurePCSSOption(options.parser, "parser");

if (options.syntax) loaderOpts.postcss.syntax = ensurePCSSOption(options.syntax, "syntax");
Expand Down
88 changes: 65 additions & 23 deletions src/loaders/postcss/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import path from "path";
import fs from "fs-extra";
import { RawSourceMap } from "source-map";
import { makeLegalIdentifier } from "@rollup/pluginutils";
import postcss from "postcss";
import cssnano from "cssnano";
import { PostCSSLoaderOptions } from "../../types";
import { PostCSSLoaderOptions, InjectOptions } from "../../types";
import { humanlizePath, normalizePath } from "../../utils/path";
import { mm } from "../../utils/sourcemap";
import resolveAsync from "../../utils/resolve-async";
Expand All @@ -18,7 +19,9 @@ import postcssNoop from "./noop";

let injectorId: string;
const testing = process.env.NODE_ENV === "test";
const reservedWords = ["css"];

const cssVarName = "css";
const reservedWords = [cssVarName];

function getClassNameDefault(name: string): string {
const id = makeLegalIdentifier(name);
Expand Down Expand Up @@ -121,16 +124,11 @@ const loader: Loader<PostCSSLoaderOptions> = {

if (options.emit) return { code: res.css, map };

const saferId = (id: string): string => safeId(id, humanlizePath(this.id));
const cssVarName = saferId("css");
const saferId = (id: string): string => safeId(id, path.basename(this.id));
const modulesVarName = saferId("modules");

const output = [
`const ${cssVarName} = ${JSON.stringify(res.css)}`,
`const ${modulesVarName} = ${JSON.stringify(modulesExports)}`,
`export const css = ${cssVarName}`,
`export default ${supportModules ? modulesVarName : cssVarName}`,
];
const output = [`export const ${cssVarName} = ${JSON.stringify(res.css)};`];
const dts = [`export const ${cssVarName}: string;`];

if (options.namedExports) {
const getClassName =
Expand All @@ -142,9 +140,9 @@ const loader: Loader<PostCSSLoaderOptions> = {
if (name !== newName)
this.warn(`Exported \`${name}\` as \`${newName}\` in ${humanlizePath(this.id)}`);

if (!modulesExports[newName]) modulesExports[newName] = modulesExports[name];

output.push(`export const ${newName} = ${JSON.stringify(modulesExports[name])}`);
const fmt = JSON.stringify(modulesExports[name]);
output.push(`export const ${newName} = ${fmt};`);
if (options.dts) dts.push(`export const ${newName}: ${fmt};`);
}
}

Expand All @@ -154,27 +152,71 @@ const loader: Loader<PostCSSLoaderOptions> = {
if (typeof options.inject === "function") {
output.push(options.inject(cssVarName, this.id));
} else {
const injectorVarName = saferId("injector");
const { treeshakeable, ...injectorOptions } =
typeof options.inject === "object" ? options.inject : ({} as InjectOptions);

const injectorName = saferId("injector");
const injectorCall = `${injectorName}(${cssVarName},${JSON.stringify(injectorOptions)});`;

if (!injectorId) {
injectorId = await resolveAsync("./inject-css", {
basedir: path.join(testing ? process.cwd() : __dirname, "runtime"),
}).then(normalizePath);
})
.then(normalizePath)
.then(JSON.stringify);
}

const injectorData =
typeof options.inject === "object" ? `,${JSON.stringify(options.inject)}` : "";
output.push(`import ${injectorName} from ${injectorId};`);

output.push(
`import ${injectorVarName} from '${injectorId}'`,
`${injectorVarName}(${cssVarName}${injectorData})`,
);
if (!treeshakeable)
output.push(`const ${modulesVarName} = ${JSON.stringify(modulesExports)};`, injectorCall);

if (treeshakeable) {
output.push("let injected = false;");
const injectorCallOnce = `if (!injected) { injected = true; ${injectorCall} }`;

if (modulesExports.inject) {
throw new Error(
"`inject` keyword is reserved when using `inject.treeshakeable` option",
);
}

let getters = "";
for (const [k, v] of Object.entries(modulesExports)) {
const name = JSON.stringify(k);
const value = JSON.stringify(v);
getters += `get ${name}() { ${injectorCallOnce} return ${value}; },\n`;
}

getters += `inject() { ${injectorCallOnce} },`;
output.push(`const ${modulesVarName} = {${getters}};`);
}
}
}

code = `${output.join(";\n")};`;
if (!options.inject)
output.push(`const ${modulesVarName} = ${JSON.stringify(modulesExports)};`);

const defaultExport = `export default ${supportModules ? modulesVarName : cssVarName};`;
output.push(defaultExport);

if (options.dts && (await fs.pathExists(this.id))) {
if (supportModules)
dts.push(
`interface ModulesExports ${JSON.stringify(modulesExports)}`,

typeof options.inject === "object" && options.inject.treeshakeable
? `interface ModulesExports {inject:()=>void}`
: "",

`declare const ${modulesVarName}: ModulesExports;`,
);

dts.push(defaultExport);
await fs.writeFile(`${this.id}.d.ts`, dts.filter(Boolean).join("\n"));
}

return { code, map, extracted };
return { code: output.filter(Boolean).join("\n"), map, extracted };
},
};

Expand Down
15 changes: 15 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export interface PostCSSLoaderOptions extends Record<string, unknown> {

/** @see {@link Options.to} */
to: Options["to"];
/** @see {@link Options.dts} */
dts: NonNullable<Options["dts"]>;
/** @see {@link Options.namedExports} */
namedExports: NonNullable<Options["namedExports"]>;
/** @see {@link Options.autoModules} */
Expand Down Expand Up @@ -93,6 +95,14 @@ export interface InjectOptions {
* - ex.: `{"id":"global"}`
*/
attributes?: Record<string, string>;
/**
* Makes injector treeshakeable,
* as it is only called when either classes are referenced directly,
* or `inject` function is called from the default export.
*
* Incompatible with `namedExports` option.
*/
treeshakeable?: boolean;
}

/** `rollup-plugin-styles`'s full option list */
Expand Down Expand Up @@ -145,6 +155,11 @@ export interface Options {
| ["emit"];
/** `to` option for PostCSS, required for some plugins */
to?: string;
/**
* Generate TypeScript declarations files for input style files
* @default false
*/
dts?: boolean;
/**
* Enable/disable or pass options for CSS `@import` resolver
* @default true
Expand Down
2 changes: 1 addition & 1 deletion src/utils/safe-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ import { makeLegalIdentifier } from "@rollup/pluginutils";
import hasher from "./hasher";

export default (id: string, ...salt: string[]): string => {
const hash = hasher([id, ...salt].join(":")).slice(0, 8);
const hash = hasher([id, "0iOXBLSx", ...salt].join(":")).slice(0, 8);
return makeLegalIdentifier(`${id}_${hash}`);
};

0 comments on commit 2990cb0

Please sign in to comment.