diff --git a/README.md b/README.md index ec8d7d1..32fb316 100644 --- a/README.md +++ b/README.md @@ -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` diff --git a/package.json b/package.json index 46be07b..38ffc3f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/index.ts b/src/index.ts index ae0244d..af3c4ec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,8 +6,6 @@ import * as fs from 'fs'; import { createFilter } from '@rollup/pluginutils'; import type { SassImporterResult, - RollupAssetInfo, - RollupChunkInfo, RollupPluginSassOptions, RollupPluginSassOutputFn, SassOptions, @@ -15,10 +13,15 @@ import type { 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 @@ -26,13 +29,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: @@ -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; } @@ -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: {}, @@ -193,7 +205,21 @@ export = function plugin( return { name: 'rollup-plugin-sass', - transform(code: string, filePath: string): Promise { + /** @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(); } @@ -246,21 +272,18 @@ export = function plugin( }, generateBundle( - generateOptions: { file?: string }, - bundle: { [fileName: string]: RollupAssetInfo | RollupChunkInfo }, + generateOptions: RollupNormalizedOutputOptions, + _: RollupOutputBundle, isWrite: boolean, - ): Promise { - 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 @@ -284,5 +307,5 @@ export = function plugin( return Promise.resolve(css); }, - } as RollupPlugin; + }; }; diff --git a/src/insertStyle.ts b/src/insertStyle.ts index 1ac581d..1814aa0 100644 --- a/src/insertStyle.ts +++ b/src/insertStyle.ts @@ -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 */ diff --git a/test/index.test.ts b/test/index.test.ts index 6f9f9dc..9c45642 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -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', @@ -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) => { @@ -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', ); diff --git a/test/snapshots/test/index.test.ts.md b/test/snapshots/test/index.test.ts.md index 6a4cfab..50b26b9 100644 --- a/test/snapshots/test/index.test.ts.md +++ b/test/snapshots/test/index.test.ts.md @@ -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 diff --git a/test/snapshots/test/index.test.ts.snap b/test/snapshots/test/index.test.ts.snap index ad19319..bb19a59 100644 Binary files a/test/snapshots/test/index.test.ts.snap and b/test/snapshots/test/index.test.ts.snap differ diff --git a/tsconfig.build.insertStyle.json b/tsconfig.build.insertStyle.json deleted file mode 100644 index 94f6d05..0000000 --- a/tsconfig.build.insertStyle.json +++ /dev/null @@ -1,8 +0,0 @@ -/* @see insertStyle.ts for additional information */ -{ - "extends": "./tsconfig.json", - "include": ["./src/insertStyle.ts"], - "compilerOptions": { - "module": "ES6" - } -} diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..3fa7686 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["./src/*"] +} diff --git a/tsconfig.build.plugin.json b/tsconfig.build.plugin.json deleted file mode 100644 index e218d24..0000000 --- a/tsconfig.build.plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -/* @see insertStyle.ts for additional information */ -{ - "extends": "./tsconfig.json", - "include": ["./src/*"], - "exclude": ["./src/insertStyle.ts"] -}