Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: output insertStyle as part of rollup bundle #163

Merged
merged 4 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
11 changes: 0 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,6 @@ sass({
});
```

**Usage caveat:**

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
65 changes: 43 additions & 22 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,41 @@ 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.
import { Plugin as RollupPlugin } from 'rollup';
import {
Plugin as RollupPlugin,
NormalizedOutputOptions as RollupNormalizedOutputOptions,
OutputBundle as RollupOutputBundle,
} from 'rollup';

type PluginState = {
// Stores interim bundle objects
styles: { id?: string; content?: string }[];

// ""; 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 +155,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`;
elycruz marked this conversation as resolved.
Show resolved Hide resolved
defaultExport = `${INSERT_STYLE_ID}(${out});`;
} else if (!rollupOptions.output) {
defaultExport = out;
}
Expand All @@ -178,13 +185,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 +203,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 @@ -246,21 +270,18 @@ export = function plugin(
},

generateBundle(
generateOptions: { file?: string },
bundle: { [fileName: string]: RollupAssetInfo | RollupChunkInfo },
generateOptions: RollupNormalizedOutputOptions,
_: RollupOutputBundle,
isWrite: boolean,
): Promise<any> {
if (
!isWrite ||
(!pluginOptions.insert &&
(!pluginState.styles.length || pluginOptions.output === false))
) {
) {
const { styles } = pluginState;
const { output, insert } = pluginOptions;

if (!isWrite || (!insert && (!styles.length || output === false))) {
return Promise.resolve();
}

const { styles } = pluginState;
const css = styles.map((style) => style.content).join('');
const { output, insert } = pluginOptions;

if (typeof output === 'string') {
return fs.promises
Expand All @@ -284,5 +305,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
107 changes: 78 additions & 29 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,21 +136,6 @@ test('should support options.data', async (t) => {

// #region insert option
{
/**
* In unit tests we are targeting `src` folder,
* so there is no `insertStyle.js` file so `rollup` issues a warning
* that we can silence.
*/
const onwarn: WarningHandlerWithDefault = (warning, defaultHandler) => {
if (
warning.code === 'UNRESOLVED_IMPORT' &&
warning.exporter?.includes('insertStyle.js')
) {
return;
}
defaultHandler(warning);
};

test('should insert CSS into head tag', async (t) => {
const outputBundle = await rollup({
input: 'test/fixtures/insert/index.js',
Expand All @@ -160,12 +145,46 @@ test('should support options.data', async (t) => {
options: TEST_SASS_OPTIONS_DEFAULT,
}),
],
onwarn,
});
const { output } = await outputBundle.generate(TEST_GENERATE_OPTIONS);
const result = getFirstChunkCode(output);

t.snapshot(result);
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 +199,56 @@ 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}");'
elycruz marked this conversation as resolved.
Show resolved Hide resolved

## 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.

4 changes: 4 additions & 0 deletions tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"include": ["./src/*"]
}
6 changes: 0 additions & 6 deletions tsconfig.build.plugin.json

This file was deleted.