Merge pull request #20268 from strapi/enh/cli

Rework create-strapi-xxx
This commit is contained in:
Alexandre BODIN 2024-06-07 12:57:22 +02:00 committed by GitHub
commit 4d3cb65f37
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
62 changed files with 674 additions and 1629 deletions

View File

@ -59,18 +59,18 @@ Install Strapi with this **Quickstart** command to create a Strapi project insta
- (Use **yarn** to install the Strapi project (recommended). [Install yarn with these docs](https://yarnpkg.com/lang/en/docs/install/).)
```bash
yarn create strapi-app my-project --quickstart
yarn create strapi
```
**or**
- (Use npm/npx to install the Strapi project.)
- (Using npx to install the Strapi project.)
```bash
npx create-strapi-app my-project --quickstart
npx create-strapi@latest
```
This command generates a brand new project with the default features (authentication, permissions, content management, content type builder & file upload). The **Quickstart** command installs Strapi using a **SQLite** database which is used for prototyping in development.
This command generates a brand new project with the default features (authentication, permissions, content management, content type builder & file upload).
Enjoy 🎉

View File

@ -1,5 +1,20 @@
{
"extends": "@strapi/typescript-utils/tsconfigs/admin",
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["../plugins/**/admin/src/**/*", "./"],
"exclude": ["node_modules/", "build/", "dist/", "**/*.test.ts"]
}

View File

@ -1,8 +1,21 @@
{
"extends": "@strapi/typescript-utils/tsconfigs/server",
"compilerOptions": {
"outDir": "dist",
"rootDir": "."
"rootDir": ".",
"module": "CommonJS",
"moduleResolution": "Node",
"lib": ["ES2020"],
"target": "ES2019",
"strict": false,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"incremental": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"noEmitOnError": true,
"noImplicitThis": true
},
"include": ["./", "src/**/*.json"],
"exclude": [

View File

@ -49,6 +49,7 @@
},
"devDependencies": {
"@strapi/pack-up": "5.0.0",
"@types/inquirer": "8.2.5",
"eslint-config-custom": "5.0.0-beta.9",
"tsconfig": "5.0.0-beta.9"
},

View File

@ -1,32 +1,34 @@
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';
import { generateNewApp, type Options as GenerateNewAppOptions } from '@strapi/generate-new';
import * as prompts from './prompts';
import type { Options } from './types';
import { detectPackageManager } from './package-manager';
import * as database from './database';
const packageJson = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf8'));
const command = new commander.Command(packageJson.name);
const databaseOptions: Array<keyof Program> = [
'dbclient',
'dbhost',
'dbport',
'dbname',
'dbusername',
'dbpassword',
'dbssl',
'dbfile',
];
command
.version(packageJson.version)
.arguments('[directory]')
.option('--no-run', 'Do not start the application after it is created')
.option('--use-npm', 'Force usage of npm instead of yarn to create the project')
.option('--debug', 'Display database connection error')
.option('--quickstart', 'Quickstart app creation')
.usage('[directory] [options]')
.option('--quickstart', 'Quickstart app creation (deprecated)')
.option('--no-run', 'Do not start the application after it is created.')
// setup options
.option('--ts, --typescript', 'Initialize the project with TypeScript (default)')
.option('--js, --javascript', 'Initialize the project with Javascript')
// Package manager options
.option('--use-npm', 'Use npm as the project package manager')
.option('--use-yarn', 'Use yarn as the project package manager')
.option('--use-pnpm', 'Use pnpm as the project package manager')
// Database options
.option('--dbclient <dbclient>', 'Database client')
.option('--dbhost <dbhost>', 'Database host')
.option('--dbport <dbport>', 'Database port')
@ -35,76 +37,92 @@ command
.option('--dbpassword <dbpassword>', 'Database password')
.option('--dbssl <dbssl>', 'Database SSL')
.option('--dbfile <dbfile>', 'Database file path for sqlite')
.option('--dbforce', 'Overwrite database content if any')
// templates
.option('--template <templateurl>', 'Specify a Strapi template')
.option('--ts, --typescript', 'Use TypeScript to generate the project')
.description('create a new application')
.action((directory, programArgs) => {
initProject(directory, programArgs);
.action((directory, options) => {
createStrapiApp(directory, options);
})
.parse(process.argv);
function generateApp(projectName: string, options: unknown) {
if (!projectName) {
async function createStrapiApp(directory: string | undefined, options: Options) {
validateOptions(options);
if (options.quickstart && !directory) {
console.error('Please specify the <directory> of your project when using --quickstart');
process.exit(1);
}
return generateNewApp(projectName, options).then(() => {
if (process.platform === 'win32') {
process.exit(0);
}
});
}
const appDirectory = directory || (await prompts.directory());
async function initProject(projectName: string, programArgs: Program) {
if (projectName) {
await checkInstallPath(resolve(projectName));
const appOptions = {
directory: appDirectory,
useTypescript: true,
packageManager: 'npm',
template: options.template,
isQuickstart: options.quickstart,
} as GenerateNewAppOptions;
if (options.javascript === true) {
appOptions.useTypescript = false;
} else if (options.typescript === true) {
appOptions.useTypescript = true;
} else {
appOptions.useTypescript = options.quickstart ? true : await prompts.typescript();
}
if (options.useNpm === true) {
appOptions.packageManager = 'npm';
} else if (options.usePnpm === true) {
appOptions.packageManager = 'pnpm';
} else if (options.useYarn === true) {
appOptions.packageManager = 'yarn';
} else {
appOptions.packageManager = detectPackageManager();
}
if (options.quickstart === true && options.run !== false) {
appOptions.runApp = true;
}
appOptions.database = await database.getDatabaseInfos(options);
return generateNewApp(appOptions)
.then(() => {
if (process.platform === 'win32') {
process.exit(0);
}
})
.catch((error) => {
console.error(`Error: ${error.message}`);
process.exit(1);
});
}
async function validateOptions(options: Options) {
const programFlags = command
.createHelp()
.visibleOptions(command)
.reduce<Array<string | undefined>>((acc, { short, long }) => [...acc, short, long], [])
.filter(Boolean);
if (programArgs.template && programFlags.includes(programArgs.template)) {
console.error(`${programArgs.template} is not a valid template`);
if (options.template && programFlags.includes(options.template)) {
console.error(`${options.template} is not a valid template`);
process.exit(1);
}
const hasDatabaseOptions = databaseOptions.some((opt) => programArgs[opt]);
if (options.javascript === true && options.typescript === true) {
console.error('You cannot use both --typescript (--ts) and --javascript (--js) flags together');
process.exit(1);
}
if (programArgs.quickstart && hasDatabaseOptions) {
if ([options.useNpm, options.usePnpm, options.useYarn].filter(Boolean).length > 1) {
console.error(
`The quickstart option is incompatible with the following options: ${databaseOptions.join(
', '
)}`
'You cannot specify multiple package managers at the same time (--use-npm, --use-pnpm, --use-yarn)'
);
process.exit(1);
}
if (hasDatabaseOptions) {
programArgs.quickstart = false; // Will disable the quickstart question because != 'undefined'
}
if (programArgs.quickstart) {
return generateApp(projectName, programArgs);
}
const prompt = await promptUser(projectName, programArgs, hasDatabaseOptions);
const directory = prompt.directory || projectName;
await checkInstallPath(resolve(directory));
const options = {
template: programArgs.template,
quickstart: prompt.quick || programArgs.quickstart,
};
const generateStrapiAppOptions = {
...programArgs,
...options,
};
return generateApp(directory, generateStrapiAppOptions);
database.validateOptions(options);
}

View File

@ -0,0 +1,103 @@
import inquirer from 'inquirer';
import type { Options, DBClient, DBConfig } from './types';
import dbQuestions from './db-questions';
const DBOptions = ['dbclient', 'dbhost', 'dbport', 'dbname', 'dbusername', 'dbpassword'];
const VALID_CLIENTS = ['sqlite', 'mysql', 'postgres'] as const;
const DEFAULT_CONFIG: DBConfig = {
client: 'sqlite',
connection: {},
};
async function dbPrompt() {
const { useDefault } = await inquirer.prompt<{ useDefault: boolean }>([
{
type: 'confirm',
name: 'useDefault',
message: 'Use the default database (sqlite) ?',
default: true,
},
]);
if (useDefault) {
return DEFAULT_CONFIG;
}
const { client } = await inquirer.prompt<{ client: DBClient }>([
{
type: 'list',
name: 'client',
message: 'Choose your default database client',
choices: ['sqlite', 'postgres', 'mysql'],
default: 'sqlite',
},
]);
const questions = dbQuestions[client].map((q) => q({ client }));
const responses = await inquirer.prompt(questions);
return {
client,
connection: responses,
};
}
export async function getDatabaseInfos(options: Options): Promise<DBConfig> {
const hasDBOptions = DBOptions.some((key) => key in options);
if (!hasDBOptions) {
if (options.quickstart) {
return DEFAULT_CONFIG;
}
return dbPrompt();
}
if (!options.dbclient) {
console.error('Please specify the database client');
process.exit(1);
}
const database: DBConfig = {
client: options.dbclient,
connection: {
host: options.dbhost,
port: options.dbport,
database: options.dbname,
username: options.dbusername,
password: options.dbpassword,
filename: options.dbfile,
},
};
if (options.dbssl !== undefined) {
database.connection.ssl = options.dbssl === 'true';
}
return database;
}
export function validateOptions(options: Options) {
if (options.dbclient && !VALID_CLIENTS.includes(options.dbclient)) {
console.error(
`Invalid --dbclient: ${options.dbclient}, expected one of ${VALID_CLIENTS.join(', ')}`
);
process.exit(1);
}
const matchingArgs = DBOptions.filter((key) => key in options);
const missingArgs = DBOptions.filter((key) => !(key in options));
if (
matchingArgs.length > 0 &&
matchingArgs.length !== DBOptions.length &&
options.dbclient !== 'sqlite'
) {
console.error(`Required database arguments are missing: ${missingArgs.join(', ')}.`);
process.exit(1);
}
}

View File

@ -1,8 +1,8 @@
import type { Question } from 'inquirer';
import type { Scope } from '../types';
import type { DBClient } from './types';
interface QuestionFactory {
(options: { scope: Scope; client: 'postgres' | 'mysql' | 'sqlite' }): Question;
(options: { client: DBClient }): Question;
}
const DEFAULT_PORTS = {
@ -11,11 +11,11 @@ const DEFAULT_PORTS = {
sqlite: undefined,
};
const database: QuestionFactory = ({ scope }) => ({
const database: QuestionFactory = () => ({
type: 'input',
name: 'database',
message: 'Database name:',
default: scope.name,
default: 'strapi',
validate(value: string) {
if (value.includes('.')) {
return `The database name can't contain a "."`;

View File

@ -0,0 +1,15 @@
export type PackageManager = 'npm' | 'yarn' | 'pnpm';
export const detectPackageManager = (): PackageManager => {
const userAgent = process.env.npm_config_user_agent || '';
if (userAgent.startsWith('yarn')) {
return 'yarn';
}
if (userAgent.startsWith('pnpm')) {
return 'pnpm';
}
return 'npm';
};

View File

@ -0,0 +1,33 @@
import inquirer from 'inquirer';
async function directory() {
const { directory } = await inquirer.prompt<{
directory: string;
}>([
{
type: 'input',
default: 'my-strapi-project',
name: 'directory',
message: 'What is the name of your project?',
},
]);
return directory;
}
async function typescript() {
const { useTypescript } = await inquirer.prompt<{
useTypescript: boolean;
}>([
{
type: 'confirm',
name: 'useTypescript',
message: 'Do you want to use Typescript ?',
default: true,
},
]);
return useTypescript;
}
export { directory, typescript };

View File

@ -1,9 +1,10 @@
export interface Program {
noRun?: boolean;
export interface Options {
useNpm?: boolean;
debug?: boolean;
usePnpm?: boolean;
useYarn?: boolean;
quickstart?: boolean;
dbclient?: string;
run?: boolean;
dbclient?: DBClient;
dbhost?: string;
dbport?: string;
dbname?: string;
@ -11,7 +12,22 @@ export interface Program {
dbpassword?: string;
dbssl?: string;
dbfile?: string;
dbforce?: boolean;
template?: string;
typescript?: boolean;
javascript?: boolean;
}
export type DBClient = 'mysql' | 'postgres' | 'sqlite';
export type DBConfig = {
client: DBClient;
connection: {
host?: string;
port?: string;
database?: string;
username?: string;
password?: string;
filename?: string;
ssl?: boolean;
};
};

View File

@ -1,39 +0,0 @@
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

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

View File

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

View File

@ -1,22 +0,0 @@
Copyright (c) 2015-present Strapi Solutions SAS
Portions of the Strapi software are licensed as follows:
* All software that resides under an "ee/" directory (the “EE Software”), if that directory exists, is licensed under the license defined in "ee/LICENSE".
* All software outside of the above-mentioned directories or restrictions above is available under the "MIT Expat" license as set forth below.
MIT Expat License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,35 +0,0 @@
# Create strapi starter
This package includes the `create-strapi-starter` CLI to simplify creating a Strapi project using starters and templates
## How to use
### Quick usage (recommended)
Using yarn create command
```
yarn create strapi-starter my-project starter-url
```
Using npx
```
npx create-strapi-starter my-project starter-url
```
### Manual install
Using yarn
```
yarn global add create-strapi-app
create-strapi-starter my-project starter-url
```
Using npm
```
npm install -g create-strapi-app
create-strapi-starter my-project starter-url
```

View File

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

View File

@ -1,65 +0,0 @@
{
"name": "create-strapi-starter",
"version": "5.0.0-beta.9",
"description": "Generate a new Strapi application.",
"keywords": [
"create-strapi-starter",
"create",
"new",
"generate",
"strapi"
],
"homepage": "https://strapi.io",
"bugs": {
"url": "https://github.com/strapi/strapi/issues"
},
"repository": {
"type": "git",
"url": "git://github.com/strapi/strapi.git"
},
"license": "SEE LICENSE IN LICENSE",
"author": {
"name": "Strapi Solutions SAS",
"email": "hi@strapi.io",
"url": "https://strapi.io"
},
"maintainers": [
{
"name": "Strapi Solutions SAS",
"email": "hi@strapi.io",
"url": "https://strapi.io"
}
],
"main": "",
"bin": "./bin/index.js",
"files": [
"dist/",
"bin/"
],
"scripts": {
"build": "pack-up build",
"clean": "run -T rimraf ./dist",
"lint": "run -T eslint .",
"watch": "pack-up watch"
},
"dependencies": {
"@strapi/generate-new": "5.0.0-beta.9",
"chalk": "4.1.2",
"ci-info": "4.0.0",
"commander": "8.3.0",
"execa": "5.1.1",
"fs-extra": "11.2.0",
"inquirer": "8.2.5",
"ora": "5.4.1"
},
"devDependencies": {
"@strapi/pack-up": "5.0.0",
"@types/fs-extra": "11.0.4",
"eslint-config-custom": "5.0.0-beta.9",
"tsconfig": "5.0.0-beta.9"
},
"engines": {
"node": ">=18.0.0 <=20.x.x",
"npm": ">=6.0.0"
}
}

View File

@ -1,14 +0,0 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { defineConfig } from '@strapi/pack-up';
export default defineConfig({
bundles: [
{
source: './src/create-strapi-starter.ts',
import: './dist/create-strapi-starter.mjs',
require: './dist/create-strapi-starter.js',
},
],
dist: './dist',
runtime: 'node',
});

View File

@ -1,111 +0,0 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import commander, { CommanderError } from 'commander';
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: Array<keyof Program> = [
'dbclient',
'dbhost',
'dbport',
'dbname',
'dbusername',
'dbpassword',
'dbssl',
'dbfile',
];
program
.version(packageJson.version)
.arguments('[directory], [starter]')
.option('--use-npm', 'Force usage of npm instead of yarn to create the project')
.option('--debug', 'Display database connection error')
.option('--quickstart', 'Quickstart app creation')
.option('--dbclient <dbclient>', 'Database client')
.option('--dbhost <dbhost>', 'Database host')
.option('--dbport <dbport>', 'Database port')
.option('--dbname <dbname>', 'Database name')
.option('--dbusername <dbusername>', 'Database username')
.option('--dbpassword <dbpassword>', 'Database password')
.option('--dbssl <dbssl>', 'Database SSL')
.option('--dbfile <dbfile>', 'Database file path for sqlite')
.option('--dbforce', 'Overwrite database content if any')
.description(
'Create a fullstack monorepo application using the strapi backend template specified in the provided starter'
)
.action((directory, starter, programArgs) => {
const projectArgs: ProjectArgs = { projectName: directory, starter };
initProject(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'
);
// eslint-disable-next-line no-process-exit
process.exit(1);
}
return buildStarter(projectArgs, programArgs);
}
async function initProject(projectArgs: ProjectArgs, programArgs: Program) {
const hasIncompatibleQuickstartOptions = incompatibleQuickstartOptions.some(
(opt) => programArgs[opt]
);
if (programArgs.quickstart && hasIncompatibleQuickstartOptions) {
console.error(
`The quickstart option is incompatible with the following options: ${incompatibleQuickstartOptions.join(
', '
)}`
);
process.exit(1);
}
if (hasIncompatibleQuickstartOptions) {
programArgs.quickstart = false; // Will disable the quickstart question because != 'undefined'
}
const { projectName, starter } = projectArgs;
if (programArgs.quickstart) {
return generateApp(projectArgs, programArgs);
}
const prompt = await promptUser(projectName, starter, programArgs);
const promptProjectArgs = {
projectName: prompt.directory || projectName,
starter: prompt.starter || starter,
};
return generateApp(promptProjectArgs, {
...programArgs,
quickstart: prompt.quick || programArgs.quickstart,
});
}
try {
program.parse(process.argv);
} catch (err) {
if (err instanceof CommanderError) {
if (err.exitCode && err.exitCode !== 0) {
program.outputHelp();
}
}
}

View File

@ -1,131 +0,0 @@
############################
# OS X
############################
.DS_Store
.AppleDouble
.LSOverride
Icon
.Spotlight-V100
.Trashes
._*
############################
# Linux
############################
*~
############################
# Windows
############################
Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
*.cab
*.msi
*.msm
*.msp
############################
# Packages
############################
*.7z
*.csv
*.dat
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
*.com
*.class
*.dll
*.exe
*.o
*.seed
*.so
*.swo
*.swp
*.swn
*.swm
*.out
*.pid
############################
# Logs and databases
############################
.tmp
*.log
*.sql
*.sqlite
*.sqlite3
############################
# Misc.
############################
*#
ssl
.idea
nbproject
public/uploads/*
!public/uploads/.gitkeep
.tsbuildinfo
.eslintcache
############################
# Node.js
############################
lib-cov
lcov.info
pids
logs
results
node_modules
.node_history
############################
# Tests
############################
coverage
############################
# Package managers
############################
.yarn/*
!.yarn/cache
!.yarn/unplugged
!.yarn/patches
!.yarn/releases
!.yarn/sdks
!.yarn/versions
.pnp.*
yarn-error.log
############################
# Strapi
############################
.env
license.txt
exports
*.cache
dist
build
.strapi-updater.json

View File

@ -1,24 +0,0 @@
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,186 +0,0 @@
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';
import { generateNewApp } from '@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';
function readStarterJson(filePath: string, starter: string) {
try {
const data = fse.readFileSync(filePath);
return JSON.parse(data.toString());
} catch (err) {
stopProcess(`Could not find ${chalk.yellow('starter.json')} in ${chalk.yellow(starter)}`);
}
}
async function initPackageJson(rootPath: string, projectName: string, { useYarn }: Options = {}) {
const packageManager = useYarn ? 'yarn --cwd' : 'npm run --prefix';
try {
await fse.writeJson(
join(rootPath, 'package.json'),
{
name: projectName,
private: true,
version: '0.0.0',
scripts: {
'develop:backend': `${packageManager} backend develop`,
'develop:frontend': `wait-on http://localhost:1337/admin && ${packageManager} frontend develop`,
develop: 'cross-env FORCE_COLOR=1 npm-run-all -l -p develop:*',
},
devDependencies: {
'npm-run-all': '4.1.5',
'wait-on': '5.2.1',
'cross-env': '7.0.3',
},
},
{
spaces: 2,
}
);
} catch (err) {
stopProcess(`Failed to create ${chalk.yellow('package.json')} in ${chalk.yellow(rootPath)}`);
}
}
async function installWithLogs(path: string, options: Options) {
const installPrefix = chalk.yellow('Installing dependencies:');
const loader = ora(installPrefix).start();
const logInstall = (chunk = '') => {
loader.text = `${installPrefix} ${chunk.toString().split('\n').join(' ')}`;
};
const runner = runInstall(path, options);
runner.stdout?.on('data', logInstall);
runner.stderr?.on('data', logInstall);
await runner;
loader.stop();
console.log(`Dependencies installed ${chalk.green('successfully')}.`);
}
async function getStarterInfo(starter: string, { useYarn }: Options = {}) {
const isLocalStarter = ['./', '../', '/'].some((filePrefix) => starter.startsWith(filePrefix));
let starterPath;
let starterParentPath;
let starterPackageInfo: PackageInfo | undefined;
if (isLocalStarter) {
// Starter is a local directory
console.log('Installing local starter.');
starterPath = resolve(starter);
} else {
// Starter should be an npm package. Fetch starter info
starterPackageInfo = await getStarterPackageInfo(starter, { useYarn });
console.log(`Installing ${chalk.yellow(starterPackageInfo.name)} starter.`);
// Download starter repository to a temporary directory
starterParentPath = await fse.mkdtemp(join(os.tmpdir(), 'strapi-'));
starterPath = await downloadNpmStarter(starterPackageInfo, starterParentPath, { useYarn });
}
return { isLocalStarter, starterPath, starterParentPath, starterPackageInfo };
}
/**
* @param {Object} projectArgs - The arguments for create a project
* @param {string|null} projectArgs.projectName - The name/path of project
* @param {string|null} projectArgs.starter - The npm package of the starter
* @param {Object} program - Commands for generating new application
*/
export default async function buildStarter(
{ projectName, starter }: { projectName: string; starter: string },
program: Program
) {
const hasYarnInstalled = hasYarn();
const { isLocalStarter, starterPath, starterParentPath, starterPackageInfo } =
await getStarterInfo(starter, { useYarn: hasYarnInstalled });
// Project directory
const rootPath = resolve(projectName);
const projectBasename = basename(rootPath);
const starterJson = readStarterJson(join(starterPath, 'starter.json'), starter);
try {
await fse.ensureDir(rootPath);
} catch (error) {
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
const frontendPath = join(rootPath, 'frontend');
try {
await fse.copy(join(starterPath, 'starter'), frontendPath, {
overwrite: true,
recursive: true,
});
} catch (error) {
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 && starterParentPath) {
await fse.remove(starterParentPath);
}
// Set command options for Strapi app
const generateStrapiAppOptions = {
...program,
starter: starterPackageInfo?.name,
run: false,
};
if (starterJson.template.version) {
generateStrapiAppOptions.template = `${starterJson.template.name}@${starterJson.template.version}`;
} else {
generateStrapiAppOptions.template = starterJson.template.name;
}
// Create strapi app using the template
await generateNewApp(join(rootPath, 'backend'), generateStrapiAppOptions);
// Install frontend dependencies
console.log(`Creating Strapi starter frontend at ${chalk.yellow(frontendPath)}.`);
console.log('Installing frontend dependencies');
await installWithLogs(frontendPath, { useYarn: hasYarnInstalled });
// Setup monorepo
initPackageJson(rootPath, projectBasename, { useYarn: hasYarnInstalled });
// Add gitignore
try {
const gitignore = join(__dirname, '..', 'resources', 'gitignore');
await fse.copy(gitignore, join(rootPath, '.gitignore'));
} catch (err) {
logger.warn(`Failed to create file: ${chalk.yellow('.gitignore')}`);
}
await installWithLogs(rootPath, { useYarn: hasYarnInstalled });
if (!ciEnv.isCI) {
await initGit(rootPath);
}
console.log(chalk.green('Starting the app'));
await runApp(rootPath, { useYarn: hasYarnInstalled });
}

View File

@ -1,45 +0,0 @@
import { execSync } from 'child_process';
import execa from 'execa';
import logger from './logger';
import type { Options } from '../types';
// TODO: Refactor run install, use the methods available in @strapi/utils instead
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

@ -1,87 +0,0 @@
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,11 +0,0 @@
import execa from 'execa';
export default function hasYarn() {
try {
const { exitCode } = execa.commandSync('yarn --version', { shell: true });
if (exitCode === 0) return true;
} catch (err) {
return false;
}
}

View File

@ -1,15 +0,0 @@
import chalk from 'chalk';
export default {
error(message: string) {
console.error(`${chalk.red('error')}: ${message}`);
},
warn(message: string) {
console.log(`${chalk.yellow('warning')}: ${message}`);
},
info(message: string) {
console.log(`${chalk.blue('info')}: ${message}`);
},
};

View File

@ -1,45 +0,0 @@
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: ReadonlyArray<inquirer.DistinctQuestion<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,
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

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

View File

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

View File

@ -1,3 +0,0 @@
{
"extends": "./tsconfig.json"
}

View File

@ -1,5 +0,0 @@
{
"extends": "tsconfig/base.json",
"include": ["src", "packup.config.ts"],
"exclude": ["node_modules"]
}

View File

@ -59,18 +59,18 @@ Install Strapi with this **Quickstart** command to create a Strapi project insta
- (Use **yarn** to install the Strapi project (recommended). [Install yarn with these docs](https://yarnpkg.com/lang/en/docs/install/).)
```bash
yarn create strapi-app my-project --quickstart
yarn create strapi
```
**or**
- (Use npm/npx to install the Strapi project.)
- (Using npx to install the Strapi project.)
```bash
npx create-strapi-app my-project --quickstart
npx create-strapi@latest
```
This command generates a brand new project with the default features (authentication, permissions, content management, content type builder & file upload). The **Quickstart** command installs Strapi using a **SQLite** database which is used for prototyping in development.
This command generates a brand new project with the default features (authentication, permissions, content management, content type builder & file upload).
Enjoy 🎉

View File

@ -0,0 +1,99 @@
import { TemplateFile } from '@strapi/pack-up';
import { outdent } from 'outdent';
interface TsConfigFiles {
tsconfigFile: TemplateFile;
tsconfigBuildFile: TemplateFile;
}
const ADMIN: TsConfigFiles = {
tsconfigFile: {
name: 'admin/tsconfig.json',
contents: outdent`
{
"compilerOptions: {
target: 'ESNext',
module: 'ESNext',
moduleResolution: 'Bundler',
useDefineForClassFields: true,
lib: ['DOM', 'DOM.Iterable', 'ESNext'],
allowJs: false,
skipLibCheck: true,
esModuleInterop: true,
allowSyntheticDefaultImports: true,
strict: true,
forceConsistentCasingInFileNames: true,
resolveJsonModule: true,
noEmit: true,
jsx: 'react-jsx',
},
"include": ["./src", "./custom.d.ts"],
"compilerOptions": {
"rootDir": "../",
"baseUrl": ".",
},
}
`,
},
tsconfigBuildFile: {
name: 'admin/tsconfig.build.json',
contents: outdent`
{
"extends": "./tsconfig",
"include": ["./src", "./custom.d.ts"],
"exclude": ["**/*.test.ts", "**/*.test.tsx"],
"compilerOptions": {
"rootDir": "../",
"baseUrl": ".",
"outDir": "./dist",
}
}
`,
},
};
const SERVER: TsConfigFiles = {
tsconfigFile: {
name: 'server/tsconfig.json',
contents: outdent`
{
"include": ["./src"],
"compilerOptions": {
"rootDir": "../",
"baseUrl": ".",
"module": "CommonJS",
"moduleResolution": "Node",
"lib": ["ES2020"],
"target": "ES2019",
"strict": false,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"incremental": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"noEmitOnError": true,
"noImplicitThis": true
},
}
`,
},
tsconfigBuildFile: {
name: 'server/tsconfig.build.json',
contents: outdent`
{
"extends": "./tsconfig",
"include": ["./src"],
"exclude": ["**/*.test.ts"],
"compilerOptions": {
"rootDir": "../",
"baseUrl": ".",
"outDir": "./dist",
}
}
`,
},
};
export { ADMIN as adminTsconfigFiles, SERVER as serverTsconfigFiles };

View File

@ -44,7 +44,6 @@
"watch": "pack-up watch"
},
"dependencies": {
"@sentry/node": "7.112.2",
"chalk": "^4.1.2",
"execa": "5.1.1",
"fs-extra": "11.2.0",
@ -58,6 +57,7 @@
"devDependencies": {
"@strapi/pack-up": "5.0.0",
"@types/fs-extra": "11.0.4",
"@types/inquirer": "8.2.5",
"copyfiles": "2.4.1"
},
"engines": {

View File

@ -1,35 +0,0 @@
import { merge } from 'lodash';
import { trackUsage } from './utils/usage';
import defaultConfigs from './utils/db-configs';
import clientDependencies from './utils/db-client-dependencies';
import getClientName from './utils/db-client-name';
import createProject from './create-project';
import type { ClientName, Configuration, Scope } from './types';
export default async (scope: Scope) => {
console.log('Creating a project from the database CLI arguments.');
await trackUsage({ event: 'didChooseCustomDatabase', scope });
const { client } = scope.database ?? {};
if (!client) {
throw new Error('Missing client');
}
const configuration: Configuration = {
client: getClientName({ client }),
connection: merge(
{},
defaultConfigs[client as keyof typeof defaultConfigs] || {},
scope.database
),
dependencies: {
...clientDependencies({ scope, client } as { scope: Scope; client: ClientName }),
...scope.additionalsDependencies,
},
};
return createProject(scope, configuration);
};

View File

@ -1,91 +0,0 @@
import inquirer from 'inquirer';
import { merge } from 'lodash';
import { trackUsage } from './utils/usage';
import defaultConfigs from './utils/db-configs';
import clientDependencies from './utils/db-client-dependencies';
import dbQuestions from './utils/db-questions';
import createProject from './create-project';
import type { Configuration, Scope } from './types';
const LANGUAGES = {
javascript: 'JavaScript',
typescript: 'TypeScript',
};
export default async (scope: Scope) => {
if (!scope.useTypescript) {
const language = await askAboutLanguages();
scope.useTypescript = language === LANGUAGES.typescript;
}
await trackUsage({ event: 'didChooseCustomDatabase', scope });
const configuration = await askDbInfosAndTest(scope).catch((error) => {
return trackUsage({ event: 'didNotConnectDatabase', scope, error }).then(() => {
throw error;
});
});
console.log();
console.log('Creating a project with custom database options.');
await trackUsage({ event: 'didConnectDatabase', scope });
return createProject(scope, configuration);
};
async function askDbInfosAndTest(scope: Scope) {
const { client, connection } = await askDatabaseInfos(scope);
return {
client,
connection,
dependencies: {
...clientDependencies({ client }),
...scope.additionalsDependencies,
},
} as Configuration;
}
async function askDatabaseInfos(scope: Scope) {
const { client } = await inquirer.prompt<{ client: 'sqlite' | 'postgres' | 'mysql' }>([
{
type: 'list',
name: 'client',
message: 'Choose your default database client',
choices: ['sqlite', 'postgres', 'mysql'],
default: 'sqlite',
},
]);
const questions = dbQuestions[client].map((q) => q({ scope, client }));
if (!questions) {
return { client };
}
const responses = await inquirer.prompt(questions);
const connection = merge({}, defaultConfigs[client] || {}, {
client,
connection: responses,
});
return {
client,
connection,
};
}
async function askAboutLanguages() {
const { language } = await inquirer.prompt([
{
type: 'list',
name: 'language',
message: 'Choose your preferred language',
choices: Object.values(LANGUAGES),
default: LANGUAGES.javascript,
},
]);
return language;
}

View File

@ -3,35 +3,36 @@ import { join } from 'path';
import fse from 'fs-extra';
import chalk from 'chalk';
import execa from 'execa';
import ora from 'ora';
import _ from 'lodash';
import stopProcess from './utils/stop-process';
import { trackUsage, captureStderr } from './utils/usage';
import { trackUsage } from './utils/usage';
import mergeTemplate from './utils/merge-template.js';
import tryGitInit from './utils/git';
import packageJSON from './resources/json/common/package.json';
import createPackageJSON from './resources/json/common/package.json';
import jsconfig from './resources/json/js/jsconfig.json';
import adminTsconfig from './resources/json/ts/tsconfig-admin.json';
import serverTsconfig from './resources/json/ts/tsconfig-server.json';
import { createDatabaseConfig, generateDbEnvVariables } from './resources/templates/database';
import createEnvFile from './resources/templates/env';
import { Configuration, Scope, isStderrError } from './types';
import { Scope, isStderrError } from './types';
export default async function createProject(
scope: Scope,
{ client, connection, dependencies }: Configuration
) {
console.log(`Creating a new Strapi application at ${chalk.green(scope.rootPath)}.`);
console.log('Creating files.');
const resources = join(__dirname, 'resources');
export default async function createProject(scope: Scope) {
console.log(`Creating a new Strapi application at ${chalk.green(scope.rootPath)}.\n`);
const { rootPath, useTypescript } = scope;
const resources = join(__dirname, 'resources');
const language = useTypescript ? 'ts' : 'js';
if (!scope.isQuickstart) {
await trackUsage({ event: 'didChooseCustomDatabase', scope });
} else {
await trackUsage({ event: 'didChooseQuickstart', scope });
}
try {
const language = useTypescript ? 'ts' : 'js';
// copy files
await fse.copy(join(resources, 'files', language), rootPath);
@ -53,64 +54,36 @@ export default async function createProject(
// Copy common dot files
copyDotFilesFromSubDirectory('common');
// Copy JS dot files
// For now we only support javascript and typescript, so if we're not using
// typescript, then we can assume we're using javascript. We'll need to change
// this behavior when we'll abstract the supported languages even more.
if (!useTypescript) {
copyDotFilesFromSubDirectory('js');
}
await trackUsage({ event: 'didCopyProjectFiles', scope });
// copy templates
await fse.writeJSON(
join(rootPath, 'package.json'),
packageJSON({
strapiDependencies: scope.strapiDependencies,
additionalsDependencies: dependencies,
strapiVersion: scope.strapiVersion,
projectName: _.kebabCase(scope.name),
uuid: scope.uuid,
packageJsonStrapi: scope.packageJsonStrapi,
}),
{
spaces: 2,
}
);
await createPackageJSON(scope);
await trackUsage({ event: 'didWritePackageJSON', scope });
if (useTypescript) {
const filesMap = {
'tsconfig-admin.json.js': 'src/admin',
'tsconfig-server.json.js': '.',
};
const tsConfigs = [
{
path: 'src/admin/tsconfig.json',
content: adminTsconfig(),
},
{
path: 'tsconfig.json',
content: serverTsconfig(),
},
];
for (const [fileName, path] of Object.entries(filesMap)) {
const destPath = join(rootPath, path, 'tsconfig.json');
if (fileName === 'tsconfig-admin.json.js') {
await fse.writeJSON(destPath, adminTsconfig(), { spaces: 2 });
}
if (fileName === 'tsconfig-server.json.js') {
await fse.writeJSON(destPath, serverTsconfig(), { spaces: 2 });
}
for (const { path, content } of tsConfigs) {
await fse.writeJSON(join(rootPath, path), content, { spaces: 2 });
}
} else {
const filesMap = { 'jsconfig.json.js': '.' };
for (const [, path] of Object.entries(filesMap)) {
const destPath = join(rootPath, path, 'jsconfig.json');
await fse.writeJSON(destPath, jsconfig(), { spaces: 2 });
}
await fse.writeJSON(join(rootPath, 'jsconfig.json'), jsconfig(), { spaces: 2 });
}
// ensure node_modules is created
await fse.ensureDir(join(rootPath, 'node_modules'));
// create config/database
await fse.appendFile(join(rootPath, '.env'), generateDbEnvVariables({ client, connection }));
await fse.appendFile(join(rootPath, '.env'), generateDbEnvVariables(scope));
await fse.writeFile(
join(rootPath, `config/database.${language}`),
createDatabaseConfig({ useTypescript })
@ -138,44 +111,25 @@ export default async function createProject(
await trackUsage({ event: 'willInstallProjectDependencies', scope });
const installPrefix = chalk.yellow('Installing dependencies:');
const loader = ora(installPrefix).start();
const logInstall = (chunk = '') => {
loader.text = `${installPrefix} ${chunk.toString().split('\n').join(' ')}`;
};
console.log(`Installing dependencies with ${chalk.bold(scope.packageManager)}\n`);
try {
if (scope.installDependencies !== false) {
const runner = runInstall(scope);
runner.stdout?.on('data', logInstall);
runner.stderr?.on('data', logInstall);
await runner;
await runInstall(scope);
}
loader.stop();
console.log(`Dependencies installed ${chalk.green('successfully')}.`);
await trackUsage({ event: 'didInstallProjectDependencies', scope });
} catch (error) {
const stderr = isStderrError(error) ? error.stderr : '';
loader.stop();
await trackUsage({
event: 'didNotInstallProjectDependencies',
scope,
error: stderr.slice(-1024),
});
console.error(`${chalk.red('Error')} while installing dependencies:`);
console.error(stderr);
await captureStderr('didNotInstallProjectDependencies', error);
console.log(chalk.black.bgWhite(' Keep trying!'));
console.log();
console.log(
chalk.bold(
'Oh, it seems that you encountered errors while installing dependencies in your project.'
@ -183,12 +137,10 @@ export default async function createProject(
);
console.log(`Don't give up, your project was created correctly.`);
console.log(
`Fix the issues mentioned in the installation errors and try to run the following command:`
`Fix the issues mentioned in the installation errors and try to run the following command`
);
console.log();
console.log(
`cd ${chalk.green(rootPath)} && ${chalk.cyan(scope.useYarn ? 'yarn' : 'npm')} install`
);
console.log(`cd ${chalk.green(rootPath)} && ${chalk.cyan(scope.packageManager)} install`);
console.log();
stopProcess();
@ -205,7 +157,7 @@ export default async function createProject(
console.log();
console.log(`Your application was created at ${chalk.green(rootPath)}.\n`);
const cmd = chalk.cyan(scope.useYarn ? 'yarn' : 'npm run');
const cmd = chalk.cyan(`${scope.packageManager} run`);
console.log('Available commands in your project:');
console.log();
@ -228,19 +180,54 @@ export default async function createProject(
console.log(` ${chalk.cyan('cd')} ${rootPath}`);
console.log(` ${cmd} develop`);
console.log();
if (scope.runApp !== true) return;
console.log(`Running your Strapi application.`);
try {
await trackUsage({ event: 'willStartServer', scope });
await execa('npm', ['run', 'develop'], {
stdio: 'inherit',
cwd: scope.rootPath,
env: {
FORCE_COLOR: '1',
},
});
} catch (error) {
if (typeof error === 'string' || error instanceof Error) {
await trackUsage({
event: 'didNotStartServer',
scope,
error,
});
}
process.exit(1);
}
}
const installArguments = ['install', '--production', '--no-optional'];
function runInstall({ rootPath, useYarn }: Scope) {
if (useYarn) {
// Increase timeout for slow internet connections.
installArguments.push('--network-timeout 1000000');
const installArguments = ['install'];
return execa('yarnpkg', installArguments, {
cwd: rootPath,
stdin: 'ignore',
});
const installArgumentsMap = {
npm: ['--legacy-peer-deps'],
yarn: ['--network-timeout 1000000'],
pnpm: [],
};
function runInstall({ rootPath, packageManager }: Scope) {
const options: execa.Options = {
cwd: rootPath,
stdio: 'inherit',
env: {
...process.env,
NODE_ENV: 'development',
},
};
if (packageManager in installArgumentsMap) {
installArguments.push(...(installArgumentsMap[packageManager] ?? []));
}
return execa('npm', installArguments, { cwd: rootPath, stdin: 'ignore' });
return execa(packageManager, installArguments, options);
}

View File

@ -1,48 +0,0 @@
import execa from 'execa';
import { trackUsage, captureStderr } from './utils/usage';
import defaultConfigs from './utils/db-configs.js';
import clientDependencies from './utils/db-client-dependencies.js';
import createProject from './create-project';
import type { Configuration, Scope } from './types';
export default async function createQuickStartProject(scope: Scope) {
console.log('Creating a quickstart project.');
await trackUsage({ event: 'didChooseQuickstart', scope });
// get default sqlite config
const client = 'sqlite';
const configuration: Configuration = {
client,
connection: defaultConfigs[client],
dependencies: { ...clientDependencies({ client }), ...scope.additionalsDependencies },
};
await createProject(scope, configuration);
if (scope.runQuickstartApp !== true) return;
console.log(`Running your Strapi application.`);
try {
await trackUsage({ event: 'willStartServer', scope });
await execa('npm', ['run', 'develop'], {
stdio: 'inherit',
cwd: scope.rootPath,
env: {
FORCE_COLOR: '1',
},
});
} catch (error) {
if (typeof error === 'string' || error instanceof Error) {
await trackUsage({
event: 'didNotStartServer',
scope,
error,
});
await captureStderr('didNotStartServer', error);
}
process.exit(1);
}
}

View File

@ -1,28 +0,0 @@
import { trackUsage } from './utils/usage';
import checkInstallPath from './utils/check-install-path';
import createCLIDatabaseProject from './create-cli-db-project';
import createCustomizedProject from './create-customized-project';
import createQuickStartProject from './create-quickstart-project';
import type { Scope } from './types';
export default async (scope: Scope) => {
const hasDatabaseConfig = Boolean(scope.database);
// check rootPath is empty
checkInstallPath(scope.rootPath);
await trackUsage({ event: 'willCreateProject', scope });
// if database config is provided don't test the connection and create the project directly
if (hasDatabaseConfig) {
return createCLIDatabaseProject(scope);
}
// if cli quickstart create project with default sqlite options
if (scope.quick === true) {
return createQuickStartProject(scope);
}
// create a project with full list of questions
return createCustomizedProject(scope);
};

View File

@ -3,41 +3,37 @@ import { readFileSync } from 'node:fs';
import os from 'node:os';
import readline from 'node:readline';
import crypto from 'crypto';
import * as sentry from '@sentry/node';
import hasYarn from './utils/has-yarn';
import checkRequirements from './utils/check-requirements';
import { trackError, captureException } from './utils/usage';
import parseDatabaseArguments from './utils/parse-db-arguments';
import generateNew from './generate-new';
import machineID from './utils/machine-id';
import type { Scope, NewOptions } from './types';
export { default as checkInstallPath } from './utils/check-install-path';
import checkRequirements from './utils/check-requirements';
import { trackError, trackUsage } from './utils/usage';
import machineID from './utils/machine-id';
import type { Scope, Options } from './types';
import checkInstallPath from './utils/check-install-path';
import createProject from './create-project';
import { addDatabaseDependencies } from './utils/database';
export type { Options };
const packageJson = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf8'));
export const generateNewApp = (projectDirectory: string, options: Partial<NewOptions>) => {
sentry.init({
dsn: 'https://841d2b2c9b4d4b43a4cde92794cb705a@sentry.io/1762059',
});
export const generateNewApp = async (options: Options) => {
checkRequirements();
const rootPath = resolve(projectDirectory);
const rootPath = await checkInstallPath(options.directory);
const tmpPath = join(os.tmpdir(), `strapi${crypto.randomBytes(6).toString('hex')}`);
const useNpm = options.useNpm !== undefined;
const scope: Scope = {
rootPath,
name: basename(rootPath),
// disable quickstart run app after creation
runQuickstartApp: options.run !== false,
useTypescript: options.useTypescript,
packageManager: options.packageManager,
database: options.database,
runApp: options.runApp ?? false,
isQuickstart: options.isQuickstart,
// use pacakge version as strapiVersion (all packages have the same version);
strapiVersion: packageJson.version,
debug: options.debug !== undefined,
quick: options.quickstart,
template: options.template,
packageJsonStrapi: {
template: options.template,
@ -47,50 +43,50 @@ export const generateNewApp = (projectDirectory: string, options: Partial<NewOpt
docker: process.env.DOCKER === 'true',
deviceId: machineID(),
tmpPath,
// use yarn if available and --use-npm isn't true
useYarn: !useNpm && hasYarn(),
installDependencies: true,
strapiDependencies: [
'@strapi/strapi',
'@strapi/plugin-users-permissions',
'@strapi/plugin-cloud',
],
additionalsDependencies: {
devDependencies: {},
dependencies: {
'@strapi/strapi': packageJson.version,
'@strapi/plugin-users-permissions': packageJson.version,
'@strapi/plugin-cloud': packageJson.version,
// third party
react: '^18.0.0',
'react-dom': '^18.0.0',
'react-router-dom': '^6.0.0',
'styled-components': '^6.0.0',
},
useTypescript: Boolean(options.typescript),
};
sentry.configureScope(function configureScope(sentryScope) {
const tags = {
os: os.type(),
osPlatform: os.platform(),
osArch: os.arch(),
osRelease: os.release(),
version: scope.strapiVersion,
nodeVersion: process.versions.node,
docker: scope.docker,
if (scope.useTypescript) {
scope.devDependencies = {
...scope.devDependencies,
typescript: '^5',
'@types/node': '^20',
'@types/react': '^18',
'@types/react-dom': '^18',
};
}
(Object.keys(tags) as Array<keyof typeof tags>).forEach((tag) => {
sentryScope.setTag(tag, tags[tag]);
});
});
addDatabaseDependencies(scope);
parseDatabaseArguments({ scope, args: options });
initCancelCatcher();
return generateNew(scope).catch((error) => {
console.error(error);
return captureException(error).then(() => {
return trackError({ scope, error }).then(() => {
process.exit(1);
});
try {
await trackUsage({ event: 'willCreateProject', scope });
// create a project with full list of questions
return await createProject(scope);
} catch (error) {
if (!(error instanceof Error)) {
throw error;
}
console.log(`\n${error.message}\n`);
return trackError({ scope, error }).then(() => {
process.exit(1);
});
});
}
};
function initCancelCatcher() {

View File

@ -1,3 +0,0 @@
.cache
build
**/node_modules/**

View File

@ -1,27 +0,0 @@
{
"parser": "babel-eslint",
"extends": "eslint:recommended",
"env": {
"commonjs": true,
"es6": true,
"node": true,
"browser": false
},
"parserOptions": {
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": false
},
"sourceType": "module"
},
"globals": {
"strapi": true
},
"rules": {
"indent": ["error", 2, { "SwitchCase": 1 }],
"linebreak-style": ["error", "unix"],
"no-console": 0,
"quotes": ["error", "single"],
"semi": ["error", "always"]
}
}

View File

@ -1,28 +1,13 @@
import { Scope } from '../../../types';
import { join } from 'path';
import { kebabCase } from 'lodash';
import fse from 'fs-extra';
import engines from './engines';
import type { Scope } from '../../../types';
type OptsScope = Pick<
Scope,
'strapiDependencies' | 'additionalsDependencies' | 'strapiVersion' | 'uuid' | 'packageJsonStrapi'
>;
interface Opts extends OptsScope {
projectName: string;
}
export default (opts: Opts) => {
const {
strapiDependencies,
additionalsDependencies,
strapiVersion,
projectName,
uuid,
packageJsonStrapi,
} = opts;
// Finally, return the JSON.
return {
name: projectName,
export default async (scope: Scope) => {
const pkg = {
name: kebabCase(scope.name),
private: true,
version: '0.1.0',
description: 'A Strapi application',
@ -32,22 +17,21 @@ export default (opts: Opts) => {
build: 'strapi build',
strapi: 'strapi',
},
devDependencies: {},
dependencies: {
...strapiDependencies.reduce<Record<string, string>>((acc, key) => {
acc[key] = strapiVersion;
return acc;
}, {}),
...additionalsDependencies,
},
devDependencies: scope.devDependencies,
dependencies: scope.dependencies,
author: {
name: 'A Strapi developer',
},
strapi: {
uuid,
...packageJsonStrapi,
...scope.packageJsonStrapi,
uuid: scope.uuid,
},
engines,
license: 'MIT',
};
// copy templates
await fse.writeJSON(join(scope.rootPath, 'package.json'), pkg, {
spaces: 2,
});
};

View File

@ -1,6 +1,20 @@
export default () => ({
extends: '@strapi/typescript-utils/tsconfigs/admin',
compilerOptions: {
target: 'ESNext',
module: 'ESNext',
moduleResolution: 'Bundler',
useDefineForClassFields: true,
lib: ['DOM', 'DOM.Iterable', 'ESNext'],
allowJs: false,
skipLibCheck: true,
esModuleInterop: true,
allowSyntheticDefaultImports: true,
strict: true,
forceConsistentCasingInFileNames: true,
resolveJsonModule: true,
noEmit: true,
jsx: 'react-jsx',
},
include: ['../plugins/**/admin/src/**/*', './'],
exclude: ['node_modules/', 'build/', 'dist/', '**/*.test.ts'],
});

View File

@ -1,11 +1,20 @@
export default () => ({
extends: '@strapi/typescript-utils/tsconfigs/server',
compilerOptions: {
module: 'CommonJS',
moduleResolution: 'Node',
lib: ['ES2020'],
target: 'ES2019',
strict: false,
skipLibCheck: true,
forceConsistentCasingInFileNames: true,
incremental: true,
esModuleInterop: true,
resolveJsonModule: true,
noEmitOnError: true,
noImplicitThis: true,
outDir: 'dist',
rootDir: '.',
},
include: [
// Include root files
'./',

View File

@ -1,7 +1,7 @@
import fs from 'fs';
import path from 'path';
import _ from 'lodash';
import type { DatabaseInfo } from '../../types';
import type { Scope } from '../../types';
export const createDatabaseConfig = ({ useTypescript }: { useTypescript: boolean }) => {
const language = useTypescript ? 'ts' : 'js';
@ -13,21 +13,17 @@ export const createDatabaseConfig = ({ useTypescript }: { useTypescript: boolean
return compile();
};
export const generateDbEnvVariables = ({
connection,
client,
}: {
connection: DatabaseInfo;
client: string;
}) => {
const tmpl = fs.readFileSync(path.join(__dirname, 'database-templates', `${client}.template`));
export const generateDbEnvVariables = (scope: Scope) => {
const tmpl = fs.readFileSync(
path.join(__dirname, 'database-templates', `${scope.database.client}.template`)
);
const compile = _.template(tmpl.toString());
return compile({
client,
client: scope.database.client,
connection: {
...connection.connection,
ssl: connection.connection.ssl || false,
...scope.database.connection,
ssl: scope.database.connection?.ssl || false,
},
});
};

View File

@ -1,5 +1,8 @@
# Server
HOST=0.0.0.0
PORT=1337
# Secrets
APP_KEYS=<%= appKeys %>
API_TOKEN_SALT=<%= apiTokenSalt %>
ADMIN_JWT_SECRET=<%= adminJwtToken %>

View File

@ -1,55 +1,57 @@
export type PackageManager = 'npm' | 'yarn' | 'pnpm';
export interface Scope {
name?: string;
rootPath: string;
template?: string;
strapiVersion: string;
strapiDependencies: Array<string>;
installDependencies?: boolean;
additionalsDependencies: Record<string, string>;
devDependencies: Record<string, string>;
dependencies: Record<string, string>;
docker: boolean;
useYarn: boolean;
useTypescript: boolean;
runQuickstartApp: boolean;
quick?: boolean;
packageManager: PackageManager;
runApp: boolean;
isQuickstart?: boolean;
uuid?: string;
deviceId?: string;
dbforce?: boolean;
database?: DatabaseInfo;
debug?: boolean;
database: DatabaseInfo;
tmpPath: string;
packageJsonStrapi: Record<string, unknown>;
useTypescript: boolean;
}
export interface NewOptions {
useNpm: boolean;
run: boolean;
debug: boolean;
quickstart: boolean;
template: string;
starter: string;
typescript: boolean;
dbforce: boolean;
dbssl: string;
dbclient: string;
dbhost: string;
dbport: string;
dbname: string;
dbusername: string;
dbpassword: string;
dbfile: string;
}
export interface Options {
directory: string;
export interface Configuration {
client: string;
connection: DatabaseInfo;
dependencies: Record<string, string>;
packageManager: PackageManager;
runApp?: boolean;
template?: string;
starter?: string;
isQuickstart?: boolean;
useTypescript: boolean;
database: {
client: ClientName;
connection?: {
host?: string;
port?: string;
database?: string;
username?: string;
password?: string;
filename?: string;
ssl?: boolean;
};
};
}
export type ClientName = 'mysql' | 'postgres' | 'sqlite';
export interface DatabaseInfo {
client?: string;
connection: {
client: ClientName;
connection?: {
host?: string;
port?: string;
database?: string;
@ -58,7 +60,6 @@ export interface DatabaseInfo {
filename?: string;
ssl?: boolean;
};
useNullAsDefault?: boolean;
}
export interface PackageInfo {

View File

@ -1,9 +1,16 @@
import { resolve } from 'node:path';
import chalk from 'chalk';
import fse from 'fs-extra';
import stopProcess from './stop-process';
// Checks if the an empty directory exists at rootPath
export default async (rootPath: string) => {
export default async (directory: string): Promise<string> => {
if (!directory) {
stopProcess(`⛔️ Please provide a project name.`);
}
const rootPath = resolve(directory);
if (await fse.pathExists(rootPath)) {
const stat = await fse.stat(rootPath);
@ -24,4 +31,6 @@ export default async (rootPath: string) => {
);
}
}
return rootPath;
};

View File

@ -0,0 +1,14 @@
import type { Scope } from '../types';
const sqlClientModule = {
mysql: { mysql2: '3.9.4' },
postgres: { pg: '8.8.0' },
sqlite: { 'better-sqlite3': '9.4.3' },
};
export function addDatabaseDependencies(scope: Scope) {
scope.dependencies = {
...scope.dependencies,
...sqlClientModule[scope.database.client],
};
}

View File

@ -1,11 +0,0 @@
/**
* Client
*/
export default ({ client }: { client: string }) => {
switch (client) {
case 'sqlite-legacy':
return 'sqlite';
default:
return client;
}
};

View File

@ -1,13 +0,0 @@
/**
* Default db infos
*/
export default {
sqlite: {
connection: {
filename: '.tmp/data.db',
},
useNullAsDefault: true,
},
postgres: {},
mysql: {},
};

View File

@ -27,8 +27,15 @@ export default async function tryGitInit(rootDir: string) {
await execa('git', ['init'], { stdio: 'ignore', cwd: rootDir });
await execa('git', ['add', '.'], { stdio: 'ignore', cwd: rootDir });
await execa('git', ['commit', '-m', 'Initial commit from Strapi'], {
stdio: 'ignore',
cwd: rootDir,
});
return true;
} catch (_) {
} catch (e) {
console.error('Error while trying to initialize git:', e);
return false;
}
}

View File

@ -1,10 +0,0 @@
import execa from 'execa';
export default function hasYarn() {
try {
const { exitCode } = execa.commandSync('yarn --version', { shell: true });
return exitCode === 0;
} catch (err) {
return false;
}
}

View File

@ -1,52 +0,0 @@
import chalk from 'chalk';
import stopProcess from './stop-process';
import type { Scope, DatabaseInfo, NewOptions } from '../types';
interface Options {
scope: Scope;
args: Partial<NewOptions>;
}
const DB_ARGS = ['dbclient', 'dbhost', 'dbport', 'dbname', 'dbusername', 'dbpassword'];
const VALID_CLIENTS = ['sqlite', 'mysql', 'postgres'];
export default function parseDatabaseArguments({ scope, args }: Options) {
const argKeys = Object.keys(args);
const matchingArgs = DB_ARGS.filter((key) => argKeys.includes(key));
const missingArgs = DB_ARGS.filter((key) => !argKeys.includes(key));
if (matchingArgs.length === 0) return;
if (matchingArgs.length !== DB_ARGS.length && args.dbclient !== 'sqlite') {
return stopProcess(`Required database arguments are missing: ${missingArgs.join(', ')}.`);
}
if (!args.dbclient || !VALID_CLIENTS.includes(args.dbclient)) {
return stopProcess(
`Invalid client ${chalk.yellow(args.dbclient)}. Possible choices: ${VALID_CLIENTS.join(
', '
)}.`
);
}
scope.dbforce = args.dbforce !== undefined;
const database: DatabaseInfo = {
client: args.dbclient,
connection: {
host: args.dbhost,
port: args.dbport,
database: args.dbname,
username: args.dbusername,
password: args.dbpassword,
filename: args.dbfile,
},
};
if (args.dbssl !== undefined) {
database.connection.ssl = args.dbssl === 'true';
}
scope.database = database;
}

View File

@ -1,7 +1,6 @@
import os from 'os';
import _ from 'lodash';
import * as sentry from '@sentry/node';
import { Scope, StderrError, isStderrError } from '../types';
import { Scope, StderrError } from '../types';
type TrackError = Error | string | StderrError;
@ -12,43 +11,6 @@ function addPackageJsonStrapiMetadata(metadata: Record<string, unknown>, scope:
return _.defaults(metadata, packageJsonStrapi);
}
export async function captureException(error: Error) {
try {
sentry.captureException(error);
await sentry.flush();
} catch (err) {
/** ignore errors */
return Promise.resolve();
}
}
async function captureError(message: string) {
try {
sentry.captureMessage(message, 'error');
await sentry.flush();
} catch (err) {
/** ignore errors */
return Promise.resolve();
}
}
export function captureStderr(name: string, error: unknown) {
if (isStderrError(error) && error.stderr.trim() !== '') {
error.stderr
.trim()
.split('\n')
.forEach((line) => {
sentry.addBreadcrumb({
category: 'stderr',
message: line,
level: 'error',
});
});
}
return captureError(name);
}
const getProperties = (scope: Scope, error?: TrackError) => {
const eventProperties = {
error: typeof error === 'string' ? error : error && error.message,
@ -63,11 +25,11 @@ const getProperties = (scope: Scope, error?: TrackError) => {
const groupProperties = {
version: scope.strapiVersion,
docker: scope.docker,
useYarn: scope.useYarn,
// useYarn: scope.useYarn,
useTypescriptOnServer: scope.useTypescript,
useTypescriptOnAdmin: scope.useTypescript,
isHostedOnStrapiCloud: process.env.STRAPI_HOSTING === 'strapi.cloud',
noRun: (scope.runQuickstartApp !== true).toString(),
noRun: (scope.runApp !== true).toString(),
projectId: scope.uuid,
};

View File

@ -1,23 +0,0 @@
'use strict';
const path = require('path');
const fs = require('fs-extra');
const adminTsConfig = require('../../tsconfigs/admin.json');
module.exports = async (dest) => {
const tsConfig = {
...adminTsConfig,
include: ['../../../src/admin/*', '../../../src/**/**/admin/src/*'],
exclude: ['node_modules', '**/*.test.js', '*.js'],
};
const filePath = path.join(dest, 'admin', 'src', 'tsconfig.json');
try {
await fs.ensureFile(filePath);
await fs.writeJSON(filePath, tsConfig, { spaces: 2 });
} catch (err) {
console.log(err);
}
};

View File

@ -1,5 +0,0 @@
'use strict';
const createTSConfigFile = require('./create-tsconfig-file');
module.exports = { createTSConfigFile };

View File

@ -2,15 +2,12 @@
const compile = require('./compile');
const compilers = require('./compilers');
const admin = require('./admin');
const utils = require('./utils');
const generators = require('./generators');
module.exports = {
compile,
compilers,
admin,
generators,
...utils,
};

View File

@ -1,19 +0,0 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"noEmit": true,
"jsx": "react-jsx"
}
}

View File

@ -4,7 +4,7 @@ const path = require('path');
const fs = require('fs');
const { rimraf } = require('rimraf');
const execa = require('execa');
const generateNew = require('../../packages/generators/app/dist/generate-new');
const createProject = require('../../packages/generators/app/dist/create-project');
/**
* Deletes a test app
@ -21,30 +21,27 @@ const cleanTestApp = async (appPath) => {
* @param {database} options.database - Arguments to create the testApp with the provided database params
*/
const generateTestApp = async ({ appPath, database, template, link = false }) => {
const pkg = require(path.resolve(__dirname, '../../packages/core/strapi/package.json'));
const scope = {
database,
rootPath: path.resolve(appPath),
name: path.basename(appPath),
packageManager: 'yarn',
// disable quickstart run app after creation
runQuickstartApp: false,
runApp: false,
// use package version as strapiVersion (all packages have the same version);
strapiVersion: require(path.resolve(__dirname, '../../packages/core/strapi/package.json'))
.version,
debug: false,
quick: false,
strapiVersion: pkg.version,
isQuickstart: false,
uuid: undefined,
deviceId: null,
// use yarn if available and --use-npm isn't true
useYarn: true,
installDependencies: false,
strapiDependencies: [
'@strapi/strapi',
'@strapi/plugin-users-permissions',
'@strapi/plugin-graphql',
'@strapi/plugin-documentation',
'@strapi/plugin-cloud',
],
additionalsDependencies: {
dependencies: {
'@strapi/strapi': pkg.version,
'@strapi/plugin-users-permissions': pkg.version,
'@strapi/plugin-graphql': pkg.version,
'@strapi/plugin-documentation': pkg.version,
'@strapi/plugin-cloud': pkg.version,
react: '18.2.0',
'react-dom': '18.2.0',
'react-router-dom': '^6.0.0',
@ -53,7 +50,7 @@ const generateTestApp = async ({ appPath, database, template, link = false }) =>
template: template ? path.resolve(template) : template,
};
await generateNew(scope);
await createProject(scope);
if (link) {
await linkPackages(scope);
}

View File

@ -7842,9 +7842,9 @@ __metadata:
version: 0.0.0-use.local
resolution: "@strapi/generate-new@workspace:packages/generators/app"
dependencies:
"@sentry/node": "npm:7.112.2"
"@strapi/pack-up": "npm:5.0.0"
"@types/fs-extra": "npm:11.0.4"
"@types/inquirer": "npm:8.2.5"
chalk: "npm:^4.1.2"
copyfiles: "npm:2.4.1"
execa: "npm:5.1.1"
@ -9401,6 +9401,15 @@ __metadata:
languageName: node
linkType: hard
"@types/inquirer@npm:8.2.5":
version: 8.2.5
resolution: "@types/inquirer@npm:8.2.5"
dependencies:
"@types/through": "npm:*"
checksum: ceb0fde9fd128061085f60265b2dd9545040e11aa29ded52641a302979c961dcc93988348a73444a404e9849805e1c44b53870fad787dacc3fa25f5176dc94a2
languageName: node
linkType: hard
"@types/inquirer@npm:^6.5.0":
version: 6.5.0
resolution: "@types/inquirer@npm:6.5.0"
@ -13789,6 +13798,7 @@ __metadata:
dependencies:
"@strapi/generate-new": "npm:5.0.0-beta.9"
"@strapi/pack-up": "npm:5.0.0"
"@types/inquirer": "npm:8.2.5"
commander: "npm:8.3.0"
eslint-config-custom: "npm:5.0.0-beta.9"
inquirer: "npm:8.2.5"
@ -13798,27 +13808,6 @@ __metadata:
languageName: unknown
linkType: soft
"create-strapi-starter@workspace:packages/cli/create-strapi-starter":
version: 0.0.0-use.local
resolution: "create-strapi-starter@workspace:packages/cli/create-strapi-starter"
dependencies:
"@strapi/generate-new": "npm:5.0.0-beta.9"
"@strapi/pack-up": "npm:5.0.0"
"@types/fs-extra": "npm:11.0.4"
chalk: "npm:4.1.2"
ci-info: "npm:4.0.0"
commander: "npm:8.3.0"
eslint-config-custom: "npm:5.0.0-beta.9"
execa: "npm:5.1.1"
fs-extra: "npm:11.2.0"
inquirer: "npm:8.2.5"
ora: "npm:5.4.1"
tsconfig: "npm:5.0.0-beta.9"
bin:
create-strapi-starter: ./bin/index.js
languageName: unknown
linkType: soft
"create-strapi@workspace:packages/cli/create-strapi":
version: 0.0.0-use.local
resolution: "create-strapi@workspace:packages/cli/create-strapi"