From 801bb0954cb8f3db9c4694274b97a958080d18c0 Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Wed, 25 Sep 2024 23:19:38 +0200 Subject: [PATCH 1/3] feat: output insertStyle as part of rollup bundle --- README.md | 11 --- package.json | 2 +- src/index.ts | 48 +++++++---- src/insertStyle.ts | 7 +- test/index.test.ts | 107 ++++++++++++++++++------- test/snapshots/test/index.test.ts.md | 13 ++- test/snapshots/test/index.test.ts.snap | Bin 885 -> 888 bytes tsconfig.build.insertStyle.json | 8 -- tsconfig.build.json | 4 + tsconfig.build.plugin.json | 6 -- 10 files changed, 124 insertions(+), 82 deletions(-) delete mode 100644 tsconfig.build.insertStyle.json create mode 100644 tsconfig.build.json delete mode 100644 tsconfig.build.plugin.json 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 5cc4989..7261e6a 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,6 +13,7 @@ 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. @@ -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: @@ -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; } @@ -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: {}, @@ -193,7 +199,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(); } @@ -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 { + generateBundle(generateOptions, _, isWrite) { if ( !isWrite || (!pluginOptions.insert && @@ -284,5 +300,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 ad1931905ef6366fe6a97c32eace4442613ae38f..bb19a592bd2bb6ea7be67783a9b318fc51b6961e 100644 GIT binary patch literal 888 zcmV-;1Bd)URzVNk>UX!yE7jya6wO_WqK~onwa}MZ(qH`J0_@zWLcXYcd4yIUx4fc+E{q7M4Ski67G=^K~wrjk`xQ5tu1{#6oOf{`R!`udd2;w zu2pVa`BLdJ1I&iZUxO8x^|a^@xc0SKHYkR(wQ4m3JUazw8AO$b)K5V?Kg0|nmB1dX zq~7M!QFh~TpjUbRL)EkYW7R{4d zOl?M_xpVluSRgfz@x7MOp%ROyiz4HeT908DlGU9gsc2wdo4u0qeVJpo;@FYx`#7R_ zGZHbVoWiYy0);_@)`6=MT5=AH3|{9LtR3sKK8`~QD`zpCmbD@@QEosgtD%idfMu;? zCZCRCCW|>H)3IU01$wW*<9-c&=AqwPj9F|m*Ao_%1xZn9NFPPqG$#&9JUz^Lni=IO zNq)hXH<_aDtjm}s|94-{h%e96?YlEcyF2Z|<~YXp0eEJy|G zjaMmCxAi3yyE%&OxsWhwJ#Occvb_-FI9*Gam3(yCc(U~ujU3seYVM3!KTN2s?BM^j O!v6uJ0mZ+@3;+N&fuV{3 literal 885 zcmV-*1B(1XRzVA;CP@ z9jF_PzaNVT00000000BsRy}VMK@fF72+18C5(tI}WI}=?QBcGrBA65bA&}gp2srEA zv3>E~?rCSwaby{UD5%oXCnX&v9S#40pTIxh51_rTyYIc3*`337 z%T>Hb2S;ce?s?F7=rPW&-A}!z#?Tl-M6~{?z!c#~ zfdg1gc3VtVjepc@xEcr)=B8!^6Pr+7gwIIkqAr|K_D5ZHyU{_ z+9$o3+X_i@yW!J(fz(=z_c~eyQrsUeij-RtJ%wFBMtN~kQNy0ny@K+6o?*9Y*^$|I zaYFH?BqC5Tg=;Yd3j7eQd|M?{wHxLcyv#6IKlW#18ixc{%;IKJ){;<%xdBHS1zltU z4C~~Xe9Bbq$5Px7&1Um6R)egRH>EAV?lRwLKdcg;>0f; z3%V5x^0nefq%>$WftFT38STgdNe3Bo-(~x7Bv7wt&R+_bg zPE(>km#nVAIPq%6SN}0UAa2&uAGpz|=%O-9>v_Zl-F;`k9bgOcpq4X}HEx zyx~$Vyx41qz1B#|?IXOkIH?3SJtcy|amCFP&70HC`<%9-L1d;2VQm`EaN1CT@dE?9 ztj~yR$2X8u@!{wns6dys&>dXmUF5xlrACV8Ac6moQ)NR^pwW(F2BiTUIAp+)u+eyt zFtyWLOtGJ#*sTPFVe4^IiOcp}jH7f_F)jIUwdrK*&l(xBan; 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"] -} From de8223d7f7362264006c01003d2346a68605eebe Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Fri, 18 Oct 2024 14:17:08 +0200 Subject: [PATCH 2/3] refactor: explicit types on generateBundle bundle functions --- src/index.ts | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index 7261e6a..b9fafce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,7 +17,11 @@ 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 @@ -265,18 +269,19 @@ export = function plugin( ); // @note do not `catch` here - let error propagate to rollup level. }, - generateBundle(generateOptions, _, isWrite) { - if ( - !isWrite || - (!pluginOptions.insert && - (!pluginState.styles.length || pluginOptions.output === false)) - ) { + generateBundle( + generateOptions: RollupNormalizedOutputOptions, + _: RollupOutputBundle, + isWrite: boolean, + ) { + 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 From 2c5594f3d482d278a75cd48a57dc30465d278359 Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Wed, 30 Oct 2024 17:41:32 -0400 Subject: [PATCH 3/3] feat: virtual-insert-func - updated comment Updated comment to state result of code a bit clearer. --- src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index b9fafce..af3c4ec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -156,7 +156,9 @@ const processRenderResponse = ( if (rollupOptions.insert) { /** * Include import using {@link INSERT_STYLE_ID} as source. - * It will be resolved to insert style function using `resolvedID` and `load` hooks + * 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 ${INSERT_STYLE_ID} from '${INSERT_STYLE_ID}';\n`; defaultExport = `${INSERT_STYLE_ID}(${out});`;