create-stator 1.1.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,13 +1,19 @@
1
1
  #!/usr/bin/env node
2
- // Stator scaffolder. Interactive (clack) when run bare; every prompt has a
3
- // flag so CI and scripts stay prompt-free:
4
- // pnpm create stator → prompts for directory + template
5
- // pnpm create stator my-app --template minimal → no prompts
6
- 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'
7
13
  import { basename, join, resolve } from 'node:path'
8
- import { fileURLToPath } from 'node:url'
9
14
  import { parseArgs } from 'node:util'
10
15
  import * as p from '@clack/prompts'
16
+ import { downloadTemplate } from 'giget'
11
17
 
12
18
  const TEMPLATES = [
13
19
  {
@@ -15,12 +21,24 @@ const TEMPLATES = [
15
21
  label: 'Minimal',
16
22
  hint: 'one machine, one page — the smallest real app',
17
23
  },
24
+ {
25
+ value: 'todomvc',
26
+ label: 'TodoMVC',
27
+ hint: 'the classic, with server-owned todos and zero-JS editing',
28
+ },
18
29
  ]
19
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
+
20
37
  const { values: flags, positionals } = parseArgs({
21
38
  allowPositionals: true,
22
39
  options: {
23
40
  template: { type: 'string', short: 't' },
41
+ ref: { type: 'string' },
24
42
  help: { type: 'boolean', short: 'h' },
25
43
  },
26
44
  })
@@ -29,6 +47,8 @@ if (flags.help) {
29
47
  console.log(
30
48
  `Usage: pnpm create stator [directory] [--template ${TEMPLATES.map((t) => t.value).join('|')}]`,
31
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')
32
52
  process.exit(0)
33
53
  }
34
54
 
@@ -59,42 +79,54 @@ try {
59
79
 
60
80
  // --- template ---
61
81
  let template = flags.template
62
- if (template && !TEMPLATES.some((t) => t.value === template)) {
63
- p.cancel(`Unknown template "${template}". Available: ${TEMPLATES.map((t) => t.value).join(', ')}`)
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
+ )
64
88
  process.exit(1)
65
89
  }
66
90
  if (!template) {
67
- if (TEMPLATES.length === 1) {
68
- template = TEMPLATES[0].value
69
- } else {
70
- const answer = await p.select({ message: 'Which template?', options: TEMPLATES })
71
- bailIfCancelled(answer)
72
- template = answer
73
- }
91
+ const answer = await p.select({ message: 'Which template?', options: TEMPLATES })
92
+ bailIfCancelled(answer)
93
+ template = answer
74
94
  }
75
95
 
76
- // --- scaffold ---
96
+ // --- fetch ---
97
+ const ref = flags.ref ? `#${flags.ref}` : ''
98
+ const source = isRemoteSource(template) ? template : `${FIRST_PARTY}/${template}${ref}`
77
99
  const s = p.spinner()
78
- s.start('Copying template')
79
- const templateDir = fileURLToPath(new URL(`./templates/${template}`, import.meta.url))
80
- await cp(templateDir, dest, { recursive: true })
81
-
82
- // npm strips dotfiles from published packages; ship as _gitignore and rename.
100
+ s.start(`Fetching ${template}`)
83
101
  try {
84
- await stat(join(dest, '_gitignore'))
85
- await rename(join(dest, '_gitignore'), join(dest, '.gitignore'))
86
- } catch {
87
- // 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)
88
109
  }
89
110
 
111
+ // --- stamp ---
90
112
  const name = basename(dest)
91
113
  .toLowerCase()
92
114
  .replace(/[^a-z0-9-]/g, '-')
93
115
  const pkgPath = join(dest, 'package.json')
94
- const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
95
- pkg.name = name
96
- await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
97
- s.stop(`Scaffolded ${name} (${template})`)
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
+ }
98
130
 
99
131
  p.note([`cd ${target}`, 'pnpm install', 'pnpm dev'].join('\n'), 'Next steps')
100
132
  p.outro('Docs: https://docs.statorjs.dev · Demo: https://demo.statorjs.dev')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-stator",
3
- "version": "1.1.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
- "templates"
11
+ "index.js"
13
12
  ],
14
13
  "repository": {
15
14
  "type": "git",
@@ -21,6 +20,7 @@
21
20
  "node": ">=20"
22
21
  },
23
22
  "dependencies": {
24
- "@clack/prompts": "^1.7.0"
23
+ "@clack/prompts": "^1.7.0",
24
+ "giget": "^1.2.3"
25
25
  }
26
26
  }
@@ -1,31 +0,0 @@
1
- # A Stator app
2
-
3
- ```sh
4
- pnpm install
5
- pnpm dev # dev server with live reload + the wire inspector
6
- pnpm typecheck # sync generated component types, then tsc
7
- pnpm build # production build into dist/
8
- pnpm start # serve the production build
9
- ```
10
-
11
- ## The mental model
12
-
13
- State lives **on the server**, in machines. Pages declare what they read;
14
- when an event changes a machine, the server diffs and sends small JSON
15
- patches to the browser. There is no client renderer and no API layer.
16
-
17
- - `machines/counter.ts` — a session-scoped machine: typed events, guarded
18
- transitions, selectors. Session state survives a page reload because the
19
- server owns it.
20
- - `routes/index.stator` — a page: frontmatter declares `Stator.reads`, the
21
- template calls `read(machine, selector)` wherever live values render.
22
- - `templates/layout.stator` — a component with a `<children />` slot.
23
-
24
- Try it: run `pnpm dev`, click **+1**, then reload the page — the count
25
- persists. Open the inspector (bottom of the page) and click again to watch
26
- the patch arrive on the wire.
27
-
28
- ## Learn more
29
-
30
- - Docs: https://docs.statorjs.dev — start with the tutorial
31
- - Reference app: https://demo.statorjs.dev ([source](https://github.com/statorjs/stator/tree/main/apps/store))
@@ -1,3 +0,0 @@
1
- node_modules/
2
- dist/
3
- .stator/
@@ -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.1.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)
@@ -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
- }
@@ -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,33 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "Bundler",
6
- "lib": [
7
- "ES2022",
8
- "DOM",
9
- "DOM.Iterable"
10
- ],
11
- "strict": true,
12
- "noUncheckedIndexedAccess": true,
13
- "esModuleInterop": true,
14
- "skipLibCheck": true,
15
- "isolatedModules": true,
16
- "allowImportingTsExtensions": true,
17
- "noEmit": true,
18
- "rootDirs": [
19
- ".",
20
- ".stator/types"
21
- ],
22
- "jsx": "preserve"
23
- },
24
- "include": [
25
- "**/*.ts",
26
- "**/*.stator"
27
- ],
28
- "exclude": [
29
- "node_modules",
30
- "static",
31
- "dist"
32
- ]
33
- }