Merge branch 'main' into main

This commit is contained in:
Luis Rodriguez 2023-04-17 11:21:57 -05:00 committed by GitHub
commit 38cf9f837d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 521 additions and 473 deletions

View File

@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: [14, 16, 18]
node: [18]
steps:
- uses: actions/checkout@v3
with:

View File

@ -21,5 +21,5 @@ Alternatively, you can use [`yarn link`](https://classic.yarnpkg.com/lang/en/doc
Once the link is setup, run the following command from the root of the monorepo
```
yarn lerna clean && yarn setup
yarn clean && yarn setup
```

View File

@ -1,23 +0,0 @@
---
title: How to install packages
slug: /how-to-install-packages
tags:
- lerna
- packages
---
# Best practices for installing packages in Strapi
When working with the Strapi monorepo, it's important to follow best practices for installing packages to avoid potential issues and ensure consistent results. Instead of using the standard **`yarn add`** command, we recommend using **`yarn lerna add <package_name> --scope @strapi/<module_name>`** for installing packages. Actually, you may encounter the following error using `yarn add`:
`An unexpected error occurred: "expected workspace package to exist for \"@typescript-eslint/typescript-estree\'`
This approach uses Lerna, a tool for managing JavaScript projects with multiple packages, to ensure that the package is installed in the correct location(s) and version across all modules that include it. The **`--scope`** flag specifies the specific module(s) that the package should be installed in, ensuring that it's only installed where it's needed.
By using this method, Strapi developers can avoid issues with mismatched package versions or unnecessary dependencies in certain modules. This can help to keep the codebase clean and maintainable, and reduce the potential for conflicts or issues in the future.
Overall, we recommend using **`yarn lerna add`** with the **`--scope`** flag for installing packages in the Strapi mono repo, to ensure consistent and reliable results.
## Resources
- [Lerna Docs](https://futurestud.io/tutorials/lerna-install-dependencies-for-a-specific-package)

View File

@ -188,15 +188,6 @@ const sidebars = {
},
items: [],
},
{
type: 'category',
label: 'How to install packages in a module',
link: {
type: 'doc',
id: 'how-to-install-packages',
},
items: [],
},
],
api: [{ type: 'autogenerated', dirName: 'api' }],
community: [{ type: 'autogenerated', dirName: 'community' }],

View File

@ -1,2 +1,4 @@
node_modules/
.eslintrc.js
dist/
bin/

View File

@ -1,4 +1,4 @@
module.exports = {
root: true,
extends: ['custom/back'],
extends: ['custom/typescript'],
};

View File

@ -0,0 +1,5 @@
#!/usr/bin/env node
'use strict';
require('../dist/create-strapi-app');

View File

@ -1,5 +0,0 @@
#!/usr/bin/env node
'use strict';
require('./create-strapi-app');

View File

@ -35,11 +35,23 @@
"url": "https://strapi.io"
}
],
"main": "./index.js",
"bin": "./index.js",
"bin": "./bin/index.js",
"files": [
"./dist",
"./bin"
],
"scripts": {
"build": "run -T tsc",
"build:ts": "run -T tsc",
"watch": "run -T tsc -w --preserveWatchOutput",
"clean": "run -T rimraf ./dist",
"prepublishOnly": "yarn clean && yarn build",
"lint": "run -T eslint ."
},
"devDependencies": {
"eslint-config-custom": "*",
"tsconfig": "*"
},
"engines": {
"node": ">=14.19.1 <=18.x.x",
"npm": ">=6.0.0"

View File

@ -1,15 +1,15 @@
'use strict';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import commander from 'commander';
import { checkInstallPath, generateNewApp } from '@strapi/generate-new';
import promptUser from './utils/prompt-user';
import type { Program } from './types';
const { resolve } = require('path');
const commander = require('commander');
const { checkInstallPath, generateNewApp } = require('@strapi/generate-new');
const promptUser = require('./utils/prompt-user');
// eslint-disable-next-line import/extensions
const packageJson = require('./package.json');
const packageJson = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf8'));
const program = new commander.Command();
const command = new commander.Command(packageJson.name);
const databaseOptions = [
const databaseOptions: Array<keyof Program> = [
'dbclient',
'dbhost',
'dbport',
@ -20,8 +20,7 @@ const databaseOptions = [
'dbfile',
];
program
.command(packageJson.name)
command
.version(packageJson.version)
.arguments('[directory]')
.option('--no-run', 'Do not start the application after it is created')
@ -40,12 +39,12 @@ program
.option('--template <templateurl>', 'Specify a Strapi template')
.option('--ts, --typescript', 'Use TypeScript to generate the project')
.description('create a new application')
.action((directory, options) => {
initProject(directory, program, options);
.action((directory, programArgs) => {
initProject(directory, programArgs);
})
.parse(process.argv);
function generateApp(projectName, options) {
function generateApp(projectName: string, options: unknown) {
if (!projectName) {
console.error('Please specify the <directory> of your project when using --quickstart');
process.exit(1);
@ -58,22 +57,25 @@ function generateApp(projectName, options) {
});
}
async function initProject(projectName, program, inputOptions) {
async function initProject(projectName: string, programArgs: Program) {
if (projectName) {
await checkInstallPath(resolve(projectName));
}
const programFlags = program.options
.reduce((acc, { short, long }) => [...acc, short, long], [])
const programFlags = command
.createHelp()
.visibleOptions(command)
.reduce<Array<string | undefined>>((acc, { short, long }) => [...acc, short, long], [])
.filter(Boolean);
if (inputOptions.template && programFlags.includes(inputOptions.template)) {
console.error(`${inputOptions.template} is not a valid template`);
if (programArgs.template && programFlags.includes(programArgs.template)) {
console.error(`${programArgs.template} is not a valid template`);
process.exit(1);
}
const hasDatabaseOptions = databaseOptions.some((opt) => inputOptions[opt]);
const hasDatabaseOptions = databaseOptions.some((opt) => programArgs[opt]);
if (inputOptions.quickstart && hasDatabaseOptions) {
if (programArgs.quickstart && hasDatabaseOptions) {
console.error(
`The quickstart option is incompatible with the following options: ${databaseOptions.join(
', '
@ -83,24 +85,24 @@ async function initProject(projectName, program, inputOptions) {
}
if (hasDatabaseOptions) {
inputOptions.quickstart = false; // Will disable the quickstart question because != 'undefined'
programArgs.quickstart = false; // Will disable the quickstart question because != 'undefined'
}
if (inputOptions.quickstart) {
return generateApp(projectName, inputOptions);
if (programArgs.quickstart) {
return generateApp(projectName, programArgs);
}
const prompt = await promptUser(projectName, inputOptions, hasDatabaseOptions);
const prompt = await promptUser(projectName, programArgs, hasDatabaseOptions);
const directory = prompt.directory || projectName;
await checkInstallPath(resolve(directory));
const options = {
template: inputOptions.template,
quickstart: prompt.quick || inputOptions.quickstart,
template: programArgs.template,
quickstart: prompt.quick || programArgs.quickstart,
};
const generateStrapiAppOptions = {
...inputOptions,
...programArgs,
...options,
};

View File

@ -0,0 +1 @@
declare module '@strapi/generate-new';

View File

@ -0,0 +1,17 @@
export interface Program {
noRun?: boolean;
useNpm?: boolean;
debug?: boolean;
quickstart?: boolean;
dbclient?: string;
dbhost?: string;
dbport?: string;
dbname?: string;
dbusername?: string;
dbpassword?: string;
dbssl?: string;
dbfile?: string;
dbforce?: boolean;
template?: string;
typescript?: boolean;
}

View File

@ -0,0 +1,39 @@
import inquirer from 'inquirer';
import type { Program } from '../types';
interface Answers {
directory: string;
quick: boolean;
}
export default async function promptUser(
projectName: string,
program: Program,
hasDatabaseOptions: boolean
) {
return inquirer.prompt<Answers>([
{
type: 'input',
default: 'my-strapi-project',
name: 'directory',
message: 'What would you like to name your project?',
when: !projectName,
},
{
type: 'list',
name: 'quick',
message: 'Choose your installation type',
when: !program.quickstart && !hasDatabaseOptions,
choices: [
{
name: 'Quickstart (recommended)',
value: true,
},
{
name: 'Custom (manual settings)',
value: false,
},
],
},
]);
}

View File

@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true
},
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@ -0,0 +1,8 @@
{
"extends": "tsconfig/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "**/__tests__/**"]
}

View File

@ -1,46 +0,0 @@
'use strict';
const inquirer = require('inquirer');
/**
* @param {string|null} projectName - The name/path of project
* @param {string|null} template - The Github repo of the template
* @returns Object containting prompt answers
*/
module.exports = async function promptUser(projectName, program, hasDatabaseOptions) {
const questions = await getPromptQuestions(projectName, program, hasDatabaseOptions);
return inquirer.prompt(questions);
};
/**
* @param {string|null} projectName - The name of the project
* @param {string|null} template - The template the project should use
* @returns Array of prompt question objects
*/
async function getPromptQuestions(projectName, program, hasDatabaseOptions) {
return [
{
type: 'input',
default: 'my-strapi-project',
name: 'directory',
message: 'What would you like to name your project?',
when: !projectName,
},
{
type: 'list',
name: 'quick',
message: 'Choose your installation type',
when: !program.quickstart && !hasDatabaseOptions,
choices: [
{
name: 'Quickstart (recommended)',
value: true,
},
{
name: 'Custom (manual settings)',
value: false,
},
],
},
];
}

View File

@ -1,2 +1,4 @@
node_modules/
.eslintrc.js
dist/
bin/

View File

@ -1,4 +1,4 @@
module.exports = {
root: true,
extends: ['custom/back'],
extends: ['custom/typescript'],
};

View File

@ -0,0 +1,5 @@
#!/usr/bin/env node
'use strict';
require('../dist/create-strapi-starter');

View File

@ -1,5 +0,0 @@
#!/usr/bin/env node
'use strict';
require('./create-strapi-starter');

View File

@ -30,9 +30,17 @@
"url": "https://strapi.io"
}
],
"main": "./index.js",
"bin": "./index.js",
"bin": "./bin/index.js",
"files": [
"./dist",
"./bin"
],
"scripts": {
"build": "run -T tsc",
"build:ts": "run -T tsc",
"watch": "run -T tsc -w --preserveWatchOutput",
"clean": "run -T rimraf ./dist",
"prepublishOnly": "yarn clean && yarn build",
"lint": "run -T eslint ."
},
"dependencies": {
@ -45,6 +53,10 @@
"inquirer": "8.2.5",
"ora": "5.4.1"
},
"devDependencies": {
"eslint-config-custom": "*",
"tsconfig": "*"
},
"engines": {
"node": ">=14.19.1 <=18.x.x",
"npm": ">=6.0.0"

View File

@ -1,15 +1,22 @@
'use strict';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
const commander = require('commander');
import commander, { CommanderError } from 'commander';
// eslint-disable-next-line import/extensions
const packageJson = require('./package.json');
const buildStarter = require('./utils/build-starter');
const promptUser = require('./utils/prompt-user');
import buildStarter from './utils/build-starter';
import promptUser from './utils/prompt-user';
import type { Program } from './types';
interface ProjectArgs {
projectName: string;
starter: string;
}
const packageJson = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf8'));
const program = new commander.Command(packageJson.name);
const incompatibleQuickstartOptions = [
const incompatibleQuickstartOptions: Array<keyof Program> = [
'dbclient',
'dbhost',
'dbport',
@ -39,12 +46,12 @@ program
'Create a fullstack monorepo application using the strapi backend template specified in the provided starter'
)
.action((directory, starter, programArgs) => {
const projectArgs = { projectName: directory, starter };
const projectArgs: ProjectArgs = { projectName: directory, starter };
initProject(projectArgs, programArgs);
});
function generateApp(projectArgs, programArgs) {
function generateApp(projectArgs: ProjectArgs, programArgs: Program) {
if (!projectArgs.projectName || !projectArgs.starter) {
console.error(
'Please specify the <directory> and <starter> of your project when using --quickstart'
@ -56,12 +63,12 @@ function generateApp(projectArgs, programArgs) {
return buildStarter(projectArgs, programArgs);
}
async function initProject(projectArgs, program) {
async function initProject(projectArgs: ProjectArgs, programArgs: Program) {
const hasIncompatibleQuickstartOptions = incompatibleQuickstartOptions.some(
(opt) => program[opt]
(opt) => programArgs[opt]
);
if (program.quickstart && hasIncompatibleQuickstartOptions) {
if (programArgs.quickstart && hasIncompatibleQuickstartOptions) {
console.error(
`The quickstart option is incompatible with the following options: ${incompatibleQuickstartOptions.join(
', '
@ -71,34 +78,34 @@ async function initProject(projectArgs, program) {
}
if (hasIncompatibleQuickstartOptions) {
program.quickstart = false; // Will disable the quickstart question because != 'undefined'
programArgs.quickstart = false; // Will disable the quickstart question because != 'undefined'
}
const { projectName, starter } = projectArgs;
if (program.quickstart) {
return generateApp(projectArgs, program);
if (programArgs.quickstart) {
return generateApp(projectArgs, programArgs);
}
const prompt = await promptUser(projectName, starter, program);
const prompt = await promptUser(projectName, starter, programArgs);
const promptProjectArgs = {
projectName: prompt.directory || projectName,
starter: prompt.starter || starter,
};
const programArgs = {
...program,
quickstart: prompt.quick || program.quickstart,
};
return generateApp(promptProjectArgs, programArgs);
return generateApp(promptProjectArgs, {
...programArgs,
quickstart: prompt.quick || programArgs.quickstart,
});
}
try {
program.parse(process.argv);
} catch (err) {
if (err.exitCode && err.exitCode !== 0) {
program.outputHelp();
if (err instanceof CommanderError) {
if (err.exitCode && err.exitCode !== 0) {
program.outputHelp();
}
}
}

View File

@ -0,0 +1 @@
declare module '@strapi/generate-new';

View File

@ -0,0 +1,24 @@
export interface Options {
useYarn?: boolean;
}
export interface PackageInfo {
name: string;
version: string;
}
export interface Program {
useNpm?: boolean;
debug?: boolean;
quickstart?: boolean;
dbclient?: string;
dbhost?: string;
dbport?: string;
dbname?: string;
dbusername?: string;
dbpassword?: string;
dbssl?: string;
dbfile?: string;
dbforce?: boolean;
template?: string;
}

View File

@ -1,39 +1,29 @@
'use strict';
import { resolve, join, basename } from 'path';
import os from 'os';
import fse from 'fs-extra';
import ora from 'ora';
import ciEnv from 'ci-info';
import chalk from 'chalk';
const { resolve, join, basename } = require('path');
const os = require('os');
const fse = require('fs-extra');
const ora = require('ora');
const ciEnv = require('ci-info');
const chalk = require('chalk');
import { generateNewApp } from '@strapi/generate-new';
const { generateNewApp } = require('@strapi/generate-new');
import hasYarn from './has-yarn';
import { runInstall, runApp, initGit } from './child-process';
import { getStarterPackageInfo, downloadNpmStarter } from './fetch-npm-starter';
import logger from './logger';
import stopProcess from './stop-process';
import type { Options, PackageInfo, Program } from '../types';
const hasYarn = require('./has-yarn');
const { runInstall, runApp, initGit } = require('./child-process');
const { getStarterPackageInfo, downloadNpmStarter } = require('./fetch-npm-starter');
const logger = require('./logger');
const stopProcess = require('./stop-process');
/**
* @param {string} - filePath Path to starter.json file
*/
function readStarterJson(filePath, starter) {
function readStarterJson(filePath: string, starter: string) {
try {
const data = fse.readFileSync(filePath);
return JSON.parse(data);
return JSON.parse(data.toString());
} catch (err) {
stopProcess(`Could not find ${chalk.yellow('starter.json')} in ${chalk.yellow(starter)}`);
}
}
/**
* @param {string} rootPath - Path to the project directory
* @param {string} projectName - Name of the project
* @param {Object} options
* @param {boolean} options.useYarn - Use yarn instead of npm
*/
async function initPackageJson(rootPath, projectName, { useYarn } = {}) {
async function initPackageJson(rootPath: string, projectName: string, { useYarn }: Options = {}) {
const packageManager = useYarn ? 'yarn --cwd' : 'npm run --prefix';
try {
@ -59,16 +49,11 @@ async function initPackageJson(rootPath, projectName, { useYarn } = {}) {
}
);
} catch (err) {
stopProcess(`Failed to create ${chalk.yellow(`package.json`)} in ${chalk.yellow(rootPath)}`);
stopProcess(`Failed to create ${chalk.yellow('package.json')} in ${chalk.yellow(rootPath)}`);
}
}
/**
* @param {string} path The directory path for install
* @param {Object} options
* @param {boolean} options.useYarn Use yarn instead of npm
*/
async function installWithLogs(path, options) {
async function installWithLogs(path: string, options: Options) {
const installPrefix = chalk.yellow('Installing dependencies:');
const loader = ora(installPrefix).start();
const logInstall = (chunk = '') => {
@ -76,8 +61,8 @@ async function installWithLogs(path, options) {
};
const runner = runInstall(path, options);
runner.stdout.on('data', logInstall);
runner.stderr.on('data', logInstall);
runner.stdout?.on('data', logInstall);
runner.stderr?.on('data', logInstall);
await runner;
@ -85,17 +70,12 @@ async function installWithLogs(path, options) {
console.log(`Dependencies installed ${chalk.green('successfully')}.`);
}
/**
* @param {string} starter The name of the starter as provided by the user
* @param {Object} options
* @param {boolean} options.useYarn Use yarn instead of npm
*/
async function getStarterInfo(starter, { useYarn } = {}) {
async function getStarterInfo(starter: string, { useYarn }: Options = {}) {
const isLocalStarter = ['./', '../', '/'].some((filePrefix) => starter.startsWith(filePrefix));
let starterPath;
let starterParentPath;
let starterPackageInfo = {};
let starterPackageInfo: PackageInfo | undefined;
if (isLocalStarter) {
// Starter is a local directory
@ -120,7 +100,10 @@ async function getStarterInfo(starter, { useYarn } = {}) {
* @param {string|null} projectArgs.starter - The npm package of the starter
* @param {Object} program - Commands for generating new application
*/
module.exports = async function buildStarter({ projectName, starter }, program) {
export default async function buildStarter(
{ projectName, starter }: { projectName: string; starter: string },
program: Program
) {
const hasYarnInstalled = await hasYarn();
const { isLocalStarter, starterPath, starterParentPath, starterPackageInfo } =
await getStarterInfo(starter, { useYarn: hasYarnInstalled });
@ -133,7 +116,11 @@ module.exports = async function buildStarter({ projectName, starter }, program)
try {
await fse.ensureDir(rootPath);
} catch (error) {
stopProcess(`Failed to create ${chalk.yellow(rootPath)}: ${error.message}`);
if (error instanceof Error) {
stopProcess(`Failed to create ${chalk.yellow(rootPath)}: ${error.message}`);
}
stopProcess(`Failed to create ${chalk.yellow(rootPath)}: ${error}`);
}
// Copy the downloaded frontend folder to the project folder
@ -145,18 +132,22 @@ module.exports = async function buildStarter({ projectName, starter }, program)
recursive: true,
});
} catch (error) {
stopProcess(`Failed to create ${chalk.yellow(frontendPath)}: ${error.message}`);
if (error instanceof Error) {
stopProcess(`Failed to create ${chalk.yellow(frontendPath)}: ${error.message}`);
}
stopProcess(`Failed to create ${chalk.yellow(frontendPath)}`);
}
// Delete the starter directory if it was downloaded
if (!isLocalStarter) {
if (!isLocalStarter && starterParentPath) {
await fse.remove(starterParentPath);
}
// Set command options for Strapi app
const generateStrapiAppOptions = {
...program,
starter: starterPackageInfo.name,
starter: starterPackageInfo?.name,
run: false,
};
if (starterJson.template.version) {
@ -192,4 +183,4 @@ module.exports = async function buildStarter({ projectName, starter }, program)
console.log(chalk.green('Starting the app'));
await runApp(rootPath, { useYarn: hasYarnInstalled });
};
}

View File

@ -0,0 +1,44 @@
import { execSync } from 'child_process';
import execa from 'execa';
import logger from './logger';
import type { Options } from '../types';
export function runInstall(path: string, { useYarn }: Options = {}) {
return execa(useYarn ? 'yarn' : 'npm', ['install'], {
cwd: path,
stdin: 'ignore',
});
}
export function runApp(rootPath: string, { useYarn }: Options = {}) {
if (useYarn) {
return execa('yarn', ['develop'], {
stdio: 'inherit',
cwd: rootPath,
});
}
return execa('npm', ['run', 'develop'], {
stdio: 'inherit',
cwd: rootPath,
});
}
export async function initGit(rootPath: string) {
try {
await execa('git', ['init'], {
cwd: rootPath,
});
} catch (err) {
logger.warn('Could not initialize a git repository');
}
try {
await execa('git', ['add', '-A'], { cwd: rootPath });
execSync('git commit -m "Create Strapi starter project"', {
cwd: rootPath,
});
} catch (err) {
logger.warn('Could not create initial git commit');
}
}

View File

@ -0,0 +1,87 @@
import path from 'path';
import execa from 'execa';
import chalk from 'chalk';
import stopProcess from './stop-process';
import type { Options, PackageInfo } from '../types';
/**
* Gets the package version on npm. Will fail if the package does not exist
*/
async function getPackageInfo(packageName: string, options?: Options): Promise<PackageInfo> {
const { useYarn } = options ?? {};
// Use yarn if possible because it's faster
if (useYarn) {
const { stdout } = await execa('yarn', ['info', packageName, '--json']);
const yarnInfo = JSON.parse(stdout);
return {
name: yarnInfo.data.name,
version: yarnInfo.data.version,
};
}
// Fallback to npm
const { stdout } = await execa('npm', ['view', packageName, 'name', 'version', '--silent']);
// Use regex to parse name and version from CLI result
const match = stdout.match(/(?<=')(.*?)(?=')/gm);
if (!match) {
throw new Error('No match for name@version');
}
const [name, version] = match;
return { name, version };
}
/**
* Get the version and full package name of the starter
*/
export async function getStarterPackageInfo(
starter: string,
options?: Options
): Promise<PackageInfo> {
const { useYarn } = options ?? {};
// Check if starter is a shorthand
try {
const longhand = `@strapi/starter-${starter}`;
return await getPackageInfo(longhand, { useYarn });
} catch (error) {
// Ignore error, we now know it's not a shorthand
}
// Fetch version of the non-shorthand package
try {
return await getPackageInfo(starter, { useYarn });
} catch (error) {
return stopProcess(`Could not find package ${chalk.yellow(starter)} on npm`);
}
}
/**
* Download a starter package from the npm registry
*/
export async function downloadNpmStarter(
packageInfo: PackageInfo,
parentDir: string,
options?: Options
): Promise<string> {
const { name, version } = packageInfo;
const { useYarn } = options ?? {};
// Download from npm, using yarn if possible
if (useYarn) {
await execa('yarn', ['add', `${name}@${version}`, '--no-lockfile', '--silent'], {
cwd: parentDir,
});
} else {
await execa('npm', ['install', `${name}@${version}`, '--no-save', '--silent'], {
cwd: parentDir,
});
}
// Return the path of the actual starter
const exactStarterPath = path.dirname(
require.resolve(`${name}/package.json`, { paths: [parentDir] })
);
return exactStarterPath;
}

View File

@ -1,8 +1,6 @@
'use strict';
import execa from 'execa';
const execa = require('execa');
module.exports = async function hasYarn() {
export default async function hasYarn() {
try {
const { exitCode } = await execa.commandSync('yarn --version', { shell: true });
@ -10,4 +8,4 @@ module.exports = async function hasYarn() {
} catch (err) {
return false;
}
};
}

View File

@ -1,17 +1,15 @@
'use strict';
import chalk from 'chalk';
const chalk = require('chalk');
module.exports = {
error(message) {
export default {
error(message: string) {
console.error(`${chalk.red('error')}: ${message}`);
},
warn(message) {
warn(message: string) {
console.log(`${chalk.yellow('warning')}: ${message}`);
},
info(message) {
info(message: string) {
console.log(`${chalk.blue('info')}: ${message}`);
},
};

View File

@ -0,0 +1,45 @@
import inquirer from 'inquirer';
import type { Program } from '../types';
interface Answers {
directory: string;
quick: boolean;
starter: string;
}
// Prompts the user with required questions to create the project and return the answers
export default async function promptUser(projectName: string, starter: string, program: Program) {
const questions: inquirer.QuestionCollection = [
{
type: 'input',
default: 'my-strapi-project',
name: 'directory',
message: 'What would you like to name your project?',
when: !projectName,
},
{
type: 'list',
name: 'quick',
message: 'Choose your installation type',
when: !program.quickstart,
choices: [
{
name: 'Quickstart (recommended)',
value: true,
},
{
name: 'Custom (manual settings)',
value: false,
},
],
},
{
type: 'input',
name: 'starter',
when: !starter,
message: 'Please provide the npm package name of the starter you want to use:',
},
];
return inquirer.prompt<Answers>(questions);
}

View File

@ -0,0 +1,9 @@
import logger from './logger';
export default function stopProcess(message: string) {
if (message) {
logger.error(message);
}
return process.exit(1);
}

View File

@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true
},
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@ -0,0 +1,8 @@
{
"extends": "tsconfig/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "**/__tests__/**"]
}

View File

@ -1,61 +0,0 @@
'use strict';
const { execSync } = require('child_process');
const execa = require('execa');
const logger = require('./logger');
/**
* @param {string} path Path to directory (frontend, backend)
* @param {Object} options
* @param {boolean} options.useYarn Use yarn instead of npm
*/
function runInstall(path, { useYarn } = {}) {
return execa(useYarn ? 'yarn' : 'npm', ['install'], {
cwd: path,
stdin: 'ignore',
});
}
/**
* @param {string} rootPath
* @param {Object} options
* @param {boolean} options.useYarn
*/
function runApp(rootPath, { useYarn } = {}) {
if (useYarn) {
return execa('yarn', ['develop'], {
stdio: 'inherit',
cwd: rootPath,
});
}
return execa('npm', ['run', 'develop'], {
stdio: 'inherit',
cwd: rootPath,
});
}
async function initGit(rootPath) {
try {
await execa('git', ['init'], {
cwd: rootPath,
});
} catch (err) {
logger.warn(`Could not initialize a git repository`);
}
try {
await execa(`git`, [`add`, `-A`], { cwd: rootPath });
execSync(`git commit -m "Create Strapi starter project"`, {
cwd: rootPath,
});
} catch (err) {
logger.warn(`Could not create initial git commit`);
}
}
module.exports = {
runInstall,
runApp,
initGit,
};

View File

@ -1,86 +0,0 @@
'use strict';
const path = require('path');
const execa = require('execa');
const chalk = require('chalk');
const stopProcess = require('./stop-process');
/**
* Gets the package version on npm. Will fail if the package does not exist
* @param {string} packageName Name to look up on npm, may include a specific version
* @param {Object} options
* @param {boolean} options.useYarn Yarn instead of npm
* @returns {Object}
*/
async function getPackageInfo(packageName, { useYarn } = {}) {
// Use yarn if possible because it's faster
if (useYarn) {
const { stdout } = await execa('yarn', ['info', packageName, '--json']);
const yarnInfo = JSON.parse(stdout);
return {
name: yarnInfo.data.name,
version: yarnInfo.data.version,
};
}
// Fallback to npm
const { stdout } = await execa('npm', ['view', packageName, 'name', 'version', '--silent']);
// Use regex to parse name and version from CLI result
const [name, version] = stdout.match(/(?<=')(.*?)(?=')/gm);
return { name, version };
}
/**
* Get the version and full package name of the starter
* @param {string} starter - The name of the starter as provided by the user
* @param {Object} options
* @param {boolean} options.useYarn - Use yarn instead of npm
* @returns {Object} - Full name and version of the starter package on npm
*/
async function getStarterPackageInfo(starter, { useYarn } = {}) {
// Check if starter is a shorthand
try {
const longhand = `@strapi/starter-${starter}`;
const packageInfo = await getPackageInfo(longhand, { useYarn });
// Hasn't crashed so it is indeed a shorthand
return packageInfo;
} catch (error) {
// Ignore error, we now know it's not a shorthand
}
// Fetch version of the non-shorthand package
try {
return getPackageInfo(starter, { useYarn });
} catch (error) {
stopProcess(`Could not find package ${chalk.yellow(starter)} on npm`);
}
}
/**
* Download a starter package from the npm registry
* @param {Object} packageInfo - Starter's npm package information
* @param {string} packageInfo.name
* @param {string} packageInfo.version
* @param {string} parentDir - Path inside of which we install the starter
* @param {Object} options
* @param {boolean} options.useYarn - Use yarn instead of npm
*/
async function downloadNpmStarter({ name, version }, parentDir, { useYarn } = {}) {
// Download from npm, using yarn if possible
if (useYarn) {
await execa('yarn', ['add', `${name}@${version}`, '--no-lockfile', '--silent'], {
cwd: parentDir,
});
} else {
await execa('npm', ['install', `${name}@${version}`, '--no-save', '--silent'], {
cwd: parentDir,
});
}
// Return the path of the actual starter
const exactStarterPath = path.dirname(
require.resolve(`${name}/package.json`, { paths: [parentDir] })
);
return exactStarterPath;
}
module.exports = { getStarterPackageInfo, downloadNpmStarter };

View File

@ -1,62 +0,0 @@
'use strict';
const inquirer = require('inquirer');
/**
* @param {string|null} projectName - The name/path of project
* @param {string|null} starterUrl - The GitHub repo of the starter
* @returns Object containting prompt answers
*/
module.exports = async function promptUser(projectName, starter, program) {
const mainQuestions = [
{
type: 'input',
default: 'my-strapi-project',
name: 'directory',
message: 'What would you like to name your project?',
when: !projectName,
},
{
type: 'list',
name: 'quick',
message: 'Choose your installation type',
when: !program.quickstart,
choices: [
{
name: 'Quickstart (recommended)',
value: true,
},
{
name: 'Custom (manual settings)',
value: false,
},
],
},
];
const [mainResponse, starterQuestion] = await Promise.all([
inquirer.prompt(mainQuestions),
getStarterQuestion(),
]);
const starterResponse = await inquirer.prompt({
name: 'starter',
when: !starter,
...starterQuestion,
});
return { ...mainResponse, ...starterResponse };
};
/**
*
* @returns Prompt question object
*/
async function getStarterQuestion() {
// Ask user to manually input his starter
// TODO: find way to suggest the possible v4 starters
return {
type: 'input',
message: 'Please provide the npm package name of the starter you want to use:',
};
}

View File

@ -1,8 +0,0 @@
'use strict';
const logger = require('./logger');
module.exports = function stopProcess(message) {
if (message) logger.error(message);
process.exit(1);
};

View File

@ -222,6 +222,8 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
trackUsageRef.current('didDeleteEntry', trackerProperty);
replace(redirectionLink);
return Promise.resolve(data);
} catch (err) {
trackUsageRef.current('didNotDeleteEntry', { error: err, ...trackerProperty });
@ -229,13 +231,9 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
return Promise.reject(err);
}
},
[id, slug, toggleNotification, del]
[id, slug, toggleNotification, del, redirectionLink, replace]
);
const onDeleteSucceeded = useCallback(() => {
replace(redirectionLink);
}, [redirectionLink, replace]);
const onPost = useCallback(
async (body, trackerProperty) => {
const endPoint = `${getRequestUrl(`collection-types/${slug}`)}${rawQuery}`;
@ -409,7 +407,6 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
isCreatingEntry,
isLoadingForData: isLoading,
onDelete,
onDeleteSucceeded,
onPost,
onPublish,
onDraftRelationCheck,

View File

@ -172,6 +172,9 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, slug }) => {
trackUsageRef.current('didDeleteEntry', trackerProperty);
setIsCreatingEntry(true);
dispatch(initForm(rawQuery, true));
return Promise.resolve(data);
} catch (err) {
trackUsageRef.current('didNotDeleteEntry', { error: err, ...trackerProperty });
@ -181,15 +184,9 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, slug }) => {
return Promise.reject(err);
}
},
[del, slug, displayErrors, toggleNotification, searchToSend]
[del, slug, displayErrors, toggleNotification, searchToSend, dispatch, rawQuery]
);
const onDeleteSucceeded = useCallback(() => {
setIsCreatingEntry(true);
dispatch(initForm(rawQuery, true));
}, [dispatch, rawQuery]);
const onPost = useCallback(
async (body, trackerProperty) => {
const endPoint = getRequestUrl(`${slug}${rawQuery}`);
@ -370,7 +367,6 @@ const SingleTypeFormWrapper = ({ allLayoutData, children, slug }) => {
isCreatingEntry,
isLoadingForData: isLoading,
onDelete,
onDeleteSucceeded,
onPost,
onDraftRelationCheck,
onPublish,

View File

@ -8,14 +8,14 @@ import PropTypes from 'prop-types';
import { getTrad } from '../../../utils';
import { connect, select } from './utils';
const DeleteLink = ({ isCreatingEntry, onDelete, onDeleteSucceeded, trackerProperty }) => {
const [showWarningDelete, setWarningDelete] = useState(false);
const DeleteLink = ({ onDelete, trackerProperty }) => {
const [displayDeleteConfirmation, setDisplayDeleteConfirmation] = useState(false);
const [isModalConfirmButtonLoading, setIsModalConfirmButtonLoading] = useState(false);
const { formatMessage } = useIntl();
const { formatAPIError } = useAPIErrorHandler(getTrad);
const toggleNotification = useNotification();
const toggleWarningDelete = () => setWarningDelete((prevState) => !prevState);
const toggleWarningDelete = () => setDisplayDeleteConfirmation((prevState) => !prevState);
const handleConfirmDelete = async () => {
try {
@ -27,7 +27,6 @@ const DeleteLink = ({ isCreatingEntry, onDelete, onDeleteSucceeded, trackerPrope
setIsModalConfirmButtonLoading(false);
toggleWarningDelete();
onDeleteSucceeded();
} catch (err) {
setIsModalConfirmButtonLoading(false);
toggleWarningDelete();
@ -38,10 +37,6 @@ const DeleteLink = ({ isCreatingEntry, onDelete, onDeleteSucceeded, trackerPrope
}
};
if (isCreatingEntry) {
return null;
}
return (
<>
<Button onClick={toggleWarningDelete} size="S" startIcon={<Trash />} variant="danger-light">
@ -50,9 +45,10 @@ const DeleteLink = ({ isCreatingEntry, onDelete, onDeleteSucceeded, trackerPrope
defaultMessage: 'Delete this entry',
})}
</Button>
<ConfirmDialog
isConfirmButtonLoading={isModalConfirmButtonLoading}
isOpen={showWarningDelete}
isOpen={displayDeleteConfirmation}
onConfirm={handleConfirmDelete}
onToggleDialog={toggleWarningDelete}
/>
@ -61,9 +57,7 @@ const DeleteLink = ({ isCreatingEntry, onDelete, onDeleteSucceeded, trackerPrope
};
DeleteLink.propTypes = {
isCreatingEntry: PropTypes.bool.isRequired,
onDelete: PropTypes.func.isRequired,
onDeleteSucceeded: PropTypes.func.isRequired,
trackerProperty: PropTypes.object.isRequired,
};

View File

@ -77,7 +77,6 @@ const EditView = ({ allowedActions, isSingleType, goBack, slug, id, origin, user
isCreatingEntry,
isLoadingForData,
onDelete,
onDeleteSucceeded,
onPost,
onPublish,
onDraftRelationCheck,
@ -221,12 +220,8 @@ const EditView = ({ allowedActions, isSingleType, goBack, slug, id, origin, user
</LinkButton>
</CheckPermissions>
{allowedActions.canDelete && (
<DeleteLink
isCreatingEntry={isCreatingEntry}
onDelete={onDelete}
onDeleteSucceeded={onDeleteSucceeded}
/>
{allowedActions.canDelete && !isCreatingEntry && (
<DeleteLink onDelete={onDelete} />
)}
</Flex>
</Box>

View File

@ -187,7 +187,7 @@ const SocialLinks = () => {
<GridGap>
{socialLinksExtended.map(({ icon, link, name }) => {
return (
<GridItem col={6} s={12} key={name}>
<GridItem col={6} s={12} key={name.id}>
<LinkCustom size="L" startIcon={icon} variant="tertiary" href={link} isExternal>
{formatMessage(name)}
</LinkCustom>

View File

@ -60,7 +60,7 @@
"error.contentTypeName.reserved-name": "This name cannot be used in your project as it might break other functionalities",
"error.validation.enum-duplicate": "Duplicate values are not allowed (only alphanumeric characters are taken into account).",
"error.validation.enum-empty-string": "Empty strings are not allowed",
"error.validation.enum-regex": "At least one value is invalid. Values should have at least one alphabetical character preceeding the first occurence of a number.",
"error.validation.enum-regex": "At least one value is invalid. Values should have at least one alphabetical character preceding the first occurence of a number.",
"error.validation.minSupMax": "Can't be superior",
"error.validation.positive": "Must be a positive number",
"error.validation.regex": "Regex pattern is invalid",

View File

@ -121,6 +121,28 @@ describe('Given I have some relations in the database', () => {
]);
});
});
describe('When you connect a relation before one with null order', () => {
test('Then it replaces null order values to 1 and properly reorders relations', () => {
const orderer = relationsOrderer(
[
{ id: 2, order: null },
{ id: 3, order: null },
],
'id',
'order'
);
orderer.connect([{ id: 4, position: { before: 3 } }, { id: 5 }]);
expect(orderer.get()).toMatchObject([
{ id: 2, order: 1 },
{ id: 4, order: 0.5 },
{ id: 3, order: 1 },
{ id: 5, order: 1.5 },
]);
});
});
});
describe('Given there are no relations in the database', () => {

View File

@ -135,7 +135,7 @@ const relationsOrderer = (initArr, idColumn, orderColumn, strict) => {
const computedRelations = _.castArray(initArr || []).map((r) => ({
init: true,
id: r[idColumn],
order: r[orderColumn],
order: r[orderColumn] || 1,
}));
const maxOrder = _.maxBy('order', computedRelations)?.order || 0;

View File

@ -57,15 +57,18 @@ class Database {
async function commit() {
if (notNestedTransaction) {
transactionCtx.clear();
await trx.commit();
}
}
async function rollback() {
if (notNestedTransaction) {
transactionCtx.clear();
await trx.rollback();
}
}
if (!cb) {
return {
commit,

View File

@ -6,11 +6,19 @@ const storage = new AsyncLocalStorage();
const transactionCtx = {
async run(store, cb) {
return storage.run(store, cb);
return storage.run({ trx: store }, cb);
},
get() {
return storage.getStore();
const store = storage.getStore();
return store?.trx;
},
clear() {
const store = storage.getStore();
if (store?.trx) {
store.trx = null;
}
},
};

View File

@ -536,17 +536,17 @@ class Strapi {
// plugins
await this.container.get('modules')[lifecycleName]();
// user
const userLifecycleFunction = this.app && this.app[lifecycleName];
if (isFunction(userLifecycleFunction)) {
await userLifecycleFunction({ strapi: this });
}
// 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)) {
await userLifecycleFunction({ strapi: this });
}
}
getModel(uid) {

View File

@ -61,7 +61,7 @@ const contentTypeSchemaValidator = yup.object().shape({
// should match the GraphQL regex
if (!regressedValues.every((value) => GRAPHQL_ENUM_REGEX.test(value))) {
const message = `Invalid enumeration value. Values should have at least one alphabetical character preceeding the first occurence of a number. Update your enumeration '${attrName}'.`;
const message = `Invalid enumeration value. Values should have at least one alphabetical character preceding the first occurence of a number. Update your enumeration '${attrName}'.`;
return this.createError({ message });
}

View File

@ -24,7 +24,7 @@ module.exports = {
'node/no-missing-import': 'off',
'@typescript-eslint/brace-style': 'off', // TODO: fix conflict with prettier/prettier in data-transfer/engine/index.ts
// to be cleaned up throughout codebase (too many to fix at the moment)
'@typescript-eslint/no-use-before-define': 'warn',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/comma-dangle': 'off',
},
overrides: [

View File

@ -4,6 +4,7 @@
"compilerOptions": {
"declaration": true,
"sourceMap": true,
"esModuleInterop": true
"esModuleInterop": true,
"resolveJsonModule": true
}
}

View File

@ -13604,9 +13604,11 @@ __metadata:
dependencies:
"@strapi/generate-new": 4.9.1
commander: 8.3.0
eslint-config-custom: "*"
inquirer: 8.2.5
tsconfig: "*"
bin:
create-strapi-app: ./index.js
create-strapi-app: ./bin/index.js
languageName: unknown
linkType: soft
@ -13618,12 +13620,14 @@ __metadata:
chalk: 4.1.2
ci-info: 3.8.0
commander: 8.3.0
eslint-config-custom: "*"
execa: 5.1.1
fs-extra: 10.0.0
inquirer: 8.2.5
ora: 5.4.1
tsconfig: "*"
bin:
create-strapi-starter: ./index.js
create-strapi-starter: ./bin/index.js
languageName: unknown
linkType: soft