jest-preview
Version:
Preview your Jest tests in a browser
376 lines (363 loc) • 15.2 kB
JavaScript
'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;