create-jst 0.3.1 → 0.4.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/README.md +15 -3
- package/package.json +16 -4
- package/src/application/projectInitializer.mjs +34 -17
- package/src/application/projectMigrator.mjs +16 -0
- package/src/cli.mjs +37 -13
- package/src/composition/createProjectApplication.mjs +4 -6
- package/src/domain/migrationCatalog.mjs +34 -0
- package/src/domain/projectSpecification.mjs +29 -10
- package/src/infrastructure/degitTemplateGateway.mjs +5 -0
- package/src/infrastructure/nodeMigrationGateway.mjs +68 -0
- package/src/infrastructure/nodeProjectGateway.mjs +107 -36
- package/src/jst.mjs +40 -0
- package/src/ui/command.mjs +80 -38
- package/src/ui/reporter.mjs +13 -9
- package/src/application/eventBus.mjs +0 -12
package/README.md
CHANGED
|
@@ -6,12 +6,24 @@ Create a web application from the [JST](https://github.com/jst-stack/jst) templa
|
|
|
6
6
|
npm create jst@latest my-app
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
The wizard derives application metadata from the project directory, so it only asks about choices that change
|
|
9
|
+
The wizard derives application metadata from the project directory, so it only asks about choices that change generated output: package manager, CSS or SCSS Modules, dependency installation, and Git initialization. Creation happens in a temporary sibling directory; failures and signals clean it up before the final atomic rename.
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
npm create jst@latest my-app -- --yes
|
|
13
|
-
npm create jst@latest my-app -- --package-manager pnpm
|
|
13
|
+
npm create jst@latest my-app -- --package-manager pnpm
|
|
14
14
|
npm create jst@latest my-app -- --dry-run
|
|
15
|
+
npm create jst@latest my-app -- --example showcase
|
|
15
16
|
```
|
|
16
17
|
|
|
17
|
-
Use `--help` to view
|
|
18
|
+
Use `--help` to view deterministic flags for automation. The initializer never overwrites a non-empty directory and released versions resolve immutable template/showcase revisions.
|
|
19
|
+
|
|
20
|
+
## Migrations
|
|
21
|
+
|
|
22
|
+
Run migrations from a clean Git working tree:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx --package=create-jst jst migrate --from 0.3.0 --to 0.4.0 --dry-run
|
|
26
|
+
npx --package=create-jst jst migrate --from 0.3.0 --to 0.4.0
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Migrations reject unsupported paths and customized source shapes before writing. Applied files are restored if the project quality gate fails.
|
package/package.json
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-jst",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Create a web application from the JST template.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"packageManager": "npm@11.6.2",
|
|
6
7
|
"bin": {
|
|
7
|
-
"create-jst": "src/cli.mjs"
|
|
8
|
+
"create-jst": "src/cli.mjs",
|
|
9
|
+
"jst": "src/jst.mjs"
|
|
8
10
|
},
|
|
9
11
|
"files": [
|
|
10
12
|
"src"
|
|
11
13
|
],
|
|
12
14
|
"engines": {
|
|
13
|
-
"node": ">=24 <25"
|
|
15
|
+
"node": ">=24.15.0 <25"
|
|
14
16
|
},
|
|
15
17
|
"scripts": {
|
|
16
|
-
"
|
|
18
|
+
"lint": "eslint . --max-warnings 0",
|
|
19
|
+
"benchmark": "node scripts/run-benchmarks.mjs",
|
|
20
|
+
"benchmark:update": "node scripts/run-benchmarks.mjs --write-baseline",
|
|
21
|
+
"test": "node --test test/integration.test.mjs test/migration.test.mjs test/options.test.mjs test/projectInitializer.test.mjs",
|
|
22
|
+
"test:generated": "node --test test/generatedProject.test.mjs",
|
|
23
|
+
"check": "npm run lint && npm test && npm pack --dry-run",
|
|
24
|
+
"prepublishOnly": "npm run check"
|
|
17
25
|
},
|
|
18
26
|
"repository": {
|
|
19
27
|
"type": "git",
|
|
@@ -27,5 +35,9 @@
|
|
|
27
35
|
"dependencies": {
|
|
28
36
|
"@clack/prompts": "^1.8.1",
|
|
29
37
|
"degit": "^3.10.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@eslint/js": "^10.0.1",
|
|
41
|
+
"eslint": "^10.10.0"
|
|
30
42
|
}
|
|
31
43
|
}
|
|
@@ -1,28 +1,45 @@
|
|
|
1
|
-
export function createProjectInitializer({
|
|
1
|
+
export function createProjectInitializer({ onProgress = () => undefined, projectGateway, templateGateway }) {
|
|
2
2
|
return {
|
|
3
3
|
async execute(specification) {
|
|
4
4
|
await projectGateway.assertDestinationIsEmpty(specification.destination)
|
|
5
|
+
const temporaryDestination = await projectGateway.prepareDestination(specification.destination)
|
|
6
|
+
const temporarySpecification = Object.freeze({ ...specification, destination: temporaryDestination })
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
try {
|
|
9
|
+
onProgress({ type: 'template.download.started', specification })
|
|
10
|
+
await templateGateway.clone(specification.template, temporaryDestination)
|
|
11
|
+
if (specification.example === 'clean') {
|
|
12
|
+
await projectGateway.validateTemplate(temporarySpecification)
|
|
13
|
+
}
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
onProgress({ type: 'project.configuration.started', specification })
|
|
16
|
+
if (specification.example === 'showcase') {
|
|
17
|
+
await projectGateway.configureExample(temporarySpecification)
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
await projectGateway.configureTemplate(temporarySpecification)
|
|
21
|
+
}
|
|
11
22
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
if (specification.packageManager !== 'npm') {
|
|
24
|
+
onProgress({ type: 'package-manager.configuration.started', specification })
|
|
25
|
+
await projectGateway.configurePackageManager(temporaryDestination, specification.packageManager)
|
|
26
|
+
}
|
|
27
|
+
if (specification.initGit) {
|
|
28
|
+
onProgress({ type: 'git.initialization.started', specification })
|
|
29
|
+
await projectGateway.initializeGit(temporaryDestination)
|
|
30
|
+
}
|
|
31
|
+
if (specification.install) {
|
|
32
|
+
onProgress({ type: 'dependencies.installation.started', specification })
|
|
33
|
+
await projectGateway.installDependencies(temporaryDestination, specification.packageManager)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
await projectGateway.commitDestination(temporaryDestination, specification.destination)
|
|
37
|
+
onProgress({ type: 'project.initialized', specification })
|
|
19
38
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
39
|
+
catch (error) {
|
|
40
|
+
await projectGateway.cleanupDestination(temporaryDestination)
|
|
41
|
+
throw error
|
|
23
42
|
}
|
|
24
|
-
|
|
25
|
-
events.publish({ type: 'project.initialized', specification })
|
|
26
43
|
},
|
|
27
44
|
}
|
|
28
45
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function createProjectMigrator({ gateway }) {
|
|
2
|
+
return {
|
|
3
|
+
async execute({ directory, dryRun, from, migrations, to }) {
|
|
4
|
+
await gateway.assertSafeGitState(directory)
|
|
5
|
+
const files = await gateway.readMigrationFiles(directory, migrations)
|
|
6
|
+
for (const migration of migrations) {
|
|
7
|
+
migration.apply(files)
|
|
8
|
+
}
|
|
9
|
+
if (dryRun) {
|
|
10
|
+
return { changedFiles: [...files.keys()].sort(), from, to }
|
|
11
|
+
}
|
|
12
|
+
await gateway.writeTransaction(directory, files, () => gateway.verify(directory))
|
|
13
|
+
return { changedFiles: [...files.keys()].sort(), from, to }
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
}
|
package/src/cli.mjs
CHANGED
|
@@ -3,34 +3,58 @@
|
|
|
3
3
|
import process from 'node:process'
|
|
4
4
|
import { realpathSync } from 'node:fs'
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import packageMetadata from '../package.json' with { type: 'json' }
|
|
6
7
|
import { createProjectApplication } from './composition/createProjectApplication.mjs'
|
|
7
8
|
import { parseCommandLine, printHelp, resolveProjectRequest } from './ui/command.mjs'
|
|
8
9
|
import { createReporter } from './ui/reporter.mjs'
|
|
9
10
|
|
|
10
|
-
if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url))
|
|
11
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
12
|
+
await run(process.argv.slice(2))
|
|
13
|
+
}
|
|
11
14
|
|
|
12
|
-
export async function run(argv, environment = process,
|
|
15
|
+
export async function run(argv, environment = process, injectedApplication) {
|
|
13
16
|
const reporter = createReporter(environment.stdout, environment.env)
|
|
17
|
+
const signals = registerCancellation(environment)
|
|
14
18
|
try {
|
|
15
19
|
const command = parseCommandLine(argv)
|
|
16
|
-
if (command.values.help)
|
|
17
|
-
|
|
20
|
+
if (command.values.help) {
|
|
21
|
+
return printHelp(environment.stdout)
|
|
22
|
+
}
|
|
23
|
+
if (command.values.version) {
|
|
24
|
+
return environment.stdout.write(`${packageMetadata.version}\n`)
|
|
25
|
+
}
|
|
18
26
|
|
|
19
27
|
const request = await resolveProjectRequest(command, environment)
|
|
20
|
-
if (request.dryRun)
|
|
28
|
+
if (request.dryRun) {
|
|
29
|
+
return reporter.plan(request.specification, request)
|
|
30
|
+
}
|
|
21
31
|
|
|
22
32
|
reporter.start(request)
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
await application.initializer.execute(request.specification)
|
|
26
|
-
}
|
|
27
|
-
finally {
|
|
28
|
-
unsubscribe()
|
|
29
|
-
}
|
|
33
|
+
const application = injectedApplication ?? createProjectApplication({ onProgress: event => reporter.progress(event), signal: signals.signal })
|
|
34
|
+
await application.initializer.execute(request.specification)
|
|
30
35
|
}
|
|
31
36
|
catch (error) {
|
|
32
|
-
if (error instanceof Error && error.name === 'PromptCancelledError')
|
|
37
|
+
if (error instanceof Error && error.name === 'PromptCancelledError') {
|
|
38
|
+
return
|
|
39
|
+
}
|
|
33
40
|
reporter.error(error instanceof Error ? error.message : String(error))
|
|
34
41
|
environment.exitCode = 1
|
|
35
42
|
}
|
|
43
|
+
finally {
|
|
44
|
+
signals.dispose()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function registerCancellation(environment) {
|
|
49
|
+
const controller = new globalThis.AbortController()
|
|
50
|
+
const abort = () => controller.abort(new Error('Setup cancelled by signal.'))
|
|
51
|
+
environment.once?.('SIGINT', abort)
|
|
52
|
+
environment.once?.('SIGTERM', abort)
|
|
53
|
+
return {
|
|
54
|
+
dispose() {
|
|
55
|
+
environment.off?.('SIGINT', abort)
|
|
56
|
+
environment.off?.('SIGTERM', abort)
|
|
57
|
+
},
|
|
58
|
+
signal: controller.signal,
|
|
59
|
+
}
|
|
36
60
|
}
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import process from 'node:process'
|
|
2
|
-
import { createEventBus } from '../application/eventBus.mjs'
|
|
3
2
|
import { createProjectInitializer } from '../application/projectInitializer.mjs'
|
|
4
3
|
import { createDegitTemplateGateway } from '../infrastructure/degitTemplateGateway.mjs'
|
|
5
4
|
import { createNodeProjectGateway } from '../infrastructure/nodeProjectGateway.mjs'
|
|
6
5
|
|
|
7
|
-
export function createProjectApplication({ nodeExecutable = process.execPath } = {}) {
|
|
8
|
-
const events = createEventBus()
|
|
6
|
+
export function createProjectApplication({ nodeExecutable = process.execPath, onProgress, signal } = {}) {
|
|
9
7
|
const initializer = createProjectInitializer({
|
|
10
|
-
|
|
11
|
-
projectGateway: createNodeProjectGateway({ nodeExecutable }),
|
|
8
|
+
onProgress,
|
|
9
|
+
projectGateway: createNodeProjectGateway({ nodeExecutable, signal }),
|
|
12
10
|
templateGateway: createDegitTemplateGateway(),
|
|
13
11
|
})
|
|
14
|
-
return {
|
|
12
|
+
return { initializer }
|
|
15
13
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const CURRENT_PROJECT_VERSION = '0.4.0'
|
|
2
|
+
|
|
3
|
+
export const migrations = Object.freeze([
|
|
4
|
+
Object.freeze({
|
|
5
|
+
from: '0.3.0',
|
|
6
|
+
to: CURRENT_PROJECT_VERSION,
|
|
7
|
+
files: Object.freeze(['eslint.config.js']),
|
|
8
|
+
apply(files) {
|
|
9
|
+
const eslintConfig = files.get('eslint.config.js')
|
|
10
|
+
if (!eslintConfig?.includes("jst.configs['strict']")) {
|
|
11
|
+
throw new Error("Migration requires eslint.config.js to use jst.configs['strict']; migrate custom ESLint composition manually.")
|
|
12
|
+
}
|
|
13
|
+
const withPolicy = eslintConfig.includes("from './jst.config.ts'")
|
|
14
|
+
? eslintConfig
|
|
15
|
+
: `import policy from './jst.config.ts'\n${eslintConfig}`
|
|
16
|
+
files.set('eslint.config.js', withPolicy.replace("jst.configs['strict']", 'jst.createConfig(policy)'))
|
|
17
|
+
files.set('jst.config.ts', "import { defineConfig } from '@jst-stack/eslint-plugin/policy'\n\nexport default defineConfig({})\n")
|
|
18
|
+
},
|
|
19
|
+
}),
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
export function resolveMigrationPath(from, to = CURRENT_PROJECT_VERSION) {
|
|
23
|
+
const path = []
|
|
24
|
+
let version = from
|
|
25
|
+
while (version !== to) {
|
|
26
|
+
const migration = migrations.find(candidate => candidate.from === version)
|
|
27
|
+
if (!migration || path.includes(migration)) {
|
|
28
|
+
throw new Error(`No supported migration path from ${from} to ${to}.`)
|
|
29
|
+
}
|
|
30
|
+
path.push(migration)
|
|
31
|
+
version = migration.to
|
|
32
|
+
}
|
|
33
|
+
return Object.freeze(path)
|
|
34
|
+
}
|
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
export const COLOR_SCHEMES = ['light', 'dark', 'auto']
|
|
2
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'
|
|
4
|
-
export const
|
|
3
|
+
export const PACKAGE_MANAGERS = ['npm', 'pnpm']
|
|
4
|
+
export const STYLE_LANGUAGES = ['css', 'scss']
|
|
5
|
+
export const EXAMPLES = ['clean', 'showcase']
|
|
6
|
+
export const TEMPLATE_SOURCE = 'jst-stack/jst#v0.4.0'
|
|
7
|
+
export const SHOWCASE_SOURCE = 'jst-stack/jst-showcase#d3625bda10ddb9c39599cc061e3f49cdb6be3922'
|
|
5
8
|
|
|
6
9
|
export function buildProjectSpecification(input) {
|
|
7
10
|
const specification = Object.freeze({
|
|
8
11
|
colorScheme: input.colorScheme ?? 'dark',
|
|
9
|
-
demo: input.demo ?? 'remove',
|
|
10
12
|
description: input.description ?? `${input.title} web application.`,
|
|
11
13
|
destination: input.destination,
|
|
14
|
+
example: input.example ?? 'clean',
|
|
12
15
|
initGit: input.initGit ?? true,
|
|
13
16
|
install: input.install ?? true,
|
|
14
17
|
language: input.language ?? 'en',
|
|
15
18
|
name: input.name,
|
|
16
19
|
packageManager: input.packageManager ?? 'npm',
|
|
17
20
|
primaryColor: input.primaryColor ?? 'lime',
|
|
18
|
-
|
|
21
|
+
style: input.style ?? 'css',
|
|
22
|
+
template: input.template ?? (input.example === 'showcase' ? SHOWCASE_SOURCE : TEMPLATE_SOURCE),
|
|
19
23
|
title: input.title,
|
|
20
24
|
})
|
|
21
25
|
validateProjectSpecification(specification)
|
|
@@ -26,12 +30,27 @@ export function validateProjectSpecification(specification) {
|
|
|
26
30
|
if (!/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(specification.name) || specification.name.length > 214) {
|
|
27
31
|
throw new Error(`Invalid npm package name: ${specification.name}`)
|
|
28
32
|
}
|
|
29
|
-
if (!specification.destination)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (!
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
if (!specification.destination) {
|
|
34
|
+
throw new Error('Project destination is required.')
|
|
35
|
+
}
|
|
36
|
+
if (!specification.title?.trim()) {
|
|
37
|
+
throw new Error('Application title is required.')
|
|
38
|
+
}
|
|
39
|
+
if (!COLOR_SCHEMES.includes(specification.colorScheme)) {
|
|
40
|
+
throw new Error(`Colour scheme must be one of: ${COLOR_SCHEMES.join(', ')}.`)
|
|
41
|
+
}
|
|
42
|
+
if (!PRIMARY_COLORS.includes(specification.primaryColor)) {
|
|
43
|
+
throw new Error(`Primary colour must be one of: ${PRIMARY_COLORS.join(', ')}.`)
|
|
44
|
+
}
|
|
45
|
+
if (!EXAMPLES.includes(specification.example)) {
|
|
46
|
+
throw new Error(`Example must be one of: ${EXAMPLES.join(', ')}.`)
|
|
47
|
+
}
|
|
48
|
+
if (!PACKAGE_MANAGERS.includes(specification.packageManager)) {
|
|
49
|
+
throw new Error(`Package manager must be one of: ${PACKAGE_MANAGERS.join(', ')}.`)
|
|
50
|
+
}
|
|
51
|
+
if (!STYLE_LANGUAGES.includes(specification.style)) {
|
|
52
|
+
throw new Error(`Style language must be one of: ${STYLE_LANGUAGES.join(', ')}.`)
|
|
53
|
+
}
|
|
35
54
|
}
|
|
36
55
|
|
|
37
56
|
export function normalizePackageName(value) {
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import degit from 'degit'
|
|
2
|
+
import { cp } from 'node:fs/promises'
|
|
3
|
+
import { isAbsolute, resolve } from 'node:path'
|
|
2
4
|
|
|
3
5
|
export function createDegitTemplateGateway() {
|
|
4
6
|
return {
|
|
5
7
|
clone(source, destination) {
|
|
8
|
+
if (isAbsolute(source) || source.startsWith('.')) {
|
|
9
|
+
return cp(resolve(source), destination, { recursive: true })
|
|
10
|
+
}
|
|
6
11
|
return degit(source, { disableCache: true, force: false, verbose: false }).clone(destination)
|
|
7
12
|
},
|
|
8
13
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process'
|
|
2
|
+
import { readFile, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { promisify } from 'node:util'
|
|
5
|
+
|
|
6
|
+
const executeFile = promisify(execFile)
|
|
7
|
+
|
|
8
|
+
export function createNodeMigrationGateway() {
|
|
9
|
+
return {
|
|
10
|
+
async assertSafeGitState(directory) {
|
|
11
|
+
try {
|
|
12
|
+
const { stdout } = await executeFile('git', ['status', '--porcelain'], { cwd: directory })
|
|
13
|
+
if (stdout.trim()) {
|
|
14
|
+
throw new Error('Migration requires a clean Git working tree.')
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error.message === 'Migration requires a clean Git working tree.') {
|
|
19
|
+
throw error
|
|
20
|
+
}
|
|
21
|
+
throw new Error('Migration requires the project to be inside a Git repository.', { cause: error })
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
async readMigrationFiles(directory, migrations) {
|
|
25
|
+
const names = new Set(migrations.flatMap(migration => migration.files))
|
|
26
|
+
const files = new Map()
|
|
27
|
+
for (const name of names) {
|
|
28
|
+
files.set(name, await readFile(join(directory, name), 'utf8'))
|
|
29
|
+
}
|
|
30
|
+
return files
|
|
31
|
+
},
|
|
32
|
+
async verify(directory) {
|
|
33
|
+
const manifest = JSON.parse(await readFile(join(directory, 'package.json'), 'utf8'))
|
|
34
|
+
const packageManager = manifest.packageManager?.startsWith('pnpm@') ? 'pnpm' : 'npm'
|
|
35
|
+
return executeFile(packageManager, ['run', 'check'], { cwd: directory })
|
|
36
|
+
},
|
|
37
|
+
async writeTransaction(directory, files, verify) {
|
|
38
|
+
const originals = new Map()
|
|
39
|
+
for (const name of files.keys()) {
|
|
40
|
+
try {
|
|
41
|
+
originals.set(name, await readFile(join(directory, name), 'utf8'))
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error.code !== 'ENOENT') {
|
|
45
|
+
throw error
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
for (const [name, content] of files) {
|
|
51
|
+
await writeFile(join(directory, name), content)
|
|
52
|
+
}
|
|
53
|
+
await verify()
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
for (const name of files.keys()) {
|
|
57
|
+
if (originals.has(name)) {
|
|
58
|
+
await writeFile(join(directory, name), originals.get(name))
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
await rm(join(directory, name), { force: true })
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
throw error
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -1,31 +1,50 @@
|
|
|
1
1
|
import { spawn, execFile } from 'node:child_process'
|
|
2
2
|
import { existsSync } from 'node:fs'
|
|
3
|
-
import { lstat, readFile, readdir, rm, writeFile } from 'node:fs/promises'
|
|
4
|
-
import { join } from 'node:path'
|
|
3
|
+
import { lstat, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { basename, dirname, join } from 'node:path'
|
|
5
|
+
import process from 'node:process'
|
|
5
6
|
import { promisify } from 'node:util'
|
|
6
7
|
|
|
7
8
|
const executeFile = promisify(execFile)
|
|
8
9
|
const installCommands = {
|
|
9
|
-
bun: ['install'],
|
|
10
10
|
npm: ['install', '--no-audit', '--no-fund'],
|
|
11
11
|
pnpm: ['install'],
|
|
12
|
-
yarn: ['install'],
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
export function createNodeProjectGateway({ nodeExecutable }) {
|
|
14
|
+
export function createNodeProjectGateway({ nodeExecutable, signal }) {
|
|
16
15
|
return {
|
|
17
16
|
assertDestinationIsEmpty,
|
|
18
|
-
|
|
17
|
+
cleanupDestination,
|
|
18
|
+
commitDestination,
|
|
19
|
+
configureExample,
|
|
20
|
+
configurePackageManager: (destination, packageManager) => configurePackageManager(destination, packageManager, signal),
|
|
19
21
|
configureTemplate,
|
|
20
22
|
initializeGit,
|
|
21
23
|
installDependencies,
|
|
24
|
+
prepareDestination,
|
|
25
|
+
validateTemplate,
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
async function assertDestinationIsEmpty(destination) {
|
|
25
|
-
if (!existsSync(destination))
|
|
29
|
+
if (!existsSync(destination)) {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
26
32
|
const stat = await lstat(destination)
|
|
27
|
-
if (!stat.isDirectory())
|
|
28
|
-
|
|
33
|
+
if (!stat.isDirectory()) {
|
|
34
|
+
throw new Error(`Destination is not a directory: ${destination}`)
|
|
35
|
+
}
|
|
36
|
+
if ((await readdir(destination)).length > 0) {
|
|
37
|
+
throw new Error(`Destination is not empty: ${destination}`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function cleanupDestination(destination) {
|
|
42
|
+
return rm(destination, { force: true, recursive: true })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function commitDestination(temporaryDestination, destination) {
|
|
46
|
+
await rm(destination, { force: true, recursive: true })
|
|
47
|
+
await rename(temporaryDestination, destination)
|
|
29
48
|
}
|
|
30
49
|
|
|
31
50
|
function configureTemplate(specification) {
|
|
@@ -38,49 +57,101 @@ export function createNodeProjectGateway({ nodeExecutable }) {
|
|
|
38
57
|
'--lang', specification.language,
|
|
39
58
|
'--color-scheme', specification.colorScheme,
|
|
40
59
|
'--primary-color', specification.primaryColor,
|
|
41
|
-
'--
|
|
42
|
-
], specification.destination)
|
|
60
|
+
'--style', specification.style,
|
|
61
|
+
], specification.destination, signal)
|
|
43
62
|
}
|
|
44
63
|
|
|
45
|
-
async function
|
|
46
|
-
|
|
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')
|
|
64
|
+
async function configureExample(specification) {
|
|
65
|
+
const manifestPath = join(specification.destination, 'package.json')
|
|
54
66
|
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
55
|
-
manifest.
|
|
67
|
+
manifest.name = specification.name
|
|
56
68
|
await writeFile(manifestPath, `${JSON.stringify(manifest, null, '\t')}\n`)
|
|
57
|
-
await rm(join(destination, 'package-lock.json'), { force: true })
|
|
58
69
|
}
|
|
59
70
|
|
|
60
71
|
function initializeGit(destination) {
|
|
61
|
-
return run('git', ['init', '--initial-branch=main'], destination)
|
|
72
|
+
return run('git', ['init', '--initial-branch=main'], destination, signal)
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
function installDependencies(destination, packageManager) {
|
|
65
|
-
return run(packageManager, installCommands[packageManager], destination)
|
|
76
|
+
return run(packageManager, installCommands[packageManager], destination, signal)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function validateTemplate(specification) {
|
|
80
|
+
const contract = JSON.parse(await readFile(join(specification.destination, 'jst.template.json'), 'utf8'))
|
|
81
|
+
if (contract.schemaVersion !== 1) {
|
|
82
|
+
throw new Error(`Unsupported template contract schema: ${contract.schemaVersion}.`)
|
|
83
|
+
}
|
|
84
|
+
const selected = {
|
|
85
|
+
colorSchemes: specification.colorScheme,
|
|
86
|
+
primaryColors: specification.primaryColor,
|
|
87
|
+
styles: specification.style,
|
|
88
|
+
}
|
|
89
|
+
for (const [option, value] of Object.entries(selected)) {
|
|
90
|
+
if (!contract.options?.[option]?.includes(value)) {
|
|
91
|
+
throw new Error(`Template ${contract.templateVersion} does not support ${option}: ${value}.`)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function configurePackageManager(destination, packageManager, signal) {
|
|
98
|
+
const manifestPath = join(destination, 'package.json')
|
|
99
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
100
|
+
delete manifest.packageManager
|
|
101
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, '\t')}\n`)
|
|
102
|
+
await rm(join(destination, 'package-lock.json'), { force: true })
|
|
103
|
+
|
|
104
|
+
let version
|
|
105
|
+
try {
|
|
106
|
+
({ stdout: version } = await executeFile(packageManager, ['--version'], { cwd: destination, signal }))
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
throw new Error(`Could not run ${packageManager}. Install it or choose another package manager.`)
|
|
110
|
+
}
|
|
111
|
+
const packageManagerEntry = ['packageManager', `${packageManager}@${version.trim()}`]
|
|
112
|
+
const entries = Object.entries(manifest).flatMap(entry => entry[0] === 'engines' ? [packageManagerEntry, entry] : [entry])
|
|
113
|
+
if (!manifest.engines) {
|
|
114
|
+
entries.push(packageManagerEntry)
|
|
115
|
+
}
|
|
116
|
+
await writeFile(manifestPath, `${JSON.stringify(Object.fromEntries(entries), null, '\t')}\n`)
|
|
117
|
+
if (packageManager === 'pnpm') {
|
|
118
|
+
await writeFile(
|
|
119
|
+
join(destination, 'pnpm-workspace.yaml'),
|
|
120
|
+
'minimumReleaseAge: 10080\nminimumReleaseAgeExcludePrune: true\ntrustPolicy: no-downgrade\ntrustPolicyIgnoreAfter: 10080\nshellEmulator: true\n',
|
|
121
|
+
)
|
|
122
|
+
await replaceReadmeCommands(destination, 'pnpm')
|
|
66
123
|
}
|
|
67
124
|
}
|
|
68
125
|
|
|
69
|
-
function
|
|
126
|
+
async function replaceReadmeCommands(destination, packageManager) {
|
|
127
|
+
const path = join(destination, 'README.md')
|
|
128
|
+
try {
|
|
129
|
+
const readme = await readFile(path, 'utf8')
|
|
130
|
+
await writeFile(path, readme.replaceAll('npm run ', `${packageManager} run `).replaceAll('npm ci', `${packageManager} install --frozen-lockfile`))
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (error?.code !== 'ENOENT') {
|
|
134
|
+
throw error
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function prepareDestination(destination) {
|
|
140
|
+
const temporaryDestination = join(dirname(destination), `.${basename(destination)}-${process.pid}-${Date.now()}.tmp`)
|
|
141
|
+
await rm(temporaryDestination, { force: true, recursive: true })
|
|
142
|
+
return temporaryDestination
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function run(command, args, cwd, signal) {
|
|
70
146
|
return new Promise((resolve, reject) => {
|
|
71
|
-
const child = spawn(command, args, { cwd, stdio: '
|
|
72
|
-
let stdout = ''
|
|
73
|
-
let stderr = ''
|
|
74
|
-
child.stdout?.on('data', chunk => {
|
|
75
|
-
stdout += chunk
|
|
76
|
-
})
|
|
77
|
-
child.stderr?.on('data', chunk => {
|
|
78
|
-
stderr += chunk
|
|
79
|
-
})
|
|
147
|
+
const child = spawn(command, args, { cwd, signal, stdio: 'inherit' })
|
|
80
148
|
child.once('error', error => reject(new Error(`Could not run ${command}: ${error.message}`)))
|
|
81
149
|
child.once('close', code => {
|
|
82
|
-
if (code === 0)
|
|
83
|
-
|
|
150
|
+
if (code === 0) {
|
|
151
|
+
resolve()
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
reject(new Error(`${command} ${args.join(' ')} failed with exit code ${code}.`))
|
|
84
155
|
})
|
|
85
156
|
})
|
|
86
157
|
}
|
package/src/jst.mjs
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { parseArgs } from 'node:util'
|
|
6
|
+
import { createProjectMigrator } from './application/projectMigrator.mjs'
|
|
7
|
+
import { resolveMigrationPath } from './domain/migrationCatalog.mjs'
|
|
8
|
+
import { createNodeMigrationGateway } from './infrastructure/nodeMigrationGateway.mjs'
|
|
9
|
+
|
|
10
|
+
export async function run(argv, environment = process, migrator = createProjectMigrator({ gateway: createNodeMigrationGateway() })) {
|
|
11
|
+
try {
|
|
12
|
+
const command = parseCommand(argv)
|
|
13
|
+
if (command.help) {
|
|
14
|
+
environment.stdout.write('Usage: jst migrate --from <version> [--to <version>] [--dry-run]\n')
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
const migrations = resolveMigrationPath(command.from, command.to)
|
|
18
|
+
const result = await migrator.execute({ directory: environment.cwd(), dryRun: command.dryRun, from: command.from, migrations, to: command.to ?? migrations.at(-1)?.to ?? command.from })
|
|
19
|
+
environment.stdout.write(`${command.dryRun ? 'Would migrate' : 'Migrated'} ${result.from} → ${result.to}: ${result.changedFiles.join(', ')}\n`)
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
environment.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
23
|
+
environment.exitCode = 1
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseCommand(argv) {
|
|
28
|
+
const { positionals, values } = parseArgs({ args: argv, allowPositionals: true, options: {
|
|
29
|
+
'dry-run': { type: 'boolean' }, from: { type: 'string' }, to: { type: 'string' }, help: { short: 'h', type: 'boolean' },
|
|
30
|
+
}, strict: true })
|
|
31
|
+
const help = Boolean(values.help || positionals[0] !== 'migrate')
|
|
32
|
+
if (!help && !values.from) {
|
|
33
|
+
throw new Error('--from is required.')
|
|
34
|
+
}
|
|
35
|
+
return { dryRun: values['dry-run'] ?? false, from: values.from, help, to: values.to }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
39
|
+
await run(process.argv.slice(2))
|
|
40
|
+
}
|
package/src/ui/command.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { basename, resolve } from 'node:path'
|
|
2
2
|
import { parseArgs } from 'node:util'
|
|
3
3
|
import * as prompts from '@clack/prompts'
|
|
4
|
-
import { buildProjectSpecification, normalizePackageName, PACKAGE_MANAGERS, toTitle } from '../domain/projectSpecification.mjs'
|
|
4
|
+
import { buildProjectSpecification, EXAMPLES, normalizePackageName, PACKAGE_MANAGERS, STYLE_LANGUAGES, toTitle } from '../domain/projectSpecification.mjs'
|
|
5
5
|
|
|
6
6
|
export function parseCommandLine(args) {
|
|
7
7
|
return parseArgs({
|
|
@@ -9,9 +9,9 @@ export function parseCommandLine(args) {
|
|
|
9
9
|
args,
|
|
10
10
|
options: {
|
|
11
11
|
'color-scheme': { type: 'string' },
|
|
12
|
-
'demo': { type: 'string' },
|
|
13
12
|
'description': { type: 'string' },
|
|
14
13
|
'dry-run': { type: 'boolean' },
|
|
14
|
+
'example': { type: 'string' },
|
|
15
15
|
'git': { type: 'boolean' },
|
|
16
16
|
'help': { short: 'h', type: 'boolean' },
|
|
17
17
|
'lang': { type: 'string' },
|
|
@@ -22,6 +22,7 @@ export function parseCommandLine(args) {
|
|
|
22
22
|
'package-manager': { type: 'string' },
|
|
23
23
|
'primary-color': { type: 'string' },
|
|
24
24
|
'skip-install': { type: 'boolean' },
|
|
25
|
+
'style': { type: 'string' },
|
|
25
26
|
'template': { type: 'string' },
|
|
26
27
|
'title': { type: 'string' },
|
|
27
28
|
'version': { short: 'v', type: 'boolean' },
|
|
@@ -33,22 +34,17 @@ export function parseCommandLine(args) {
|
|
|
33
34
|
|
|
34
35
|
export async function resolveProjectRequest(command, environment) {
|
|
35
36
|
const { positionals, values } = command
|
|
36
|
-
|
|
37
|
-
if (values.git && values['no-git']) throw new Error('Use either --git or --no-git, not both.')
|
|
38
|
-
if (values['no-install'] && values['skip-install']) throw new Error('Use either --no-install or --skip-install, not both.')
|
|
37
|
+
validateCommand(positionals, values)
|
|
39
38
|
|
|
40
39
|
const prompt = await createPrompt(environment, values)
|
|
41
40
|
try {
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
?? await prompt?.choice('
|
|
48
|
-
??
|
|
49
|
-
const packageManager = requestedPackageManager === 'auto' ? detectedPackageManager : requestedPackageManager
|
|
50
|
-
const initGit = values.git ?? !values['no-git']
|
|
51
|
-
const install = !(values['no-install'] || values['skip-install'])
|
|
41
|
+
const identity = await resolveProjectIdentity(positionals, values, environment, prompt)
|
|
42
|
+
const packageManager = await resolvePackageManager(values, environment, prompt)
|
|
43
|
+
const decisions = await resolveProjectDecisions(values, prompt)
|
|
44
|
+
const example = values.example ?? 'clean'
|
|
45
|
+
const style = values.style
|
|
46
|
+
?? await prompt?.choice('CSS Modules language', STYLE_LANGUAGES, 'css')
|
|
47
|
+
?? 'css'
|
|
52
48
|
|
|
53
49
|
return {
|
|
54
50
|
dryRun: values['dry-run'] ?? false,
|
|
@@ -56,17 +52,18 @@ export async function resolveProjectRequest(command, environment) {
|
|
|
56
52
|
terminal: Boolean(environment.stdin.isTTY),
|
|
57
53
|
specification: buildProjectSpecification({
|
|
58
54
|
colorScheme: values['color-scheme'],
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
initGit:
|
|
63
|
-
install:
|
|
55
|
+
description: values.description ?? `${identity.title} web application.`,
|
|
56
|
+
destination: resolve(environment.cwd(), identity.directory),
|
|
57
|
+
example,
|
|
58
|
+
initGit: decisions.initGit,
|
|
59
|
+
install: decisions.install,
|
|
64
60
|
language: values.lang,
|
|
65
|
-
name,
|
|
61
|
+
name: identity.name,
|
|
66
62
|
packageManager,
|
|
67
63
|
primaryColor: values['primary-color'],
|
|
64
|
+
style,
|
|
68
65
|
template: values.template,
|
|
69
|
-
title,
|
|
66
|
+
title: identity.title,
|
|
70
67
|
}),
|
|
71
68
|
}
|
|
72
69
|
}
|
|
@@ -77,9 +74,9 @@ export async function resolveProjectRequest(command, environment) {
|
|
|
77
74
|
|
|
78
75
|
export function detectPackageManager(environment = {}) {
|
|
79
76
|
const userAgent = environment.npm_config_user_agent ?? ''
|
|
80
|
-
if (userAgent.startsWith('pnpm/'))
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
if (userAgent.startsWith('pnpm/')) {
|
|
78
|
+
return 'pnpm'
|
|
79
|
+
}
|
|
83
80
|
return 'npm'
|
|
84
81
|
}
|
|
85
82
|
|
|
@@ -93,11 +90,11 @@ The interactive wizard only asks about decisions that affect the generated proje
|
|
|
93
90
|
Application title and description are derived from the directory; pass flags only when automation needs overrides.
|
|
94
91
|
|
|
95
92
|
Options:
|
|
96
|
-
--
|
|
97
|
-
--package-manager <manager> auto, npm, pnpm, yarn, or bun
|
|
93
|
+
--package-manager <manager> auto, npm, or pnpm
|
|
98
94
|
--no-install create the project without installing dependencies
|
|
99
95
|
--git, --no-git initialize a Git repository (default: enabled)
|
|
100
96
|
--dry-run show the resolved plan without writing files
|
|
97
|
+
--example <name> clean or showcase (default: clean)
|
|
101
98
|
--no-interactive, -y, --yes accept defaults without prompts
|
|
102
99
|
--title <title> override the derived application title
|
|
103
100
|
--description <description> override the derived SEO description
|
|
@@ -105,6 +102,7 @@ Options:
|
|
|
105
102
|
--lang <language> document language (default: en)
|
|
106
103
|
--color-scheme <scheme> light, dark, or auto
|
|
107
104
|
--primary-color <colour> Mantine primary colour
|
|
105
|
+
--style <language> css or scss (default: css)
|
|
108
106
|
--template <owner/repo[#ref]> use a compatible template source
|
|
109
107
|
-v, --version show the CLI version
|
|
110
108
|
-h, --help show this help
|
|
@@ -112,14 +110,16 @@ Options:
|
|
|
112
110
|
}
|
|
113
111
|
|
|
114
112
|
async function createPrompt(environment, values) {
|
|
115
|
-
if (!environment.stdin.isTTY || values.yes || values['no-interactive'])
|
|
113
|
+
if (!environment.stdin.isTTY || values.yes || values['no-interactive']) {
|
|
114
|
+
return undefined
|
|
115
|
+
}
|
|
116
116
|
prompts.intro('create-jst')
|
|
117
117
|
return {
|
|
118
118
|
async choice(label, choices, fallback) {
|
|
119
119
|
return resolvePrompt(await prompts.select({
|
|
120
120
|
initialValue: fallback,
|
|
121
121
|
message: label,
|
|
122
|
-
options: choices.map(value => ({ hint: choiceHint(value), label:
|
|
122
|
+
options: choices.map(value => ({ hint: choiceHint(value), label: value, value })),
|
|
123
123
|
}))
|
|
124
124
|
},
|
|
125
125
|
close() {
|
|
@@ -134,24 +134,66 @@ async function createPrompt(environment, values) {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
function choiceLabel(value) {
|
|
138
|
-
return {
|
|
139
|
-
keep: 'Keep the JST demo',
|
|
140
|
-
remove: 'Start with a clean application',
|
|
141
|
-
}[value] ?? value
|
|
142
|
-
}
|
|
143
|
-
|
|
144
137
|
function choiceHint(value) {
|
|
145
138
|
return {
|
|
146
139
|
npm: 'default',
|
|
147
|
-
remove: 'recommended',
|
|
148
140
|
}[value]
|
|
149
141
|
}
|
|
150
142
|
|
|
151
143
|
function resolvePrompt(value) {
|
|
152
|
-
if (!prompts.isCancel(value))
|
|
144
|
+
if (!prompts.isCancel(value)) {
|
|
145
|
+
return value
|
|
146
|
+
}
|
|
153
147
|
prompts.cancel('Setup cancelled.')
|
|
154
148
|
const error = new Error('Setup cancelled.')
|
|
155
149
|
error.name = 'PromptCancelledError'
|
|
156
150
|
throw error
|
|
157
151
|
}
|
|
152
|
+
|
|
153
|
+
function validateCommand(positionals, values) {
|
|
154
|
+
if (positionals.length > 1) {
|
|
155
|
+
throw new Error('Only one project directory can be provided.')
|
|
156
|
+
}
|
|
157
|
+
if (values.git && values['no-git']) {
|
|
158
|
+
throw new Error('Use either --git or --no-git, not both.')
|
|
159
|
+
}
|
|
160
|
+
if (values['no-install'] && values['skip-install']) {
|
|
161
|
+
throw new Error('Use either --no-install or --skip-install, not both.')
|
|
162
|
+
}
|
|
163
|
+
if (values.example && !EXAMPLES.includes(values.example)) {
|
|
164
|
+
throw new Error(`Example must be one of: ${EXAMPLES.join(', ')}.`)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function resolveProjectIdentity(positionals, values, environment, prompt) {
|
|
169
|
+
const directory = positionals[0]
|
|
170
|
+
?? await prompt?.text('Project directory', 'my-jst-app')
|
|
171
|
+
?? 'my-jst-app'
|
|
172
|
+
const name = values.name
|
|
173
|
+
?? normalizePackageName(basename(resolve(environment.cwd(), directory)))
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
directory,
|
|
177
|
+
name,
|
|
178
|
+
title: values.title ?? toTitle(name),
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function resolvePackageManager(values, environment, prompt) {
|
|
183
|
+
const detected = detectPackageManager(environment.env)
|
|
184
|
+
const requested = values['package-manager']
|
|
185
|
+
?? await prompt?.choice('Package manager', PACKAGE_MANAGERS, detected)
|
|
186
|
+
?? detected
|
|
187
|
+
|
|
188
|
+
return requested === 'auto' ? detected : requested
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function resolveProjectDecisions(values, prompt) {
|
|
192
|
+
const initGit = values.git ?? !values['no-git']
|
|
193
|
+
const install = !(values['no-install'] || values['skip-install'])
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
initGit: prompt ? await prompt.confirm('Initialize a Git repository?', initGit) : initGit,
|
|
197
|
+
install: prompt ? await prompt.confirm('Install dependencies?', install) : install,
|
|
198
|
+
}
|
|
199
|
+
}
|
package/src/ui/reporter.mjs
CHANGED
|
@@ -28,11 +28,12 @@ export function createReporter(output, environment = {}) {
|
|
|
28
28
|
plan(specification, request) {
|
|
29
29
|
usesClack = request.terminal
|
|
30
30
|
if (usesClack) {
|
|
31
|
-
if (!request.interactive)
|
|
31
|
+
if (!request.interactive) {
|
|
32
|
+
prompts.intro('create-jst')
|
|
33
|
+
}
|
|
32
34
|
prompts.note([
|
|
33
35
|
`Template: ${specification.template}`,
|
|
34
36
|
`Package manager: ${specification.packageManager}`,
|
|
35
|
-
`Starter demo: ${specification.demo}`,
|
|
36
37
|
`Install dependencies: ${specification.install ? 'yes' : 'no'}`,
|
|
37
38
|
`Initialize Git: ${specification.initGit ? 'yes' : 'no'}`,
|
|
38
39
|
].join('\n'), specification.destination)
|
|
@@ -43,23 +44,27 @@ export function createReporter(output, environment = {}) {
|
|
|
43
44
|
output.write(` destination ${specification.destination}\n`)
|
|
44
45
|
output.write(` template ${specification.template}\n`)
|
|
45
46
|
output.write(` package manager ${specification.packageManager}\n`)
|
|
46
|
-
output.write(` starter demo ${specification.demo}\n`)
|
|
47
47
|
output.write(` install ${specification.install ? 'yes' : 'no'}\n`)
|
|
48
48
|
output.write(` git ${specification.initGit ? 'yes' : 'no'}\n\n`)
|
|
49
49
|
},
|
|
50
50
|
start(request) {
|
|
51
51
|
usesClack = request.terminal
|
|
52
52
|
if (usesClack) {
|
|
53
|
-
if (!request.interactive)
|
|
53
|
+
if (!request.interactive) {
|
|
54
|
+
prompts.intro('create-jst')
|
|
55
|
+
}
|
|
54
56
|
return
|
|
55
57
|
}
|
|
56
58
|
output.write(`\n${paint('38;5;203', 'JST')} ${dim('A foundation for replaceable layers')}\n\n`)
|
|
57
59
|
},
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
progress(event) {
|
|
61
|
+
if (event.type === 'project.initialized') {
|
|
62
|
+
return this.success(event.specification)
|
|
63
|
+
}
|
|
61
64
|
const message = progressMessages[event.type]
|
|
62
|
-
if (!message)
|
|
65
|
+
if (!message) {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
63
68
|
if (usesClack) {
|
|
64
69
|
activeSpinner?.stop()
|
|
65
70
|
activeSpinner = prompts.spinner()
|
|
@@ -67,7 +72,6 @@ export function createReporter(output, environment = {}) {
|
|
|
67
72
|
return
|
|
68
73
|
}
|
|
69
74
|
output.write(`${dim(' ◌')} ${message(event.specification)}\n`)
|
|
70
|
-
})
|
|
71
75
|
},
|
|
72
76
|
success(specification) {
|
|
73
77
|
if (usesClack) {
|
|
@@ -1,12 +0,0 @@
|
|
|
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
|
-
}
|