create-stx 0.2.3
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.ts +703 -0
- package/package.json +31 -0
package/index.ts
ADDED
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* create-stx
|
|
4
|
+
*
|
|
5
|
+
* Scaffold a new STX application.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* bun create stx my-app
|
|
9
|
+
* bunx create-stx my-app
|
|
10
|
+
* npx create-stx my-app
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs'
|
|
14
|
+
import path from 'node:path'
|
|
15
|
+
import { spawn } from 'node:child_process'
|
|
16
|
+
|
|
17
|
+
// Colors for terminal output
|
|
18
|
+
const colors = {
|
|
19
|
+
reset: '\x1b[0m',
|
|
20
|
+
bright: '\x1b[1m',
|
|
21
|
+
dim: '\x1b[2m',
|
|
22
|
+
green: '\x1b[32m',
|
|
23
|
+
yellow: '\x1b[33m',
|
|
24
|
+
blue: '\x1b[34m',
|
|
25
|
+
cyan: '\x1b[36m',
|
|
26
|
+
red: '\x1b[31m',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function log(message: string) {
|
|
30
|
+
console.log(message)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function success(message: string) {
|
|
34
|
+
console.log(`${colors.green}✓${colors.reset} ${message}`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function error(message: string) {
|
|
38
|
+
console.error(`${colors.red}✗${colors.reset} ${message}`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function info(message: string) {
|
|
42
|
+
console.log(`${colors.blue}ℹ${colors.reset} ${message}`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Template files
|
|
46
|
+
const templates = {
|
|
47
|
+
'pages/index.stx': `<script>
|
|
48
|
+
export const title = 'Welcome to STX'
|
|
49
|
+
export const description = 'Build modern web apps with intuitive templating'
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<!DOCTYPE html>
|
|
53
|
+
<html lang="en">
|
|
54
|
+
<head>
|
|
55
|
+
<meta charset="UTF-8">
|
|
56
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
57
|
+
<title>{{ title }}</title>
|
|
58
|
+
<link rel="stylesheet" href="/dist/app.css">
|
|
59
|
+
<style>
|
|
60
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
61
|
+
body {
|
|
62
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
63
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
64
|
+
min-height: 100vh;
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
justify-content: center;
|
|
68
|
+
color: white;
|
|
69
|
+
}
|
|
70
|
+
.container { text-align: center; padding: 2rem; }
|
|
71
|
+
h1 { font-size: 3rem; margin-bottom: 1rem; }
|
|
72
|
+
p { font-size: 1.25rem; opacity: 0.9; margin-bottom: 2rem; }
|
|
73
|
+
.counter { margin: 2rem 0; }
|
|
74
|
+
.counter button {
|
|
75
|
+
padding: 0.5rem 1.5rem;
|
|
76
|
+
margin: 0 0.5rem;
|
|
77
|
+
font-size: 1.5rem;
|
|
78
|
+
border: 2px solid white;
|
|
79
|
+
background: transparent;
|
|
80
|
+
color: white;
|
|
81
|
+
border-radius: 8px;
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
}
|
|
84
|
+
.counter button:hover { background: white; color: #764ba2; }
|
|
85
|
+
.counter span { font-size: 2rem; margin: 0 1rem; }
|
|
86
|
+
.links { display: flex; gap: 1rem; justify-content: center; }
|
|
87
|
+
a {
|
|
88
|
+
color: white;
|
|
89
|
+
text-decoration: none;
|
|
90
|
+
padding: 0.75rem 1.5rem;
|
|
91
|
+
border: 2px solid white;
|
|
92
|
+
border-radius: 8px;
|
|
93
|
+
transition: all 0.2s;
|
|
94
|
+
}
|
|
95
|
+
a:hover { background: white; color: #764ba2; }
|
|
96
|
+
</style>
|
|
97
|
+
<!-- Load stores synchronously -->
|
|
98
|
+
<script>
|
|
99
|
+
var xhr = new XMLHttpRequest();
|
|
100
|
+
xhr.open('GET', '/js/stores.js', false);
|
|
101
|
+
xhr.send();
|
|
102
|
+
if (xhr.status === 200) eval(xhr.responseText);
|
|
103
|
+
</script>
|
|
104
|
+
</head>
|
|
105
|
+
<body>
|
|
106
|
+
<div class="container">
|
|
107
|
+
<h1>{{ title }}</h1>
|
|
108
|
+
<p>{{ description }}</p>
|
|
109
|
+
|
|
110
|
+
<!-- Counter demo using stores -->
|
|
111
|
+
<div class="counter">
|
|
112
|
+
<button id="decrement">-</button>
|
|
113
|
+
<span id="count">0</span>
|
|
114
|
+
<button id="increment">+</button>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div class="links">
|
|
118
|
+
<a href="https://stx.sh" target="_blank">Documentation</a>
|
|
119
|
+
<a href="https://github.com/stacksjs/stx" target="_blank">GitHub</a>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<script>
|
|
124
|
+
(function() {
|
|
125
|
+
var stores = window.AppStores;
|
|
126
|
+
var appStore = stores.appStore;
|
|
127
|
+
var appActions = stores.appActions;
|
|
128
|
+
|
|
129
|
+
// Subscribe to store changes
|
|
130
|
+
appStore.subscribe(function(state) {
|
|
131
|
+
document.getElementById('count').textContent = state.count;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Button handlers
|
|
135
|
+
document.getElementById('increment').onclick = appActions.increment;
|
|
136
|
+
document.getElementById('decrement').onclick = appActions.decrement;
|
|
137
|
+
})();
|
|
138
|
+
</script>
|
|
139
|
+
</body>
|
|
140
|
+
</html>
|
|
141
|
+
`,
|
|
142
|
+
|
|
143
|
+
'components/Button.stx': `<!--
|
|
144
|
+
Button Component
|
|
145
|
+
|
|
146
|
+
Props:
|
|
147
|
+
- text: Button text
|
|
148
|
+
- type: Button type (primary, secondary, danger)
|
|
149
|
+
- disabled: Whether button is disabled
|
|
150
|
+
-->
|
|
151
|
+
<button
|
|
152
|
+
class="btn btn-{{ type || 'primary' }}"
|
|
153
|
+
{{ disabled ? 'disabled' : '' }}
|
|
154
|
+
>
|
|
155
|
+
{{ text || slot }}
|
|
156
|
+
</button>
|
|
157
|
+
|
|
158
|
+
<style scoped>
|
|
159
|
+
.btn {
|
|
160
|
+
padding: 0.5rem 1rem;
|
|
161
|
+
border: none;
|
|
162
|
+
border-radius: 6px;
|
|
163
|
+
font-size: 1rem;
|
|
164
|
+
cursor: pointer;
|
|
165
|
+
transition: all 0.2s;
|
|
166
|
+
}
|
|
167
|
+
.btn-primary {
|
|
168
|
+
background: #667eea;
|
|
169
|
+
color: white;
|
|
170
|
+
}
|
|
171
|
+
.btn-primary:hover {
|
|
172
|
+
background: #5a67d8;
|
|
173
|
+
}
|
|
174
|
+
.btn-secondary {
|
|
175
|
+
background: #e2e8f0;
|
|
176
|
+
color: #4a5568;
|
|
177
|
+
}
|
|
178
|
+
.btn-danger {
|
|
179
|
+
background: #fc8181;
|
|
180
|
+
color: white;
|
|
181
|
+
}
|
|
182
|
+
.btn:disabled {
|
|
183
|
+
opacity: 0.5;
|
|
184
|
+
cursor: not-allowed;
|
|
185
|
+
}
|
|
186
|
+
</style>
|
|
187
|
+
`,
|
|
188
|
+
|
|
189
|
+
'public/.gitkeep': '',
|
|
190
|
+
|
|
191
|
+
'public/js/.gitkeep': '',
|
|
192
|
+
|
|
193
|
+
'lib/store.ts': `/**
|
|
194
|
+
* Lightweight reactive store implementation
|
|
195
|
+
* Based on STX state-management patterns
|
|
196
|
+
*/
|
|
197
|
+
|
|
198
|
+
export type Subscriber<T> = (value: T, previousValue: T | undefined) => void
|
|
199
|
+
export type Unsubscribe = () => void
|
|
200
|
+
|
|
201
|
+
export interface StoreOptions<T> {
|
|
202
|
+
name?: string
|
|
203
|
+
persist?: {
|
|
204
|
+
key?: string
|
|
205
|
+
storage?: 'local' | 'session'
|
|
206
|
+
debounce?: number
|
|
207
|
+
}
|
|
208
|
+
onChange?: (value: T, prev: T | undefined) => void
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface Store<T> {
|
|
212
|
+
get: () => T
|
|
213
|
+
set: (value: T | ((prev: T) => T)) => void
|
|
214
|
+
update: (partial: Partial<T> | ((prev: T) => Partial<T>)) => void
|
|
215
|
+
subscribe: (subscriber: Subscriber<T>) => Unsubscribe
|
|
216
|
+
reset: () => void
|
|
217
|
+
name?: string
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function createStore<T extends object>(
|
|
221
|
+
initialValue: T,
|
|
222
|
+
options: StoreOptions<T> = {}
|
|
223
|
+
): Store<T> {
|
|
224
|
+
const { name, persist, onChange } = options
|
|
225
|
+
|
|
226
|
+
let currentValue = { ...initialValue }
|
|
227
|
+
const subscribers = new Set<Subscriber<T>>()
|
|
228
|
+
let persistTimeout: ReturnType<typeof setTimeout> | null = null
|
|
229
|
+
|
|
230
|
+
// Load persisted value
|
|
231
|
+
if (persist && typeof window !== 'undefined') {
|
|
232
|
+
const storage = persist.storage === 'session' ? sessionStorage : localStorage
|
|
233
|
+
const key = persist.key || \`app:\${name || 'store'}\`
|
|
234
|
+
try {
|
|
235
|
+
const stored = storage.getItem(key)
|
|
236
|
+
if (stored) {
|
|
237
|
+
currentValue = { ...initialValue, ...JSON.parse(stored) }
|
|
238
|
+
}
|
|
239
|
+
} catch { /* Invalid stored value */ }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const persistValue = (value: T) => {
|
|
243
|
+
if (!persist || typeof window === 'undefined') return
|
|
244
|
+
const storage = persist.storage === 'session' ? sessionStorage : localStorage
|
|
245
|
+
const key = persist.key || \`app:\${name || 'store'}\`
|
|
246
|
+
if (persist.debounce) {
|
|
247
|
+
if (persistTimeout) clearTimeout(persistTimeout)
|
|
248
|
+
persistTimeout = setTimeout(() => storage.setItem(key, JSON.stringify(value)), persist.debounce)
|
|
249
|
+
} else {
|
|
250
|
+
storage.setItem(key, JSON.stringify(value))
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const notify = (newValue: T, prevValue: T | undefined) => {
|
|
255
|
+
for (const subscriber of subscribers) {
|
|
256
|
+
try { subscriber(newValue, prevValue) } catch (e) { console.error('[store]', e) }
|
|
257
|
+
}
|
|
258
|
+
if (onChange) onChange(newValue, prevValue)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
get: () => currentValue,
|
|
263
|
+
set: (value) => {
|
|
264
|
+
const prevValue = currentValue
|
|
265
|
+
const newValue = typeof value === 'function' ? (value as (prev: T) => T)(currentValue) : value
|
|
266
|
+
if (newValue !== prevValue) {
|
|
267
|
+
currentValue = newValue
|
|
268
|
+
persistValue(newValue)
|
|
269
|
+
notify(newValue, prevValue)
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
update: (partial) => {
|
|
273
|
+
const prevValue = currentValue
|
|
274
|
+
const updates = typeof partial === 'function' ? (partial as (prev: T) => Partial<T>)(currentValue) : partial
|
|
275
|
+
currentValue = { ...currentValue, ...updates }
|
|
276
|
+
persistValue(currentValue)
|
|
277
|
+
notify(currentValue, prevValue)
|
|
278
|
+
},
|
|
279
|
+
subscribe: (subscriber) => {
|
|
280
|
+
subscribers.add(subscriber)
|
|
281
|
+
subscriber(currentValue, undefined)
|
|
282
|
+
return () => subscribers.delete(subscriber)
|
|
283
|
+
},
|
|
284
|
+
reset: () => {
|
|
285
|
+
const prevValue = currentValue
|
|
286
|
+
currentValue = { ...initialValue }
|
|
287
|
+
persistValue(currentValue)
|
|
288
|
+
notify(currentValue, prevValue)
|
|
289
|
+
},
|
|
290
|
+
name
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
`,
|
|
294
|
+
|
|
295
|
+
'lib/composables/index.ts': `/**
|
|
296
|
+
* Composables - Browser API utilities (Nuxt-style)
|
|
297
|
+
*/
|
|
298
|
+
export * from './use-storage'
|
|
299
|
+
export * from './use-cookie'
|
|
300
|
+
`,
|
|
301
|
+
|
|
302
|
+
'lib/composables/use-storage.ts': `/**
|
|
303
|
+
* useStorage - Reactive localStorage/sessionStorage wrapper
|
|
304
|
+
*/
|
|
305
|
+
export interface StorageOptions<T> {
|
|
306
|
+
defaultValue?: T
|
|
307
|
+
serializer?: {
|
|
308
|
+
read: (raw: string) => T
|
|
309
|
+
write: (value: T) => string
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function useStorage<T>(
|
|
314
|
+
key: string,
|
|
315
|
+
storage: Storage = localStorage,
|
|
316
|
+
options: StorageOptions<T> = {}
|
|
317
|
+
) {
|
|
318
|
+
const { defaultValue, serializer = { read: JSON.parse, write: JSON.stringify } } = options
|
|
319
|
+
let listeners: Array<(value: T | undefined) => void> = []
|
|
320
|
+
|
|
321
|
+
const get = (): T | undefined => {
|
|
322
|
+
try {
|
|
323
|
+
const raw = storage.getItem(key)
|
|
324
|
+
return raw !== null ? serializer.read(raw) : defaultValue
|
|
325
|
+
} catch {
|
|
326
|
+
return defaultValue
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const set = (value: T): void => {
|
|
331
|
+
try {
|
|
332
|
+
storage.setItem(key, serializer.write(value))
|
|
333
|
+
listeners.forEach(fn => fn(value))
|
|
334
|
+
} catch (e) {
|
|
335
|
+
console.error('[useStorage] Failed to set:', e)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const remove = (): void => {
|
|
340
|
+
storage.removeItem(key)
|
|
341
|
+
listeners.forEach(fn => fn(undefined))
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const subscribe = (fn: (value: T | undefined) => void) => {
|
|
345
|
+
listeners.push(fn)
|
|
346
|
+
fn(get())
|
|
347
|
+
return () => { listeners = listeners.filter(l => l !== fn) }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return { get, set, remove, subscribe }
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export const useLocalStorage = <T>(key: string, options?: StorageOptions<T>) =>
|
|
354
|
+
useStorage<T>(key, localStorage, options)
|
|
355
|
+
|
|
356
|
+
export const useSessionStorage = <T>(key: string, options?: StorageOptions<T>) =>
|
|
357
|
+
useStorage<T>(key, sessionStorage, options)
|
|
358
|
+
`,
|
|
359
|
+
|
|
360
|
+
'lib/composables/use-cookie.ts': `/**
|
|
361
|
+
* useCookie - Cookie management utility
|
|
362
|
+
*/
|
|
363
|
+
export interface CookieOptions {
|
|
364
|
+
expires?: number | Date
|
|
365
|
+
path?: string
|
|
366
|
+
domain?: string
|
|
367
|
+
secure?: boolean
|
|
368
|
+
sameSite?: 'Strict' | 'Lax' | 'None'
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function getCookie(name: string): string | undefined {
|
|
372
|
+
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'))
|
|
373
|
+
return match ? decodeURIComponent(match[2]) : undefined
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function setCookie(name: string, value: string, options: CookieOptions = {}): void {
|
|
377
|
+
let cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value)
|
|
378
|
+
if (options.expires) {
|
|
379
|
+
const date = options.expires instanceof Date
|
|
380
|
+
? options.expires
|
|
381
|
+
: new Date(Date.now() + options.expires * 864e5)
|
|
382
|
+
cookie += '; expires=' + date.toUTCString()
|
|
383
|
+
}
|
|
384
|
+
if (options.path) cookie += '; path=' + options.path
|
|
385
|
+
if (options.domain) cookie += '; domain=' + options.domain
|
|
386
|
+
if (options.secure) cookie += '; secure'
|
|
387
|
+
if (options.sameSite) cookie += '; samesite=' + options.sameSite
|
|
388
|
+
document.cookie = cookie
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function removeCookie(name: string, options: CookieOptions = {}): void {
|
|
392
|
+
setCookie(name, '', { ...options, expires: -1 })
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function useCookie<T = string>(name: string, options: CookieOptions = {}) {
|
|
396
|
+
const serialize = (val: T): string =>
|
|
397
|
+
typeof val === 'string' ? val : JSON.stringify(val)
|
|
398
|
+
const deserialize = (raw: string): T => {
|
|
399
|
+
try { return JSON.parse(raw) as T } catch { return raw as T }
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
get: (): T | undefined => {
|
|
404
|
+
const raw = getCookie(name)
|
|
405
|
+
return raw !== undefined ? deserialize(raw) : undefined
|
|
406
|
+
},
|
|
407
|
+
set: (value: T) => setCookie(name, serialize(value), options),
|
|
408
|
+
remove: () => removeCookie(name, options)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
`,
|
|
412
|
+
|
|
413
|
+
'lib/stores/index.ts': `/**
|
|
414
|
+
* Stores - Central export
|
|
415
|
+
*/
|
|
416
|
+
export * from './app'
|
|
417
|
+
export { createStore } from '../store'
|
|
418
|
+
export * from '../composables'
|
|
419
|
+
`,
|
|
420
|
+
|
|
421
|
+
'lib/stores/app.ts': `/**
|
|
422
|
+
* App Store - Example application state
|
|
423
|
+
*/
|
|
424
|
+
import { createStore } from '../store'
|
|
425
|
+
|
|
426
|
+
export interface AppState {
|
|
427
|
+
count: number
|
|
428
|
+
theme: 'light' | 'dark'
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export const appStore = createStore<AppState>({
|
|
432
|
+
count: 0,
|
|
433
|
+
theme: 'light'
|
|
434
|
+
}, {
|
|
435
|
+
name: 'app',
|
|
436
|
+
persist: { key: 'app:state', storage: 'local' }
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
export const appActions = {
|
|
440
|
+
increment: () => appStore.update(s => ({ count: s.count + 1 })),
|
|
441
|
+
decrement: () => appStore.update(s => ({ count: s.count - 1 })),
|
|
442
|
+
setTheme: (theme: AppState['theme']) => appStore.update({ theme })
|
|
443
|
+
}
|
|
444
|
+
`,
|
|
445
|
+
|
|
446
|
+
'lib/build.ts': `/**
|
|
447
|
+
* Build script to bundle stores for browser use
|
|
448
|
+
*/
|
|
449
|
+
import { build } from 'bun'
|
|
450
|
+
import { writeFileSync, readFileSync } from 'fs'
|
|
451
|
+
|
|
452
|
+
const result = await build({
|
|
453
|
+
entrypoints: ['./lib/stores/index.ts'],
|
|
454
|
+
outdir: './public/js',
|
|
455
|
+
target: 'browser',
|
|
456
|
+
format: 'iife',
|
|
457
|
+
naming: 'stores.js',
|
|
458
|
+
minify: false,
|
|
459
|
+
define: { 'process.env.NODE_ENV': '"production"' }
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
if (!result.success) {
|
|
463
|
+
console.error('Build failed:', result.logs)
|
|
464
|
+
process.exit(1)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Wrap to export globally
|
|
468
|
+
const bundlePath = './public/js/stores.js'
|
|
469
|
+
let bundle = readFileSync(bundlePath, 'utf-8')
|
|
470
|
+
bundle = bundle.replace(/^\\(\\(\\) => \\{/, 'window.AppStores = (function() {')
|
|
471
|
+
bundle = bundle.replace(/\\}\\)\\(\\);?\\s*$/, ' return exports_stores;\\n})();')
|
|
472
|
+
|
|
473
|
+
writeFileSync(bundlePath, \`/**
|
|
474
|
+
* App Stores - Browser Bundle
|
|
475
|
+
* Auto-generated, do not edit
|
|
476
|
+
*/
|
|
477
|
+
\${bundle}
|
|
478
|
+
\`)
|
|
479
|
+
console.log('Stores built to public/js/stores.js')
|
|
480
|
+
`,
|
|
481
|
+
|
|
482
|
+
'stx.config.ts': `import type { StxConfig } from '@stacksjs/stx'
|
|
483
|
+
|
|
484
|
+
const config: StxConfig = {
|
|
485
|
+
// Pages directory for file-based routing
|
|
486
|
+
pagesDir: 'pages',
|
|
487
|
+
|
|
488
|
+
// Components directory
|
|
489
|
+
componentsDir: 'components',
|
|
490
|
+
|
|
491
|
+
// Public assets directory
|
|
492
|
+
publicDir: 'public',
|
|
493
|
+
|
|
494
|
+
// Enable caching in production
|
|
495
|
+
cache: process.env.NODE_ENV === 'production',
|
|
496
|
+
|
|
497
|
+
// Debug mode in development
|
|
498
|
+
debug: process.env.NODE_ENV !== 'production',
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export default config
|
|
502
|
+
`,
|
|
503
|
+
|
|
504
|
+
'package.json': (name: string) => JSON.stringify({
|
|
505
|
+
name,
|
|
506
|
+
version: '0.0.1',
|
|
507
|
+
type: 'module',
|
|
508
|
+
scripts: {
|
|
509
|
+
dev: 'bun run build:stores && bun run --bun stx dev',
|
|
510
|
+
build: 'bun run build:stores && bun run --bun stx build',
|
|
511
|
+
'build:stores': 'bun run lib/build.ts',
|
|
512
|
+
preview: 'bun run --bun stx preview',
|
|
513
|
+
},
|
|
514
|
+
dependencies: {},
|
|
515
|
+
devDependencies: {
|
|
516
|
+
'@stacksjs/stx': 'latest',
|
|
517
|
+
'bun-plugin-stx': 'latest',
|
|
518
|
+
typescript: '^5.0.0',
|
|
519
|
+
},
|
|
520
|
+
}, null, 2),
|
|
521
|
+
|
|
522
|
+
'bunfig.toml': `preload = ["bun-plugin-stx"]
|
|
523
|
+
`,
|
|
524
|
+
|
|
525
|
+
'tsconfig.json': JSON.stringify({
|
|
526
|
+
compilerOptions: {
|
|
527
|
+
target: 'ESNext',
|
|
528
|
+
module: 'ESNext',
|
|
529
|
+
moduleResolution: 'bundler',
|
|
530
|
+
strict: true,
|
|
531
|
+
esModuleInterop: true,
|
|
532
|
+
skipLibCheck: true,
|
|
533
|
+
resolveJsonModule: true,
|
|
534
|
+
declaration: true,
|
|
535
|
+
outDir: 'dist',
|
|
536
|
+
rootDir: '.',
|
|
537
|
+
},
|
|
538
|
+
include: ['**/*.ts', '**/*.stx'],
|
|
539
|
+
exclude: ['node_modules', 'dist'],
|
|
540
|
+
}, null, 2),
|
|
541
|
+
|
|
542
|
+
'.gitignore': `# Dependencies
|
|
543
|
+
node_modules/
|
|
544
|
+
|
|
545
|
+
# Build output
|
|
546
|
+
dist/
|
|
547
|
+
.stx/
|
|
548
|
+
|
|
549
|
+
# Environment
|
|
550
|
+
.env
|
|
551
|
+
.env.local
|
|
552
|
+
.env.*.local
|
|
553
|
+
|
|
554
|
+
# IDE
|
|
555
|
+
.vscode/
|
|
556
|
+
.idea/
|
|
557
|
+
*.swp
|
|
558
|
+
*.swo
|
|
559
|
+
|
|
560
|
+
# OS
|
|
561
|
+
.DS_Store
|
|
562
|
+
Thumbs.db
|
|
563
|
+
|
|
564
|
+
# Logs
|
|
565
|
+
*.log
|
|
566
|
+
npm-debug.log*
|
|
567
|
+
`,
|
|
568
|
+
|
|
569
|
+
'README.md': (name: string) => `# ${name}
|
|
570
|
+
|
|
571
|
+
A modern web application built with [STX](https://stx.sh).
|
|
572
|
+
|
|
573
|
+
## Getting Started
|
|
574
|
+
|
|
575
|
+
\`\`\`bash
|
|
576
|
+
# Start development server
|
|
577
|
+
bun dev
|
|
578
|
+
|
|
579
|
+
# Build for production
|
|
580
|
+
bun run build
|
|
581
|
+
|
|
582
|
+
# Preview production build
|
|
583
|
+
bun run preview
|
|
584
|
+
\`\`\`
|
|
585
|
+
|
|
586
|
+
## Project Structure
|
|
587
|
+
|
|
588
|
+
\`\`\`
|
|
589
|
+
${name}/
|
|
590
|
+
pages/ # File-based routing
|
|
591
|
+
index.stx # Homepage (/)
|
|
592
|
+
components/ # Reusable components
|
|
593
|
+
public/ # Static assets
|
|
594
|
+
stx.config.ts # STX configuration
|
|
595
|
+
\`\`\`
|
|
596
|
+
|
|
597
|
+
## Learn More
|
|
598
|
+
|
|
599
|
+
- [STX Documentation](https://stx.sh)
|
|
600
|
+
- [GitHub Repository](https://github.com/stacksjs/stx)
|
|
601
|
+
`,
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function createProject(projectName: string, targetDir: string) {
|
|
605
|
+
log('')
|
|
606
|
+
log(`${colors.bright}${colors.blue} Creating STX app in ${colors.cyan}${targetDir}${colors.reset}`)
|
|
607
|
+
log('')
|
|
608
|
+
|
|
609
|
+
// Create directories
|
|
610
|
+
const dirs = ['pages', 'components', 'public', 'public/js', 'lib', 'lib/stores', 'lib/composables']
|
|
611
|
+
for (const dir of dirs) {
|
|
612
|
+
const dirPath = path.join(targetDir, dir)
|
|
613
|
+
fs.mkdirSync(dirPath, { recursive: true })
|
|
614
|
+
}
|
|
615
|
+
success('Created project structure')
|
|
616
|
+
|
|
617
|
+
// Write template files
|
|
618
|
+
for (const [filePath, content] of Object.entries(templates)) {
|
|
619
|
+
const fullPath = path.join(targetDir, filePath)
|
|
620
|
+
const fileContent = typeof content === 'function' ? content(projectName) : content
|
|
621
|
+
fs.writeFileSync(fullPath, fileContent)
|
|
622
|
+
}
|
|
623
|
+
success('Created template files')
|
|
624
|
+
|
|
625
|
+
// Install dependencies
|
|
626
|
+
info('Installing dependencies...')
|
|
627
|
+
|
|
628
|
+
return new Promise<void>((resolve) => {
|
|
629
|
+
const child = spawn('bun', ['install'], {
|
|
630
|
+
cwd: targetDir,
|
|
631
|
+
stdio: 'inherit',
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
child.on('close', (code) => {
|
|
635
|
+
if (code === 0) {
|
|
636
|
+
success('Installed dependencies')
|
|
637
|
+
} else {
|
|
638
|
+
log('')
|
|
639
|
+
info('Could not install STX packages from npm (not yet published)')
|
|
640
|
+
info('For local development, link the packages manually:')
|
|
641
|
+
log(` ${colors.dim}cd ${targetDir}${colors.reset}`)
|
|
642
|
+
log(` ${colors.dim}bun link @stacksjs/stx${colors.reset}`)
|
|
643
|
+
log(` ${colors.dim}bun link bun-plugin-stx${colors.reset}`)
|
|
644
|
+
}
|
|
645
|
+
resolve()
|
|
646
|
+
})
|
|
647
|
+
|
|
648
|
+
child.on('error', () => {
|
|
649
|
+
info('Could not run bun install')
|
|
650
|
+
resolve()
|
|
651
|
+
})
|
|
652
|
+
})
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
async function main() {
|
|
656
|
+
const args = process.argv.slice(2)
|
|
657
|
+
|
|
658
|
+
// Get project name from args or prompt
|
|
659
|
+
let projectName = args[0]
|
|
660
|
+
|
|
661
|
+
if (!projectName) {
|
|
662
|
+
// Default to 'my-stx-app' if no name provided
|
|
663
|
+
projectName = 'my-stx-app'
|
|
664
|
+
info(`No project name provided, using "${projectName}"`)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Validate project name
|
|
668
|
+
if (!/^[a-zA-Z0-9-_]+$/.test(projectName)) {
|
|
669
|
+
error('Project name can only contain letters, numbers, dashes, and underscores')
|
|
670
|
+
process.exit(1)
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const targetDir = path.resolve(process.cwd(), projectName)
|
|
674
|
+
|
|
675
|
+
// Check if directory exists
|
|
676
|
+
if (fs.existsSync(targetDir)) {
|
|
677
|
+
const files = fs.readdirSync(targetDir)
|
|
678
|
+
if (files.length > 0) {
|
|
679
|
+
error(`Directory "${projectName}" already exists and is not empty`)
|
|
680
|
+
process.exit(1)
|
|
681
|
+
}
|
|
682
|
+
} else {
|
|
683
|
+
fs.mkdirSync(targetDir, { recursive: true })
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
try {
|
|
687
|
+
await createProject(projectName, targetDir)
|
|
688
|
+
|
|
689
|
+
log('')
|
|
690
|
+
log(`${colors.green}${colors.bright} Done!${colors.reset} Created ${projectName}`)
|
|
691
|
+
log('')
|
|
692
|
+
log(' To get started:')
|
|
693
|
+
log('')
|
|
694
|
+
log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
|
|
695
|
+
log(` ${colors.cyan}bun dev${colors.reset}`)
|
|
696
|
+
log('')
|
|
697
|
+
} catch (err) {
|
|
698
|
+
error(`Failed to create project: ${err}`)
|
|
699
|
+
process.exit(1)
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
main()
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-stx",
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "Create a new STX application",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-stx": "./index.ts"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.ts",
|
|
11
|
+
"templates"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"stx",
|
|
15
|
+
"create",
|
|
16
|
+
"scaffold",
|
|
17
|
+
"template",
|
|
18
|
+
"bun"
|
|
19
|
+
],
|
|
20
|
+
"author": "Chris Breuer <chris@stacksjs.org>",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/stacksjs/stx.git",
|
|
25
|
+
"directory": "packages/create-stx"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/stacksjs/stx/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/stacksjs/stx#readme"
|
|
31
|
+
}
|