UNPKG

apostrophe

Version:

The Apostrophe Content Management System.

952 lines (857 loc) • 34.4 kB
// this should be loaded first const opentelemetry = require('./lib/opentelemetry'); const path = require('path'); const url = require('url'); const _ = require('lodash'); const argv = require('boring')({ end: true }); const fs = require('fs'); const { stripIndent } = require('common-tags'); const cluster = require('cluster'); const { cpus } = require('os'); const process = require('process'); const npmResolve = require('resolve'); const glob = require('./lib/glob.js'); const moogRequire = require('./lib/moog-require'); let defaults = require('./defaults.js'); // ## Top-level options // // `cluster` // // If set to `true`, Apostrophe will spawn as many processes as // there are CPU cores on the server, or a minimum of 2, and balance // incoming connections among them. This ensures availability while one // process is restarting due to a crash and also increases scalability if // the server has multiple CPU cores. // // If set to an object with a `processes` property, that many // processes are started. If `processes` is 0 or a negative number, // it is added to the number of CPU cores reported by the server. // Notably, `-1` can be a good way to reserve one CPU core for MongoDB // in a single-server deployment. // // However when in cluster mode no fewer than 2 processes will be // started as there is no availability benefit without at least 2. // // If a child process exits with a failure status code it will be // restarted. However, if it exits in less than 20 seconds after // startup there will be a 20 second delay to avoid flooding logs // and pinning the CPU. // // Alternatively the `APOS_CLUSTER_PROCESSES` environment variable // can be set to a number, which will effectively set the cluster // option to `cluster: { processes: n }`. // // `openTelemetryProvider` // // If set, Apostrophe will register it as a global OpenTelemetry tracer // provider. The expected value is an object, an instance of TracerProvider. If // the Node SDK is used in the application instead of manual configuration, the // provider instance is only available as a private property: // `sdkInstance._tracerProvider`. An issue can be opened to discuss the exposure // of a public getter with the OpenTelemetry developers. // // `beforeExit` // // If set, Apostrophe will invoke it (await) before invoking process.exit. // `beforeExit` may be an async function, will be awaited, and takes no // arguments. // // `pnpm` // A boolean to force on or off the pnpm related build routines. If not set, // an automated check will be performed to determine if pnpm is in use. We offer // an option, because automated check is not 100% reliable. Monorepo tools are // often hiding package management specifics (lock files, node_module // structure, etc.) in a centralized store. // // ## Awaiting the Apostrophe function // // The apos function is async, but in typical cases you do not // need to await it. If you simply call it, Apostrophe will // start up and listen for connections forever, or run a // task and exit, as appropriate. On failure, the error is // printed to stderr and the process exits. // // If you do `await` the function, then your code will continue // after apostrophe successfully begins listening for // connections. However note it will still exit on errors. // // To avoid exiting on errors, pass the `exit: false` option. // This can option also can be used to allow awaiting a command line // task, as they also normally exit on completion. // // If `options.cluster` is truthy, the function quickly resolves to // `null` in the primary process. In the child process it resolves as // documented above. // The actual entry point, a wrapper that enables the telemetry and starts the // root span module.exports = async function(options) { const telemetry = opentelemetry(options); let spanName = 'apostrophe:boot'; const guardTime = 20000; // Detect cluster options if (process.env.APOS_CLUSTER_PROCESSES) { options.cluster = { processes: parseInt(process.env.APOS_CLUSTER_PROCESSES) }; } if (options.cluster && (process.env.NODE_ENV !== 'production')) { console.log('NODE_ENV is not set to production, disabling cluster mode'); options.cluster = false; } // Execute if cluster enabled if (options.cluster && !argv._.length) { // For bc with node 14 and below we need to check both if (cluster.isPrimary || cluster.isMaster) { // Activate and return the callback return value return telemetry.startActiveSpan(`${spanName}:primary`, async (span) => { let processes = options.cluster.processes || cpus().length; if (processes <= 0) { processes = cpus().length + processes; } let capped = ''; if (processes > cpus().length) { processes = cpus().length; capped = ' (capped to number of CPU cores)'; } if (processes < 2) { processes = 2; if (capped) { capped = ' (less than 2 cores, capped to minimum of 2)'; } else { capped = ' (using minimum of 2)'; } } console.log(`Starting ${processes} cluster child processes${capped}`); for (let i = 0; i < processes; i++) { clusterFork(); } cluster.on('exit', (worker, code, signal) => { if (code !== 0) { if ((Date.now() - worker.bornAt) < guardTime) { console.error(`Worker process ${worker.process.pid} failed in ${seconds(Date.now() - worker.bornAt)}, waiting ${seconds(guardTime)} before restart`); setTimeout(() => { respawn(worker); }, guardTime); } else { respawn(worker); } } }); span.end(); if (typeof options.beforeExit === 'function') { await options.beforeExit(); } return null; }); } else { // continue as a worker operation, the pid should be recorded // by the auto instrumentation spanName += ':worker'; console.log(`Cluster worker ${process.pid} started`); } } // Create and activate the root span for the boot tracer const self = await telemetry.startActiveSpan(spanName, async (span) => { const res = await apostrophe(options, telemetry, span); span.setStatus(telemetry.api.SpanStatusCode.OK); span.end(); return res; }); return self; }; // The actual apostrophe bootstrap async function apostrophe(options, telemetry, rootSpan) { // The core is not a true moog object but it must look enough like one // to participate as an async event emitter const self = { __meta: { name: 'apostrophe' } }; // Terminates the process. Emits the `apostrophe:beforeExit` async event; // use this mechanism to invoke any pre-exit application level tasks. Any // `beforeExit` handler errors will be ignored. // Invokes and awaits `options.beforeExit` function if available, // passing as arguments the exit code and message (if any). self._exit = async function(code = 0, message) { try { if (self.emit) { await self.emit('beforeExit'); } } catch (e) { // we are at the point where errors are ignored, // if emitter is already registered, all handler errors // are already recorded by the event module instrumentation console.error('beforeExit emit error', e); } if (code !== 0) { telemetry.handleError(rootSpan, message); } else { rootSpan.setStatus({ code: telemetry.api.SpanStatusCode.OK, message }); } rootSpan.end(); if (typeof options.beforeExit === 'function') { try { await options.beforeExit(code, message); } catch (e) { console.error('beforeExit handler error', e); } } process.exit(code); }; try { const matches = process.version.match(/^v(\d+)/); const version = parseInt(matches[1]); if (version < 18) { throw new Error('Apostrophe requires at least Node.js 18.x.'); } // The core must have a reference to itself in order to use the // promise event emitter code self.apos = self; // Register the telemetry API as a pseudo module self.apos.telemetry = telemetry; Object.assign(self, require('./modules/@apostrophecms/module/lib/events.js')(self)); // Determine root module and root directory const { root, rootDir, npmRootDir, selfDir } = buildRoot(options); self.root = root; self.rootDir = rootDir; self.npmRootDir = npmRootDir; self.selfDir = selfDir; self.getNpmPath = (name) => { try { return getNpmPath(name, self.npmRootDir); } catch (e) { // Not found via npm. This does not mean it doesn't // exist as a project-level thing return null; } }; // Signals to various (build related) places that we are running a pnpm // installation. The relevant option, if set, has a higher precedence over // the automated check. self.isPnpm = options.pnpm ?? fs.existsSync(path.join(self.npmRootDir, 'pnpm-lock.yaml')); testModule(); self.options = await mergeConfiguration(options, defaults); await autodetectBundles(); acceptGlobalOptions(); // Module-based async events (self.on and self.emit of each module, // handlers are usually registered via `handlers` in the module // definition rather than `self.on`) self.eventHandlers = {}; // Destroys the Apostrophe object, freeing resources such as // HTTP server ports and database connections. Does **not** // delete any data; the persistent database and media files // remain available for the next startup. Emits the // `apostrophe:destroy` async event; use this mechanism to free your own // server-side resources that could prevent garbage // collection by the JavaScript engine, such as timers // and intervals. self.destroy = async function() { await self.emit('destroy'); }; // Returns true if Apostrophe is running as a command line task // rather than as a server self.isTask = function() { return !!self.argv._.length; }; // Returns an array of modules that are instances of the given // module name, i.e. they are of that type or they extend it. // For instance, `apos.instancesOf('@apostrophecms/piece-type')` returns // an array of active modules in your project that extend // pieces, such as `@apostrophecms/user` and // your own piece types self.instancesOf = function(name) { return _.filter(self.modules, function(apostropheModule) { return self.synth.instanceOf(apostropheModule, name); }); }; // Returns true if the object is an instance of the given // moog class name or a subclass thereof. A convenience wrapper // for `apos.synth.instanceOf` self.instanceOf = function(object, name) { return self.synth.instanceOf(object, name); }; // So the asset module can figure out what other modules // are out there and what icons they need without // actually instantiating them self.modulesToBeInstantiated = modulesToBeInstantiated; self.eventAliases = {}; self.aliasEvent('modulesReady', 'modulesRegistered'); self.aliasEvent('afterInit', 'ready'); await defineModules(); await instantiateModules(); await lintModules(); await self.emit('modulesRegistered'); // formerly modulesReady self.apos.schema.validateAllSchemas(); self.apos.schema.registerAllSchemas(); await self.apos.lock.withLock('@apostrophecms/migration:migrate', async () => { await self.apos.migration.migrate(self.argv); // Inserts the global doc in the default locale if it does not exist; // same for other singleton piece types registered by other modules for (const apostropheModule of Object.values(self.modules)) { if (self.instanceOf(apostropheModule, '@apostrophecms/piece-type') && apostropheModule.options.singletonAuto) { await apostropheModule.insertIfMissing(); } } await self.apos.page.implementParkAllInDefaultLocale(); await self.apos.doc.replicate(); // emits beforeReplicate and afterReplicate events // Replicate will have created the parked pages across locales if needed, // but we may still need to reset parked properties await self.apos.page.implementParkAllInOtherLocales(); }); await self.emit('ready'); // formerly afterInit if (self.taskRan) { await self._exit(); } else { const after = { exit: null }; await self.emit('run', self.isTask(), after); if (after.exit !== null) { await self._exit(after.exit); } } return self; } catch (e) { if (options.exit !== false) { console.error(e); await self._exit(1, e); } } // SUPPORTING FUNCTIONS BEGIN HERE // Merge configuration from defaults, data/local.js and app.js async function mergeConfiguration(options, defaults) { let config = {}; let local = {}; const localPath = options.__localPath || '/data/local.js'; const reallyLocalPath = self.rootDir + localPath; if (fs.existsSync(reallyLocalPath)) { local = await self.root.import(reallyLocalPath); } // Otherwise making a second apos instance // uses the same modified defaults object config = _.cloneDeep(options.__testDefaults || defaults); _.merge(config, options); if (typeof (local) === 'function') { if (local.length === 1) { _.merge(config, local(self)); } else if (local.length === 2) { local(self, config); } else { throw new Error('data/local.js may export an object, a function that takes apos as an argument and returns an object, OR a function that takes apos and config as objects and directly modifies config'); } } else { _.merge(config, local || {}); } return config; } async function nestedModuleSubdirs() { if (!options.nestedModuleSubdirs) { return; } const configs = glob(self.localModules + '/**/modules.js', { follow: true }); for (const config of configs) { try { _.merge(self.options.modules, await self.root.import(config)); } catch (e) { console.error(stripIndent` When nestedModuleSubdirs is active, any modules.js file beneath: ${self.localModules} must export an object containing configuration for Apostrophe modules. The file: ${config} did not parse. `); throw e; } } } async function autodetectBundles() { const apostropheModules = Object.keys(self.options.modules); for (const apostropheModuleName of apostropheModules) { const npmPath = self.getNpmPath(apostropheModuleName); if (!npmPath) { continue; } const apostropheModule = await self.root.import(npmPath); if (apostropheModule.bundle) { self.options.bundles = (self.options.bundles || []).concat(apostropheModuleName); const bundleModules = apostropheModule.bundle.modules; for (const bundleModuleName of bundleModules) { if (!apostropheModules.includes(bundleModuleName)) { const bundledModule = await self.root.import( path.resolve( path.dirname(npmPath), apostropheModule.bundle.directory, bundleModuleName, 'index.js' ) ); if (bundledModule.improve) { self.options.modules[bundleModuleName] = {}; } } } } } } function acceptGlobalOptions() { // Truly global options not specific to a module if (options.testModule) { // Test command lines have arguments not // intended as command line task arguments self.argv = { _: [] }; self.options.shortName = self.options.shortName || 'test'; } else if (options.argv) { // Allow injection of any set of command line arguments. // Useful with multiple instances self.argv = options.argv; } else { self.argv = argv; } self.shortName = self.options.shortName; if (!self.shortName) { throw 'Specify the `shortName` option and set it to the name of your project\'s repository or folder'; } self.title = self.options.title; // Environment variable override self.options.baseUrl = process.env.APOS_BASE_URL || self.options.baseUrl; self.baseUrl = self.options.baseUrl; self.prefix = self.options.prefix || ''; } // Tweak the Apostrophe environment suitably for // unit testing a separate npm module that extends // Apostrophe, like @apostrophecms/workflow. For instance, // a node_modules subdirectory with a symlink to the // module itself is created so that the module can // be found by Apostrophe during testing. Invoked // when options.testModule is true. There must be a // test/ or tests/ subdir of the module containing // a test.js file that runs under mocha via devDependencies. // If `options.testModule` is a string it will be used as a // namespace for the test module. function testModule() { if (!options.testModule) { return; } if (!options.shortName) { options.shortName = 'test'; } defaults = _.cloneDeep(defaults); _.defaults(defaults, { '@apostrophecms/express': {} }); _.defaults(defaults['@apostrophecms/express'], { port: 7900, secret: 'irrelevant' }); const m = self.root; checkTestModule(); // Allow tests to be in test/ or in tests/ const testDir = path.dirname(m.filename); const moduleDir = testDir.replace(/\/tests?$/, ''); if (testDir === moduleDir) { throw new Error('Test file must be in test/ or tests/ subdirectory of module'); } const pkgName = require(`${moduleDir}/package.json`).name; let pkgNamespace = ''; if (pkgName.includes('/')) { const parts = pkgName.split('/'); pkgNamespace = '/' + parts.slice(0, parts.length - 1).join('/'); } if (!fs.existsSync(testDir + '/node_modules')) { fs.mkdirSync(testDir + '/node_modules' + pkgNamespace, { recursive: true }); fs.symlinkSync(moduleDir, testDir + '/node_modules/' + pkgName, 'dir'); } // Makes sure we encounter mocha along the way // and throws an exception if we don't function checkTestModule() { const testFor = `node_modules${path.sep}mocha`; if (!require.main.filename.includes(testFor)) { throw new Error('mocha does not seem to be running, is this really a test?'); } } } async function defineModules() { // Set moog-require up to create our module manager objects self.localModules = self.options.modulesSubdir || self.options.__testLocalModules || (self.rootDir + '/modules'); const synth = await moogRequire({ root: self.root, bundles: [ 'apostrophe' ].concat(self.options.bundles || []), localModules: self.localModules, defaultBaseClass: '@apostrophecms/module', sections: [ 'helpers', 'handlers', 'routes', 'apiRoutes', 'restApiRoutes', 'renderRoutes', 'middleware', 'customTags', 'components', 'tasks' ], nestedModuleSubdirs: self.options.nestedModuleSubdirs, unparsedSections: [ 'queries', 'extendQueries', 'icons', 'i18n', 'webpack', 'build', 'commands', 'before' ] }); self.synth = synth; // Just like on the browser side, we can // call apos.define rather than apos.synth.define self.define = self.synth.define; self.redefine = self.synth.redefine; self.create = self.synth.create; await nestedModuleSubdirs(); for (const [ name, options ] of Object.entries(self.options.modules)) { await synth.define(name, options); } // Apostrophe prefers that any improvements to @apostrophecms/global // be applied before any project level version of @apostrophecms/global synth.applyImprovementsBeforeProjectLevel(); return synth; } // Reorder modules based on their `before` property. async function sortModules(moduleNames) { // The module names that have a `before` property const beforeModules = []; // The metadata quick access of all modules const modules = {}; // Recursion guard const recursionGuard = {}; // The sorted modules result const sorted = []; // The base module sort metadata for (const name of moduleNames) { const metadata = await self.synth.getMetadata(name); const before = Object.values(metadata.before).reverse().find(name => typeof name === 'string'); if (before) { beforeModules.push(name); } modules[name] = { before, beforeSelf: [] }; } // Loop through the modules that have a `before` property, // validate and fill the initial `beforeSelf` metadata (first pass). for (const name of beforeModules) { const m = modules[name]; const before = m.before; if (m.before === name) { throw new Error(`Module "${name}" has a 'before' property that references itself.`); } if (!modules[before]) { throw new Error(`Module "${name}" has a 'before' property that references a non-existent module: "${before}".`); } // Add the current module name to the target's beforeSelf. modules[before].beforeSelf.push(name); } // Loop through the modules that have a `before` properties // now that we have the initial metadata (second pass). // This takes care of edge cases like `before` that points to another module // that has a `before` property itself, circular `before` references, etc. // in a very predictable way. for (const name of beforeModules) { const m = modules[name]; const target = modules[m.before]; if (!target) { continue; } // Add all the modules that want to be before this one to the target's // beforeSelf. Do this recursively for every module from the beforeSelf // array that has own `beforeSelf` members. addBeforeSelfRecursive(name, m.beforeSelf, target.beforeSelf); } // Fill in the sorted array, first wins when uniquefy-ing. for (const name of moduleNames) { sorted.push(...modules[name].beforeSelf, name); } // A unique array of sorted module names. return [ ...new Set(sorted) ]; function addBeforeSelfRecursive(moduleName, beforeSelf, target) { if (beforeSelf.length === 0) { return; } if (recursionGuard[moduleName]) { return; } recursionGuard[moduleName] = true; beforeSelf.forEach((name) => { if (recursionGuard[name]) { return; } target.unshift(name); addBeforeSelfRecursive(name, modules[name].beforeSelf, target); }); } } async function instantiateModules() { self.modules = {}; const sorted = await sortModules(modulesToBeInstantiated()); for (const item of sorted) { // module registers itself in self.modules const apostropheModule = await self.synth.create(item, { apos: self }); await apostropheModule.emit('moduleReady'); } } function modulesToBeInstantiated() { return Object.keys(self.options.modules).filter(name => { const improvement = self.synth.isImprovement(name); return !(self.options.modules[name] && (improvement || self.options.modules[name].instantiate === false)); }); } async function lintModules() { const validSteps = []; for (const apostropheModule of Object.values(self.modules)) { for (const step of apostropheModule.__meta.chain) { validSteps.push(step.name); } } if (!fs.existsSync(self.localModules)) { return; } const dirs = fs.readdirSync(self.localModules); for (const dir of dirs) { if (dir.match(/^@/)) { const nsDirs = fs.readdirSync(`${self.localModules}/${dir}`); for (let nsDir of nsDirs) { nsDir = `${dir}/${nsDir}`; await testDir(nsDir); } } else { testDir(dir); } } async function testDir(name) { if (name.startsWith('.')) { return; } // Projects that have different theme modules activated at different times // are a frequent source of false positives for this warning, so ignore // seemingly unused modules with "theme" in the name if (!validSteps.includes(name)) { try { // It's a project level modules definition, skip it. if (fs.existsSync(path.resolve(self.localModules, name, 'modules.js'))) { return; } const submodule = await self.root.import(path.resolve(self.localModules, name, 'index.js')); if ( submodule && submodule.options && submodule.options.ignoreUnusedFolderWarning ) { return; } } catch (e) { // index.js might not exist, that's fine for our purposes } if (name.match(/^apostrophe-/)) { warn( 'namespace-apostrophe-modules', stripIndent` You have a ${self.localModules}/${name} folder. You are probably trying to configure an official Apostrophe module, but those are namespaced now. Your directory should be renamed ${self.localModules}/${name.replace(/^apostrophe-/, '@apostrophecms/')} If you get this warning for your own, original module, do not use the "apostrophe-" prefix. It is reserved. ` ); } else { warn('orphan-modules', `You have a ${self.localModules}/${name} folder, but that module is not activated in app.js\nand it is not a base class of any other active module. Right now that code doesn't do anything.`); } } function warn(name, message) { if (self.util) { self.util.warnDevOnce(name, message); } else { // apos.util not in play, this can be the case in our bootstrap tests if (self.argv[`ignore-${name}`]) { return; } /* eslint-disable-next-line no-console */ console.warn(message); } } } for (const [ name, apostropheModule ] of Object.entries(self.modules)) { if (name.match(/^apostrophe-/)) { self.util.warnDevOnce( 'namespace-apostrophe-modules', stripIndent` You have configured an ${name} module. You are probably trying to configure an official Apostrophe module, but those are namespaced now. Your module should be renamed ${name.replace(/^apostrophe-/, '@apostrophecms/')} If you get this warning for your own original module, do not use the "apostrophe-" prefix. It is reserved. ` ); } const moduleNameRegex = /\./; if (name.match(moduleNameRegex)) { self.util.warnDevOnce( 'module-name-periods', stripIndent` You have configured a module named ${name}. Modules names may not include periods. Please change this to avoid bugs. ` ); } if (apostropheModule.options.extends && ((typeof apostropheModule.options.extends) === 'string')) { lint(`The module ${name} contains an "extends" option. This is probably a\nmistake. In Apostrophe "extend" is used to extend other modules.`); } if ( apostropheModule.options.singletonWarningIfNot && (name !== apostropheModule.options.singletonWarningIfNot) ) { lint(`The module ${name} extends ${apostropheModule.options.singletonWarningIfNot}, which is normally\na singleton (Apostrophe creates only one instance of it). Two competing\ninstances will lead to problems. If you are adding project-level code to it,\njust use modules/${apostropheModule.options.singletonWarningIfNot}/index.js and do not use "extend".\nIf you are improving it via an npm module, use "improve" rather than "extend".\nIf neither situation applies you should probably just make a new module that does\nnot extend anything.\n\nIf you are sure you know what you are doing, you can set the\nsingletonWarningIfNot: false option for this module.`); } if (name.match(/-widget$/) && (!extending(apostropheModule)) && (!apostropheModule.options.ignoreNoExtendWarning)) { lint(`The module ${name} does not extend anything.\n\nA -widget module usually extends @apostrophecms/widget-type or another widget type.\nOr possibly you forgot to npm install something.\n\nIf you are sure you are doing the right thing, set the\nignoreNoExtendWarning option to true for this module.`); } else if (name.match(/-page$/) && (name !== '@apostrophecms/page') && (!extending(apostropheModule)) && (!apostropheModule.options.ignoreNoExtendWarning)) { lint(`The module ${name} does not extend anything.\n\nA -page module usually extends @apostrophecms/page-type or\n@apostrophecms/piece-page-type or another page type.\nOr possibly you forgot to npm install something.\n\nIf you are sure you are doing the right thing, set the\nignoreNoExtendWarning option to true for this module.`); } else if ( !extending(apostropheModule) && !hasCode(name) && !isBundle(name) && !apostropheModule.options.ignoreNoCodeWarning ) { lint(`The module ${name} does not extend anything and does not have any code.\n\nThis usually means that you:\n\n1. Forgot to "extend" another module\n2. Configured a module that comes from npm without npm installing it\n3. Simply haven't written your "index.js" yet\n\nIf you really want a module with no code, set the ignoreNoCodeWarning option\nto true for this module.`); } } function hasCode(name) { let d = self.synth.definitions[name]; if (doesWork(d)) { return true; } if (self.synth.isMy(d.__meta.name)) { // None at project level, but maybe at npm level, look there d = d.extend; } // If we got to the base class of all modules, the module // has no construct of its own if (self.synth.myToOriginal(d.__meta.name) === '@apostrophecms/module') { return false; } return doesWork(d); } function doesWork(d) { const countsAsWork = [ 'routes', 'apiRoutes', 'renderRoutes', 'renderRoutes', 'init', 'methods', 'beforeSuperClass', 'handlers', 'helpers', 'restApiRoutes', 'middleware', 'customTags', 'components', 'tasks' ]; const code = countsAsWork.find(property => d[property]); if (code) { return true; } const subdirs = [ 'ui/apos', 'ui/src', 'ui/public', 'public', 'i18n' ]; if (d.__meta.dirname && subdirs.find(dir => fs.existsSync(`${d.__meta.dirname}/${dir}`))) { // Assets that will be bundled, or localizations, instead of server code return true; } return false; } function isBundle(name) { const d = self.synth.definitions[name]; return d.bundle || (d.extend && d.extend.bundle); } function extending(apostropheModule) { // If the module extends no other module, then it will // have up to four entries in its inheritance chain: // project level self, npm level self, `apostrophe-modules` // project-level and `apostrophe-modules` npm level. return apostropheModule.__meta.chain.length > 4; } function lint(s) { self.util.warnDev(stripIndent` It looks like you may have made a mistake in your code:\n${s} `); } } }; const abstractClasses = [ '@apostrophecms/module', '@apostrophecms/widget-type', '@apostrophecms/page-type', '@apostrophecms/piece-type', '@apostrophecms/piece-page-type', '@apostrophecms/doc-type' ]; module.exports.bundle = { modules: abstractClasses.concat(_.keys(defaults.modules)), directory: 'modules' }; function seconds(msec) { return (Math.round(msec / 100) / 10) + ' seconds'; } function clusterFork() { const worker = cluster.fork(); worker.bornAt = Date.now(); } function respawn(worker) { console.error(`Respawning worker process ${worker.process.pid}`); clusterFork(); } module.exports.buildRoot = buildRoot; function buildRoot(options) { const root = getRoot(options); const rootDir = options.rootDir || path.dirname(root.filename); const npmRootDir = options.npmRootDir || rootDir; const selfDir = __dirname; return { root, rootDir, npmRootDir, selfDir }; } function getRoot(options) { const root = options.root; if (root?.filename && root?.require) { return { filename: root.filename, import: async (id) => root.require(id), require: (id) => root.require(id) }; } if (root?.url) { // Apostrophe was started from an ESM project const filename = url.fileURLToPath(root.url); const dynamicImport = async (id) => { const { default: defaultExport, ...rest } = await import(id); return defaultExport || rest; }; return { filename, import: dynamicImport, require: (id) => { console.warn(`self.apos.root.require is now async, please verify that you await the promise (${id})`); return dynamicImport(id); } }; } // Legacy commonjs logic function getLegacyRoot() { let _module = module; let m = _module; while (m.parent && m.parent.filename) { // The test file is the root as far as we are concerned, // not mocha itself if (m.parent.filename.match(/\/node_modules\/mocha\//)) { return m; } m = m.parent; _module = m; } return _module; } const legacyRoot = getLegacyRoot(); return { filename: legacyRoot.filename, import: async (id) => legacyRoot.require(id), require: (id) => legacyRoot.require(id) }; }; module.exports.getNpmPath = getNpmPath; function getNpmPath(name, baseDir) { return npmResolve.sync(name, { basedir: path.resolve(baseDir) }); }