UNPKG

jest-preview

Version:

Preview your Jest tests in a browser

320 lines (309 loc) 10 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'); const CACHE_FOLDER = "./node_modules/.cache/jest-preview"; const SASS_LOAD_PATHS_CONFIG = "cache-sass-load-paths.config"; function createCacheFolderIfNeeded() { if (!fs.existsSync(CACHE_FOLDER)) { fs.mkdirSync(CACHE_FOLDER, { recursive: true }); } } 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); } function isPreProcessor(filename) { return isSass(filename) || isLess(filename); } function spawnSyncWithNoColor(command, args) { return child_process.spawnSync(command, args, { env: { ...process.env, NO_COLOR: "1", // widely honored FORCE_COLOR: "0", // chalk/kleur/supports-color TERM: "dumb" // many CLIs disable color on 'dumb' } }); } function havePostCss() { 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 = spawnSyncWithNoColor("node", [tempFileName]); fs.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(filename.split(process.cwd())[1]); } 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 usePostCssExplicitly = havePostCss(); if (!isModule && !isPreProcessorFile && !usePostCssExplicitly) { 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);` }; } if (isSass(filename)) { cssSrc = processSass(filename); } if (isLess(filename)) { cssSrc = processLess(filename); } if (usePostCssExplicitly || isModule) { console.timeEnd(`Processing ${relativeFilename}`); return processPostCss(cssSrc, filename, { useConfigFile: usePostCssExplicitly, isModule }); } console.timeEnd(`Processing ${relativeFilename}`); return { code: `const style = document.createElement('style'); style.appendChild(document.createTextNode(${JSON.stringify(cssSrc)})); document.head.appendChild(style); module.exports = {}` }; } 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.join( CACHE_FOLDER, crypto.randomBytes(16).toString("hex") ); fs.writeFileSync(tempFileName, content); return tempFileName; } function processPostCss(src, filename, options = { useConfigFile: true, isModule: false }) { var _a; 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 cssSrc = ${JSON.stringify(src)}; let plugins = []; if (isModule) { plugins.unshift(require('postcss-import')()) plugins.push( ${cssModulesPluginsContent}, ) } postcss(plugins) .process(cssSrc, { from: ${JSON.stringify(filename)} }) .then((result) => { console.log('css|||', result.css); console.log('---') });`; } const tempFileName = createTempFile(processPostCssFileContent); const result = spawnSyncWithNoColor("node", [tempFileName]); fs.unlink(tempFileName, (error) => { if (error) throw error; }); const stderr = (_a = result.stderr) == null ? 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.join( CACHE_FOLDER, SASS_LOAD_PATHS_CONFIG ); let sassLoadPathsConfig; if (fs.existsSync(sassLoadPathsConfigPath)) { const sassLoadPathsString = fs.readFileSync(path.join(CACHE_FOLDER, SASS_LOAD_PATHS_CONFIG), "utf8").trim(); sassLoadPathsConfig = JSON.parse(sassLoadPathsString); } else { sassLoadPathsConfig = []; } let cssResult; 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 url$1.substring(1), url.pathToFileURL("node_modules/") ); }; if (sass.compile) { cssResult = sass.compile(filename, { loadPaths: sassLoadPathsConfig, importers: [ { findFileUrl(url) { return tildeImporter(url); } } ] }).css; } 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 = spawnSyncWithNoColor("node", [tempFileName]); fs.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;