create-stator 1.0.0 → 1.2.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/index.js CHANGED
@@ -1,54 +1,139 @@
1
1
  #!/usr/bin/env node
2
- // Minimal Stator scaffolder: copies the embedded template into a new
3
- // directory and stamps the project name. No prompts, no network.
4
- import { cp, readdir, readFile, rename, stat, writeFile } from 'node:fs/promises'
2
+ // Stator scaffolder. First-party templates live in the monorepo's
3
+ // `examples/` directory and are FETCHED at scaffold time (create-astro
4
+ // style) they update with every push, and `--template` accepts any giget
5
+ // source (`github:user/repo/path`) so community templates work for free.
6
+ //
7
+ // Interactive (clack) when run bare; every prompt has a flag so CI and
8
+ // scripts stay prompt-free:
9
+ // pnpm create stator → prompts
10
+ // pnpm create stator my-app --template todomvc → no prompts
11
+ // pnpm create stator my-app -t github:you/your-template
12
+ import { readdir, readFile, writeFile } from 'node:fs/promises'
5
13
  import { basename, join, resolve } from 'node:path'
6
- import { fileURLToPath } from 'node:url'
14
+ import { parseArgs } from 'node:util'
15
+ import * as p from '@clack/prompts'
16
+ import { downloadTemplate } from 'giget'
7
17
 
8
- const target = process.argv[2]
9
- if (!target) {
10
- console.log('Usage: pnpm create stator <directory>')
11
- console.log(' npm create stator@latest <directory>')
12
- process.exit(1)
18
+ const TEMPLATES = [
19
+ {
20
+ value: 'minimal',
21
+ label: 'Minimal',
22
+ hint: 'one machine, one page — the smallest real app',
23
+ },
24
+ {
25
+ value: 'todomvc',
26
+ label: 'TodoMVC',
27
+ hint: 'the classic, with server-owned todos and zero-JS editing',
28
+ },
29
+ ]
30
+
31
+ /** First-party templates resolve into the monorepo's examples/. */
32
+ const FIRST_PARTY = 'gh:statorjs/stator/examples'
33
+ /** Scaffolded apps get a real semver for the framework (the examples
34
+ * themselves use workspace linking in-repo). Bumped with releases. */
35
+ const STATOR_RANGE = '^1.1.0'
36
+
37
+ const { values: flags, positionals } = parseArgs({
38
+ allowPositionals: true,
39
+ options: {
40
+ template: { type: 'string', short: 't' },
41
+ ref: { type: 'string' },
42
+ help: { type: 'boolean', short: 'h' },
43
+ },
44
+ })
45
+
46
+ if (flags.help) {
47
+ console.log(
48
+ `Usage: pnpm create stator [directory] [--template ${TEMPLATES.map((t) => t.value).join('|')}]`,
49
+ )
50
+ console.log(' pnpm create stator my-app --template github:user/repo/path')
51
+ console.log(' --ref <branch|tag> fetch first-party templates from a specific ref')
52
+ process.exit(0)
13
53
  }
14
54
 
55
+ p.intro('create-stator')
56
+
57
+ // --- target directory ---
58
+ let target = positionals[0]
59
+ if (!target) {
60
+ const answer = await p.text({
61
+ message: 'Where should the project live?',
62
+ placeholder: './my-stator-app',
63
+ validate: (v) => (v.trim() === '' ? 'A directory is required.' : undefined),
64
+ })
65
+ bailIfCancelled(answer)
66
+ target = answer
67
+ }
15
68
  const dest = resolve(process.cwd(), target)
69
+
16
70
  try {
17
71
  const existing = await readdir(dest)
18
72
  if (existing.length > 0) {
19
- console.error(`create-stator: ${target} exists and is not empty — refusing to overwrite.`)
73
+ p.cancel(`${target} exists and is not empty — refusing to overwrite.`)
20
74
  process.exit(1)
21
75
  }
22
76
  } catch {
23
77
  // does not exist — fine
24
78
  }
25
79
 
26
- const templateDir = fileURLToPath(new URL('./template', import.meta.url))
27
- await cp(templateDir, dest, { recursive: true })
80
+ // --- template ---
81
+ let template = flags.template
82
+ const isRemoteSource = (t) => t.includes(':') || t.includes('/')
83
+ if (template && !isRemoteSource(template) && !TEMPLATES.some((t) => t.value === template)) {
84
+ p.cancel(
85
+ `Unknown template "${template}". Available: ${TEMPLATES.map((t) => t.value).join(', ')} ` +
86
+ `(or any giget source, e.g. github:user/repo/path)`,
87
+ )
88
+ process.exit(1)
89
+ }
90
+ if (!template) {
91
+ const answer = await p.select({ message: 'Which template?', options: TEMPLATES })
92
+ bailIfCancelled(answer)
93
+ template = answer
94
+ }
28
95
 
29
- // npm strips dotfiles from published packages; ship as _gitignore and rename.
96
+ // --- fetch ---
97
+ const ref = flags.ref ? `#${flags.ref}` : ''
98
+ const source = isRemoteSource(template) ? template : `${FIRST_PARTY}/${template}${ref}`
99
+ const s = p.spinner()
100
+ s.start(`Fetching ${template}`)
30
101
  try {
31
- await stat(join(dest, '_gitignore'))
32
- await rename(join(dest, '_gitignore'), join(dest, '.gitignore'))
33
- } catch {
34
- // already named .gitignore (running from the repo)
102
+ await downloadTemplate(source, { dir: dest, force: true })
103
+ } catch (err) {
104
+ s.stop('Fetch failed')
105
+ p.cancel(
106
+ `Could not download "${source}" — scaffolding needs network access.\n ${String(err?.message ?? err)}`,
107
+ )
108
+ process.exit(1)
35
109
  }
36
110
 
111
+ // --- stamp ---
37
112
  const name = basename(dest)
38
113
  .toLowerCase()
39
114
  .replace(/[^a-z0-9-]/g, '-')
40
115
  const pkgPath = join(dest, 'package.json')
41
- const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
42
- pkg.name = name
43
- await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
44
-
45
- console.log(`
46
- Scaffolded ${name} into ${dest}
116
+ try {
117
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
118
+ pkg.name = name
119
+ // In-repo examples link the framework via the workspace; a scaffolded
120
+ // app pins the published release instead.
121
+ if (pkg.dependencies?.['@statorjs/stator'] === 'workspace:*') {
122
+ pkg.dependencies['@statorjs/stator'] = STATOR_RANGE
123
+ }
124
+ delete pkg.private
125
+ await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
126
+ s.stop(`Scaffolded ${name} (${template})`)
127
+ } catch {
128
+ s.stop(`Scaffolded ${name} (${template}) — no package.json to stamp`)
129
+ }
47
130
 
48
- Next steps:
49
- cd ${target}
50
- pnpm install
51
- pnpm dev # http://localhost:3000
131
+ p.note([`cd ${target}`, 'pnpm install', 'pnpm dev'].join('\n'), 'Next steps')
132
+ p.outro('Docs: https://docs.statorjs.dev · Demo: https://demo.statorjs.dev')
52
133
 
53
- Other scripts: pnpm typecheck · pnpm build · pnpm start
54
- `)
134
+ function bailIfCancelled(v) {
135
+ if (p.isCancel(v)) {
136
+ p.cancel('Cancelled.')
137
+ process.exit(0)
138
+ }
139
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-stator",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Scaffold a new Stator app: pnpm create stator my-app",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -8,8 +8,7 @@
8
8
  "create-stator": "./index.js"
9
9
  },
10
10
  "files": [
11
- "index.js",
12
- "template"
11
+ "index.js"
13
12
  ],
14
13
  "repository": {
15
14
  "type": "git",
@@ -19,5 +18,9 @@
19
18
  "homepage": "https://github.com/statorjs/stator#readme",
20
19
  "engines": {
21
20
  "node": ">=20"
21
+ },
22
+ "dependencies": {
23
+ "@clack/prompts": "^1.7.0",
24
+ "giget": "^1.2.3"
22
25
  }
23
26
  }
@@ -1,3 +0,0 @@
1
- node_modules/
2
- dist/
3
- .stator/
package/template/build.ts DELETED
@@ -1,7 +0,0 @@
1
- import { dirname, resolve } from 'node:path'
2
- import { fileURLToPath } from 'node:url'
3
- import { buildApp } from '@statorjs/stator/build'
4
-
5
- const here = dirname(fileURLToPath(import.meta.url))
6
- const result = await buildApp({ root: here, outDir: resolve(here, 'dist') })
7
- console.log(`stator: built ${result.compiled} components → ${result.outDir}`)
@@ -1,27 +0,0 @@
1
- import { defineMachine } from '@statorjs/stator/server'
2
-
3
- type Events = { type: 'INCREMENT' } | { type: 'RESET' }
4
-
5
- export default defineMachine({
6
- name: 'CounterMachine',
7
- lifecycle: 'session',
8
- events: {} as Events,
9
- context: { count: 0 },
10
- initial: 'idle',
11
- states: {
12
- idle: {
13
- on: {
14
- INCREMENT: (ctx) => {
15
- ctx.count += 1
16
- },
17
- RESET: (ctx) => {
18
- ctx.count = 0
19
- },
20
- },
21
- },
22
- },
23
- selectors: {
24
- count: (ctx) => ctx.count,
25
- label: (ctx) => `count is ${ctx.count}`,
26
- },
27
- })
@@ -1,23 +0,0 @@
1
- {
2
- "name": "my-stator-app",
3
- "version": "0.0.0",
4
- "private": true,
5
- "type": "module",
6
- "scripts": {
7
- "sync": "tsx sync.ts",
8
- "typecheck": "tsx sync.ts && tsc --noEmit",
9
- "dev": "tsx watch server.ts",
10
- "build": "tsx build.ts",
11
- "start": "tsx start.ts"
12
- },
13
- "dependencies": {
14
- "@statorjs/stator": "^1.0.0"
15
- },
16
- "devDependencies": {
17
- "@types/node": "^22.7.0",
18
- "tsx": "^4.19.0",
19
- "typescript": "^5.6.0",
20
- "vite": "^6.0.0",
21
- "pino-pretty": "^13.1.3"
22
- }
23
- }
@@ -1,14 +0,0 @@
1
- ---
2
- import Counter from '../machines/counter.ts'
3
- import Layout from '../templates/layout.stator'
4
-
5
- const [counter] = Stator.reads([Counter])
6
- ---
7
- <Layout>
8
- <section class="counter">
9
- <h1>Hello, Stator</h1>
10
- <p class="count">{read(counter, (c) => c.label)}</p>
11
- <button on:click={() => counter.send({ type: 'INCREMENT' })}>+1</button>
12
- <button on:click={() => counter.send({ type: 'RESET' })}>reset</button>
13
- </section>
14
- </Layout>
@@ -1,19 +0,0 @@
1
- import { dirname, resolve } from 'node:path'
2
- import { fileURLToPath } from 'node:url'
3
- import { createDevApp } from '@statorjs/stator/dev'
4
-
5
- const here = dirname(fileURLToPath(import.meta.url))
6
- const port = Number(process.env.PORT ?? 3000)
7
-
8
- // Dev server: Vite compiles `.stator` on the way in, with live reload and the
9
- // inspector toolbar. Production runs `pnpm build && pnpm start` instead.
10
- // Sessions default to the in-memory store — swap in RedisStore (from
11
- // '@statorjs/stator/server') to survive restarts.
12
- const app = await createDevApp({
13
- root: here,
14
- machinesDir: resolve(here, 'machines'),
15
- routesDir: resolve(here, 'routes'),
16
- staticDir: resolve(here, 'static'),
17
- })
18
-
19
- await app.listen(port)
package/template/start.ts DELETED
@@ -1,25 +0,0 @@
1
- import { stat } from 'node:fs/promises'
2
- import { dirname, resolve } from 'node:path'
3
- import { fileURLToPath } from 'node:url'
4
- import { loadProductionHead } from '@statorjs/stator/build'
5
- import { createApp, logger } from '@statorjs/stator/server'
6
-
7
- const here = dirname(fileURLToPath(import.meta.url))
8
- const dist = resolve(here, 'dist')
9
- const port = Number(process.env.PORT ?? 3000)
10
-
11
- try {
12
- await stat(resolve(dist, 'routes'))
13
- } catch {
14
- logger.error({}, 'no dist/ found — run `pnpm build` first')
15
- process.exit(1)
16
- }
17
-
18
- const app = await createApp({
19
- machinesDir: resolve(dist, 'machines'),
20
- routesDir: resolve(dist, 'routes'),
21
- staticDir: resolve(dist, 'static'),
22
- headExtras: await loadProductionHead(dist),
23
- })
24
-
25
- await app.listen(port)
@@ -1,5 +0,0 @@
1
- :root { font-family: system-ui, sans-serif; color-scheme: light dark; }
2
- body { margin: 0; display: grid; place-items: center; min-height: 100vh; }
3
- .counter { text-align: center; }
4
- .count { font-size: 1.5rem; font-variant-numeric: tabular-nums; }
5
- button { font: inherit; padding: 0.4rem 1rem; margin: 0 0.25rem; cursor: pointer; }
@@ -1,11 +0,0 @@
1
- /**
2
- * Ambient type for `.stator` single-file components, for this app's TypeScript
3
- * project. (Stator apps include one of these, like Vite apps include env.d.ts.)
4
- */
5
- declare module '*.stator' {
6
- import type { HtmlFragment } from '@statorjs/stator/template'
7
-
8
- // biome-ignore lint/suspicious/noExplicitAny: the wildcard fallback must admit every component's props — `Record<string, unknown>` would reject interfaces without index signatures
9
- const component: (props?: any) => HtmlFragment
10
- export default component
11
- }
package/template/sync.ts DELETED
@@ -1,7 +0,0 @@
1
- import { dirname } from 'node:path'
2
- import { fileURLToPath } from 'node:url'
3
- import { syncTypes } from '@statorjs/stator/build'
4
-
5
- const here = dirname(fileURLToPath(import.meta.url))
6
- const { written } = await syncTypes(here)
7
- console.log(`stator: wrote ${written} .stator.d.ts files`)
@@ -1,12 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>stator app</title>
7
- <link rel="stylesheet" href="/static/app.css" />
8
- </head>
9
- <body>
10
- <main><children /></main>
11
- </body>
12
- </html>
@@ -1,18 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "Bundler",
6
- "lib": ["ES2022", "DOM"],
7
- "strict": true,
8
- "noUncheckedIndexedAccess": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "isolatedModules": true,
12
- "allowImportingTsExtensions": true,
13
- "noEmit": true,
14
- "rootDirs": [".", ".stator/types"]
15
- },
16
- "include": ["**/*.ts"],
17
- "exclude": ["node_modules", "static", "dist"]
18
- }