Migrate latest to ts

This commit is contained in:
Alexandre Bodin 2023-09-07 01:12:44 +02:00
parent c779954631
commit ddaa9e6775
18 changed files with 328 additions and 1387 deletions

View File

@ -0,0 +1,14 @@
import type { Utils } from '@strapi/strapi';
const expectExit = async (code: number, fn: Utils.Function.Any) => {
const exit = jest.spyOn(process, 'exit').mockImplementation((number) => {
throw new Error(`process.exit: ${number}`);
});
await expect(async () => {
await fn();
}).rejects.toThrow('process.exit');
expect(exit).toHaveBeenCalledWith(code);
exit.mockRestore();
};
export { expectExit };

View File

@ -1,4 +0,0 @@
{
"name": "testing",
"version": "0.0.0"
}

View File

@ -1,509 +0,0 @@
'use strict';
const fs = require('fs/promises');
const path = require('path');
const {
loadPkg,
validatePkg,
validateExportsOrdering,
parseExports,
getExportExtensionMap,
} = require('../pkg');
const loggerMock = {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
};
describe('pkg', () => {
const tmpfolder = path.resolve(__dirname, '.tmp');
afterEach(() => {
jest.resetAllMocks();
});
describe('loadPkg', () => {
beforeEach(async () => {
await fs.mkdir(tmpfolder);
await fs.copyFile(
path.resolve(__dirname, 'fixtures', 'test.pkg.json'),
path.resolve(tmpfolder, 'package.json')
);
});
afterEach(async () => {
await fs.rm(tmpfolder, { recursive: true });
});
it('should succesfully load the package.json closest to the cwd provided & call the debug logger', async () => {
const pkg = await loadPkg({ cwd: tmpfolder, logger: loggerMock });
expect(pkg).toMatchInlineSnapshot(`
{
"name": "testing",
"version": "0.0.0",
}
`);
expect(loggerMock.debug).toHaveBeenCalled();
});
it('should throw an error if it cannot find a package.json', async () => {
await expect(
loadPkg({ cwd: '/', logger: loggerMock })
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Could not find a package.json in the current directory"`
);
});
});
describe('validatePkg', () => {
it("should return the validated package.json if it's valid", async () => {
const pkg = {
name: 'testing',
version: '0.0.0',
};
const validatedPkg = await validatePkg({ pkg });
expect(validatedPkg).toMatchInlineSnapshot(`
{
"name": "testing",
"version": "0.0.0",
}
`);
});
it('should fail if a required field is missing and call the error logger with the correct message', async () => {
expect(() =>
validatePkg({
pkg: {
version: '0.0.0',
},
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`"'name' in 'package.json' is required as type 'string'"`
);
expect(() =>
validatePkg({
pkg: {
name: 'testing',
},
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`"'version' in 'package.json' is required as type 'string'"`
);
});
it('should fail if a required field does not match the correct type and call the error logger with the correct message', async () => {
expect(() =>
validatePkg({
pkg: {
name: 'testing',
version: 0,
},
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`"'version' in 'package.json' must be of type 'string' (recieved 'number')"`
);
});
it("should fail if the regex for a field doesn't match and call the error logger with the correct message", async () => {
expect(() =>
validatePkg({
pkg: {
name: 'testing',
version: '0.0.0',
exports: {
apple: './apple.xyzx',
},
},
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`"'exports.apple' in 'package.json' must be of type '/^\\.\\/.*\\.json$/' (recieved the value './apple.xyzx')"`
);
expect(() =>
validatePkg({
pkg: {
name: 'testing',
version: '0.0.0',
type: 'something',
},
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`"'type' in 'package.json' must be of type '/(commonjs|module)/' (recieved the value 'something')"`
);
});
it('should fail if the exports object does not match expectations', async () => {
expect(() =>
validatePkg({
pkg: {
name: 'testing',
version: '0.0.0',
exports: 'hello',
},
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`"'exports' in 'package.json' must be of type 'object' (recieved 'string')"`
);
expect(() =>
validatePkg({
pkg: {
name: 'testing',
version: '0.0.0',
exports: {
'./package.json': './package.json',
'./admin': {
import: './admin/index.js',
something: 'xyz',
},
},
},
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`"'exports["./admin"]' in 'package.json' contains the unknown key something, for compatability only the following keys are allowed: ['types', 'source', 'import', 'require', 'default']"`
);
});
});
describe('validateExportsOrdering', () => {
it('should throw if there are no exports at all and log that error', async () => {
const pkg = {
name: 'testing',
version: '0.0.0',
};
await expect(
validateExportsOrdering({ pkg, logger: loggerMock })
).rejects.toThrowErrorMatchingInlineSnapshot(
`"'package.json' must contain a 'main' and 'module' property"`
);
});
it("should return the package if there is at least a 'main' or 'module' property", async () => {
const pkg = {
name: 'testing',
version: '0.0.0',
main: './index.js',
};
const validatedPkg = await validateExportsOrdering({ pkg, logger: loggerMock });
expect(validatedPkg).toMatchInlineSnapshot(`
{
"main": "./index.js",
"name": "testing",
"version": "0.0.0",
}
`);
});
it('should return the package if there is an exports property with a valid structure', async () => {
const pkg = {
name: 'testing',
version: '0.0.0',
exports: {
'./package.json': './package.json',
'./admin': {
types: './admin/index.d.ts',
import: './admin/index.js',
require: './admin/index.cjs',
default: './admin/index.js',
},
},
};
const validatedPkg = await validateExportsOrdering({ pkg, logger: loggerMock });
expect(validatedPkg).toMatchInlineSnapshot(`
{
"exports": {
"./admin": {
"default": "./admin/index.js",
"import": "./admin/index.js",
"require": "./admin/index.cjs",
"types": "./admin/index.d.ts",
},
"./package.json": "./package.json",
},
"name": "testing",
"version": "0.0.0",
}
`);
});
it('should throw if the types property is not the first in an export object', async () => {
const pkg = {
name: 'testing',
version: '0.0.0',
exports: {
'./admin': {
import: './admin/index.js',
types: './admin/index.d.ts',
},
},
};
await expect(
validateExportsOrdering({ pkg, logger: loggerMock })
).rejects.toThrowErrorMatchingInlineSnapshot(
`"exports["./admin"]: the 'types' property should be the first property"`
);
});
it('should log a warning if the require property comes before the import property in an export object', async () => {
const pkg = {
name: 'testing',
version: '0.0.0',
exports: {
'./admin': {
require: './admin/index.cjs',
import: './admin/index.js',
},
},
};
await validateExportsOrdering({ pkg, logger: loggerMock });
expect(loggerMock.warn.mock.calls[0]).toMatchInlineSnapshot(`
[
"exports["./admin"]: the 'import' property should come before the 'require' property",
]
`);
});
it('should log a warning if the import property comes the module property in an export object', async () => {
const pkg = {
name: 'testing',
version: '0.0.0',
exports: {
'./admin': {
import: './admin/index.js',
module: './admin/index.js',
},
},
};
await validateExportsOrdering({ pkg, logger: loggerMock });
expect(loggerMock.warn.mock.calls[0]).toMatchInlineSnapshot(`
[
"exports["./admin"]: the 'module' property should come before 'import' property",
]
`);
});
});
describe('getExportExtensionMap', () => {
it('should just return the default mapping', async () => {
expect(getExportExtensionMap()).toMatchInlineSnapshot(`
{
"commonjs": {
"cjs": ".js",
"es": ".mjs",
},
"module": {
"cjs": ".cjs",
"es": ".js",
},
}
`);
});
});
describe('parseExports', () => {
const extMap = getExportExtensionMap();
it('should by default return a root exports map using the standard export fields from the pkg.json', () => {
const pkg = {
types: './dist/index.d.ts',
main: './dist/index.js',
module: './dist/index.mjs',
source: './src/index.ts',
};
expect(parseExports({ pkg, extMap })).toMatchInlineSnapshot(`
[
{
"_path": ".",
"default": "./dist/index.mjs",
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"source": "./src/index.ts",
"types": "./dist/index.d.ts",
},
]
`);
});
it("should not return anything if the standard export fields don't exist and there is no export map", () => {
const pkg = {};
expect(parseExports({ pkg, extMap })).toMatchInlineSnapshot(`[]`);
});
it('should return a combination of the standard export fields and the export map if they both exist', () => {
const pkg = {
types: './dist/index.d.ts',
main: './dist/index.js',
module: './dist/index.mjs',
source: './src/index.ts',
exports: {
'./package.json': './package.json',
'./admin': {
types: './admin/index.d.ts',
import: './admin/index.mjs',
require: './admin/index.js',
default: './admin/index.js',
source: './src/admin/index.js',
},
},
};
expect(parseExports({ pkg, extMap })).toMatchInlineSnapshot(`
[
{
"_path": ".",
"default": "./dist/index.mjs",
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"source": "./src/index.ts",
"types": "./dist/index.d.ts",
},
{
"_exported": true,
"_path": "./admin",
"default": "./admin/index.js",
"import": "./admin/index.mjs",
"require": "./admin/index.js",
"source": "./src/admin/index.js",
"types": "./admin/index.d.ts",
},
]
`);
});
it('should return just the exports map if there are no standard export fields and the export map exists', () => {
const pkg = {
exports: {
'./package.json': './package.json',
'./admin': {
types: './admin/index.d.ts',
import: './admin/index.mjs',
require: './admin/index.js',
default: './admin/index.js',
source: './src/admin/index.js',
},
},
};
expect(parseExports({ pkg, extMap })).toMatchInlineSnapshot(`
[
{
"_exported": true,
"_path": "./admin",
"default": "./admin/index.js",
"import": "./admin/index.mjs",
"require": "./admin/index.js",
"source": "./src/admin/index.js",
"types": "./admin/index.d.ts",
},
]
`);
});
it('should throw an error if you try to use an exports map without supplying an export for the package.json file', () => {
const pkg = {
exports: {
'./admin': {
types: './admin/index.d.ts',
import: './admin/index.mjs',
require: './admin/index.js',
default: './admin/index.js',
source: './src/admin/index.js',
},
},
};
expect(() => parseExports({ pkg, extMap })).toThrowErrorMatchingInlineSnapshot(`
"
- package.json: \`exports["./package.json"] must be declared."
`);
});
it('should throw an error if the pkg.json type is undefined and you try to export like a module', () => {
const pkg = {
exports: {
'./package.json': './package.json',
'./admin': {
types: './admin/index.d.ts',
import: './admin/index.js',
require: './admin/index.cjs',
default: './admin/index.cjs',
source: './src/admin/index.js',
},
},
};
expect(() => parseExports({ pkg, extMap, type: 'module' }))
.toThrowErrorMatchingInlineSnapshot(`
"
- package.json with \`type: "undefined"\` - \`exports["./admin"].require\` must end with ".js"
- package.json with \`type: "undefined"\` - \`exports["./admin"].import\` must end with ".mjs""
`);
});
it('should throw an error if the pkg.json type is commonjs and you try to export like a module', () => {
const pkg = {
type: 'commonjs',
exports: {
'./package.json': './package.json',
'./admin': {
types: './admin/index.d.ts',
import: './admin/index.js',
require: './admin/index.cjs',
default: './admin/index.cjs',
source: './src/admin/index.js',
},
},
};
expect(() => parseExports({ pkg, extMap, type: 'module' }))
.toThrowErrorMatchingInlineSnapshot(`
"
- package.json with \`type: "commonjs"\` - \`exports["./admin"].require\` must end with ".js"
- package.json with \`type: "commonjs"\` - \`exports["./admin"].import\` must end with ".mjs""
`);
});
it('should throw an error if the pkg.json type is module and you try to export like a commonjs', () => {
const pkg = {
type: 'module',
exports: {
'./package.json': './package.json',
'./admin': {
types: './admin/index.d.ts',
import: './admin/index.mjs',
require: './admin/index.js',
default: './admin/index.js',
source: './src/admin/index.js',
},
},
};
expect(() => parseExports({ pkg, extMap, type: 'module' }))
.toThrowErrorMatchingInlineSnapshot(`
"
- package.json with \`type: "module"\` - \`exports["./admin"].require\` must end with ".cjs"
- package.json with \`type: "module"\` - \`exports["./admin"].import\` must end with ".js""
`);
});
});
});

View File

@ -1,68 +0,0 @@
'use strict';
const { Command } = require('commander');
const strapiCommands = {
'admin/create-user': require('./actions/admin/create-user/command'),
'admin/reset-user-password': require('./actions/admin/reset-user-password/command'),
build: require('./actions/build-command/command'), // in 'build-command' to avoid problems with 'build' being commonly ignored
'components/list': require('./actions/components/list/command'),
'configuration/dump': require('./actions/configuration/dump/command'),
'configuration/restore': require('./actions/configuration/restore/command'),
console: require('./actions/console/command'),
'content-types/list': require('./actions/content-types/list/command'),
'controllers/list': require('./actions/controllers/list/command'),
develop: require('./actions/develop/command'),
export: require('./actions/export/command'),
generate: require('./actions/generate/command'),
'hooks/list': require('./actions/hooks/list/command'),
import: require('./actions/import/command'),
install: require('./actions/install/command'),
'middlewares/list': require('./actions/middlewares/list/command'),
new: require('./actions/new/command'),
'plugin/build': require('./actions/plugin/build-command/command'),
'policies/list': require('./actions/policies/list/command'),
report: require('./actions/report/command'),
'routes/list': require('./actions/routes/list/command'),
'services/list': require('./actions/services/list/command'),
start: require('./actions/start/command'),
'telemetry/disable': require('./actions/telemetry/disable/command'),
'telemetry/enable': require('./actions/telemetry/enable/command'),
'templates/generate': require('./actions/templates/generate/command'),
transfer: require('./actions/transfer/command'),
'ts/generate-types': require('./actions/ts/generate-types/command'),
uninstall: require('./actions/uninstall/command'),
version: require('./actions/version/command'),
'watch-admin': require('./actions/watch-admin/command'),
};
const buildStrapiCommand = (argv, command = new Command()) => {
// Initial program setup
command.storeOptionsAsProperties(false).allowUnknownOption(true);
// Help command
command.helpOption('-h, --help', 'Display help for command');
command.addHelpCommand('help [command]', 'Display help for command');
// Load all commands
Object.keys(strapiCommands).forEach((name) => {
try {
// Add this command to the Commander command object
strapiCommands[name]({ command, argv });
} catch (e) {
console.error(`Failed to load command ${name}`, e);
}
});
return command;
};
const runStrapiCommand = async (argv = process.argv, command = new Command()) => {
await buildStrapiCommand(argv, command).parseAsync(argv);
};
module.exports = {
runStrapiCommand,
buildStrapiCommand,
strapiCommands,
};

View File

@ -1,453 +0,0 @@
import type { Database } from '@strapi/database';
import type { Comomn, EntityService, Shared } from '@strapi/strapi';
// TODO move custom fields types to a separate file
interface CustomFieldServerOptions {
/**
* The name of the custom field
*/
name: string;
/**
* The name of the plugin creating the custom field
*/
pluginId?: string;
/**
* The existing Strapi data type the custom field uses
*/
type: string;
/**
* Settings for the input size in the Admin UI
*/
inputSize?: {
default: 4 | 6 | 8 | 12;
isResizable: boolean;
};
}
interface CustomFields {
register: (customFields: CustomFieldServerOptions[] | CustomFieldServerOptions) => void;
}
/**
* The Strapi interface implemented by the main Strapi class.
*/
export interface Strapi {
/**
* Getter for the Strapi enterprise edition configuration
*/
readonly EE: any;
/**
* Getter for the Strapi configuration container
*/
readonly config: any;
/**
* Getter for the Strapi admin container
*/
readonly admin: any;
/**
* Getter for the Strapi auth container
*/
readonly auth: any;
/**
* Getter for the Strapi content API container
*/
readonly contentAPI: any;
/**
* Getter for the Strapi sanitizers container
*/
readonly sanitizers: any;
/**
* Getter for the Strapi validators container
*/
readonly validators: any;
/**
* Getter for the Strapi services container
*
* It returns all the registered services
*/
readonly services: Shared.Services;
/**
* Find a service using its unique identifier
*/
service<TService extends Common.Service = Common.Service>(uid: string): TService | undefined;
/**
* Getter for the Strapi controllers container
*
* It returns all the registered controllers
*/
readonly controllers: Shared.Controllers;
/**
* Find a controller using its unique identifier
*/
controller<TContentTypeUID extends Common.UID.Controller>(
uid: TContentTypeUID
): Shared.Controllers[TContentTypeUID];
/**
* Getter for the Strapi content types container
*
* It returns all the registered content types
*/
readonly contentTypes: any;
/**
* Find a content type using its unique identifier
*/
contentType(uid: string): any;
/**
* Getter for the Strapi component container
*
* It returns all the registered components
*/
readonly components: any;
/**
* The custom fields registry
*
* It returns the custom fields interface
*/
readonly customFields: CustomFields;
/**
* Getter for the Strapi policies container
*
* It returns all the registered policies
*/
readonly policies: any;
/**
* Find a policy using its name
*/
policy(name: string): any;
/**
* Getter for the Strapi middlewares container
*
* It returns all the registered middlewares
*/
readonly middlewares: any;
/**
* Find a middleware using its name
*/
middleware(): any;
/**
* Getter for the Strapi plugins container
*
* It returns all the registered plugins
*/
readonly plugins: any;
/**
* Find a plugin using its name
*/
plugin(name: string): any;
/**
* Getter for the Strapi hooks container
*
* It returns all the registered hooks
*/
readonly hooks: any;
/**
* Find a hook using its name
*/
hook(): any;
/**
* Getter for the Strapi APIs container
*
* It returns all the registered APIs
*/
readonly api: any;
/**
* Strapi Register Lifecycle.
*
* - Load
* - The user application
* - The plugins
* - The admin
* - The APIs
* - The components
* - The middlewares
* - The policies
* - Trigger Strapi internal bootstrap
* - Create the webhooks runner
* - Create the internal hooks registry.
* - Init the telemetry cron job and middleware
* - Run all the `register` lifecycle methods loaded by the user application or the enabled plugins
*/
register(): Promise<Strapi>;
/**
* Bootstraping phase.
*
* - Load all the content types
* - Initialize the database layer
* - Initialize the entity service
* - Run the schemas/database synchronization
* - Start the webhooks and initializing middlewares and routes
* - Run all the `bootstrap` lifecycle methods loaded by the
* user application or the enabled plugins
*/
bootstrap(): Promise<Strapi>;
/**
* Destroy phase
*
* - Destroy Strapi server
* - Run all the `destroy` lifecycle methods loaded by the
* user application or the enabled plugins
* - Cleanup the event hub
* - Gracefully stop the database
* - Stop the telemetry and cron instance
* - Cleanup the global scope by removing global.strapi
*/
destroy(): Promise<void>;
/**
* Run all functions registered for a given lifecycle. (Strapi core, user app, plugins)
*/
runLifecyclesFunctions<T extends Lifecycles[keyof Lifecycles]>(lifecycleName: T): Promise<void>;
/**
* Load the application if needed and start the server
*/
start(): Promise<void>;
/**
* Stop the server and provide a custom error and message
*/
stopWithError<TError = unknown>(error: TError, customMessage?: string): void;
/**
* Gracefully stop the server
* Call the destroy method.
*/
stop(code?: number): void;
/**
* Load the server and the user application.
* It basically triggers the register and bootstrap phases
*/
load(): Promise<Strapi>;
/**
* Restart the server and reload all the configuration.
* It re-runs all the lifecycles phases.
*
* @example
* ``` ts
* setImmediate(() => strapi.reload());
* ```
*/
reload(): () => void;
/**
* Initialize and start all the webhooks registered in the webhook store
*/
startWebhooks(): Promise<void>;
/**
* Method called when the server is fully initialized and listen to incomming requests.
* It handles tasks such as logging the startup message
* or automatically opening the administration panel.
*/
postListen(): Promise<void>;
/**
* Start listening for incomming requests
*/
listen(): Promise<void | Error>;
/**
* Opent he administration panel in a browser if the option is enabled.
* You can disable it using the admin.autoOpen configuration variable.
*
* Note: It only works in development envs.
*/
openAdmin(options: { isInitialized: boolean }): Promise<void>;
/**
* Load the admin panel server logic into the server code and initialize its configuration.
*/
loadAdmin(): Promise<void>;
/**
* Resolve every enabled plugin and load them into the application.
*/
loadPlugins(): Promise<void>;
/**
* Load every global policies in the policies container by
* reading from the `strapi.dirs.dist.policies` directory.
*/
loadPolicies(): Promise<void>;
/**
* Load every APIs and their components (config, routes, controllers, services,
* policies, middlewares, content-types) in the API container.
*/
loadAPIs(): Promise<void>;
/**
* Resolve every components in the user application and store them in `strapi.components`
*/
loadComponents(): Promise<void>;
/**
* Load every global and core middlewares in the middlewares container by
* reading from the `strapi.dirs.dist.middlewares` and internal middlewares directory.
*/
loadMiddlewares(): Promise<void>;
/**
* Load the user application in the server by reading the `src/index.js` file.
*/
loadApp(): Promise<void>;
/**
* Add internal hooks to the hooks container.
* Those hooks are meant for internal usage and might break in future releases.
*/
registerInternalHooks(): void;
/**
* Find a model (content-type, component) based on its unique identifier.
*/
getModel(uid: string): any;
/**
* Binds database queries for a specific model based on its unique identifier.
*/
query(uid: string): any;
/**
* Main Strapi container holding all the registries and providers (config, content-types, services, policies, etc...)
*/
container: any;
/**
* References to all the directories handled by Strapi
*/
dirs: StrapiDirectories;
/**
* Internal flag used to check if the application has been loaded
*/
isLoaded: boolean;
/**
* Fully reload the application
*/
reload(): void;
/**
* Holds a reference to the Koa application and the http server used by Strapi
*/
server: any;
/**
* Strapi util used to manage application files
*/
fs: any;
/**
* Event hub used to send and receive events from anywhere in the application
*/
eventHub: any;
/**
* Internal util used to log stats and messages on application Startup
*/
startupLogger: any;
/**
* Strapi logger used to send errors, warning or information messages
*/
log: any;
/**
* Used to manage cron within Strapi
*/
cron: any;
/**
* Telemetry util used to collect anonymous data on the application usage
*/
telemetry: any;
/**
* Used to access ctx from anywhere within the Strapi application
*/
requestContext: any;
/**
* Strapi DB layer instance
*/
db: Database;
/**
* Core Store accessor
*/
store: any;
/**
* Entity Validator instance
*/
entityValidator: any;
/**
* Entity Service instance
*/
entityService: EntityService.EntityService;
}
export interface Lifecycles {
REGISTER: 'register';
BOOTSTRAP: 'bootstrap';
DESTROY: 'destroy';
}
export interface StrapiDirectories {
static: {
public: string;
};
app: {
root: string;
src: string;
api: string;
components: string;
extensions: string;
policies: string;
middlewares: string;
config: string;
};
dist: {
root: string;
src: string;
api: string;
components: string;
extensions: string;
policies: string;
middlewares: string;
config: string;
};
}

View File

@ -1,27 +1,24 @@
'use strict';
const fs = require('fs/promises');
const boxen = require('boxen');
const chalk = require('chalk');
const ora = require('ora');
const { createLogger } = require('../../../utils/logger');
const { notifyExperimentalCommand } = require('../../../utils/helpers');
const {
import fs from 'fs/promises';
import boxen from 'boxen';
import chalk from 'chalk';
import ora from 'ora';
import { createLogger } from '../../../utils/logger';
import { notifyExperimentalCommand } from '../../../utils/helpers';
import {
loadPkg,
validatePkg,
validateExportsOrdering,
getExportExtensionMap,
} = require('../../../utils/pkg');
const { createBuildContext, createBuildTasks } = require('../../../builders/packages');
const { buildTaskHandlers } = require('../../../builders/tasks');
} from '../../../utils/pkg';
import { createBuildContext, createBuildTasks } from '../../../builders/packages';
import { buildTaskHandlers } from '../../../builders/tasks';
/**
*
* @param {object} args
* @param {boolean} args.force
* @param {boolean} args.debug
*/
module.exports = async ({ force, debug }) => {
interface ActionOptions {
force?: boolean;
debug?: boolean;
}
export default async ({ force, debug }: ActionOptions) => {
const logger = createLogger({ debug, timestamp: false });
try {
/**
@ -106,13 +103,10 @@ module.exports = async ({ force, debug }) => {
}
for (const task of buildTasks) {
/**
* @type {import('../../../builders/tasks').TaskHandler<any>}
*/
const handler = buildTaskHandlers[task.type];
const handler = buildTaskHandlers(task);
handler.print(ctx, task);
await handler.run(ctx, task).catch((err) => {
await handler.run(ctx, task).catch((err: NodeJS.ErrnoException) => {
if (err instanceof Error) {
logger.error(err.message);
}
@ -124,14 +118,16 @@ module.exports = async ({ force, debug }) => {
logger.error(
'There seems to be an unexpected error, try again with --debug for more information \n'
);
console.log(
chalk.red(
boxen(err.stack, {
padding: 1,
align: 'left',
})
)
);
if (err instanceof Error && err.stack) {
console.log(
chalk.red(
boxen(err.stack, {
padding: 1,
align: 'left',
})
)
);
}
process.exit(1);
}
};

View File

@ -1,13 +1,11 @@
'use strict';
const { forceOption } = require('../../../utils/commander');
const { getLocalScript } = require('../../../utils/helpers');
import { forceOption } from '../../../utils/commander';
import { getLocalScript } from '../../../utils/helpers';
import type { StrapiCommand } from '../../../types';
/**
* `$ strapi plugin:build`
* @param {import('../../../../types/core/commands').AddCommandOptions} options
*/
module.exports = ({ command }) => {
const command: StrapiCommand = ({ command }) => {
command
.command('plugin:build')
.description('Bundle your strapi plugin for publishing.')
@ -15,3 +13,5 @@ module.exports = ({ command }) => {
.option('-d, --debug', 'Enable debugging mode with verbose logs', false)
.action(getLocalScript('plugin/build-command'));
};
export default command;

View File

@ -1,35 +1,40 @@
'use strict';
import path from 'path';
import browserslistToEsbuild from 'browserslist-to-esbuild';
import { parseExports, PackageJson, ExtMap, Export } from '../utils/pkg';
import type { Logger } from '../utils/logger';
import type { ViteTask } from './tasks/vite';
import type { DtsTask } from './tasks/dts';
const path = require('path');
const browserslistToEsbuild = require('browserslist-to-esbuild');
interface BuildContextArgs {
cwd: string;
extMap: ExtMap;
logger: Logger;
pkg: PackageJson;
}
const { parseExports } = require('../utils/pkg');
export type Target = 'node' | 'web' | '*';
/**
* @typedef {Object} BuildContextArgs
* @property {string} cwd
* @property {import('../utils/pkg').ExtMap} extMap
* @property {import('../utils/logger').Logger} logger
* @property {import('../utils/pkg').PackageJson} pkg
*/
export type Targets = {
[target in Target]: string[];
};
/**
* @typedef {Object} Targets
* @property {string[]} node
* @property {string[]} web
* @property {string[]} *
*/
export interface BuildContext {
cwd: string;
exports: Record<string, Export>;
external: string[];
extMap: ExtMap;
logger: Logger;
pkg: PackageJson;
targets: Targets;
distPath: string;
}
/**
* @typedef {Object} BuildContext
* @property {string} cwd
* @property {import('../utils/pkg').Export[]} exports
* @property {string[]} external
* @property {import('../utils/pkg').ExtMap} extMap
* @property {import('../utils/logger').Logger} logger
* @property {import('../utils/pkg').PackageJson} pkg
* @property {Targets} targets
*/
export interface BuildTask {
type: 'build:js' | 'build:dts';
entries: unknown[];
format?: 'cjs' | 'es';
[key: string]: unknown;
}
const DEFAULT_BROWSERS_LIST_CONFIG = [
'last 3 major versions',
@ -43,30 +48,33 @@ const DEFAULT_BROWSERS_LIST_CONFIG = [
* @description Create a build context for the pipeline we're creating,
* this is shared among tasks so they all use the same settings for core pieces
* such as a target, distPath, externals etc.
*
* @type {(args: BuildContextArgs) => Promise<BuildContext>}
*/
const createBuildContext = async ({ cwd, extMap, logger, pkg }) => {
const createBuildContext = async ({
cwd,
extMap,
logger,
pkg,
}: BuildContextArgs): Promise<BuildContext> => {
const targets = {
'*': browserslistToEsbuild(pkg.browserslist ?? DEFAULT_BROWSERS_LIST_CONFIG),
node: browserslistToEsbuild(['node 16.0.0']),
web: ['esnext'],
};
const exports = parseExports({ extMap, pkg }).reduce((acc, x) => {
const exportsArray = parseExports({ extMap, pkg }).reduce((acc, x) => {
const { _path: exportPath, ...exportEntry } = x;
return { ...acc, [exportPath]: exportEntry };
}, {});
}, {} as Record<string, Export>);
const external = [
...(pkg.dependencies ? Object.keys(pkg.dependencies) : []),
...(pkg.peerDependencies ? Object.keys(pkg.peerDependencies) : []),
];
const outputPaths = Object.values(exports)
const outputPaths = Object.values(exportsArray)
.flatMap((exportEntry) => {
return [exportEntry.import, exportEntry.require].filter(Boolean);
return [exportEntry.import, exportEntry.require].filter((v): v is string => Boolean(v));
})
.map((p) => path.resolve(cwd, p));
@ -86,7 +94,7 @@ const createBuildContext = async ({ cwd, extMap, logger, pkg }) => {
logger,
cwd,
pkg,
exports,
exports: exportsArray,
external,
distPath,
targets,
@ -94,17 +102,11 @@ const createBuildContext = async ({ cwd, extMap, logger, pkg }) => {
};
};
/**
* @type {(containerPath: string, itemPath: string) => boolean}
*/
const pathContains = (containerPath, itemPath) => {
const pathContains = (containerPath: string, itemPath: string): boolean => {
return !path.relative(containerPath, itemPath).startsWith('..');
};
/**
* @type {(filePaths: string[]) => string | undefined}
*/
const findCommonDirPath = (filePaths) => {
const findCommonDirPath = (filePaths: string[]) => {
/**
* @type {string | undefined}
*/
@ -138,38 +140,36 @@ const findCommonDirPath = (filePaths) => {
return commonPath;
};
/**
* @typedef {import('./tasks/vite').ViteTask | import('./tasks/dts').DtsTask} BuildTask
*/
type Task = ViteTask | DtsTask;
/**
* @description Create the build tasks for the pipeline, this
* comes from the exports map we've created in the build context.
* But handles each export line uniquely with space to add more
* as the standard develops.
*
* @type {(args: BuildContext) => Promise<BuildTask[]>}
*/
const createBuildTasks = async (ctx) => {
/**
* @type {BuildTask[]}
*/
const tasks = [];
const createBuildTasks = async (ctx: BuildContext): Promise<Task[]> => {
const tasks: Task[] = [];
/**
* @type {import('./tasks/dts').DtsTask}
*/
const dtsTask = {
const dtsTask: DtsTask = {
type: 'build:dts',
entries: [],
};
/**
* @type {Record<string, import('./tasks/vite').ViteTask>}
*/
const viteTasks = {};
const viteTasks: Record<string, ViteTask> = {};
const createViteTask = (format, runtime, { output, ...restEntry }) => {
const createViteTask = (
format: 'cjs' | 'es',
runtime: Target,
{
output,
...restEntry
}: {
output: string;
path: string;
entry: string;
}
) => {
const buildId = `${format}:${output}`;
if (viteTasks[buildId]) {
@ -208,11 +208,8 @@ const createBuildTasks = async (ctx) => {
});
}
/**
* @type {keyof Target}
*/
// eslint-disable-next-line no-nested-ternary
const runtime = exp._path.includes('strapi-admin')
const runtime: Target = exp._path.includes('strapi-admin')
? 'web'
: exp._path.includes('strapi-server')
? 'node'
@ -224,7 +221,7 @@ const createBuildTasks = async (ctx) => {
*/
createViteTask('cjs', runtime, {
path: exp._path,
entry: exp.source,
entry: exp.source ?? 'src/index.ts',
output: exp.require,
});
}
@ -235,7 +232,7 @@ const createBuildTasks = async (ctx) => {
*/
createViteTask('es', runtime, {
path: exp._path,
entry: exp.source,
entry: exp.source ?? 'src/index.ts',
output: exp.import,
});
}
@ -246,7 +243,4 @@ const createBuildTasks = async (ctx) => {
return tasks;
};
module.exports = {
createBuildContext,
createBuildTasks,
};
export { createBuildContext, createBuildTasks };

View File

@ -1,18 +1,33 @@
'use strict';
import path from 'path';
import chalk from 'chalk';
import ora from 'ora';
import ts from 'typescript';
import type { Logger } from '../../utils/logger';
import type { TaskHandler } from '.';
import type { BuildTask } from '../packages';
const path = require('path');
const chalk = require('chalk');
const ora = require('ora');
const ts = require('typescript');
interface LoadTsConfigOptions {
cwd: string;
path: string;
}
class TSConfigNotFoundError extends Error {
// eslint-disable-next-line no-useless-constructor
// constructor(message, options) {
// super(message, options);
// }
get code() {
return 'TS_CONFIG_NOT_FOUND';
}
}
/**
* @description Load a tsconfig.json file and return the parsed config
*
* @internal
*
* @type {(args: { cwd: string; path: string }) => Promise<ts.ParsedCommandLine>)}
*/
const loadTsConfig = async ({ cwd, path }) => {
const loadTsConfig = async ({ cwd, path }: LoadTsConfigOptions) => {
const configPath = ts.findConfigFile(cwd, ts.sys.fileExists, path);
if (!configPath) {
@ -24,25 +39,19 @@ const loadTsConfig = async ({ cwd, path }) => {
return ts.parseJsonConfigFileContent(configFile.config, ts.sys, cwd);
};
class TSConfigNotFoundError extends Error {
// eslint-disable-next-line no-useless-constructor
constructor(message, options) {
super(message, options);
}
get code() {
return 'TS_CONFIG_NOT_FOUND';
}
interface BuildTypesOptions {
cwd: string;
logger: Logger;
outDir: string;
tsconfig: ts.ParsedCommandLine;
}
/**
* @description
*
* @internal
*
* @type {(args: { cwd: string; logger: import('../../utils/logger').Logger; outDir: string; tsconfig: ts.ParsedCommandLine }) => Promise<void>}
*/
const buildTypes = ({ cwd, logger, outDir, tsconfig }) => {
const buildTypes = ({ cwd, logger, outDir, tsconfig }: BuildTypesOptions) => {
const compilerOptions = {
...tsconfig.options,
declaration: true,
@ -102,23 +111,19 @@ const buildTypes = ({ cwd, logger, outDir, tsconfig }) => {
}
};
/**
* @typedef {Object} DtsTaskEntry
* @property {string} exportPath
* @property {string} sourcePath
* @property {string} targetPath
*/
export interface DtsTaskEntry {
importId: string;
exportPath: string;
sourcePath?: string;
targetPath: string;
}
/**
* @typedef {Object} DtsTask
* @property {"build:dts"} type
* @property {DtsTaskEntry[]} entries
*/
export interface DtsTask extends BuildTask {
type: 'build:dts';
entries: DtsTaskEntry[];
}
/**
* @type {import('./index').TaskHandler<DtsTask>}
*/
const dtsTask = {
const dtsTask: TaskHandler<DtsTask> = {
_spinner: null,
print(ctx, task) {
const entries = [
@ -146,7 +151,7 @@ const dtsTask = {
* TODO: this will not scale and assumes all project sourcePaths are `src/index.ts`
* so we can go back to the "root" of the project...
*/
cwd: path.join(ctx.cwd, entry.sourcePath, '..', '..'),
cwd: path.join(ctx.cwd, entry.sourcePath ?? 'src/index.ts', '..', '..'),
path: 'tsconfig.build.json',
}).catch((err) => {
if (err instanceof TSConfigNotFoundError) {
@ -187,13 +192,13 @@ const dtsTask = {
}
},
async success() {
this._spinner.succeed('Built type files');
this._spinner?.succeed('Built type files');
},
async fail(ctx, task, err) {
this._spinner.fail('Failed to build type files');
this._spinner?.fail('Failed to build type files');
throw err;
},
};
module.exports = { dtsTask };
export { dtsTask };

View File

@ -1,29 +0,0 @@
'use strict';
const { dtsTask } = require('./dts');
const { viteTask } = require('./vite');
/**
* @template Task
* @param {Task}
* @returns {Task}
*
* @typedef {Object} TaskHandler
* @property {(ctx: import("../packages").BuildContext, task: Task) => import('ora').Ora} print
* @property {(ctx: import("../packages").BuildContext, task: Task) => Promise<void>} run
* @property {(ctx: import("../packages").BuildContext, task: Task) => Promise<void>} success
* @property {(ctx: import("../packages").BuildContext, task: Task, err: unknown) => Promise<void>} fail
* @property {import('ora').Ora | null} _spinner
*/
/**
* @type {{ "build:js": TaskHandler<import("./vite").ViteTask>; "build:dts": TaskHandler<import("./dts").DtsTask>; }}}
*/
const buildTaskHandlers = {
'build:js': viteTask,
'build:dts': dtsTask,
};
module.exports = {
buildTaskHandlers,
};

View File

@ -0,0 +1,23 @@
import type { Ora } from 'ora';
import { dtsTask, DtsTask } from './dts';
import { viteTask, ViteTask } from './vite';
import type { BuildContext, BuildTask } from '../packages';
export interface TaskHandler<Task extends BuildTask> {
print: (ctx: BuildContext, task: Task) => void;
run: (ctx: BuildContext, task: Task) => Promise<void>;
success: (ctx: BuildContext, task: Task) => Promise<void>;
fail: (ctx: BuildContext, task: Task, err: unknown) => Promise<void>;
_spinner: Ora | null;
}
const handlers = {
'build:js': viteTask,
'build:dts': dtsTask,
};
const buildTaskHandlers = <T extends ViteTask | DtsTask>(t: T): TaskHandler<T> => {
return handlers[t.type] as TaskHandler<T>;
};
export { buildTaskHandlers };

View File

@ -1,17 +1,18 @@
'use strict';
/* eslint-disable @typescript-eslint/ban-ts-comment */
import path from 'path';
// @ts-ignore
import { build, createLogger, InlineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import ora from 'ora';
import chalk from 'chalk';
const path = require('path');
const { build, createLogger } = require('vite');
const react = require('@vitejs/plugin-react');
const ora = require('ora');
const chalk = require('chalk');
import type { TaskHandler } from '.';
import type { BuildContext, BuildTask, Target } from '../packages';
/**
* @internal
*
* @type {(ctx: import('../packages').BuildContext, task: ViteTask) => import('vite').UserConfig}
*/
const resolveViteConfig = (ctx, task) => {
const resolveViteConfig = (ctx: BuildContext, task: ViteTask): InlineConfig => {
const { cwd, distPath, targets, external, extMap, pkg } = ctx;
const { entries, format, output, runtime } = task;
const outputExt = extMap[pkg.type || 'commonjs'][format];
@ -22,10 +23,7 @@ const resolveViteConfig = (ctx, task) => {
customLogger.warnOnce = (msg) => ctx.logger.warn(msg);
customLogger.error = (msg) => ctx.logger.error(msg);
/**
* @type {import('vite').InlineConfig}
*/
const config = {
const config: InlineConfig = {
configFile: false,
root: cwd,
mode: 'production',
@ -79,25 +77,20 @@ const resolveViteConfig = (ctx, task) => {
return config;
};
/**
* @typedef {Object} ViteTaskEntry
* @property {string} path
* @property {string} entry
*/
interface ViteTaskEntry {
path: string;
entry: string;
}
/**
* @typedef {Object} ViteTask
* @property {"build:js"} type
* @property {ViteTaskEntry[]} entries
* @property {string} format
* @property {string} output
* @property {keyof import('../packages').Targets} runtime
*/
export interface ViteTask extends BuildTask {
type: 'build:js';
entries: ViteTaskEntry[];
format: 'cjs' | 'es';
output: string;
runtime: Target;
}
/**
* @type {import('./index').TaskHandler<ViteTask>}
*/
const viteTask = {
const viteTask: TaskHandler<ViteTask> = {
_spinner: null,
print(ctx, task) {
const targetLines = [
@ -130,15 +123,13 @@ const viteTask = {
}
},
async success() {
this._spinner.succeed('Built javascript files');
this._spinner?.succeed('Built javascript files');
},
async fail(ctx, task, err) {
this._spinner.fail('Failed to build javascript files');
this._spinner?.fail('Failed to build javascript files');
throw err;
},
};
module.exports = {
viteTask,
};
export { viteTask };

View File

@ -10,10 +10,8 @@ import consoleCommand from './actions/console/command';
import listContentTypes from './actions/content-types/list/command';
import listControllers from './actions/controllers/list/command';
import developCommand from './actions/develop/command';
// import exportCommand from './actions/export/command';
import generateCommand from './actions/generate/command';
import listHooks from './actions/hooks/list/command';
// import importCommand from './actions/import/command';
import installCommand from './actions/install/command';
import listMiddlewares from './actions/middlewares/list/command';
import newCommand from './actions/new/command';
@ -25,11 +23,11 @@ import startCommand from './actions/start/command';
import disableTelemetry from './actions/telemetry/disable/command';
import enableTelemetry from './actions/telemetry/enable/command';
import generateTemplates from './actions/templates/generate/command';
// import transferCommand from './actions/transfer/command';
import generateTsTypes from './actions/ts/generate-types/command';
import uninstallCommand from './actions/uninstall/command';
import versionCommand from './actions/version/command';
import watchAdminCommand from './actions/watch-admin/command';
import buildPluginCommand from './actions/plugin/build-command/command';
const strapiCommands = {
createAdminUser,
@ -59,6 +57,7 @@ const strapiCommands = {
uninstallCommand,
versionCommand,
watchAdminCommand,
buildPluginCommand,
} as const;
const buildStrapiCommand = (argv: string[], command = new Command()) => {

View File

@ -1,25 +1,14 @@
<<<<<<< HEAD:packages/core/strapi/src/commands/utils/helpers.ts
/* eslint-disable @typescript-eslint/no-var-requires */
import { yellow, red, green } from 'chalk';
import chalk, { yellow, red, green } from 'chalk';
import { has, isString, isArray } from 'lodash/fp';
import resolveCwd from 'resolve-cwd';
import { prompt } from 'inquirer';
import boxen from 'boxen';
import type { Command } from 'commander';
=======
'use strict';
/**
* Helper functions for the Strapi CLI
*/
const { yellow, red, green } = require('chalk');
const { isString, isArray } = require('lodash/fp');
const resolveCwd = require('resolve-cwd');
const { has } = require('lodash/fp');
const { prompt } = require('inquirer');
const boxen = require('boxen');
const chalk = require('chalk');
>>>>>>> main:packages/core/strapi/lib/commands/utils/helpers.js
const bytesPerKb = 1024;
const sizes = ['B ', 'KB', 'MB', 'GB', 'TB', 'PB'];
@ -133,16 +122,11 @@ const assertCwdContainsStrapiProject = (name: string) => {
try {
const pkgJSON = require(`${process.cwd()}/package.json`);
<<<<<<< HEAD:packages/core/strapi/src/commands/utils/helpers.ts
if (!has('dependencies.@strapi/strapi', pkgJSON)) {
logErrorAndExit();
=======
if (
!has('dependencies.@strapi/strapi', pkgJSON) &&
!has('devDependencies.@strapi/strapi', pkgJSON)
) {
logErrorAndExit(name);
>>>>>>> main:packages/core/strapi/lib/commands/utils/helpers.js
logErrorAndExit();
}
} catch (err) {
logErrorAndExit();
@ -176,9 +160,6 @@ const getLocalScript =
});
};
<<<<<<< HEAD:packages/core/strapi/src/commands/utils/helpers.ts
export {
=======
/**
* @description Notify users this is an experimental command and get them to approve first
* this can be opted out by passing `yes` as a property of the args object.
@ -194,7 +175,7 @@ export {
* }
* ```
*/
const notifyExperimentalCommand = async ({ force } = {}) => {
const notifyExperimentalCommand = async ({ force }: { force?: boolean } = {}) => {
console.log(
boxen(
`The ${chalk.bold(
@ -224,8 +205,7 @@ const notifyExperimentalCommand = async ({ force } = {}) => {
}
};
module.exports = {
>>>>>>> main:packages/core/strapi/lib/commands/utils/helpers.js
export {
exitWith,
assertUrlHasProtocol,
ifOptions,

View File

@ -1,26 +1,22 @@
'use strict';
import chalk from 'chalk';
const chalk = require('chalk');
export interface LoggerOptions {
silent?: boolean;
debug?: boolean;
timestamp?: boolean;
}
/**
* @typedef {{ silent?: boolean; debug?: boolean; timestamp?: boolean; }} LoggerOptions
*/
export interface Logger {
warnings: number;
errors: number;
debug: (...args: unknown[]) => void;
info: (...args: unknown[]) => void;
warn: (...args: unknown[]) => void;
error: (...args: unknown[]) => void;
log: (...args: unknown[]) => void;
}
/**
* @typedef {object} Logger
* @property {number} warnings
* @property {number} errors
* @property {(...args: any[]) => void} debug
* @property {(...args: any[]) => void} info
* @property {(...args: any[]) => void} warn
* @property {(...args: any[]) => void} error
* @property {(...args: any[]) => void} log
*/
/**
* @type {(options: LoggerOptions) => Logger}
*/
const createLogger = (options = {}) => {
const createLogger = (options: LoggerOptions = {}): Logger => {
const { silent = false, debug = false, timestamp = true } = options;
const state = { errors: 0, warning: 0 };
@ -92,6 +88,4 @@ const createLogger = (options = {}) => {
};
};
module.exports = {
createLogger,
};
export { createLogger };

View File

@ -1,9 +1,36 @@
'use strict';
import fs from 'fs/promises';
import path from 'path';
import chalk from 'chalk';
import * as yup from 'yup';
import type { Logger } from './logger';
const fs = require('fs/promises');
const path = require('path');
const chalk = require('chalk');
const yup = require('yup');
export interface PackageJson extends Omit<yup.Asserts<typeof packageJsonSchema>, 'exports'> {
type: Extensions;
exports?: {
[key: string]: Export | string;
};
browserslist?: string[];
}
export type Extensions = 'commonjs' | 'module';
export type ExtMap = {
[key in Extensions]: {
cjs: string;
es: string;
};
};
export interface Export {
types?: string;
source?: string;
require?: string;
import?: string;
default?: string;
}
export interface ExportWithMeta extends Export {
_path: string;
}
/**
* Utility functions for loading and validating package.json
@ -18,14 +45,19 @@ const yup = require('yup');
const packageJsonSchema = yup.object({
name: yup.string().required(),
version: yup.string().required(),
type: yup.string().matches(/(commonjs|module)/),
type: yup.mixed().oneOf(['commonjs', 'module']),
license: yup.string(),
bin: yup.mixed().oneOf([
yup.string(),
yup.object({
[yup.string()]: yup.string(),
}),
]),
bin: yup.lazy((value) =>
typeof value === 'object'
? yup.object(
Object.entries(value).reduce((acc, [key]) => {
acc[key] = yup.string().required();
return acc;
}, {} as Record<string, yup.SchemaOf<string>>)
)
: yup.string()
),
main: yup.string(),
module: yup.string(),
source: yup.string(),
@ -52,7 +84,7 @@ const packageJsonSchema = yup.object({
}
return acc;
}, {})
}, {} as Record<string, yup.SchemaOf<string> | yup.SchemaOf<Export>>)
: undefined
)
),
@ -64,18 +96,18 @@ const packageJsonSchema = yup.object({
engines: yup.object(),
});
/**
* @typedef {import('yup').Asserts<typeof packageJsonSchema>} PackageJson
*/
interface LoadPkgOptions {
cwd: string;
logger: Logger;
}
/**
* @description being a task to load the package.json starting from the current working directory
* using a shallow find for the package.json and `fs` to read the file. If no package.json is found,
* the process will throw with an appropriate error message.
*
* @type {(args: { cwd: string, logger: import('./logger').Logger }) => Promise<object>}
*/
const loadPkg = async ({ cwd, logger }) => {
const loadPkg = async ({ cwd, logger }: LoadPkgOptions): Promise<PackageJson> => {
const directory = path.resolve(cwd);
const pkgPath = path.join(directory, 'package.json');
@ -95,10 +127,8 @@ const loadPkg = async ({ cwd, logger }) => {
/**
* @description validate the package.json against a standardised schema using `yup`.
* If the validation fails, the process will throw with an appropriate error message.
*
* @type {(args: { pkg: object }) => Promise<PackageJson | null>}
*/
const validatePkg = async ({ pkg }) => {
const validatePkg = async ({ pkg }: { pkg: PackageJson }) => {
try {
const validatedPkg = await packageJsonSchema.validate(pkg, {
strict: true,
@ -111,14 +141,14 @@ const validatePkg = async ({ pkg }) => {
case 'required':
throw new Error(
`'${err.path}' in 'package.json' is required as type '${chalk.magenta(
yup.reach(packageJsonSchema, err.path).type
yup.reach(packageJsonSchema, err.path ?? '').type
)}'`
);
case 'matches':
throw new Error(
`'${err.path}' in 'package.json' must be of type '${chalk.magenta(
err.params.regex
)}' (recieved the value '${chalk.magenta(err.params.value)}')`
err.params?.regex
)}' (recieved the value '${chalk.magenta(err.params?.value)}')`
);
/**
* This will only be thrown if there are keys in the export map
@ -127,7 +157,7 @@ const validatePkg = async ({ pkg }) => {
case 'noUnknown':
throw new Error(
`'${err.path}' in 'package.json' contains the unknown key ${chalk.magenta(
err.params.unknown
err.params?.unknown
)}, for compatability only the following keys are allowed: ${chalk.magenta(
"['types', 'source', 'import', 'require', 'default']"
)}`
@ -135,8 +165,8 @@ const validatePkg = async ({ pkg }) => {
default:
throw new Error(
`'${err.path}' in 'package.json' must be of type '${chalk.magenta(
err.params.type
)}' (recieved '${chalk.magenta(typeof err.params.value)}')`
err.params?.type
)}' (recieved '${chalk.magenta(typeof err.params?.value)}')`
);
}
}
@ -145,15 +175,21 @@ const validatePkg = async ({ pkg }) => {
}
};
interface ValidateExportsOrderingOptions {
pkg: PackageJson;
logger: Logger;
}
/**
* @description validate the `exports` property of the package.json against a set of rules.
* If the validation fails, the process will throw with an appropriate error message. If
* there is no `exports` property we check the standard export-like properties on the root
* of the package.json.
*
* @type {(args: { pkg: object, logger: import('./logger').Logger }) => Promise<PackageJson>}
*/
const validateExportsOrdering = async ({ pkg, logger }) => {
const validateExportsOrdering = async ({
pkg,
logger,
}: ValidateExportsOrderingOptions): Promise<PackageJson> => {
if (pkg.exports) {
const exports = Object.entries(pkg.exports);
@ -195,7 +231,7 @@ const validateExportsOrdering = async ({ pkg, logger }) => {
};
/** @internal */
function assertFirst(key, arr) {
function assertFirst(key: string, arr: string[]) {
const aIdx = arr.indexOf(key);
// if not found, then we don't care
@ -207,7 +243,7 @@ function assertFirst(key, arr) {
}
/** @internal */
function assertLast(key, arr) {
function assertLast(key: string, arr: string[]) {
const aIdx = arr.indexOf(key);
// if not found, then we don't care
@ -219,7 +255,7 @@ function assertLast(key, arr) {
}
/** @internal */
function assertOrder(keyA, keyB, arr) {
function assertOrder(keyA: string, keyB: string, arr: string[]) {
const aIdx = arr.indexOf(keyA);
const bIdx = arr.indexOf(keyB);
@ -231,24 +267,10 @@ function assertOrder(keyA, keyB, arr) {
return aIdx < bIdx;
}
/**
* @typedef {Object} Extensions
* @property {string} commonjs
* @property {string} esm
*/
/**
* @typedef {Object} ExtMap
* @property {Extensions} commonjs
* @property {Extensions} esm
*/
/**
* @internal
*
* @type {ExtMap}
*/
const DEFAULT_PKG_EXT_MAP = {
const DEFAULT_PKG_EXT_MAP: ExtMap = {
// pkg.type: "commonjs"
commonjs: {
cjs: '.js',
@ -266,21 +288,24 @@ const DEFAULT_PKG_EXT_MAP = {
* We potentially might need to support legacy exports or as package
* development continues we have space to tweak this.
*
* @type {() => ExtMap}
*/
const getExportExtensionMap = () => {
const getExportExtensionMap = (): ExtMap => {
return DEFAULT_PKG_EXT_MAP;
};
interface ValidateExportsOptions {
extMap: ExtMap;
pkg: PackageJson;
}
/**
* @internal
*
* @description validate the `require` and `import` properties of a given exports maps from the package.json
* returning if any errors are found.
*
* @type {(_exports: unknown, options: {extMap: ExtMap; pkg: PackageJson}) => string[]}
*/
const validateExports = (_exports, options) => {
const validateExports = (_exports: ExportWithMeta[], options: ValidateExportsOptions): string[] => {
const { extMap, pkg } = options;
const ext = extMap[pkg.type || 'commonjs'];
@ -303,27 +328,17 @@ const validateExports = (_exports, options) => {
return errors;
};
/**
* @typedef {Object} Export
* @property {string} _path the path of the export, `.` for the root.
* @property {string=} types the path to the types file
* @property {string} source the path to the source file
* @property {string=} require the path to the commonjs require file
* @property {string=} import the path to the esm import file
* @property {string=} default the path to the default file
*/
interface ParseExportsOptions {
extMap: ExtMap;
pkg: PackageJson;
}
/**
* @description parse the exports map from the package.json into a standardised
* format that we can use to generate build tasks from.
*
* @type {(args: { extMap: ExtMap, pkg: PackageJson }) => Export[]}
*/
const parseExports = ({ extMap, pkg }) => {
/**
* @type {Export}
*/
const rootExport = {
const parseExports = ({ extMap, pkg }: ParseExportsOptions): ExportWithMeta[] => {
const rootExport: ExportWithMeta = {
_path: '.',
types: pkg.types,
source: pkg.source,
@ -332,15 +347,9 @@ const parseExports = ({ extMap, pkg }) => {
default: pkg.module || pkg.main,
};
/**
* @type {Export[]}
*/
const extraExports = [];
const extraExports: ExportWithMeta[] = [];
/**
* @type {string[]}
*/
const errors = [];
const errors: string[] = [];
if (pkg.exports) {
if (!pkg.exports['./package.json']) {
@ -394,14 +403,14 @@ const parseExports = ({ extMap, pkg }) => {
});
}
const _exports = [
const _exports: ExportWithMeta[] = [
/**
* In the case of strapi plugins, we don't have a root export because we
* ship a server side and client side package. So this can be completely omitted.
*/
Object.values(rootExport).some((exp) => exp !== rootExport._path && Boolean(exp)) && rootExport,
...extraExports,
].filter(Boolean);
].filter((v): v is ExportWithMeta => Boolean(v));
errors.push(...validateExports(_exports, { extMap, pkg }));
@ -412,10 +421,4 @@ const parseExports = ({ extMap, pkg }) => {
return _exports;
};
module.exports = {
loadPkg,
validatePkg,
validateExportsOrdering,
getExportExtensionMap,
parseExports,
};
export { loadPkg, validatePkg, validateExportsOrdering, getExportExtensionMap, parseExports };

View File

@ -2,7 +2,7 @@ import type { Attribute, Common, Utils } from '../../../../../types';
import type * as Operator from './operators';
import type * as AttributeUtils from '../attributes';
import type Params from '../index';
import type * as Params from '../index';
export { Operator };

View File

@ -1,10 +1,15 @@
{
"$schema": "http://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"module": "ESNext",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "esnext",
"moduleResolution": "bundler",
"target": "es2015",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true
}
}