@dr.pogodin/react-utils
Version:
Collection of generic ReactJS components and utils
67 lines • 7.73 kB
JavaScript
/**
* Jest environment for end-to-end SSR and client-side testing. It relies on
* the standard react-utils mechanics to execute SSR of given scene, and also
* Webpack build of the code for client-side execution, it further exposes
* Jsdom environment for the client-side testing of the outcomes.
*/// BEWARE: The module is not imported into the JU module / the main assembly of
// the library, because doing so easily breaks stuff:
// 1) This module depends on Node-specific modules, which would make JU
// incompatible with JsDom if included into JU.
// 2) If this module is weakly imported from somewhere else in the lib,
// it seems to randomly break tests using it for a different reason,
// probably some sort of a require-loop, or some issues with weak
// require in that scenario.
// TODO: We need to add correct typing for environment options.
import path from"node:path";import{defaults,set}from"lodash-es";// As this environment is a part of the Jest testing utils,
// we assume development dependencies are available when it is used.
/* eslint-disable import/no-extraneous-dependencies */import register from"@babel/register/experimental-worker";import JsdomEnv from"jest-environment-jsdom";import{Volume,createFsFromVolume}from"memfs";import webpack from"webpack";/* eslint-enable import/no-extraneous-dependencies */import ssrFactory from"../../../server/renderer.js";import{setBuildInfo}from"../isomorphy/buildInfo.js";function noop(){// NOOP
}export default class E2eSsrEnv extends JsdomEnv{/**
* Loads Webpack config, and exposes it to the environment via global
* webpackConfig object.
*/loadWebpackConfig(){const optionsString=this.pragmas["webpack-config-options"];const options=optionsString?JSON.parse(optionsString):{};defaults(options,{context:this.testFolder,fs:this.global.webpackOutputFs});const factoryPath=this.pragmas["webpack-config-factory"];// eslint-disable-next-line import/no-dynamic-require, @typescript-eslint/no-require-imports
let factory=require(path.resolve(this.rootDir,factoryPath));factory="default"in factory?factory.default:factory;this.global.webpackConfig=factory(options);const fs=this.global.webpackOutputFs;let buildInfo=`${options.context}/.build-info`;if(fs.existsSync(buildInfo)){buildInfo=fs.readFileSync(buildInfo,"utf8");this.global.buildInfo=JSON.parse(buildInfo)}}/**
* Executes Webpack build.
* @return {Promise}
*/async runWebpack(){this.loadWebpackConfig();if(!this.global.webpackConfig)throw Error("Failed to load Webpack config");const compiler=webpack(this.global.webpackConfig);if(!compiler)throw Error("Failed to construct Webpack compiler");// TODO: The "as typeof compiler.outputFileSystem" piece below is
// a workaround for the Webpack regression:
// https://github.com/webpack/webpack/issues/18242
compiler.outputFileSystem=this.global.webpackOutputFs;return new Promise((done,fail)=>{compiler.run((err,stats)=>{if(err)fail(err);if(stats?.hasErrors()){// eslint-disable-next-line no-console
console.error(stats.toJson().errors);fail(Error("Webpack compilation failed"))}this.global.webpackStats=stats?.toJson();// Keeps reference to the raw Webpack stats object, which should be
// explicitly passed to the server-side renderer alongside the request,
// so that it can to pick up asset paths for different named chunks.
this.webpackStats=stats;done()})})}async runSsr(){const optionsString=this.pragmas["ssr-options"];const options=optionsString?JSON.parse(optionsString):{};// TODO: This is temporary to shortcut the logging added to SSR.
options.logger??={debug:noop,info:noop,log:noop,warn:noop};options.buildInfo??=this.global.buildInfo;let cleanup;if(options.entry){const p=path.resolve(this.testFolder,options.entry);// TODO: This sure can be replaced by a dynamic import().
// eslint-disable-next-line import/no-dynamic-require, @typescript-eslint/no-require-imports
const module=require(p);if("cleanup"in module)cleanup=module.cleanup;const exportName=options.entryExportName||"default";if(exportName in module){options.Application=module[exportName]}}const renderer=ssrFactory(this.global.webpackConfig,options);let status=200;// OK
const markup=await new Promise((done,fail)=>{void renderer(this.ssrRequest,// TODO: This will do for now, with the current implementation of
// the renderer, but it will require a rework once the renderer is
// updated to do streaming.
{cookie:noop,send:done,set:noop,status:value=>{status=value},// This is how up-to-date Webpack stats are passed to the server in
// development mode, and we use this here always, instead of having
// to pass some information via filesystem.
locals:{webpack:{devMiddleware:{stats:this.webpackStats}}}},error=>{// TODO: Strictly speaking, that error as Error casting is not all
// correct, but it works, so no need to spend time on it right now.
if(error)fail(error);else done("")})});this.global.ssrMarkup=markup;this.global.ssrOptions=options;this.global.ssrStatus=status;if(cleanup)cleanup()}constructor(config,context){const pragmas=context.docblockPragmas;const requestString=pragmas["ssr-request"];const request=requestString?JSON.parse(requestString):{};request.url??="/";request.csrfToken=noop;// This ensures the initial JsDom URL matches the value we use for SSR.
set(config.projectConfig,"testEnvironmentOptions.url",`http://localhost${request.url}`);super(config,context);this.global.dom=this.dom;this.global.webpackOutputFs=createFsFromVolume(new Volume);// Extracts necessary settings from config and context.
const{projectConfig}=config;this.rootDir=projectConfig.rootDir;this.testFolder=path.dirname(context.testPath);this.withSsr=!pragmas["no-ssr"];this.ssrRequest=request;this.pragmas=pragmas;// The usual "babel-jest" transformation setup does not apply to
// the environment code and imports from it, this workaround enables it.
const optionsString=this.pragmas["ssr-options"];const options=optionsString?JSON.parse(optionsString):{};let root;switch(options.root){case"TEST":root=this.testFolder;break;default:root=process.cwd()}register({envName:options.babelEnv,extensions:[".js",".jsx",".ts",".tsx",".svg"],root})}async setup(){await super.setup();await this.runWebpack();// NOTE: It is possible that the Webpack run above, and the SSR run below
// load different versions of the same module (CommonJS, and ES), and it may
// cause very confusing problems (e.g. see:
// https://github.com/birdofpreyru/react-utils/issues/413).
// It seems we can't reset the cache of ES modules, and Jest's module reset
// does not reset modules loaded in this enviroment module, and also only
// replacing entire cache object by and empty {} seems to help (in contrast
// to deleting all entries by their keys, as it is done within .teardown()
// method below). Thus, for now we do this as a hotfix, and we also reset
// build info to undefined, because ES module version not beeing reset
// triggers an error on the subsequent test using the environment.
// TODO: Look for a cleaner solution.
require.cache={};setBuildInfo(undefined,true);if(this.withSsr)await this.runSsr();this.global.REACT_UTILS_FORCE_CLIENT_SIDE=true}async teardown(){delete this.global.REACT_UTILS_FORCE_CLIENT_SIDE;// Resets module cache and @babel/register. Effectively this ensures that
// the next time an instance of this environment is set up, all modules are
// transformed by Babel from scratch, thus taking into account the latest
// Babel config (which may change between different environment instances,
// which does not seem to be taken into account by Babel / Node caches
// automatically).
Object.keys(require.cache).forEach(key=>{delete require.cache[key]});register.revert();await super.teardown()}}
//# sourceMappingURL=E2eSsrEnv.js.map