Skip to content

Commit

Permalink
Merge pull request #163 from marcalexiei/emit-file
Browse files Browse the repository at this point in the history
feat: output insertStyle as part of rollup bundle
  • Loading branch information
elycruz authored Oct 30, 2024
2 parents ebb8035 + 2c5594f commit 044baa0
Show file tree
Hide file tree
Showing 10 changed files with 137 additions and 88 deletions.
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
67 changes: 45 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,13 @@ 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;
* e.g., the path will completely replaced, and re-generated (as a relative path)
* by rollup.
*/
imports = `import ${insertFnName} from '${__dirname.replace(/\\/g, '/')}/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 +187,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 +205,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 +272,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 +307,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}");'

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

0 comments on commit 044baa0

Please sign in to comment.