diff --git a/package-lock.json b/package-lock.json index 256854b..ec166c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@stencil/sass", - "version": "2.0.3", + "version": "3.0.0-dev.2023.2.16.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@stencil/sass", - "version": "2.0.3", + "version": "3.0.0-dev.2023.2.16.0", "license": "MIT", "devDependencies": { "@ionic/prettier-config": "^2.0.0", @@ -14,13 +14,12 @@ "@stencil/core": "^3.0.0", "@types/jest": "^26.0.15", "@types/node": "^16.11.48", - "@types/sass": "^1.16.0", "jest": "^26.6.3", "np": "^7.0.0", "prettier": "^2.2.1", "rimraf": "^4.0.4", "rollup": "^3.5.1", - "sass": "^1.29.0", + "sass": "^1.58.3", "terser": "^5.3.8", "ts-jest": "^26.4.4", "typescript": "^4.9.4" @@ -1324,15 +1323,6 @@ "@types/node": "*" } }, - "node_modules/@types/sass": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@types/sass/-/sass-1.16.1.tgz", - "integrity": "sha512-iZUcRrGuz/Tbg3loODpW7vrQJkUtpY2fFSf4ELqqkApcS2TkZ1msk7ie8iZPB86lDOP8QOTTmuvWjc5S0R9OjQ==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -3818,6 +3808,12 @@ "minimatch": "^3.0.4" } }, + "node_modules/immutable": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.4.tgz", + "integrity": "sha512-WDxL3Hheb1JkRN3sQkyujNlL/xRjAo3rJtaU5xeufUauG66JdMr32bLj4gF+vWl84DIA3Zxw7tiAjneYzRRw+w==", + "dev": true + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -8138,18 +8134,20 @@ } }, "node_modules/sass": { - "version": "1.42.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.42.1.tgz", - "integrity": "sha512-/zvGoN8B7dspKc5mC6HlaygyCBRvnyzzgD5khiaCfglWztY99cYoiTUksVx11NlnemrcfH5CEaCpsUKoW0cQqg==", + "version": "1.58.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.58.3.tgz", + "integrity": "sha512-Q7RaEtYf6BflYrQ+buPudKR26/lH+10EmO9bBqbmPh/KeLqv8bjpTNqxe71ocONqXq+jYiCbpPUmQMS+JJPk4A==", "dev": true, "dependencies": { - "chokidar": ">=3.0.0 <4.0.0" + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { "sass": "sass.js" }, "engines": { - "node": ">=8.9.0" + "node": ">=12.0.0" } }, "node_modules/saxes": { @@ -8500,6 +8498,15 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-resolve": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", @@ -10727,15 +10734,6 @@ "@types/node": "*" } }, - "@types/sass": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@types/sass/-/sass-1.16.1.tgz", - "integrity": "sha512-iZUcRrGuz/Tbg3loODpW7vrQJkUtpY2fFSf4ELqqkApcS2TkZ1msk7ie8iZPB86lDOP8QOTTmuvWjc5S0R9OjQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -12636,6 +12634,12 @@ "minimatch": "^3.0.4" } }, + "immutable": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.4.tgz", + "integrity": "sha512-WDxL3Hheb1JkRN3sQkyujNlL/xRjAo3rJtaU5xeufUauG66JdMr32bLj4gF+vWl84DIA3Zxw7tiAjneYzRRw+w==", + "dev": true + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -15903,12 +15907,14 @@ } }, "sass": { - "version": "1.42.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.42.1.tgz", - "integrity": "sha512-/zvGoN8B7dspKc5mC6HlaygyCBRvnyzzgD5khiaCfglWztY99cYoiTUksVx11NlnemrcfH5CEaCpsUKoW0cQqg==", + "version": "1.58.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.58.3.tgz", + "integrity": "sha512-Q7RaEtYf6BflYrQ+buPudKR26/lH+10EmO9bBqbmPh/KeLqv8bjpTNqxe71ocONqXq+jYiCbpPUmQMS+JJPk4A==", "dev": true, "requires": { - "chokidar": ">=3.0.0 <4.0.0" + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" } }, "saxes": { @@ -16190,6 +16196,12 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, "source-map-resolve": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", diff --git a/package.json b/package.json index f359455..db1097a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@stencil/sass", - "version": "2.0.3", + "version": "3.0.0-dev.2023.2.16.0", "license": "MIT", "main": "dist/index.js", "module": "dist/index.mjs", @@ -31,13 +31,12 @@ "@stencil/core": "^3.0.0", "@types/jest": "^26.0.15", "@types/node": "^16.11.48", - "@types/sass": "^1.16.0", "jest": "^26.6.3", "np": "^7.0.0", "prettier": "^2.2.1", "rimraf": "^4.0.4", "rollup": "^3.5.1", - "sass": "^1.29.0", + "sass": "^1.58.3", "terser": "^5.3.8", "ts-jest": "^26.4.4", "typescript": "^4.9.4" diff --git a/rollup.config.mjs b/rollup.config.mjs index bf2ba58..e3e655a 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -2,10 +2,16 @@ import { createRequire } from 'node:module'; import pluginSass from './rollup.plugin.sass.mjs'; import rollupResolve from '@rollup/plugin-node-resolve'; +// require `package.json` in order to use its 'main' and 'module' fields to tell rollup where to output the generated +// bundles const require = createRequire(import.meta.url); const pkg = require('./package.json'); +/** + * Generate an ESM and a CJS output bundle + */ export default { + // the input is expected to exist at this location as a result of running the typescript compiler input: 'dist/index.js', plugins: [ diff --git a/rollup.plugin.sass.mjs b/rollup.plugin.sass.mjs index 8746fdb..5421971 100644 --- a/rollup.plugin.sass.mjs +++ b/rollup.plugin.sass.mjs @@ -1,17 +1,29 @@ import { fileURLToPath } from 'node:url' import { minify } from 'terser'; +/** + * A rollup plugin for bundling Sass directly into the project + */ export default function() { const sassFilePath = fileURLToPath(new URL('node_modules/sass/sass.dart.js', import.meta.url)); return { + /** + * A rollup build hook for resolving the Sass implementation module. + * @param {string} id the importee exactly as it is written in an import statement in the source code + * @returns {string | undefined} the path to the Sass implementation from the root of this project + */ resolveId(id) { if (id === 'sass') { return sassFilePath; } }, + /** + * Wraps Sass to bundle it into the project + * @param {string} code the code to modify + * @param {string} id module's identifier + * @returns {string} the modified code + */ async transform(code, id) { - // a little nudge to make it easier for - // rollup to find the cjs exports if (id === sassFilePath) { return await wrapSassImport(code); } @@ -22,6 +34,15 @@ export default function() { }; +/** + * Wraps Sass in an IIFE to make it easier for rollup to find CJS exports and minifies it. + * + * This function generates code for calling Sass' entrypoint function (`load()`) and capturing a reference to its + * `render` function. + * + * @param {string} code the Sass implementation code + * @returns {Promise} the wrapped Sass code + */ async function wrapSassImport(code) { code = ` @@ -57,6 +78,7 @@ ${code}; })(Sass); +Sass.load({}); const render = Sass.render; export { render }; `; @@ -72,8 +94,9 @@ export { render }; 'false /** NODE ENVIRONMENT **/' ); - code = removeNodeRequire(code, 'chokidar'); - code = removeNodeRequire(code, 'readline'); + code = removeCliPkgRequire(code, 'chokidar'); + code = removeCliPkgRequire(code, 'readline'); + code = removeNodeRequire(code, 'immutable'); const minified = await minify(code, { module: true }); code = minified.code; @@ -81,16 +104,43 @@ export { render }; return code } -function removeNodeRequire(code, moduleId) { - const requireStr = `require("${moduleId}")`; +/** + * Node modules are required by node_modules/sass/sass.dart.js via `_cli_pkg_requires`. + * + * This function manually removes unneeded require statements from the source. + * + * @param {string} code the code to modify + * @param {string} moduleId the module identifier found in a require-like statement + * @returns {string} the modified code + */ +function removeCliPkgRequire(code, moduleId) { + // e.g. `self.chokidar = _cli_pkg_requires.chokidar;` + const requireStr = `self.${moduleId} = _cli_pkg_requires.${moduleId};`; if (!code.includes(requireStr)) { - // node modules are required by sass.dart - // however this build doesn't use or need them - // so we'll manually remove it from the source throw new Error(`cannot find "${requireStr}" in sass.dart`); } return code.replace( requireStr, '{}' ); +} + +/** + * Node modules are required by node_modules/sass/sass.dart.js via `require`. + * + * This function manually removes unneeded require statements from the source. + * + * @param {string} code the code to modify + * @param {string} moduleId the module identifier found in a require-like statement + * @returns {string} the modified code + */ +function removeNodeRequire(code, moduleId) { + const requireStr = `require("${moduleId}")`; + if (!code.includes(requireStr)) { + throw new Error(`cannot find "${requireStr}" in sass.dart`); + } + return code.replace( + requireStr, + '{}' + ); } \ No newline at end of file diff --git a/src/diagnostics.ts b/src/diagnostics.ts index 5996131..ce78617 100644 --- a/src/diagnostics.ts +++ b/src/diagnostics.ts @@ -1,4 +1,4 @@ -import { SassException } from 'sass'; +import { LegacyException } from 'sass'; import * as d from './declarations'; /** @@ -12,7 +12,11 @@ import * as d from './declarations'; * @param filePath the path of the file that led to an error being raised * @returns the created diagnostic, or `null` if one could not be generated */ -export function loadDiagnostic(context: d.PluginCtx, sassError: SassException, filePath: string): d.Diagnostic | null { +export function loadDiagnostic( + context: d.PluginCtx, + sassError: LegacyException, + filePath: string +): d.Diagnostic | null { if (sassError == null || context == null) { return null; } diff --git a/src/index.ts b/src/index.ts index 15f2c09..3015472 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { render } from 'sass'; +import { type LegacyException, type LegacyResult, render } from 'sass'; import * as d from './declarations'; import { loadDiagnostic } from './diagnostics'; import { createResultsId, getRenderOptions, usePlugin } from './util'; @@ -46,7 +46,7 @@ export function sass(opts: d.PluginOptions = {}): d.Plugin { return new Promise((resolve) => { try { // invoke sass' compiler at this point - render(renderOpts, (err, sassResult) => { + render(renderOpts, (err: LegacyException, sassResult: LegacyResult): void => { if (err) { loadDiagnostic(context, err, fileName); results.code = `/** sass error${err && err.message ? ': ' + err.message : ''} **/`; diff --git a/src/util.ts b/src/util.ts index b00a74a..ed3c647 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,6 +1,7 @@ import * as d from './declarations'; import * as path from 'path'; -import { Importer, ImporterReturnType } from 'sass'; +import { LegacyAsyncImporter, LegacyImporterResult } from 'sass'; +import { LegacyOptions } from 'sass/types/legacy/options'; /** * Determine if the Sass plugin should be applied, based on the provided `fileName` @@ -29,12 +30,10 @@ export function getRenderOptions( sourceText: string, fileName: string, context: d.PluginCtx -): d.PluginOptions { - // create a copy of the original sass config, so we don't modify the one provided - const renderOpts = Object.assign({}, opts); - - // always set "data" from the source text - renderOpts.data = sourceText; +): LegacyOptions<'async'> { + // Create a copy of the original sass config, so we don't modify the one provided. + // Explicitly add `data` (as it's a required field) to be the source text + const renderOpts: LegacyOptions<'async'> = { ...opts, data: sourceText }; // activate indented syntax if the file extension is .sass. // this needs to be set prior to injecting global sass (as the syntax affects the import terminator) @@ -83,13 +82,13 @@ export function getRenderOptions( } // remove non-standard sass option - delete renderOpts.injectGlobalPaths; + delete (renderOpts as any).injectGlobalPaths; // the "file" config option is not valid here delete renderOpts.file; if (context.sys && typeof context.sys.resolveModuleId === 'function') { - const importers: Importer[] = []; + const importers: LegacyAsyncImporter[] = []; if (typeof renderOpts.importer === 'function') { importers.push(renderOpts.importer); } else if (Array.isArray(renderOpts.importer)) { @@ -103,7 +102,11 @@ export function getRenderOptions( * @param _prev Unused - typically, this is a string identifying the stylesheet that contained the @use or @import. * @param done a callback to return the path to the resolved path */ - const importer: Importer = (url: string, _prev: string, done: (data: ImporterReturnType) => void): void => { + const importer: LegacyAsyncImporter = ( + url: string, + _prev: string, + done: (data: LegacyImporterResult) => void + ): void => { if (typeof url === 'string') { if (url.startsWith('~')) { try { diff --git a/test/utils.spec.ts b/test/utils.spec.ts index 91e68dd..35aee90 100644 --- a/test/utils.spec.ts +++ b/test/utils.spec.ts @@ -31,7 +31,9 @@ describe('getRenderOptions', () => { }; const output = util.getRenderOptions(input, sourceText, fileName, context); expect(output.data).toBe(`@import "/my/global/variables.scss";body { color: blue; }`); - expect(output.injectGlobalPaths).toBeUndefined(); + // `injectGlobalPaths` in an input argument to the function, and does not exist on the return type (hence the type assertion) + // we have this check to verify that we have not accidentally copied it to the generated configuration + expect((output as any).injectGlobalPaths).toBeUndefined(); expect(input.injectGlobalPaths).toHaveLength(1); expect(input.injectGlobalPaths[0]).toBe('/my/global/variables.scss'); });