chore: introduce providers to simplify main class

This commit is contained in:
Alexandre Bodin 2024-03-28 00:20:20 +01:00
parent eb61511884
commit 55ae34e5dc
34 changed files with 490 additions and 372 deletions

View File

@ -531,14 +531,6 @@ TODO
TODO
:::
### `strapi.startWebhooks()`
- Returns: Promise
:::info
TODO
:::
### `strapi.reload()`
:::info

View File

@ -60,6 +60,10 @@ const containerMock = {
switch (container) {
case 'content-types':
return contentTypesContainer;
case 'webhookStore':
return {
addAllowedEvent: jest.fn(),
};
default:
return null;
}
@ -91,9 +95,6 @@ const strapiMock = {
return null;
}
},
webhookStore: {
addAllowedEvent: jest.fn(),
},
} as unknown as Core.Strapi;
const reviewWorkflowsService = reviewWorkflowsServiceFactory({ strapi: strapiMock });

View File

@ -100,7 +100,7 @@ function persistStagesJoinTables({ strapi }: { strapi: Core.Strapi }) {
const registerWebhookEvents = async ({ strapi }: { strapi: Core.Strapi }) =>
Object.entries(webhookEvents).forEach(([eventKey, event]) =>
strapi.webhookStore.addAllowedEvent(eventKey, event)
strapi.get('webhookStore').addAllowedEvent(eventKey, event)
);
export default ({ strapi }: { strapi: Core.Strapi }) => {

View File

@ -46,13 +46,13 @@ const updateWebhookValidator = webhookValidator.shape({
export default {
async listWebhooks(ctx: Context) {
const webhooks = await strapi.webhookStore.findWebhooks();
const webhooks = await strapi.get('webhookStore').findWebhooks();
ctx.send({ data: webhooks } satisfies GetWebhooks.Response);
},
async getWebhook(ctx: Context) {
const { id } = ctx.params;
const webhook = await strapi.webhookStore.findWebhook(id);
const webhook = await strapi.get('webhookStore').findWebhook(id);
if (!webhook) {
return ctx.notFound('webhook.notFound');
@ -66,9 +66,9 @@ export default {
await validateYupSchema(webhookValidator)(body);
const webhook = await strapi.webhookStore.createWebhook(body);
const webhook = await strapi.get('webhookStore').createWebhook(body);
strapi.webhookRunner.add(webhook);
strapi.get('webhookRunner').add(webhook);
ctx.created({ data: webhook } satisfies CreateWebhook.Response);
},
@ -79,13 +79,13 @@ export default {
await validateYupSchema(updateWebhookValidator)(body);
const webhook = await strapi.webhookStore.findWebhook(id);
const webhook = await strapi.get('webhookStore').findWebhook(id);
if (!webhook) {
return ctx.notFound('webhook.notFound');
}
const updatedWebhook = await strapi.webhookStore.updateWebhook(id, {
const updatedWebhook = await strapi.get('webhookStore').updateWebhook(id, {
...webhook,
...body,
});
@ -94,22 +94,22 @@ export default {
return ctx.notFound('webhook.notFound');
}
strapi.webhookRunner.update(updatedWebhook);
strapi.get('webhookRunner').update(updatedWebhook);
ctx.send({ data: updatedWebhook } satisfies UpdateWebhook.Response);
},
async deleteWebhook(ctx: Context) {
const { id } = ctx.params;
const webhook = await strapi.webhookStore.findWebhook(id);
const webhook = await strapi.get('webhookStore').findWebhook(id);
if (!webhook) {
return ctx.notFound('webhook.notFound');
}
await strapi.webhookStore.deleteWebhook(id);
await strapi.get('webhookStore').deleteWebhook(id);
strapi.webhookRunner.remove(webhook);
strapi.get('webhookRunner').remove(webhook);
ctx.body = { data: webhook } satisfies DeleteWebhook.Response;
},
@ -122,11 +122,11 @@ export default {
}
for (const id of ids) {
const webhook = await strapi.webhookStore.findWebhook(id);
const webhook = await strapi.get('webhookStore').findWebhook(id);
if (webhook) {
await strapi.webhookStore.deleteWebhook(id);
strapi.webhookRunner.remove(webhook);
await strapi.get('webhookStore').deleteWebhook(id);
strapi.get('webhookRunner').remove(webhook);
}
}
@ -136,13 +136,11 @@ export default {
async triggerWebhook(ctx: Context) {
const { id } = ctx.params;
const webhook = await strapi.webhookStore.findWebhook(id);
const webhook = await strapi.get('webhookStore').findWebhook(id);
const response = await strapi.webhookRunner.run(
webhook as Modules.WebhookStore.Webhook,
'trigger-test',
{}
);
const response = await strapi
.get('webhookRunner')
.run(webhook as Modules.WebhookStore.Webhook, 'trigger-test', {});
ctx.body = { data: response } satisfies TriggerWebhook.Response;
},

View File

@ -4,7 +4,7 @@ import history from './history';
export default async () => {
Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
strapi.webhookStore.addAllowedEvent(key, value);
strapi.get('webhookStore').addAllowedEvent(key, value);
});
getService('field-sizes').setCustomFieldInputSizes();

View File

@ -21,8 +21,11 @@ const createHistoryService = ({ strapi }: { strapi: Core.Strapi }) => {
const query = strapi.db.query(HISTORY_VERSION_UID);
const getRetentionDays = (strapi: Core.Strapi) => {
const featureConfig = strapi.ee.features.get('cms-content-history');
const licenseRetentionDays =
strapi.ee.features.get('cms-content-history')?.options.retentionDays;
typeof featureConfig === 'object' && featureConfig?.options.retentionDays;
const userRetentionDays: number = strapi.config.get('admin.history.retentionDays');
// Allow users to override the license retention days, but not to increase it

View File

@ -154,7 +154,7 @@ export const bootstrap = async ({ strapi }: { strapi: Core.Strapi }) => {
});
Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
strapi.webhookStore.addAllowedEvent(key, value);
strapi.get('webhookStore').addAllowedEvent(key, value);
});
}
};

View File

@ -60,8 +60,10 @@ const createReleaseValidationService = ({ strapi }: { strapi: Core.Strapi }) =>
},
async validatePendingReleasesLimit() {
// Use the maximum releases option if it exists, otherwise default to 3
const featureCfg = strapi.ee.features.get('cms-content-releases');
const maximumPendingReleases =
strapi.ee.features.get('cms-content-releases')?.options?.maximumReleases || 3;
(typeof featureCfg === 'object' && featureCfg?.options?.maximumReleases) || 3;
const [, pendingReleasesCount] = await strapi.db.query(RELEASE_MODEL_UID).findWithCount({
filters: {

View File

@ -3,30 +3,25 @@ import _ from 'lodash';
import { isFunction } from 'lodash/fp';
import { Logger, createLogger } from '@strapi/logger';
import { Database } from '@strapi/database';
import { hooks } from '@strapi/utils';
import type { Core, Modules, UID, Schema } from '@strapi/types';
import loadConfiguration from './configuration';
import { loadConfiguration } from './configuration';
import * as factories from './factories';
import * as utils from './utils';
import * as registries from './registries';
import * as loaders from './loaders';
import { Container } from './container';
import createStrapiFs from './services/fs';
import createEventHub from './services/event-hub';
import { createServer } from './services/server';
import createWebhookRunner, { WebhookRunner } from './services/webhook-runner';
import { webhookModel, createWebhookStore } from './services/webhook-store';
import { createCoreStore, coreStoreModel } from './services/core-store';
import { createReloader } from './services/reloader';
import { providers } from './providers';
import createEntityService from './services/entity-service';
import createQueryParamService from './services/query-params';
import createCronService from './services/cron';
import entityValidator from './services/entity-validator';
import createTelemetry from './services/metrics';
import requestContext from './services/request-context';
import createAuth from './services/auth';
import createCustomFields from './services/custom-fields';
@ -35,225 +30,115 @@ import getNumberOfDynamicZones from './services/utils/dynamic-zones';
import { FeaturesService, createFeaturesService } from './services/features';
import { createDocumentService } from './services/document-service';
// TODO: move somewhere else
import * as draftAndPublishSync from './migrations/draft-publish';
import { coreStoreModel } from './services/core-store';
import { createConfigProvider } from './services/config';
/**
* Resolve the working directories based on the instance options.
*
* Behavior:
* - `appDir` is the directory where Strapi will write every file (schemas, generated APIs, controllers or services)
* - `distDir` is the directory where Strapi will read configurations, schemas and any compiled code
*
* Default values:
* - If `appDir` is `undefined`, it'll be set to `process.cwd()`
* - If `distDir` is `undefined`, it'll be set to `appDir`
*/
const resolveWorkingDirectories = (opts: { appDir?: string; distDir?: string }) => {
const cwd = process.cwd();
class Strapi extends Container implements Core.Strapi {
app: any;
const appDir = opts.appDir ? path.resolve(cwd, opts.appDir) : cwd;
const distDir = opts.distDir ? path.resolve(cwd, opts.distDir) : appDir;
isLoaded: boolean = false;
return { app: appDir, dist: distDir };
};
internal_config: Record<string, unknown> = {};
const reloader = (strapi: Strapi) => {
const state = {
shouldReload: 0,
isWatching: true,
};
constructor(opts: StrapiOptions) {
super();
function reload() {
if (state.shouldReload > 0) {
// Reset the reloading state
state.shouldReload -= 1;
reload.isReloading = false;
return;
}
this.internal_config = loadConfiguration(opts);
if (strapi.config.get('autoReload')) {
process.send?.('reload');
this.registerInternalServices();
for (const provider of providers) {
provider.init?.(this);
}
}
Object.defineProperty(reload, 'isWatching', {
configurable: true,
enumerable: true,
set(value) {
// Special state when the reloader is disabled temporarly (see GraphQL plugin example).
if (state.isWatching === false && value === true) {
state.shouldReload += 1;
}
state.isWatching = value;
},
get() {
return state.isWatching;
},
});
get admin(): Core.Module {
return this.get('admin');
}
reload.isReloading = false;
reload.isWatching = true;
get EE(): boolean {
// @ts-expect-error: init is private
this.ee.init(this.dirs.app.root, this.log);
return utils.ee.isEE;
}
return reload;
};
get ee(): Core.Strapi['ee'] {
return utils.ee;
}
class Strapi extends Container implements Core.Strapi {
server: Modules.Server.Server;
get dirs(): Core.StrapiDirectories {
return this.config.get('dirs');
}
log: Logger;
get reload(): Core.Reloader {
return this.get('reload');
}
fs: Core.StrapiFS;
get db(): Database {
return this.get('db');
}
eventHub: Modules.EventHub.EventHub;
get requestContext(): Modules.RequestContext.RequestContext {
return this.get('requestContext');
}
startupLogger: Core.StartupLogger;
get customFields(): Modules.CustomFields.CustomFields {
return this.get('customFields');
}
cron: Modules.Cron.CronService;
webhookRunner: WebhookRunner;
webhookStore: Modules.WebhookStore.WebhookStore;
store: Modules.CoreStore.CoreStore;
entityValidator: Modules.EntityValidator.EntityValidator;
get entityValidator(): Modules.EntityValidator.EntityValidator {
return this.get('entityValidator');
}
/**
* @deprecated `strapi.entityService` will be removed in the next major version
*/
entityService: Modules.EntityService.EntityService;
get entityService(): Modules.EntityService.EntityService {
return this.get('entityService');
}
documents: Modules.Documents.Service;
get documents(): Modules.Documents.Service {
return this.get('documents');
}
telemetry: Modules.Metrics.TelemetryService;
get features(): FeaturesService {
return this.get('features');
}
requestContext: Modules.RequestContext.RequestContext;
get fetch(): Modules.Fetch.Fetch {
return this.get('fetch');
}
customFields: Modules.CustomFields.CustomFields;
get cron(): Modules.Cron.CronService {
return this.get('cron');
}
fetch: Modules.Fetch.Fetch;
get log(): Logger {
return this.get('logger');
}
dirs: Core.StrapiDirectories;
get startupLogger(): Core.StartupLogger {
return this.get('startupLogger');
}
admin?: Core.Module;
get eventHub(): Modules.EventHub.EventHub {
return this.get('eventHub');
}
isLoaded: boolean;
get fs(): Core.StrapiFS {
return this.get('fs');
}
db: Database;
get server(): Modules.Server.Server {
return this.get('server');
}
app: any;
get telemetry(): Modules.Metrics.TelemetryService {
return this.get('telemetry');
}
EE?: boolean;
reload: Core.Reloader;
features: FeaturesService;
// @ts-expect-error - Assigned in constructor
ee: Core.Strapi['ee'];
constructor(opts: StrapiOptions = {}) {
super();
utils.destroyOnSignal(this);
const rootDirs = resolveWorkingDirectories(opts);
// Load the app configuration from the dist directory
const appConfig = loadConfiguration(rootDirs, opts);
// Instantiate the Strapi container
this.add('config', registries.config(appConfig, this))
.add('content-types', registries.contentTypes())
.add('components', registries.components())
.add('services', registries.services(this))
.add('policies', registries.policies())
.add('middlewares', registries.middlewares())
.add('hooks', registries.hooks())
.add('controllers', registries.controllers(this))
.add('modules', registries.modules(this))
.add('plugins', registries.plugins(this))
.add('custom-fields', registries.customFields(this))
.add('apis', registries.apis(this))
.add('sanitizers', registries.sanitizers())
.add('validators', registries.validators())
.add('query-params', createQueryParamService(this))
.add('content-api', createContentAPI(this))
.add('auth', createAuth())
.add('models', registries.models());
// Create a mapping of every useful directory (for the app, dist and static directories)
this.dirs = utils.getDirs(rootDirs, { strapi: this });
// Strapi state management variables
this.isLoaded = false;
this.reload = reloader(this);
// Instantiate the Koa app & the HTTP server
this.server = createServer(this);
// Strapi utils instantiation
this.fs = createStrapiFs(this);
this.eventHub = createEventHub();
this.startupLogger = utils.createStartupLogger(this);
const logConfig = {
level: 'http', // Strapi defaults to level 'http'
...this.config.get('logger'), // DEPRECATED
...this.config.get('server.logger.config'),
};
this.log = createLogger(logConfig);
this.cron = createCronService();
this.telemetry = createTelemetry(this);
this.requestContext = requestContext;
this.customFields = createCustomFields(this);
this.fetch = utils.createStrapiFetch(this);
this.features = createFeaturesService(this);
this.db = new Database(
_.merge(this.config.get('database'), {
settings: {
migrations: {
dir: path.join(this.dirs.app.root, 'database/migrations'),
},
},
})
);
// init webhook runner
this.webhookRunner = createWebhookRunner({
eventHub: this.eventHub,
logger: this.log,
configuration: this.config.get('server.webhooks', {}),
fetch: this.fetch,
});
this.store = createCoreStore({ db: this.db });
this.webhookStore = createWebhookStore({ db: this.db });
this.entityValidator = entityValidator;
this.entityService = createEntityService({
strapi: this,
db: this.db,
});
this.documents = createDocumentService(this);
utils.createUpdateNotifier(this).notify();
Object.defineProperty<Strapi>(this, 'EE', {
get: () => {
utils.ee.init(this.dirs.app.root, this.log);
return utils.ee.isEE;
},
configurable: false,
});
Object.defineProperty<Strapi>(this, 'ee', {
get: () => utils.ee,
configurable: false,
});
get store(): Modules.CoreStore.CoreStore {
return this.get('coreStore');
}
get config() {
@ -358,24 +243,45 @@ class Strapi extends Container implements Core.Strapi {
}
}
async destroy() {
this.log.info('Shutting down Strapi');
await this.server.destroy();
await this.runLifecyclesFunctions(utils.LIFECYCLES.DESTROY);
this.eventHub.destroy();
await this.db?.destroy();
this.telemetry.destroy();
this.cron.destroy();
process.removeAllListeners();
// @ts-expect-error: Allow clean delete of global.strapi to allow re-instanciation
delete global.strapi;
this.log.info('Strapi has been shut down');
// TODO: split into more providers
registerInternalServices() {
// Instantiate the Strapi container
this.add('config', () => createConfigProvider(this.internal_config, this))
.add('query-params', createQueryParamService(this))
.add('content-api', createContentAPI(this))
.add('auth', createAuth())
.add('server', () => createServer(this))
.add('fs', () => createStrapiFs(this))
.add('eventHub', () => createEventHub())
.add('startupLogger', () => utils.createStartupLogger(this))
.add('logger', () => {
return createLogger({
level: 'http', // Strapi defaults to level 'http'
...this.config.get('logger'), // DEPRECATED
...this.config.get('server.logger.config'),
});
})
.add('fetch', () => utils.createStrapiFetch(this))
.add('features', () => createFeaturesService(this))
.add('requestContext', requestContext)
.add('customFields', createCustomFields(this))
.add('entityValidator', entityValidator)
.add('entityService', () => createEntityService({ strapi: this, db: this.db }))
.add('documents', () => createDocumentService(this))
.add(
'db',
() =>
new Database(
_.merge(this.config.get('database'), {
settings: {
migrations: {
dir: path.join(this.dirs.app.root, 'database/migrations'),
},
},
})
)
)
.add('reload', () => createReloader(this));
}
sendStartupTelemetry() {
@ -475,24 +381,22 @@ class Strapi extends Container implements Core.Strapi {
process.exit(exitCode);
}
registerInternalHooks() {
this.get('hooks').set('strapi::content-types.beforeSync', hooks.createAsyncParallelHook());
this.get('hooks').set('strapi::content-types.afterSync', hooks.createAsyncParallelHook());
async load() {
await this.register();
await this.bootstrap();
this.hook('strapi::content-types.beforeSync').register(draftAndPublishSync.disable);
this.hook('strapi::content-types.afterSync').register(draftAndPublishSync.enable);
this.isLoaded = true;
return this;
}
async register() {
await loaders.loadApplicationContext(this);
this.get('models').add(coreStoreModel).add(webhookModel);
this.registerInternalHooks();
this.telemetry.register();
for (const provider of providers) {
await provider.register?.(this);
}
await this.runLifecyclesFunctions(utils.LIFECYCLES.REGISTER);
// NOTE: Swap type customField for underlying data type
utils.convertCustomFieldType(this);
@ -510,13 +414,6 @@ class Strapi extends Container implements Core.Strapi {
await this.db.init({ models });
if (this.config.get('server.cron.enabled', true)) {
const cronTasks = this.config.get('server.cron.tasks', {});
this.cron.add(cronTasks);
}
this.telemetry.bootstrap();
let oldContentTypes;
if (await this.db.getSchemaConnection().hasTable(coreStoreModel.tableName)) {
oldContentTypes = await this.store.get({
@ -549,8 +446,6 @@ class Strapi extends Container implements Core.Strapi {
value: this.contentTypes,
});
await this.startWebhooks();
await this.server.initMiddlewares();
this.server.initRouting();
@ -558,41 +453,39 @@ class Strapi extends Container implements Core.Strapi {
await this.runLifecyclesFunctions(utils.LIFECYCLES.BOOTSTRAP);
this.cron.start();
for (const provider of providers) {
await provider.bootstrap?.(this);
}
return this;
}
async load() {
await this.register();
await this.bootstrap();
async destroy() {
this.log.info('Shutting down Strapi');
await this.runLifecyclesFunctions(utils.LIFECYCLES.DESTROY);
this.isLoaded = true;
return this;
}
async startWebhooks() {
const webhooks = await this.webhookStore?.findWebhooks();
if (!webhooks) {
return;
for (const provider of providers) {
await provider.destroy?.(this);
}
for (const webhook of webhooks) {
this.webhookRunner?.add(webhook);
}
await this.server.destroy();
this.eventHub.destroy();
await this.db?.destroy();
process.removeAllListeners();
// @ts-expect-error: Allow clean delete of global.strapi to allow re-instanciation
delete global.strapi;
this.log.info('Strapi has been shut down');
}
async runLifecyclesFunctions(lifecycleName: 'register' | 'bootstrap' | 'destroy') {
// plugins
await this.get('modules')[lifecycleName]();
// admin
const adminLifecycleFunction = this.admin && this.admin[lifecycleName];
if (isFunction(adminLifecycleFunction)) {
await adminLifecycleFunction({ strapi: this });
}
// user
const userLifecycleFunction = this.app && this.app[lifecycleName];
if (isFunction(userLifecycleFunction)) {
@ -621,8 +514,8 @@ class Strapi extends Container implements Core.Strapi {
}
export interface StrapiOptions {
appDir?: string;
distDir?: string;
appDir: string;
distDir: string;
autoReload?: boolean;
serveAdminPanel?: boolean;
}

View File

@ -35,6 +35,7 @@ const RESTRICTED_FILENAMES = [
'packageJsonStrapi',
'info',
'autoReload',
'dirs',
// probably mistaken/typo filenames
...Object.keys(MISTAKEN_FILENAMES),

View File

@ -1,5 +1,8 @@
import { join, resolve } from 'path';
import { get } from 'lodash/fp';
import type { Core } from '@strapi/types';
import type { StrapiOptions } from '../Strapi';
export type Options = {
app: string;
@ -7,8 +10,8 @@ export type Options = {
};
export const getDirs = (
{ app: appDir, dist: distDir }: Options,
{ strapi }: { strapi: Core.Strapi }
{ appDir, distDir }: StrapiOptions,
config: { server: Partial<Core.Config.Server> }
): Core.StrapiDirectories => ({
dist: {
root: distDir,
@ -31,6 +34,6 @@ export const getDirs = (
config: join(appDir, 'config'),
},
static: {
public: resolve(appDir, strapi.config.get('server.dirs.public')),
public: resolve(appDir, get('server.dirs.public', config)),
},
});

View File

@ -5,9 +5,12 @@ import _ from 'lodash';
import { omit } from 'lodash/fp';
import dotenv from 'dotenv';
import type { Core } from '@strapi/types';
import { getConfigUrls, getAbsoluteAdminUrl, getAbsoluteServerUrl } from './urls';
import { getConfigUrls, getAbsoluteAdminUrl, getAbsoluteServerUrl } from './urls';
import loadConfigDir from './config-loader';
import { getDirs } from './get-dirs';
import type { StrapiOptions } from '../Strapi';
dotenv.config({ path: process.env.ENV_PATH });
@ -45,9 +48,8 @@ const defaultConfig = {
} satisfies Partial<Core.Config.Api>,
};
export default (dirs: { app: string; dist: string }, initialConfig: any = {}) => {
const { app: appDir, dist: distDir } = dirs;
const { autoReload = false, serveAdminPanel = true } = initialConfig;
export const loadConfiguration = (opts: StrapiOptions) => {
const { appDir, distDir, autoReload = false, serveAdminPanel = true } = opts;
const pkgJSON = require(path.resolve(appDir, 'package.json'));
@ -83,6 +85,7 @@ export default (dirs: { app: string; dist: string }, initialConfig: any = {}) =>
_.set(config, 'admin.url', adminUrl);
_.set(config, 'admin.path', adminPath);
_.set(config, 'admin.absoluteUrl', getAbsoluteAdminUrl(config));
_.set(config, 'dirs', getDirs(opts, config));
return config;
};

View File

@ -11,7 +11,7 @@ interface EE {
enabled: boolean;
licenseInfo: {
licenseKey?: string;
features?: Array<{ name: string } | string>;
features?: Array<{ name: string; [key: string]: any } | string>;
expireAt?: string;
seats?: number;
type?: string;

View File

@ -1,15 +1,22 @@
import type { Core } from '@strapi/types';
import Strapi, { type StrapiOptions } from './Strapi';
import { destroyOnSignal, resolveWorkingDirectories, createUpdateNotifier } from './utils';
export { default as compileStrapi } from './compile';
export * as factories from './factories';
export const createStrapi = (options: StrapiOptions = {}): Core.Strapi => {
const strapi = new Strapi(options);
export const createStrapi = (options: Partial<StrapiOptions> = {}): Core.Strapi => {
const strapi = new Strapi({
...options,
...resolveWorkingDirectories(options),
});
destroyOnSignal(strapi);
createUpdateNotifier(strapi);
// TODO: deprecate and remove in next major
global.strapi = strapi as unknown as Required<Core.Strapi>;
global.strapi = strapi;
return strapi;
};

View File

@ -3,7 +3,7 @@ import type { Core, Struct } from '@strapi/types';
import { getGlobalId } from '../domain/content-type';
export default async function loadAdmin(strapi: Core.Strapi) {
strapi.admin = require('@strapi/admin/strapi-server');
// strapi.admin = require('@strapi/admin/strapi-server');
strapi.get('services').add(`admin::`, strapi.admin?.services);
strapi.get('controllers').add(`admin::`, strapi.admin?.controllers);

View File

@ -6,7 +6,6 @@ import loadMiddlewares from './middlewares';
import loadComponents from './components';
import loadPolicies from './policies';
import loadPlugins from './plugins';
import loadAdmin from './admin';
import loadSanitizers from './sanitizers';
import loadValidators from './validators';
@ -16,7 +15,6 @@ export async function loadApplicationContext(strapi: Core.Strapi) {
loadSanitizers(strapi),
loadValidators(strapi),
loadPlugins(strapi),
loadAdmin(strapi),
loadAPIs(strapi),
loadComponents(strapi),
loadMiddlewares(strapi),

View File

@ -0,0 +1,23 @@
import type { Core } from '@strapi/types';
import loadAdmin from '../loaders/admin';
export default {
init(strapi: Core.Strapi) {
strapi.add('admin', () => require('@strapi/admin/strapi-server'));
},
async register(strapi: Core.Strapi) {
await loadAdmin(strapi);
await strapi.get('admin')?.register({ strapi });
},
async bootstrap(strapi: Core.Strapi) {
await strapi.get('admin')?.bootstrap({ strapi });
},
async destroy(strapi: Core.Strapi) {
await strapi.get('admin')?.destroy({ strapi });
},
};

View File

@ -0,0 +1,10 @@
import type { Core } from '@strapi/types';
import { createCoreStore, coreStoreModel } from '../services/core-store';
export default {
init(strapi: Core.Strapi) {
strapi.get('models').add(coreStoreModel);
strapi.add('coreStore', () => createCoreStore({ db: strapi.db }));
},
};

View File

@ -0,0 +1,20 @@
import type { Core } from '@strapi/types';
import createCronService from '../services/cron';
export default {
init(strapi: Core.Strapi) {
strapi.add('cron', () => createCronService());
},
async bootstrap(strapi: Core.Strapi) {
if (strapi.config.get('server.cron.enabled', true)) {
const cronTasks = strapi.config.get('server.cron.tasks', {});
strapi.get('cron').add(cronTasks);
}
strapi.get('cron').start();
},
async destroy(strapi: Core.Strapi) {
strapi.get('cron').destroy();
},
};

View File

@ -0,0 +1,15 @@
import admin from './admin';
import coreStore from './coreStore';
import cron from './cron';
import registries from './registries';
import telemetry from './telemetry';
import webhooks from './webhooks';
type Provider = {
init?: (strapi: any) => void;
register?: (strapi: any) => Promise<void>;
bootstrap?: (strapi: any) => Promise<void>;
destroy?: (strapi: any) => Promise<void>;
};
export const providers: Provider[] = [registries, admin, coreStore, webhooks, telemetry, cron];

View File

@ -0,0 +1,38 @@
import type { Core } from '@strapi/types';
import { hooks } from '@strapi/utils';
import * as registries from '../registries';
import { loadApplicationContext } from '../loaders';
import * as draftAndPublishSync from '../migrations/draft-publish';
export default {
init(strapi: Core.Strapi) {
strapi
.add('content-types', () => registries.contentTypes())
.add('components', () => registries.components())
.add('services', () => registries.services(strapi))
.add('policies', () => registries.policies())
.add('middlewares', () => registries.middlewares())
.add('hooks', () => registries.hooks())
.add('controllers', () => registries.controllers(strapi))
.add('modules', () => registries.modules(strapi))
.add('plugins', () => registries.plugins(strapi))
.add('custom-fields', () => registries.customFields(strapi))
.add('apis', () => registries.apis(strapi))
.add('models', () => registries.models())
.add('sanitizers', registries.sanitizers())
.add('validators', registries.validators());
},
async register(strapi: Core.Strapi) {
await loadApplicationContext(strapi);
strapi.get('hooks').set('strapi::content-types.beforeSync', hooks.createAsyncParallelHook());
strapi.get('hooks').set('strapi::content-types.afterSync', hooks.createAsyncParallelHook());
strapi.hook('strapi::content-types.beforeSync').register(draftAndPublishSync.disable);
strapi.hook('strapi::content-types.afterSync').register(draftAndPublishSync.enable);
},
// async bootstrap(strapi: Core.Strapi) {
// },
// async destroy(strapi: Core.Strapi) {},
};

View File

@ -0,0 +1,18 @@
import type { Core } from '@strapi/types';
import createTelemetry from '../services/metrics';
export default {
init(strapi: Core.Strapi) {
strapi.add('telemetry', () => createTelemetry(strapi));
},
async register(strapi: Core.Strapi) {
strapi.get('telemetry').register();
},
async bootstrap(strapi: Core.Strapi) {
strapi.get('telemetry').bootstrap();
},
async destroy(strapi: Core.Strapi) {
strapi.get('telemetry').destroy();
},
};

View File

@ -0,0 +1,30 @@
import type { Core } from '@strapi/types';
import { createWebhookStore, webhookModel } from '../services/webhook-store';
import createWebhookRunner from '../services/webhook-runner';
export default {
init(strapi: Core.Strapi) {
strapi.get('models').add(webhookModel);
strapi.add('webhookStore', () => createWebhookStore({ db: strapi.db }));
strapi.add('webhookRunner', () =>
createWebhookRunner({
eventHub: strapi.eventHub,
logger: strapi.log,
configuration: strapi.config.get('server.webhooks', {}),
fetch: strapi.fetch,
})
);
},
async bootstrap(strapi: Core.Strapi) {
const webhooks = await strapi.get('webhookStore').findWebhooks();
if (!webhooks) {
return;
}
for (const webhook of webhooks) {
strapi.get('webhookRunner').add(webhook);
}
},
};

View File

@ -1,31 +1,31 @@
import configProvider from '../config';
import { createConfigProvider } from '../../services/config';
const logLevel = 'warn';
describe('config', () => {
test('returns objects for partial paths', () => {
const config = configProvider({ default: { child: 'val' } });
const config = createConfigProvider({ default: { child: 'val' } });
expect(config.get('default')).toEqual({ child: 'val' });
});
test('supports full string paths', () => {
const config = configProvider({ default: { child: 'val' } });
const config = createConfigProvider({ default: { child: 'val' } });
expect(config.get('default.child')).toEqual('val');
});
test('supports array paths', () => {
const config = configProvider({ default: { child: 'val' } });
const config = createConfigProvider({ default: { child: 'val' } });
expect(config.get(['default', 'child'])).toEqual('val');
});
test('accepts initial values', () => {
const config = configProvider({ default: 'val', foo: 'bar' });
const config = createConfigProvider({ default: 'val', foo: 'bar' });
expect(config.get('default')).toEqual('val');
expect(config.get('foo')).toEqual('bar');
});
test('accepts uid in paths', () => {
const config = configProvider({
const config = createConfigProvider({
'api::myapi': { foo: 'val' },
'plugin::myplugin': { foo: 'bar' },
});
@ -39,7 +39,7 @@ describe('config', () => {
test('`get` supports `plugin::` prefix', () => {
const consoleSpy = jest.spyOn(console, logLevel).mockImplementation(() => {});
const config = configProvider({
const config = createConfigProvider({
'plugin::myplugin': { foo: 'bar' },
});
@ -51,7 +51,7 @@ describe('config', () => {
test('`get` supports `plugin::model` in array path', () => {
const consoleSpy = jest.spyOn(console, logLevel).mockImplementation(() => {});
const config = configProvider({
const config = createConfigProvider({
'plugin::myplugin': { foo: 'bar' },
});
@ -64,7 +64,7 @@ describe('config', () => {
test('`get` supports `plugin.` prefix in string path', () => {
const consoleSpy = jest.spyOn(console, logLevel).mockImplementation(() => {});
const config = configProvider({
const config = createConfigProvider({
'plugin::myplugin': { foo: 'bar' },
});
@ -76,7 +76,7 @@ describe('config', () => {
test('`get` supports `plugin.model` prefix in array path', () => {
const consoleSpy = jest.spyOn(console, logLevel).mockImplementation(() => {});
const config = configProvider({
const config = createConfigProvider({
'plugin::myplugin': { foo: 'bar' },
});
@ -88,7 +88,7 @@ describe('config', () => {
test('`get` supports `plugin` + `model` in array path', () => {
const consoleSpy = jest.spyOn(console, logLevel).mockImplementation(() => {});
const config = configProvider({
const config = createConfigProvider({
'plugin::myplugin': { foo: 'bar' },
});
@ -99,7 +99,7 @@ describe('config', () => {
test('`set` supports `plugin.` prefix in string path', () => {
const consoleSpy = jest.spyOn(console, logLevel).mockImplementation(() => {});
const config = configProvider({
const config = createConfigProvider({
'plugin::myplugin': { foo: 'bar' },
});
config.set('plugin.myplugin.thing', 'val');
@ -112,7 +112,7 @@ describe('config', () => {
test('`set` supports `plugin.` prefix in array path', () => {
const consoleSpy = jest.spyOn(console, logLevel).mockImplementation(() => {});
const config = configProvider({
const config = createConfigProvider({
'plugin::myplugin': { foo: 'bar' },
});
config.set(['plugin.myplugin', 'thing'], 'val');
@ -125,7 +125,7 @@ describe('config', () => {
test('`has` supports `plugin.` prefix in string path', () => {
const consoleSpy = jest.spyOn(console, logLevel).mockImplementation(() => {});
const config = configProvider({
const config = createConfigProvider({
'plugin::myplugin': { foo: 'bar' },
});
@ -138,7 +138,7 @@ describe('config', () => {
test('`has` supports `plugin.` prefix in array path', () => {
const consoleSpy = jest.spyOn(console, logLevel).mockImplementation(() => {});
const config = configProvider({
const config = createConfigProvider({
'plugin::myplugin': { foo: 'bar' },
});
@ -152,7 +152,7 @@ describe('config', () => {
const consoleSpy = jest.spyOn(console, logLevel).mockImplementation(() => {});
const logSpy = jest.fn();
const config = configProvider(
const config = createConfigProvider(
{
'plugin::myplugin': { foo: 'bar' },
},
@ -166,7 +166,7 @@ describe('config', () => {
});
test('get does NOT support deprecation for other prefixes', () => {
const config = configProvider({
const config = createConfigProvider({
'api::myapi': { foo: 'bar' },
});
@ -174,7 +174,7 @@ describe('config', () => {
});
test('set does NOT support deprecation for other prefixes', () => {
const config = configProvider({
const config = createConfigProvider({
'api::myapi': { foo: 'bar' },
});

View File

@ -8,7 +8,6 @@ export { default as controllers } from './controllers';
export { default as modules } from './modules';
export { default as plugins } from './plugins';
export { default as customFields } from './custom-fields';
export { default as config } from './config';
export { default as apis } from './apis';
export { default as sanitizers } from './sanitizers';
export { default as validators } from './validators';

View File

@ -1,10 +1,19 @@
import type { Core } from '@strapi/types';
import { get, set, has, isString, isNumber, isArray, type PropertyPath } from 'lodash';
type State = {
config: Config;
};
type Config = Record<string, unknown>;
export default (initialConfig = {}, strapi?: Core.Strapi | Core.Strapi): Core.ConfigProvider => {
const _config: Config = { ...initialConfig }; // not deep clone because it would break some config
export const createConfigProvider = (
initialConfig: Record<string, unknown> = {},
strapi?: Core.Strapi | Core.Strapi
): Core.ConfigProvider => {
const state: State = {
config: { ...initialConfig }, // not deep clone because it would break some config
};
// Accessing model configs with dot (.) was deprecated between v4->v5, but to avoid a major breaking change
// we will still support certain namespaces, currently only 'plugin.'
@ -39,16 +48,16 @@ export default (initialConfig = {}, strapi?: Core.Strapi | Core.Strapi): Core.Co
};
return {
..._config, // TODO: to remove
...state.config, // TODO: to remove
get(path: PropertyPath, defaultValue?: unknown) {
return get(_config, transformDeprecatedPaths(path), defaultValue);
return get(state.config, transformDeprecatedPaths(path), defaultValue);
},
set(path: PropertyPath, val: unknown) {
set(_config, transformDeprecatedPaths(path), val);
set(state.config, transformDeprecatedPaths(path), val);
return this;
},
has(path: PropertyPath) {
return has(_config, transformDeprecatedPaths(path));
return has(state.config, transformDeprecatedPaths(path));
},
};
};

View File

@ -0,0 +1,41 @@
import type { Core } from '@strapi/types';
export const createReloader = (strapi: Core.Strapi) => {
const state = {
shouldReload: 0,
isWatching: true,
};
function reload() {
if (state.shouldReload > 0) {
// Reset the reloading state
state.shouldReload -= 1;
reload.isReloading = false;
return;
}
if (strapi.config.get('autoReload')) {
process.send?.('reload');
}
}
Object.defineProperty(reload, 'isWatching', {
configurable: true,
enumerable: true,
set(value) {
// Special state when the reloader is disabled temporarly (see GraphQL plugin example).
if (state.isWatching === false && value === true) {
state.shouldReload += 1;
}
state.isWatching = value;
},
get() {
return state.isWatching;
},
});
reload.isReloading = false;
reload.isWatching = true;
return reload;
};

View File

@ -1,6 +1,6 @@
export { openBrowser } from './open-browser';
export { isInitialized } from './is-initialized';
export { getDirs } from './get-dirs';
export { getDirs } from '../configuration/get-dirs';
export { ee } from './ee';
export { createUpdateNotifier } from './update-notifier';
export { createStrapiFetch, Fetch } from './fetch';
@ -9,3 +9,4 @@ export { createStartupLogger } from './startup-logger';
export { transformContentTypesToModels } from './transform-content-types-to-models';
export { destroyOnSignal } from './signals';
export { LIFECYCLES } from './lifecycles';
export { resolveWorkingDirectories } from './resolve-working-dirs';

View File

@ -0,0 +1,21 @@
import path from 'node:path';
/**
* Resolve the working directories based on the instance options.
*
* Behavior:
* - `appDir` is the directory where Strapi will write every file (schemas, generated APIs, controllers or services)
* - `distDir` is the directory where Strapi will read configurations, schemas and any compiled code
*
* Default values:
* - If `appDir` is `undefined`, it'll be set to `process.cwd()`
* - If `distDir` is `undefined`, it'll be set to `appDir`
*/
export const resolveWorkingDirectories = (opts: { appDir?: string; distDir?: string }) => {
const cwd = process.cwd();
const appDir = opts.appDir ? path.resolve(cwd, opts.appDir) : cwd;
const distDir = opts.distDir ? path.resolve(cwd, opts.distDir) : appDir;
return { appDir, distDir };
};

View File

@ -83,21 +83,21 @@ export const createUpdateNotifier = (strapi: Core.Strapi) => {
console.log(message);
};
return {
notify({ checkInterval = CHECK_INTERVAL, notifInterval = NOTIF_INTERVAL } = {}) {
// TODO v6: Remove this warning
if (env.bool('STRAPI_DISABLE_UPDATE_NOTIFICATION', false)) {
strapi.log.warn(
'STRAPI_DISABLE_UPDATE_NOTIFICATION is no longer supported. Instead, set logger.updates.enabled to false in your server configuration.'
);
}
function notify({ checkInterval = CHECK_INTERVAL, notifInterval = NOTIF_INTERVAL } = {}) {
// TODO v6: Remove this warning
if (env.bool('STRAPI_DISABLE_UPDATE_NOTIFICATION', false)) {
strapi.log.warn(
'STRAPI_DISABLE_UPDATE_NOTIFICATION is no longer supported. Instead, set logger.updates.enabled to false in your server configuration.'
);
}
if (!strapi.config.get('server.logger.updates.enabled') || !config) {
return;
}
if (!strapi.config.get('server.logger.updates.enabled') || !config) {
return;
}
display(notifInterval);
checkUpdate(checkInterval); // doesn't need to await
},
};
display(notifInterval);
checkUpdate(checkInterval); // doesn't need to await
}
notify();
};

View File

@ -17,8 +17,6 @@ export interface Strapi extends Container {
eventHub: Modules.EventHub.EventHub;
startupLogger: StartupLogger;
cron: Modules.Cron.CronService;
webhookRunner: Modules.WebhookRunner.WebhookRunner;
webhookStore: Modules.WebhookStore.WebhookStore;
store: Modules.CoreStore.CoreStore;
/**
* @deprecated will be removed in the next major
@ -35,7 +33,7 @@ export interface Strapi extends Container {
customFields: Modules.CustomFields.CustomFields;
fetch: Modules.Fetch.Fetch;
dirs: StrapiDirectories;
admin?: Core.Module;
admin: Core.Module;
isLoaded: boolean;
db: Database;
app: any;
@ -46,7 +44,7 @@ export interface Strapi extends Container {
features: {
isEnabled: (feature: string) => boolean;
list: () => { name: string; [key: string]: any }[];
get: (feature: string) => { name: string; [key: string]: any };
get: (feature: string) => string | { name: string; [key: string]: any } | undefined;
};
};
features: Modules.Features.FeaturesService;
@ -84,10 +82,8 @@ export interface Strapi extends Container {
listen(): Promise<void>;
stopWithError(err: unknown, customMessage?: string): never;
stop(exitCode?: number): never;
registerInternalHooks(): void;
register(): Promise<Strapi>;
bootstrap(): Promise<Strapi>;
startWebhooks(): Promise<void>;
runLifecyclesFunctions(lifecycleName: 'register' | 'bootstrap' | 'destroy'): Promise<void>;
getModel<TSchemaUID extends UID.Schema>(
uid: TSchemaUID
@ -151,14 +147,3 @@ export interface StrapiDirectories {
config: string;
};
}
export interface StrapiOptions {
appDir?: string;
distDir?: string;
autoReload?: boolean;
serveAdminPanel?: boolean;
}
export interface StrapiConstructor {
new (options?: StrapiOptions): Strapi;
}

View File

@ -13,12 +13,12 @@ export type * as UID from './uid';
declare global {
// eslint-disable-next-line vars-on-top,no-var
var strapi: Required<Strapi>;
var strapi: Strapi;
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace NodeJS {
interface Global {
strapi: Required<Strapi>;
strapi: Strapi;
}
}
}

View File

@ -18,6 +18,16 @@ describe('Upload plugin bootstrap function', () => {
const registerMany = jest.fn(() => {});
global.strapi = {
get(name: string) {
switch (name) {
case 'webhookStore':
return {
addAllowedEvent: jest.fn(),
};
default:
return null;
}
},
dirs: {
dist: { root: process.cwd() },
app: { root: process.cwd() },
@ -65,9 +75,6 @@ describe('Upload plugin bootstrap function', () => {
set: setStore,
};
},
webhookStore: {
addAllowedEvent: jest.fn(),
},
} as any;
await bootstrap({ strapi });

View File

@ -50,7 +50,7 @@ export async function bootstrap({ strapi }: { strapi: Core.Strapi }) {
const registerWebhookEvents = async () =>
Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
strapi.webhookStore.addAllowedEvent(key, value);
strapi.get('webhookStore').addAllowedEvent(key, value);
});
const registerPermissionActions = async () => {