jest-preview
Version:
Preview your Jest tests in a browser
539 lines (524 loc) • 16.3 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var fs = require('fs');
var path = require('path');
var crypto = require('crypto');
var child_process = require('child_process');
var url = require('url');
var camelcase = require('camelcase');
var slash = require('slash');
var core = require('@svgr/core');
var chalk = require('chalk');
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 processFile(src, filename) {
const relativeFilenameStringified = JSON.stringify(
getRelativeFilename(filename)
);
if (filename.match(/\.svg$/)) {
const pascalCaseFilename = camelcase(path.parse(filename).name, {
pascalCase: true
});
const componentName = `Svg${pascalCaseFilename}`;
try {
const svgComponent = core.transform.sync(
src,
{
// Do not insert `import * as React from "react";`
jsxRuntime: "automatic"
},
{ componentName }
);
const babel = require("@babel/core");
const result = babel.transformSync(svgComponent, {
plugins: ["@babel/plugin-transform-react-jsx"]
});
const componentCodeWithoutExport = result.code.split("\n").slice(0, -1).join("\n");
return {
// TODO: To render actual SVG to the snapshot
code: `const React = require('react')
${componentCodeWithoutExport}
module.exports = {
__esModule: true,
default: ${relativeFilenameStringified},
ReactComponent: ${componentName}
};`
};
} catch (error) {
return {
code: `const React = require('react');
module.exports = {
__esModule: true,
default: ${relativeFilenameStringified},
ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
return {
$$typeof: Symbol.for('react.element'),
type: 'span',
ref: ref,
key: null,
props: Object.assign({}, props, {
children: ${relativeFilenameStringified}
})
};
}),
};`
};
}
}
return {
code: `module.exports = {
__esModule: true,
default: ${relativeFilenameStringified},
};`
};
}
function processFileCRA(src, filename) {
return processFile(src, filename);
}
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 materializeCssomIntoText() {
const styles = document.querySelectorAll("style");
for (let i = 0; i < styles.length; i++) {
const styleElement = styles[i];
const hadText = !!styleElement.textContent && styleElement.textContent.trim().length > 0;
if (hadText)
continue;
const sheet = styleElement.sheet;
if (!sheet)
continue;
try {
const rules = sheet.cssRules;
const out = new Array(rules.length);
for (let j = 0; j < rules.length; j++)
out[j] = rules[j].cssText;
styleElement.textContent = out.join("\n");
} catch {
}
}
}
function debug() {
materializeCssomIntoText();
if (!fs.existsSync(CACHE_FOLDER)) {
fs.mkdirSync(CACHE_FOLDER, {
recursive: true
});
}
fs.writeFileSync(
path.join(CACHE_FOLDER, "index.html"),
document.documentElement.outerHTML
);
}
function jestPreviewConfigure({
externalCss,
autoPreview = false,
publicFolder,
sassLoadPaths
} = {
autoPreview: false,
sassLoadPaths: []
}) {
if (autoPreview) {
autoRunPreview();
}
if (!fs.existsSync(CACHE_FOLDER)) {
fs.mkdirSync(CACHE_FOLDER, {
recursive: true
});
}
let sassLoadPathsConfig = [];
if (sassLoadPaths) {
sassLoadPathsConfig = sassLoadPaths.map(
(path2) => `${process.cwd()}/${path2}`
);
createCacheFolderIfNeeded();
fs.writeFileSync(
path.join(CACHE_FOLDER, SASS_LOAD_PATHS_CONFIG),
JSON.stringify(sassLoadPathsConfig)
);
}
externalCss == null ? void 0 : externalCss.forEach((cssFile) => {
console.log(
chalk.red(
"externalCss is deprecated and has no effects.\n",
"Please import css files directly in your setup file.\n",
"See the migration guide at www.jest-preview.com/blog/deprecate-externalCss"
)
);
});
if (publicFolder) {
createCacheFolderIfNeeded();
fs.writeFileSync(
path.join(CACHE_FOLDER, "cache-public.config"),
publicFolder,
{
encoding: "utf-8",
flag: "w"
}
);
}
}
function patchJestFunction(it2) {
const originalIt = it2;
const itWithPreview = (name, callback, timeout) => {
let callbackWithPreview;
if (!callback) {
callbackWithPreview = void 0;
} else {
if (callback.length > 0) {
callbackWithPreview = function(done) {
try {
return callback(done);
} catch (error) {
debug();
throw error;
}
};
} else {
callbackWithPreview = async function() {
try {
return await callback();
} catch (error) {
debug();
throw error;
}
};
}
}
return originalIt(name, callbackWithPreview, timeout);
};
return itWithPreview;
}
function autoRunPreview() {
const originalIt = it;
let itWithPreview = patchJestFunction(it);
itWithPreview.each = originalIt.each;
itWithPreview.only = patchJestFunction(originalIt.only);
itWithPreview.skip = originalIt.skip;
itWithPreview.todo = originalIt.todo;
itWithPreview.concurrent = patchJestFunction(
originalIt.concurrent
);
it = itWithPreview;
test = itWithPreview;
fit = itWithPreview.only;
}
async function configureNextJestPreview(createFinalJestConfig) {
const config = await createFinalJestConfig();
if (config.transform) {
config.transform["^.+\\.(css|scss|sass|less)$"] = "jest-preview/transforms/css";
config.transform["^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)"] = "jest-preview/transforms/file";
}
if (config.transformIgnorePatterns) {
config.transformIgnorePatterns = config.transformIgnorePatterns.filter(
(pattern) => pattern !== "^.+\\.module\\.(css|sass|scss)$"
);
}
if (config.moduleNameMapper) {
delete config.moduleNameMapper["^.+\\.module\\.(css|sass|scss)$"];
delete config.moduleNameMapper["^.+\\.(css|sass|scss)$"];
delete config.moduleNameMapper["^.+\\.(png|jpg|jpeg|gif|webp|avif|ico|bmp|svg)$"];
}
return config;
}
var index = {
debug
};
exports.configureNextJestPreview = configureNextJestPreview;
exports.debug = debug;
exports.default = index;
exports.jestPreviewConfigure = jestPreviewConfigure;
exports.processCss = processCss;
exports.processFile = processFile;
exports.processFileCRA = processFileCRA;