create-jst 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +19 -0
- package/package.json +30 -0
- package/src/application/eventBus.mjs +12 -0
- package/src/application/projectInitializer.mjs +28 -0
- package/src/cli.mjs +35 -0
- package/src/composition/createProjectApplication.mjs +15 -0
- package/src/domain/projectSpecification.mjs +43 -0
- package/src/infrastructure/degitTemplateGateway.mjs +9 -0
- package/src/infrastructure/nodeProjectGateway.mjs +82 -0
- package/src/ui/command.mjs +130 -0
- package/src/ui/reporter.mjs +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 JST Stack
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# create-jst
|
|
2
|
+
|
|
3
|
+
Create a web application from the [JST](https://github.com/jst-stack/jst) template.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm create jst@latest my-app
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The wizard derives application metadata from the project directory, so it only asks about choices that change the generated project: starter demo, package manager, dependency installation, and Git initialization.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm create jst@latest my-app -- --yes
|
|
13
|
+
npm create jst@latest my-app -- --package-manager pnpm --demo keep
|
|
14
|
+
npm create jst@latest my-app -- --dry-run
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Use `--help` to view non-interactive options. The initializer never overwrites a non-empty directory.
|
|
18
|
+
|
|
19
|
+
See [Architecture](./ARCHITECTURE.md) for the dependency boundaries and replaceable gateways.
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-jst",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Create a web application from the JST template.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-jst": "src/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=24 <25"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/jst-stack/create-jst.git"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/jst-stack/create-jst/issues"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/jst-stack/jst#readme",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"degit": "^3.10.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function createEventBus() {
|
|
2
|
+
const subscribers = new Set()
|
|
3
|
+
return {
|
|
4
|
+
publish(event) {
|
|
5
|
+
for (const subscriber of subscribers) subscriber(event)
|
|
6
|
+
},
|
|
7
|
+
subscribe(subscriber) {
|
|
8
|
+
subscribers.add(subscriber)
|
|
9
|
+
return () => subscribers.delete(subscriber)
|
|
10
|
+
},
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function createProjectInitializer({ events, projectGateway, templateGateway }) {
|
|
2
|
+
return {
|
|
3
|
+
async execute(specification) {
|
|
4
|
+
await projectGateway.assertDestinationIsEmpty(specification.destination)
|
|
5
|
+
|
|
6
|
+
events.publish({ type: 'template.download.started', specification })
|
|
7
|
+
await templateGateway.clone(specification.template, specification.destination)
|
|
8
|
+
|
|
9
|
+
events.publish({ type: 'project.configuration.started', specification })
|
|
10
|
+
await projectGateway.configureTemplate(specification)
|
|
11
|
+
|
|
12
|
+
if (specification.packageManager !== 'npm') {
|
|
13
|
+
events.publish({ type: 'package-manager.configuration.started', specification })
|
|
14
|
+
await projectGateway.configurePackageManager(specification.destination, specification.packageManager)
|
|
15
|
+
}
|
|
16
|
+
if (specification.initGit) {
|
|
17
|
+
events.publish({ type: 'git.initialization.started', specification })
|
|
18
|
+
await projectGateway.initializeGit(specification.destination)
|
|
19
|
+
}
|
|
20
|
+
if (specification.install) {
|
|
21
|
+
events.publish({ type: 'dependencies.installation.started', specification })
|
|
22
|
+
await projectGateway.installDependencies(specification.destination, specification.packageManager)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
events.publish({ type: 'project.initialized', specification })
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
import { realpathSync } from 'node:fs'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { createProjectApplication } from './composition/createProjectApplication.mjs'
|
|
7
|
+
import { parseCommandLine, printHelp, resolveProjectRequest } from './ui/command.mjs'
|
|
8
|
+
import { createReporter } from './ui/reporter.mjs'
|
|
9
|
+
|
|
10
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) await run(process.argv.slice(2))
|
|
11
|
+
|
|
12
|
+
export async function run(argv, environment = process, application = createProjectApplication()) {
|
|
13
|
+
const reporter = createReporter(environment.stdout, environment.env)
|
|
14
|
+
try {
|
|
15
|
+
const command = parseCommandLine(argv)
|
|
16
|
+
if (command.values.help) return printHelp(environment.stdout)
|
|
17
|
+
if (command.values.version) return environment.stdout.write(`${process.env.npm_package_version ?? '0.3.0'}\n`)
|
|
18
|
+
|
|
19
|
+
const request = await resolveProjectRequest(command, environment)
|
|
20
|
+
if (request.dryRun) return reporter.plan(request.specification)
|
|
21
|
+
|
|
22
|
+
reporter.start()
|
|
23
|
+
const unsubscribe = reporter.subscribe(application.events)
|
|
24
|
+
try {
|
|
25
|
+
await application.initializer.execute(request.specification)
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
unsubscribe()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
reporter.error(error instanceof Error ? error.message : String(error))
|
|
33
|
+
environment.exitCode = 1
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
import { createEventBus } from '../application/eventBus.mjs'
|
|
3
|
+
import { createProjectInitializer } from '../application/projectInitializer.mjs'
|
|
4
|
+
import { createDegitTemplateGateway } from '../infrastructure/degitTemplateGateway.mjs'
|
|
5
|
+
import { createNodeProjectGateway } from '../infrastructure/nodeProjectGateway.mjs'
|
|
6
|
+
|
|
7
|
+
export function createProjectApplication({ nodeExecutable = process.execPath } = {}) {
|
|
8
|
+
const events = createEventBus()
|
|
9
|
+
const initializer = createProjectInitializer({
|
|
10
|
+
events,
|
|
11
|
+
projectGateway: createNodeProjectGateway({ nodeExecutable }),
|
|
12
|
+
templateGateway: createDegitTemplateGateway(),
|
|
13
|
+
})
|
|
14
|
+
return { events, initializer }
|
|
15
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const COLOR_SCHEMES = ['light', 'dark', 'auto']
|
|
2
|
+
export const PRIMARY_COLORS = ['dark', 'gray', 'red', 'pink', 'grape', 'violet', 'indigo', 'blue', 'cyan', 'teal', 'green', 'lime', 'yellow', 'orange']
|
|
3
|
+
export const PACKAGE_MANAGERS = ['npm', 'pnpm', 'yarn', 'bun']
|
|
4
|
+
export const TEMPLATE_SOURCE = 'jst-stack/jst'
|
|
5
|
+
|
|
6
|
+
export function buildProjectSpecification(input) {
|
|
7
|
+
const specification = Object.freeze({
|
|
8
|
+
colorScheme: input.colorScheme ?? 'dark',
|
|
9
|
+
demo: input.demo ?? 'remove',
|
|
10
|
+
description: input.description ?? `${input.title} web application.`,
|
|
11
|
+
destination: input.destination,
|
|
12
|
+
initGit: input.initGit ?? true,
|
|
13
|
+
install: input.install ?? true,
|
|
14
|
+
language: input.language ?? 'en',
|
|
15
|
+
name: input.name,
|
|
16
|
+
packageManager: input.packageManager ?? 'npm',
|
|
17
|
+
primaryColor: input.primaryColor ?? 'lime',
|
|
18
|
+
template: input.template ?? TEMPLATE_SOURCE,
|
|
19
|
+
title: input.title,
|
|
20
|
+
})
|
|
21
|
+
validateProjectSpecification(specification)
|
|
22
|
+
return specification
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function validateProjectSpecification(specification) {
|
|
26
|
+
if (!/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(specification.name) || specification.name.length > 214) {
|
|
27
|
+
throw new Error(`Invalid npm package name: ${specification.name}`)
|
|
28
|
+
}
|
|
29
|
+
if (!specification.destination) throw new Error('Project destination is required.')
|
|
30
|
+
if (!specification.title?.trim()) throw new Error('Application title is required.')
|
|
31
|
+
if (!COLOR_SCHEMES.includes(specification.colorScheme)) throw new Error(`Colour scheme must be one of: ${COLOR_SCHEMES.join(', ')}.`)
|
|
32
|
+
if (!PRIMARY_COLORS.includes(specification.primaryColor)) throw new Error(`Primary colour must be one of: ${PRIMARY_COLORS.join(', ')}.`)
|
|
33
|
+
if (!['keep', 'remove'].includes(specification.demo)) throw new Error('Demo mode must be either keep or remove.')
|
|
34
|
+
if (!PACKAGE_MANAGERS.includes(specification.packageManager)) throw new Error(`Package manager must be one of: ${PACKAGE_MANAGERS.join(', ')}.`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function normalizePackageName(value) {
|
|
38
|
+
return value.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'jst-app'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function toTitle(value) {
|
|
42
|
+
return value.replace(/^@[^/]+\//, '').split(/[-_.]+/).filter(Boolean).map(word => word[0].toUpperCase() + word.slice(1)).join(' ')
|
|
43
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { spawn, execFile } from 'node:child_process'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { lstat, readFile, readdir, rm, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { promisify } from 'node:util'
|
|
6
|
+
|
|
7
|
+
const executeFile = promisify(execFile)
|
|
8
|
+
const installCommands = {
|
|
9
|
+
bun: ['install'],
|
|
10
|
+
npm: ['install', '--no-audit', '--no-fund'],
|
|
11
|
+
pnpm: ['install'],
|
|
12
|
+
yarn: ['install'],
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createNodeProjectGateway({ nodeExecutable }) {
|
|
16
|
+
return {
|
|
17
|
+
assertDestinationIsEmpty,
|
|
18
|
+
configurePackageManager,
|
|
19
|
+
configureTemplate,
|
|
20
|
+
initializeGit,
|
|
21
|
+
installDependencies,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function assertDestinationIsEmpty(destination) {
|
|
25
|
+
if (!existsSync(destination)) return
|
|
26
|
+
const stat = await lstat(destination)
|
|
27
|
+
if (!stat.isDirectory()) throw new Error(`Destination is not a directory: ${destination}`)
|
|
28
|
+
if ((await readdir(destination)).length > 0) throw new Error(`Destination is not empty: ${destination}`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function configureTemplate(specification) {
|
|
32
|
+
return run(nodeExecutable, [
|
|
33
|
+
'scripts/setup-template.mjs',
|
|
34
|
+
'--yes',
|
|
35
|
+
'--name', specification.name,
|
|
36
|
+
'--title', specification.title,
|
|
37
|
+
'--description', specification.description,
|
|
38
|
+
'--lang', specification.language,
|
|
39
|
+
'--color-scheme', specification.colorScheme,
|
|
40
|
+
'--primary-color', specification.primaryColor,
|
|
41
|
+
'--demo', specification.demo,
|
|
42
|
+
], specification.destination)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function configurePackageManager(destination, packageManager) {
|
|
46
|
+
let version
|
|
47
|
+
try {
|
|
48
|
+
({ stdout: version } = await executeFile(packageManager, ['--version']))
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
throw new Error(`Could not run ${packageManager}. Install it or choose another package manager.`)
|
|
52
|
+
}
|
|
53
|
+
const manifestPath = join(destination, 'package.json')
|
|
54
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
55
|
+
manifest.packageManager = `${packageManager}@${version.trim()}`
|
|
56
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, '\t')}\n`)
|
|
57
|
+
await rm(join(destination, 'package-lock.json'), { force: true })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function initializeGit(destination) {
|
|
61
|
+
return run('git', ['init', '--initial-branch=main'], destination)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function installDependencies(destination, packageManager) {
|
|
65
|
+
return run(packageManager, installCommands[packageManager], destination, true)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function run(command, args, cwd, inheritOutput = false) {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const child = spawn(command, args, { cwd, stdio: inheritOutput ? 'inherit' : 'pipe' })
|
|
72
|
+
let stderr = ''
|
|
73
|
+
child.stderr?.on('data', chunk => {
|
|
74
|
+
stderr += chunk
|
|
75
|
+
})
|
|
76
|
+
child.once('error', error => reject(new Error(`Could not run ${command}: ${error.message}`)))
|
|
77
|
+
child.once('close', code => {
|
|
78
|
+
if (code === 0) resolve()
|
|
79
|
+
else reject(new Error(`${command} ${args.join(' ')} failed.${stderr ? `\n${stderr.trim()}` : ''}`))
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { basename, resolve } from 'node:path'
|
|
2
|
+
import { parseArgs } from 'node:util'
|
|
3
|
+
import { buildProjectSpecification, normalizePackageName, PACKAGE_MANAGERS, toTitle } from '../domain/projectSpecification.mjs'
|
|
4
|
+
|
|
5
|
+
export function parseCommandLine(args) {
|
|
6
|
+
return parseArgs({
|
|
7
|
+
allowPositionals: true,
|
|
8
|
+
args,
|
|
9
|
+
options: {
|
|
10
|
+
'color-scheme': { type: 'string' },
|
|
11
|
+
'demo': { type: 'string' },
|
|
12
|
+
'description': { type: 'string' },
|
|
13
|
+
'dry-run': { type: 'boolean' },
|
|
14
|
+
'git': { type: 'boolean' },
|
|
15
|
+
'help': { short: 'h', type: 'boolean' },
|
|
16
|
+
'lang': { type: 'string' },
|
|
17
|
+
'name': { type: 'string' },
|
|
18
|
+
'no-git': { type: 'boolean' },
|
|
19
|
+
'no-install': { type: 'boolean' },
|
|
20
|
+
'no-interactive': { type: 'boolean' },
|
|
21
|
+
'package-manager': { type: 'string' },
|
|
22
|
+
'primary-color': { type: 'string' },
|
|
23
|
+
'skip-install': { type: 'boolean' },
|
|
24
|
+
'template': { type: 'string' },
|
|
25
|
+
'title': { type: 'string' },
|
|
26
|
+
'version': { short: 'v', type: 'boolean' },
|
|
27
|
+
'yes': { short: 'y', type: 'boolean' },
|
|
28
|
+
},
|
|
29
|
+
strict: true,
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function resolveProjectRequest(command, environment) {
|
|
34
|
+
const { positionals, values } = command
|
|
35
|
+
if (positionals.length > 1) throw new Error('Only one project directory can be provided.')
|
|
36
|
+
if (values.git && values['no-git']) throw new Error('Use either --git or --no-git, not both.')
|
|
37
|
+
if (values['no-install'] && values['skip-install']) throw new Error('Use either --no-install or --skip-install, not both.')
|
|
38
|
+
|
|
39
|
+
const prompt = await createPrompt(environment, values)
|
|
40
|
+
try {
|
|
41
|
+
const directory = positionals[0] ?? await prompt?.text('Project directory', 'my-jst-app') ?? 'my-jst-app'
|
|
42
|
+
const name = values.name ?? normalizePackageName(basename(resolve(environment.cwd(), directory)))
|
|
43
|
+
const title = values.title ?? toTitle(name)
|
|
44
|
+
const detectedPackageManager = detectPackageManager(environment.env)
|
|
45
|
+
const requestedPackageManager = values['package-manager']
|
|
46
|
+
?? await prompt?.choice('Package manager', PACKAGE_MANAGERS, detectedPackageManager)
|
|
47
|
+
?? detectedPackageManager
|
|
48
|
+
const packageManager = requestedPackageManager === 'auto' ? detectedPackageManager : requestedPackageManager
|
|
49
|
+
const initGit = values.git ?? !values['no-git']
|
|
50
|
+
const install = !(values['no-install'] || values['skip-install'])
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
dryRun: values['dry-run'] ?? false,
|
|
54
|
+
specification: buildProjectSpecification({
|
|
55
|
+
colorScheme: values['color-scheme'],
|
|
56
|
+
demo: values.demo ?? await prompt?.choice('Start from', ['remove', 'keep'], 'remove') ?? 'remove',
|
|
57
|
+
description: values.description ?? `${title} web application.`,
|
|
58
|
+
destination: resolve(environment.cwd(), directory),
|
|
59
|
+
initGit: prompt ? await prompt.choice('Initialize a Git repository', ['yes', 'no'], initGit ? 'yes' : 'no') === 'yes' : initGit,
|
|
60
|
+
install: prompt ? await prompt.choice('Install dependencies', ['yes', 'no'], install ? 'yes' : 'no') === 'yes' : install,
|
|
61
|
+
language: values.lang,
|
|
62
|
+
name,
|
|
63
|
+
packageManager,
|
|
64
|
+
primaryColor: values['primary-color'],
|
|
65
|
+
template: values.template,
|
|
66
|
+
title,
|
|
67
|
+
}),
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
prompt?.close()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function detectPackageManager(environment = {}) {
|
|
76
|
+
const userAgent = environment.npm_config_user_agent ?? ''
|
|
77
|
+
if (userAgent.startsWith('pnpm/')) return 'pnpm'
|
|
78
|
+
if (userAgent.startsWith('yarn/')) return 'yarn'
|
|
79
|
+
if (userAgent.startsWith('bun/')) return 'bun'
|
|
80
|
+
return 'npm'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function printHelp(output) {
|
|
84
|
+
output.write(`Create a web application from the JST template.
|
|
85
|
+
|
|
86
|
+
Usage:
|
|
87
|
+
npm create jst@latest [directory] [options]
|
|
88
|
+
|
|
89
|
+
The interactive wizard only asks about decisions that affect the generated project.
|
|
90
|
+
Application title and description are derived from the directory; pass flags only when automation needs overrides.
|
|
91
|
+
|
|
92
|
+
Options:
|
|
93
|
+
--demo <keep|remove> retain or remove the starter demo
|
|
94
|
+
--package-manager <manager> auto, npm, pnpm, yarn, or bun
|
|
95
|
+
--no-install create the project without installing dependencies
|
|
96
|
+
--git, --no-git initialize a Git repository (default: enabled)
|
|
97
|
+
--dry-run show the resolved plan without writing files
|
|
98
|
+
--no-interactive, -y, --yes accept defaults without prompts
|
|
99
|
+
--title <title> override the derived application title
|
|
100
|
+
--description <description> override the derived SEO description
|
|
101
|
+
--name <name> override the derived npm package name
|
|
102
|
+
--lang <language> document language (default: en)
|
|
103
|
+
--color-scheme <scheme> light, dark, or auto
|
|
104
|
+
--primary-color <colour> Mantine primary colour
|
|
105
|
+
--template <owner/repo[#ref]> use a compatible template source
|
|
106
|
+
-v, --version show the CLI version
|
|
107
|
+
-h, --help show this help
|
|
108
|
+
`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function createPrompt(environment, values) {
|
|
112
|
+
if (!environment.stdin.isTTY || values.yes || values['no-interactive']) return undefined
|
|
113
|
+
const { createInterface } = await import('node:readline/promises')
|
|
114
|
+
const reader = createInterface({ input: environment.stdin, output: environment.stdout })
|
|
115
|
+
return {
|
|
116
|
+
async choice(label, choices, fallback) {
|
|
117
|
+
const rendered = choices.map((choice, index) => `${index + 1}:${choice}`).join(' ')
|
|
118
|
+
const answer = (await reader.question(` ${label} (${rendered}) [${fallback}]: `)).trim()
|
|
119
|
+
if (!answer) return fallback
|
|
120
|
+
const index = Number.parseInt(answer, 10)
|
|
121
|
+
return choices[index - 1] ?? answer
|
|
122
|
+
},
|
|
123
|
+
close() {
|
|
124
|
+
reader.close()
|
|
125
|
+
},
|
|
126
|
+
async text(label, fallback) {
|
|
127
|
+
return (await reader.question(` ${label} [${fallback}]: `)).trim() || fallback
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { basename } from 'node:path'
|
|
2
|
+
|
|
3
|
+
const progressMessages = {
|
|
4
|
+
'dependencies.installation.started': specification => `Installing dependencies with ${specification.packageManager}`,
|
|
5
|
+
'git.initialization.started': () => 'Initialising Git',
|
|
6
|
+
'package-manager.configuration.started': specification => `Preparing ${specification.packageManager}`,
|
|
7
|
+
'project.configuration.started': specification => `Configuring ${specification.title}`,
|
|
8
|
+
'template.download.started': specification => `Downloading ${specification.template}`,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createReporter(output, environment = {}) {
|
|
12
|
+
const color = Boolean(output.isTTY) && !environment.NO_COLOR
|
|
13
|
+
const paint = (code, value) => color ? `\u001B[${code}m${value}\u001B[0m` : value
|
|
14
|
+
const dim = value => paint('2', value)
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
error(message) {
|
|
18
|
+
output.write(`\n${paint('31', '✕')} ${message}\n`)
|
|
19
|
+
},
|
|
20
|
+
plan(specification) {
|
|
21
|
+
output.write(`\n${paint('38;5;203', 'JST')} ${dim('plan only — no files will be written')}\n\n`)
|
|
22
|
+
output.write(` destination ${specification.destination}\n`)
|
|
23
|
+
output.write(` template ${specification.template}\n`)
|
|
24
|
+
output.write(` package manager ${specification.packageManager}\n`)
|
|
25
|
+
output.write(` starter demo ${specification.demo}\n`)
|
|
26
|
+
output.write(` install ${specification.install ? 'yes' : 'no'}\n`)
|
|
27
|
+
output.write(` git ${specification.initGit ? 'yes' : 'no'}\n\n`)
|
|
28
|
+
},
|
|
29
|
+
start() {
|
|
30
|
+
output.write(`\n${paint('38;5;203', 'JST')} ${dim('A foundation for replaceable layers')}\n\n`)
|
|
31
|
+
},
|
|
32
|
+
subscribe(events) {
|
|
33
|
+
return events.subscribe(event => {
|
|
34
|
+
if (event.type === 'project.initialized') return this.success(event.specification)
|
|
35
|
+
const message = progressMessages[event.type]
|
|
36
|
+
if (message) output.write(`${dim(' ◌')} ${message(event.specification)}\n`)
|
|
37
|
+
})
|
|
38
|
+
},
|
|
39
|
+
success(specification) {
|
|
40
|
+
output.write(`\n${paint('38;5;114', 'Ready.')} ${specification.title} is in ${paint('38;5;203', basename(specification.destination))}.\n\n`)
|
|
41
|
+
output.write(` ${dim('$')} cd ${basename(specification.destination)}\n`)
|
|
42
|
+
output.write(` ${dim('$')} ${specification.packageManager} run dev\n\n`)
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
}
|