Skip to content

Commit

Permalink
feat: output insertStyle as part of rollup bundle
Browse files Browse the repository at this point in the history
  • Loading branch information
marcalexiei committed Sep 25, 2024
1 parent fc9a95b commit 71ac1c6
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 57 deletions.
7 changes: 0 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,6 @@ sass({

There is a utility function that handles injecting individual style payloads into the page's head, which is output as `___$insertStyle` by the rollup-plugin-sass plugin.

This function is output to `./dist/node_modules/...`, in user-land builds, so you have to make sure that it isn't
ignored by your build tool(s) (E.g., rollup, webpack etc.); As a solution, you'll just have to make sure that the
directory is "included"/not-"excluded" via your build tools facilities/added-plugins/etc.

Additionally, if you're publishing an app to an internal registry, or similar, you'll have to
make sure 'dist/node_modules' isn't ignored in this scenario as well.

### `processor`

- Type: `Function`
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
},
"scripts": {
"prepare": "npm run build && npm test && husky",
"build": "npm run build-downlevel-dts && tsc --project tsconfig.build.plugin.json && tsc --project tsconfig.build.insertStyle.json",
"build": "npm run build-downlevel-dts && tsc --project tsconfig.build.json",
"build-downlevel-dts": "node scripts/clean-and-run-downlevel-dts.js",
"downlevel-dts": "downlevel-dts . ts3.5 [--to=3.5]",
"test": "nyc --reporter=html --reporter=text ava && npm run test:rollup.config.spec.ts",
Expand Down
48 changes: 32 additions & 16 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ import * as fs from "fs";
import { createFilter } from "@rollup/pluginutils";
import type {
SassImporterResult,
RollupAssetInfo,
RollupChunkInfo,
RollupPluginSassOptions,
RollupPluginSassOutputFn,
SassOptions,
RollupPluginSassProcessorFnOutput,
SassRenderResult,
} from "./types";
import { isFunction, isObject, isString, warn } from "./utils";
import insertStyle from "./insertStyle";

// @note Rollup is added as a "devDependency" so no actual symbols should be imported.
// Interfaces and non-concrete types are ok.
Expand All @@ -26,13 +25,18 @@ type PluginState = {

// ""; Used, currently to ensure that we're not pushing style objects representing
// the same file-path into `pluginState.styles` more than once.
styleMaps: { [index: string]: { id?: string; content?: string } };
styleMaps: {
[index: string]: {
id?: string;
content?: string;
};
};
};

const MATCH_SASS_FILENAME_RE = /\.sass$/;
const MATCH_NODE_MODULE_RE = /^~([a-z0-9]|@).+/i;

const insertFnName = "___$insertStyle";
const INSERT_STYLE_ID = "___$insertStyle";

/**
* Returns a sass `importer` list:
Expand Down Expand Up @@ -147,12 +151,11 @@ const processRenderResponse = (

if (rollupOptions.insert) {
/**
* Add `insertStyle` import for handling "inserting"
* *.css into *.html `head`.
* @see insertStyle.ts for additional information
* Include import using {@link INSERT_STYLE_ID} as source.
* It will be resolved to insert style function using `resolvedID` and `load` hooks
*/
imports = `import ${insertFnName} from '${__dirname}/insertStyle.js';\n`;
defaultExport = `${insertFnName}(${out});`;
imports = `import ${INSERT_STYLE_ID} from '${INSERT_STYLE_ID}';\n`;
defaultExport = `${INSERT_STYLE_ID}(${out});`;
} else if (!rollupOptions.output) {
defaultExport = out;
}
Expand All @@ -178,13 +181,16 @@ export = function plugin(
},
options,
);

const {
include = defaultIncludes,
exclude = defaultExcludes,
runtime: sassRuntime,
options: incomingSassOptions = {} as SassOptions,
} = pluginOptions;

const filter = createFilter(include || "", exclude || "");

const pluginState: PluginState = {
styles: [],
styleMaps: {},
Expand All @@ -193,7 +199,21 @@ export = function plugin(
return {
name: "rollup-plugin-sass",

transform(code: string, filePath: string): Promise<any> {
/** @see https://rollupjs.org/plugin-development/#resolveid */
resolveId(source) {
if (source === INSERT_STYLE_ID) {
return INSERT_STYLE_ID;
}
},

/** @see https://rollupjs.org/plugin-development/#load */
load(id) {
if (id === INSERT_STYLE_ID) {
return `export default ${insertStyle.toString()}`;
}
},

transform(code, filePath) {
if (!filter(filePath)) {
return Promise.resolve();
}
Expand Down Expand Up @@ -245,11 +265,7 @@ export = function plugin(
); // @note do not `catch` here - let error propagate to rollup level.
},

generateBundle(
generateOptions: { file?: string },
bundle: { [fileName: string]: RollupAssetInfo | RollupChunkInfo },
isWrite: boolean,
): Promise<any> {
generateBundle(generateOptions, _, isWrite) {
if (
!isWrite ||
(!pluginOptions.insert &&
Expand Down Expand Up @@ -284,5 +300,5 @@ export = function plugin(

return Promise.resolve(css);
},
} as RollupPlugin;
};
};
7 changes: 3 additions & 4 deletions src/insertStyle.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
/**
* Create a style tag and append to head tag
*
* @warning this file is not included directly in the source code!
* If user specifies inject option to true, an import to this file will be injected in rollup output.
* Due to this reason this file is compiled into a ESM module separated from other plugin source files.
* That is the reason of why there are two tsconfig.build files.
* @warning This function is injected inside rollup. According to this be sure
* - to not include any side-effect
* - do not import any library / other files content
*
* @return css style
*/
Expand Down
93 changes: 81 additions & 12 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,46 @@ test("should support options.data", async (t) => {
onwarn,
});
const { output } = await outputBundle.generate(TEST_GENERATE_OPTIONS);
const result = getFirstChunkCode(output);

t.snapshot(result);
debugger;

const outputFilePath = path.join(TEST_OUTPUT_DIR, "insert-bundle");

await outputBundle.write({ dir: outputFilePath });

t.is(
output.length,
1,
"has 1 chunk (we are bundling all in one single file)",
);

const [{ moduleIds, modules }] = output;

t.is(
moduleIds.filter((it) => it.endsWith("insertStyle")).length,
1,
"include insertStyle one time",
);

const actualAModuleID = moduleIds.find((it) =>
it.endsWith("actual_a.scss"),
) as string;
const actualAModule = modules[actualAModuleID];
t.truthy(actualAModule);
t.snapshot(
actualAModule.code,
"actual_a content is compiled with insertStyle",
);

const actualBModuleID = moduleIds.find((it) =>
it.endsWith("actual_b.scss"),
) as string;
const actualBModule = modules[actualBModuleID];
t.truthy(actualBModule);
t.snapshot(
actualBModule.code,
"actual_b content is compiled with insertStyle",
);
});

test("should generate chunks with import insertStyle when `insert` is true", async (t) => {
Expand All @@ -180,26 +217,58 @@ test("should support options.data", async (t) => {
options: TEST_SASS_OPTIONS_DEFAULT,
}),
],
output: {
preserveModules: true,
preserveModulesRoot: "src",
},

onwarn,
});

const { output } = await outputBundle.generate(TEST_GENERATE_OPTIONS);
const { output } = await outputBundle.generate({
...TEST_GENERATE_OPTIONS,
preserveModules: true,
preserveModulesRoot: "src",
});

const outputFilePath = path.join(TEST_OUTPUT_DIR, "insert-multiple-entry");

await outputBundle.write({
dir: outputFilePath,
preserveModules: true,
preserveModulesRoot: "src",
});

t.is(output.length, 5, "has 5 chunks");

const outputFileNames = output.map((it) => it.fileName);

t.is(
outputFileNames.filter((it) => it.startsWith("entry")).length,
2,
"1 chunk for each entry (2)",
);
t.is(
outputFileNames.filter((it) => it.startsWith("assets/actual")).length,
2,
"1 chunk for each entry style import (2)",
);
t.is(
outputFileNames.filter((it) => it.endsWith("insertStyle.js")).length,
1,
"1 chunk for insertStyle helper",
);

const styleFiles = output.filter((it) =>
it.fileName.startsWith("assets/actual"),
);

t.is(output.length, 2, "has 2 chunks");
t.true(
output.every((outputItem) => {
styleFiles.every((outputItem) => {
if (outputItem.type === "chunk") {
const insertStyleImportsCount = outputItem.imports.filter((it) =>
it.includes("/insertStyle.js"),
it.endsWith("insertStyle.js"),
).length;
return insertStyleImportsCount === 1;
}
// if is an assets there is no need to check imports
return true;
// no asset should be present here
return false;
}),
"each chunk must include insertStyle once",
);
Expand Down
13 changes: 6 additions & 7 deletions test/snapshots/test/index.test.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,13 @@ Generated by [AVA](https://avajs.dev).

## should insert CSS into head tag

> Snapshot 1
> actual_a content is compiled with insertStyle
`import ___$insertStyle from '../../../src/insertStyle.js';␊
___$insertStyle("body{color:red}");␊
___$insertStyle("body{color:green}");␊
`
'insertStyle("body{color:red}");'

> actual_b content is compiled with insertStyle
'insertStyle("body{color:green}");'

## should processor return as string

Expand Down
Binary file modified test/snapshots/test/index.test.ts.snap
Binary file not shown.
8 changes: 0 additions & 8 deletions tsconfig.build.insertStyle.json

This file was deleted.

3 changes: 1 addition & 2 deletions tsconfig.build.plugin.json → tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* @see insertStyle.ts for additional information */
{
"extends": "./tsconfig.json",
"include": ["./src/*"],
"exclude": ["./src/insertStyle.ts"]
"include": ["./src/*"]
}

0 comments on commit 71ac1c6

Please sign in to comment.