UNPKG

jest-preview

Version:

Preview your Jest tests in a browser

376 lines (363 loc) 15.2 kB
'use strict'; var fs = require('fs'); var path = require('path'); var crypto = require('crypto'); var child_process = require('child_process'); var url = require('url'); require('camelcase'); var slash = require('slash'); require('@svgr/core'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs); var path__default = /*#__PURE__*/_interopDefaultLegacy(path); var crypto__default = /*#__PURE__*/_interopDefaultLegacy(crypto); var slash__default = /*#__PURE__*/_interopDefaultLegacy(slash); const CACHE_FOLDER = './node_modules/.cache/jest-preview'; const SASS_LOAD_PATHS_CONFIG = 'cache-sass-load-paths.config'; // Create cache folder if it doesn't exist function createCacheFolderIfNeeded() { if (!fs__default["default"].existsSync(CACHE_FOLDER)) { fs__default["default"].mkdirSync(CACHE_FOLDER, { recursive: true, }); } } // https://github.com/vitejs/vite/blob/c29613013ca1c6d9c77b97e2253ed1f07e40a544/packages/vite/src/node/plugins/css.ts#L97-L98 const cssLangs = `\\.(css|less|sass|scss|styl|stylus|pcss|postcss)($|\\?)`; const cssModuleRE = new RegExp(`\\.module${cssLangs}`); function isSass(filename) { return /.(sass|scss)$/.test(filename); } function isLess(filename) { return /.(less)$/.test(filename); } // TODO: Add styl, stylus... function isPreProcessor(filename) { return isSass(filename) || isLess(filename); } function havePostCss() { // TODO: Since we executing postcssrc() twice, the overall speed is slow // We can try to process the PostCSS here to reduce the number of executions // TODO: Does this break on Windows? const checkHavePostCssFileContent = `const postcssrc = require('postcss-load-config'); postcssrc().then(({ plugins, options }) => { console.log(true) }) .catch(error=>{ if (!/No PostCSS Config found/.test(error.message)) { throw new Error("Failed to load PostCSS config", error) } console.log(false) });`; const tempFileName = createTempFile(checkHavePostCssFileContent); const result = child_process.spawnSync('node', [tempFileName]); fs__default["default"].unlink(tempFileName, (error) => { if (error) throw error; }); const stderr = result.stderr.toString('utf-8').trim(); if (stderr) console.error(stderr); if (result.error) throw result.error; return result.stdout.toString().trim() === 'true'; } function getRelativeFilename(filename) { return slash__default["default"](filename.split(process.cwd())[1]); } // TODO: We need to re-architect the CSS transform as follow // pre-processor (sass, stylus, less) => process(??) => post-processor (css modules, tailwindcss) // Reference to https://github.com/vitejs/vite/blob/c29613013ca1c6d9c77b97e2253ed1f07e40a544/packages/vite/src/node/plugins/css.ts#L652-L673 function processCss(src, filename) { const relativeFilename = getRelativeFilename(filename); console.time(`Processing ${relativeFilename}`); let cssSrc = src; const isModule = cssModuleRE.test(filename); const isPreProcessorFile = isPreProcessor(filename); const haveTailwindCss = fs__default["default"].existsSync(path__default["default"].join(process.cwd(), 'tailwind.config.js')) || fs__default["default"].existsSync(path__default["default"].join(process.cwd(), 'tailwind.config.cjs')); const usePostCssExplicitly = havePostCss(); // Pure CSS if (!isModule && !isPreProcessorFile && !usePostCssExplicitly && !haveTailwindCss) { // Transform to a javascript module that load a <link rel="stylesheet"> tag to the page. console.timeEnd(`Processing ${relativeFilename}`); return { code: `const relativeCssPath = "${relativeFilename}"; const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = relativeCssPath; document.head.appendChild(link); module.exports = JSON.stringify(relativeCssPath);`, }; } // Pre-processor (sass, stylus, less) if (isSass(filename)) { cssSrc = processSass(filename); } if (isLess(filename)) { cssSrc = processLess(filename); } // Process PostCSS (postcss.config.js can be present or not) if (usePostCssExplicitly || haveTailwindCss || isModule) { console.timeEnd(`Processing ${relativeFilename}`); return processPostCss(cssSrc, filename, { useConfigFile: usePostCssExplicitly, isModule, haveTailwindCss, }); } console.timeEnd(`Processing ${relativeFilename}`); return { code: `const style = document.createElement('style'); style.appendChild(document.createTextNode(${JSON.stringify(cssSrc)})); document.head.appendChild(style); module.exports = {}`, }; } // TODO: MEDIUM PRIORITY To research about getCacheKey // Reference: // - https://jestjs.io/docs/code-transformation#writing-custom-transformers // - https://github.com/swc-project/jest/blob/17cf883b46c050a485975d8ce96a05277cf6032f/index.ts#L37-L52 // const cacheKeyFunction = createCacheKey(); // export function getCacheKey(src: string, filename: string, ...rest): string { // const baseCacheKey = cacheKeyFunction(src, filename, ...rest); // return crypto.createHash('md5').update(baseCacheKey).digest('hex'); // } // August 22, 2022: Actually, we can take css/file transform content into account for getCacheKey. // So, each time they change, the cache will be invalidated // We cannot create async transformer if we are using CommonJS // ( Reference: https://github.com/facebook/jest/issues/11081#issuecomment-791259034 // https://github.com/facebook/jest/issues/11458 // Also, there is a inconsistency in jest docs about should `process` be required // https://jestjs.io/docs/configuration#transform-objectstring-pathtotransformer--pathtotransformer-object // A transformer must be an object with at least a process function // https://jestjs.io/docs/code-transformation#writing-custom-transformers // As can be seen, only process or processAsync is mandatory to implement) // Convert from //cssModulesExportedTokens||| {"scssModule":"_scssModule_ujx1w_1"} // --- // css||| ._scssModule_ujx1w_1 { // color: #ff0000; // } // --- // To this // { "cssModulesExportedTokens": { "scssModule":"_scssModule_ujx1w_1"}, "css":"._scssModule_ujx1w_1 { color: #ff0000 }"} function parsePostCssExternalOutput(output) { const lines = output.trim().split('---'); const result = { cssModulesExportedTokens: '', css: '', }; for (const line of lines) { const [key, value] = line.trim().split('|||'); if (key === 'cssModulesExportedTokens') { result.cssModulesExportedTokens = value; } if (key === 'css') { result.css = value; } } return result; } function createTempFile(content) { createCacheFolderIfNeeded(); const tempFileName = path__default["default"].join(CACHE_FOLDER, crypto__default["default"].randomBytes(16).toString('hex')); fs__default["default"].writeFileSync(tempFileName, content); return tempFileName; } function processPostCss(src, filename, options = { useConfigFile: true, isModule: false, haveTailwindCss: false }) { var _a; // TODO: SO SLOWWWWW. How can we speedup this? // It currently takes about 0.35 seconds to process one CSS file with PostCSS // - getCacheKey // - cache result of `postcssrc()` => very hard, since each css file, it must read the config again // - somehow speedup `spawnSync`? // - Do not execute postcssrc() twice const cssModulesPluginsContent = `require('postcss-modules')({ getJSON: (cssFileName, json, outputFileName) => { console.log('cssModulesExportedTokens|||', JSON.stringify(json)); console.log('---') }, // Use custom scoped name to prevent different hash between operating systems // Because new line characters can be different between operating systems. Reference: https://stackoverflow.com/a/10805198 // Original hash function: https://github.com/madyankin/postcss-modules/blob/7d5965d4df201ef301421a5e35805d1b47f3c914/src/generateScopedName.js#L6 generateScopedName: function (name, filename, css) { const stringHash = require('string-hash'); const i = css.indexOf('.' + name); const line = css.substr(0, i).split(/[\\r\\n|\\n|\\r]/).length; // This is not how the real app work, might be an issue if we try to make the snapshot interactive // https://github.com/nvh95/jest-preview/issues/84#issuecomment-1146578932 const removedNewLineCharactersCss = css.replace(/(\\r\\n|\\n|\\r)/g, ''); const hash = stringHash(removedNewLineCharactersCss).toString(36).substr(0, 5); return '_' + name + '_' + hash + '_' + line; }, })`; let processPostCssFileContent = `const postcss = require('postcss'); const postcssrc = require('postcss-load-config'); const isModule = ${options.isModule} const cssSrc = ${JSON.stringify(src)}; // TODO: We have to re-execute "postcssrc()" every CSS file. // Can we do better? Singleton? postcssrc().then(({ plugins, options }) => { plugins.unshift(require('postcss-import')()) if (isModule) { plugins.push( ${cssModulesPluginsContent}, ) } postcss(plugins) .process(cssSrc, { ...options, from: ${JSON.stringify(filename)} }) .then((result) => { console.log('css|||', result.css); console.log('---') }); });`; if (!options.useConfigFile) { processPostCssFileContent = `const postcss = require('postcss'); const isModule = ${options.isModule}; const haveTailwindCss = ${options.haveTailwindCss}; const cssSrc = ${JSON.stringify(src)}; let plugins = []; if (isModule) { plugins.unshift(require('postcss-import')()) plugins.push( ${cssModulesPluginsContent}, ) } if (haveTailwindCss) { // tailwindCss auto resolve config // https://github.com/tailwindlabs/tailwindcss/blob/cef02e2dc395ed5b5d31b72183cf7504b3bd76c1/src/util/resolveConfigPath.js#L45-L52 plugins.push(require("tailwindcss")()) } postcss(plugins) .process(cssSrc, { from: ${JSON.stringify(filename)} }) .then((result) => { console.log('css|||', result.css); console.log('---') });`; } const tempFileName = createTempFile(processPostCssFileContent); // We have to write this file to disk since Windows cannot process the command with long arguments const result = child_process.spawnSync('node', [tempFileName]); fs__default["default"].unlink(tempFileName, (error) => { if (error) throw error; }); // TODO: What happens if we do not pass `utf-8`? const stderr = (_a = result.stderr) === null || _a === void 0 ? void 0 : _a.toString('utf-8').trim(); if (stderr) console.error(stderr); if (result.error) throw result.error; const output = parsePostCssExternalOutput(result.stdout.toString()); return { code: `const style = document.createElement("style"); style.type = "text/css"; const styleContent = ${JSON.stringify(output.css)}; style.appendChild(document.createTextNode(styleContent.replace(/\\\\/g, ''))); document.head.appendChild(style); module.exports = ${output.cssModulesExportedTokens || '{}'}`, }; } function processSass(filename) { let sass; try { sass = require('sass'); } catch (err) { console.log(err); throw new Error('Sass not found. Please install sass and try again.'); } const sassLoadPathsConfigPath = path__default["default"].join(CACHE_FOLDER, SASS_LOAD_PATHS_CONFIG); let sassLoadPathsConfig; if (fs__default["default"].existsSync(sassLoadPathsConfigPath)) { const sassLoadPathsString = fs__default["default"] .readFileSync(path__default["default"].join(CACHE_FOLDER, SASS_LOAD_PATHS_CONFIG), 'utf8') .trim(); sassLoadPathsConfig = JSON.parse(sassLoadPathsString); } else { sassLoadPathsConfig = []; } let cssResult; // An importer that redirects relative URLs starting with "~" to `node_modules` // Reference: https://sass-lang.com/documentation/js-api/interfaces/FileImporter const tildeImporter = (url$1) => { if (!url$1.startsWith('~')) return null; return new URL( // TODO: Search in node_modules by require.resolve (monorepo) // E.g: input: ~animate-sass/animate // output: file:/Users/yourname/oss/jest-preview/node_modules/animate-sass/animate // => require.resolve('animate-sass') + animate path__default["default"].join(url.pathToFileURL('node_modules').href, url$1.substring(1))); }; if (sass.compile) { cssResult = sass.compile(filename, { loadPaths: sassLoadPathsConfig, importers: [ { findFileUrl(url) { return tildeImporter(url); }, }, ], }).css; } // Because sass.compile is only introduced since sass version 1.45.0 // For older versions, we have to use the legacy API: renderSync else if (sass.renderSync) { cssResult = sass .renderSync({ file: filename, includePaths: sassLoadPathsConfig, importer: [ function (url) { return tildeImporter(url); }, ], }) .css.toString(); } else { throw new Error('Cannot compile sass to css: No compile method is available.'); } return cssResult; } function processLess(filename) { console.log('processLess', filename); try { require('less'); } catch (err) { console.log(err); throw new Error('Less not found. Please install less and try again.'); } const processLessFileContent = `const less = require('less'); const fs = require('fs'); const path = require('path'); const cssContent = fs.readFileSync(${JSON.stringify(filename)}, 'utf8'); less.render(cssContent, { filename: ${JSON.stringify(filename)}}).then((output) => { console.log(output.css); });`; const tempFileName = createTempFile(processLessFileContent); const result = child_process.spawnSync('node', [tempFileName]); fs__default["default"].unlink(tempFileName, (error) => { if (error) throw error; }); const stderr = result.stderr.toString('utf-8').trim(); if (stderr) console.error(stderr); if (result.error) throw result.error; return result.stdout.toString(); } function process$1(src, filename) { return processCss(src, filename); } var css = { process: process$1 }; module.exports = css;