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 +61 -29
- package/package.json +4 -4
- package/templates/minimal/README.md +0 -31
- package/templates/minimal/_gitignore +0 -3
- package/templates/minimal/build.ts +0 -7
- package/templates/minimal/machines/counter.ts +0 -27
- package/templates/minimal/package.json +0 -23
- package/templates/minimal/routes/index.stator +0 -14
- package/templates/minimal/server.ts +0 -19
- package/templates/minimal/start.ts +0 -25
- package/templates/minimal/static/app.css +0 -5
- package/templates/minimal/stator-env.d.ts +0 -11
- package/templates/minimal/sync.ts +0 -7
- package/templates/minimal/templates/layout.stator +0 -12
- package/templates/minimal/tsconfig.json +0 -33
package/index.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Stator scaffolder.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
// ---
|
|
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(
|
|
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
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
95
|
-
pkg
|
|
96
|
-
|
|
97
|
-
|
|
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.
|
|
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,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
|
-
}
|