create-murasaki 0.0.6 → 0.0.8

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.
Files changed (2) hide show
  1. package/index.mjs +153 -10
  2. package/package.json +5 -2
package/index.mjs CHANGED
@@ -6,8 +6,8 @@ import { cp, mkdir, readFile, writeFile, stat } from 'node:fs/promises'
6
6
  import { existsSync } from 'node:fs'
7
7
  import { dirname, join, resolve } from 'node:path'
8
8
  import { fileURLToPath } from 'node:url'
9
- import { createInterface } from 'node:readline/promises'
10
9
  import { spawnSync } from 'node:child_process'
10
+ import { text, select, isCancel, cancel, intro, outro } from '@clack/prompts'
11
11
 
12
12
  // ── ANSI truecolor (Oomurasaki palette) ────────────────────────────────
13
13
  const BRIGHT = '\x1b[38;2;168;85;247m'
@@ -132,15 +132,136 @@ function runInstall(targetDir, pm) {
132
132
  return result.status === 0
133
133
  }
134
134
 
135
+ function exitIfCancel(value) {
136
+ if (isCancel(value)) {
137
+ cancel('Cancelled.')
138
+ process.exit(0)
139
+ }
140
+ return value
141
+ }
142
+
135
143
  async function promptForName() {
136
- const rl = createInterface({ input: process.stdin, output: process.stdout })
137
- const answer = await rl.question(` ${c(DEEP)}?${c(RESET)} ${c(BOLD)}Project name${c(RESET)} ${c(DIM)}(my-app):${c(RESET)} `)
138
- rl.close()
139
- return answer.trim() || 'my-app'
144
+ const value = await text({
145
+ message: 'Project name',
146
+ placeholder: 'my-app',
147
+ defaultValue: 'my-app',
148
+ validate(v) {
149
+ const t = (v || '').trim()
150
+ if (!t) return // empty → use defaultValue
151
+ if (!isValidPackageName(t)) return 'Use lowercase letters, digits, dot, hyphen, underscore. Start with a letter or digit.'
152
+ },
153
+ })
154
+ return exitIfCancel(value)
155
+ }
156
+
157
+ async function promptForLinter() {
158
+ const value = await select({
159
+ message: 'Which linter would you like to use?',
160
+ options: [
161
+ { value: 'biome', label: 'Biome', hint: 'fast, single tool, recommended' },
162
+ { value: 'eslint', label: 'ESLint', hint: 'classic, huge ecosystem' },
163
+ { value: 'none', label: 'None', hint: 'add your own later' },
164
+ ],
165
+ initialValue: 'biome',
166
+ })
167
+ return exitIfCancel(value)
168
+ }
169
+
170
+ // ── Linter installers ─────────────────────────────────────────────────
171
+ async function applyBiome(targetDir) {
172
+ const biomeJson = `{
173
+ "$schema": "https://biomejs.dev/schemas/2.5.1/schema.json",
174
+ "vcs": {
175
+ "enabled": true,
176
+ "clientKind": "git",
177
+ "useIgnoreFile": true
178
+ },
179
+ "files": {
180
+ "includes": ["src/**/*.{ts,tsx,js,jsx}", "!**/node_modules", "!**/dist"]
181
+ },
182
+ "formatter": {
183
+ "enabled": true,
184
+ "indentStyle": "space",
185
+ "indentWidth": 2,
186
+ "lineWidth": 100
187
+ },
188
+ "linter": {
189
+ "enabled": true,
190
+ "rules": { "recommended": true }
191
+ },
192
+ "javascript": {
193
+ "formatter": {
194
+ "quoteStyle": "single",
195
+ "semicolons": "asNeeded",
196
+ "trailingCommas": "all"
197
+ }
198
+ }
199
+ }
200
+ `
201
+ await writeFile(join(targetDir, 'biome.json'), biomeJson)
202
+ await patchPackageJson(targetDir, (pkg) => {
203
+ pkg.devDependencies = { ...(pkg.devDependencies || {}), '@biomejs/biome': '^2.5.1' }
204
+ pkg.scripts = {
205
+ ...(pkg.scripts || {}),
206
+ check: 'biome check',
207
+ format: 'biome format --write',
208
+ lint: 'biome lint',
209
+ fix: 'biome check --write',
210
+ }
211
+ })
212
+ }
213
+
214
+ async function applyEslint(targetDir) {
215
+ const eslintConfig = `// eslint.config.js — flat config (ESLint v9+)
216
+
217
+ import js from '@eslint/js'
218
+ import tseslint from 'typescript-eslint'
219
+ import reactPlugin from 'eslint-plugin-react'
220
+ import reactHooks from 'eslint-plugin-react-hooks'
221
+
222
+ export default tseslint.config(
223
+ js.configs.recommended,
224
+ ...tseslint.configs.recommended,
225
+ {
226
+ files: ['src/**/*.{ts,tsx,js,jsx}'],
227
+ plugins: { react: reactPlugin, 'react-hooks': reactHooks },
228
+ rules: {
229
+ ...reactPlugin.configs.recommended.rules,
230
+ ...reactHooks.configs.recommended.rules,
231
+ 'react/react-in-jsx-scope': 'off',
232
+ },
233
+ settings: { react: { version: 'detect' } },
234
+ },
235
+ { ignores: ['node_modules/**', 'dist/**'] },
236
+ )
237
+ `
238
+ await writeFile(join(targetDir, 'eslint.config.js'), eslintConfig)
239
+ await patchPackageJson(targetDir, (pkg) => {
240
+ pkg.devDependencies = {
241
+ ...(pkg.devDependencies || {}),
242
+ 'eslint': '^9.20.0',
243
+ '@eslint/js': '^9.20.0',
244
+ 'typescript-eslint': '^8.20.0',
245
+ 'eslint-plugin-react': '^7.37.0',
246
+ 'eslint-plugin-react-hooks': '^5.1.0',
247
+ }
248
+ pkg.scripts = {
249
+ ...(pkg.scripts || {}),
250
+ lint: 'eslint .',
251
+ 'lint:fix': 'eslint . --fix',
252
+ }
253
+ })
254
+ }
255
+
256
+ async function patchPackageJson(targetDir, mutator) {
257
+ const pkgPath = join(targetDir, 'package.json')
258
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
259
+ mutator(pkg)
260
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
140
261
  }
141
262
 
142
263
  // ── Scaffold ───────────────────────────────────────────────────────────
143
- async function scaffold(projectName) {
264
+ async function scaffold(projectName, linter) {
144
265
  const __dirname = dirname(fileURLToPath(import.meta.url))
145
266
  const templateDir = join(__dirname, 'templates', 'default')
146
267
  const targetDir = resolve(process.cwd(), projectName)
@@ -167,6 +288,15 @@ async function scaffold(projectName) {
167
288
  const pkgPatched = pkgRaw.replace(/"__PROJECT_NAME__"/, JSON.stringify(projectName))
168
289
  await writeFile(pkgPath, pkgPatched)
169
290
 
291
+ // Apply linter overlay
292
+ if (linter === 'biome') {
293
+ log(` ${c(DIM)}○${c(RESET)} Adding ${c(BOLD)}Biome${c(RESET)}...`)
294
+ await applyBiome(targetDir)
295
+ } else if (linter === 'eslint') {
296
+ log(` ${c(DIM)}○${c(RESET)} Adding ${c(BOLD)}ESLint${c(RESET)}...`)
297
+ await applyEslint(targetDir)
298
+ }
299
+
170
300
  log(` ${c(GREEN)}${c(BOLD)}✓${c(RESET)} Created ${c(BOLD)}${projectName}/${c(RESET)}`)
171
301
 
172
302
  // Install dependencies (Next.js-like behavior)
@@ -199,13 +329,26 @@ async function scaffold(projectName) {
199
329
  // ── Main ──────────────────────────────────────────────────────────────
200
330
  const banner = renderBanner()
201
331
  process.stdout.write('\n' + banner + '\n\n')
202
- process.stdout.write(` ${c(DIM)}desktop apps for Next.js developers${c(RESET)}\n`)
332
+ process.stdout.write(` ${c(DIM)}desktop apps for Next.js developers${c(RESET)}\n\n`)
333
+
334
+ // ── Parse args ────────────────────────────────────────────────────────
335
+ const argName = process.argv[2] && !process.argv[2].startsWith('--') ? process.argv[2] : null
336
+ const argLinter = (() => {
337
+ const i = process.argv.indexOf('--linter')
338
+ if (i >= 0 && process.argv[i + 1]) {
339
+ const v = process.argv[i + 1].toLowerCase()
340
+ if (v === 'biome' || v === 'eslint' || v === 'none') return v
341
+ }
342
+ return null
343
+ })()
203
344
 
204
- const argName = process.argv[2]
205
- const projectName = argName || (await promptForName())
345
+ intro('🦋 create-murasaki')
346
+ const projectName = argName || (await promptForName())
347
+ const linter = argLinter || (await promptForLinter())
348
+ outro('Setting things up...')
206
349
 
207
350
  try {
208
- await scaffold(projectName)
351
+ await scaffold(projectName, linter)
209
352
  } catch (err) {
210
353
  log(`\n ${c(RED)}✗${c(RESET)} Scaffold failed: ${err.message}\n`)
211
354
  process.exit(1)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-murasaki",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "Scaffolder for Murasaki apps. Run with `npm create murasaki@latest`.",
5
5
  "keywords": [
6
6
  "murasaki",
@@ -29,5 +29,8 @@
29
29
  "templates",
30
30
  "README.md",
31
31
  "LICENSE"
32
- ]
32
+ ],
33
+ "dependencies": {
34
+ "@clack/prompts": "^1.6.0"
35
+ }
33
36
  }