create-viteframe 1.0.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/README.md ADDED
@@ -0,0 +1,228 @@
1
+ # create-viteframe
2
+
3
+ Scaffold a new ViteFrame app — a lightweight SPA framework built on Vite with file-based routing, reactive state, and dynamic HTML components. No external JS dependencies.
4
+
5
+ ## Quick start
6
+
7
+ ```bash
8
+ npm create viteframe@latest my-app
9
+ cd my-app
10
+ npm install
11
+ npm run dev
12
+ ```
13
+
14
+ Or with pnpm / yarn:
15
+
16
+ ```bash
17
+ pnpm create viteframe my-app
18
+ yarn create viteframe my-app
19
+ ```
20
+
21
+ ---
22
+
23
+ ## What you get
24
+
25
+ ```
26
+ src/
27
+ ├── core/
28
+ │ ├── router.js # File-based SPA router (~180 lines)
29
+ │ ├── state.js # Reactive state store (~100 lines)
30
+ │ └── components.js # Dynamic component loader (~120 lines)
31
+ ├── pages/
32
+ │ ├── landing.page.html # → /
33
+ │ ├── about.page.html # → /about
34
+ │ ├── docs.page.html # → /docs
35
+ │ └── [id]post.page.html # → /post/:id
36
+ ├── components/
37
+ │ ├── header.comp.html
38
+ │ └── toast.comp.html
39
+ ├── styles/
40
+ │ └── main.css # Design tokens + utility classes
41
+ └── main.js # Bootstrap
42
+ ```
43
+
44
+ All framework code lives in your project — read it, modify it, own it.
45
+
46
+ ---
47
+
48
+ ## Routing
49
+
50
+ File name determines the route. Drop files in `src/pages/`.
51
+
52
+ | File | Route |
53
+ |------|-------|
54
+ | `landing.page.html` | `/` |
55
+ | `about.page.html` | `/about` |
56
+ | `[id]post.page.html` | `/post/:id` |
57
+ | `[id-slug]post.page.html` | `/post/:id/:slug` |
58
+ | `[lang-id]post.page.html` | `/post/:lang/:id` |
59
+
60
+ ### Programmatic navigation
61
+
62
+ ```js
63
+ window.__vf.navigate('/about')
64
+ window.__vf.navigate('/post/42', { replace: true })
65
+ ```
66
+
67
+ ---
68
+
69
+ ## State
70
+
71
+ ### Global state
72
+
73
+ Shared across all pages. Persists during navigation.
74
+
75
+ ```js
76
+ const { globalState } = window.__vf
77
+
78
+ globalState.set('user', { name: 'Alice' })
79
+ globalState.get('user')
80
+ globalState.subscribe('user', (val) => console.log(val))
81
+
82
+ // Bind value → DOM (auto-updates when state changes)
83
+ globalState.bind('user', el, u => u?.name ?? 'Guest')
84
+
85
+ // Two-way bind: input ↔ state
86
+ globalState.bindInput('search', inputEl)
87
+
88
+ // Partial merge
89
+ globalState.merge('settings', { darkMode: true })
90
+ ```
91
+
92
+ ### Page state
93
+
94
+ Isolated to a single page visit. Created inside `onMount`, discarded on navigation.
95
+
96
+ ```js
97
+ export function onMount() {
98
+ const { createPageState } = window.__vf
99
+ const state = createPageState({ count: 0, open: false })
100
+
101
+ state.update('count', n => n + 1)
102
+ state.bind('count', document.getElementById('counter'))
103
+ }
104
+ ```
105
+
106
+ ### Computed values
107
+
108
+ ```js
109
+ const { computed, globalState } = window.__vf
110
+
111
+ const fullName = computed(
112
+ ['firstName', 'lastName'],
113
+ [globalState, globalState],
114
+ (first, last) => `${first} ${last}`
115
+ )
116
+
117
+ fullName.get() // 'Alice Smith'
118
+ fullName.subscribe(v => …) // fires when either dep changes
119
+ ```
120
+
121
+ ---
122
+
123
+ ## Components
124
+
125
+ Components are `.comp.html` files in `src/components/`. They support scoped `<style>` and `<script>` blocks.
126
+
127
+ ### Use in HTML
128
+
129
+ ```html
130
+ <!-- Simple -->
131
+ <component src="header"></component>
132
+
133
+ <!-- With props via data-* attributes -->
134
+ <component src="card" data-title="Hello" data-body="World"></component>
135
+ ```
136
+
137
+ ### Template syntax
138
+
139
+ ```html
140
+ <!-- src/components/card.comp.html -->
141
+ <div class="card">
142
+ <h3>{{title}}</h3>
143
+ <p>{{body}}</p>
144
+ </div>
145
+ ```
146
+
147
+ ### Mount from JavaScript
148
+
149
+ ```js
150
+ const { mountComponent } = window.__vf
151
+
152
+ await mountComponent('#target', 'card', { title: 'Hello', body: 'World' })
153
+ ```
154
+
155
+ ---
156
+
157
+ ## Page lifecycle
158
+
159
+ Add `<script data-lifecycle>` to any page and export `onMount`:
160
+
161
+ ```html
162
+ <script data-lifecycle>
163
+ export function onMount({ params, query, globalState }) {
164
+ // params → route params e.g. { id: '42' }
165
+ // query → ?key=val e.g. { tab: 'info' }
166
+ // globalState → shared store
167
+
168
+ console.log('id:', params.id)
169
+ console.log('tab:', query.tab)
170
+
171
+ const { createPageState } = window.__vf
172
+ const state = createPageState({ count: 0 })
173
+ }
174
+ </script>
175
+ ```
176
+
177
+ ---
178
+
179
+ ## Navigation guards
180
+
181
+ In `src/main.js`:
182
+
183
+ ```js
184
+ router.beforeEach(({ pathname }) => {
185
+ if (pathname.startsWith('/admin') && !globalState.get('user')) {
186
+ router.navigate('/login')
187
+ return false // cancel navigation
188
+ }
189
+ })
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Toast notifications
195
+
196
+ Include `<component src="toast">` on a page, then anywhere:
197
+
198
+ ```js
199
+ window.toast('Saved!', 'success') // green
200
+ window.toast('Something broke', 'error') // red
201
+ window.toast('FYI', 'info', 5000) // purple, 5s
202
+ ```
203
+
204
+ ---
205
+
206
+ ## CSS design tokens
207
+
208
+ All colors, spacing, and radii are CSS variables in `src/styles/main.css`.
209
+
210
+ ```css
211
+ /* Override in your own CSS */
212
+ :root {
213
+ --accent: #your-color;
214
+ --radius: 12px;
215
+ }
216
+ ```
217
+
218
+ Dark/light theme is toggled by setting `data-theme="light"` on `<html>`:
219
+
220
+ ```js
221
+ window.__vf.globalState.set('theme', 'light')
222
+ ```
223
+
224
+ ---
225
+
226
+ ## License
227
+
228
+ MIT
package/bin/create.js ADDED
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import { execSync } from 'child_process'
5
+ import { fileURLToPath } from 'url'
6
+ import readline from 'readline'
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
+ const templateDir = path.join(__dirname, '../template')
10
+
11
+ // ─── Colors ──────────────────────────────────────────────────────
12
+ const c = {
13
+ reset: '\x1b[0m',
14
+ bold: '\x1b[1m',
15
+ dim: '\x1b[2m',
16
+ purple: '\x1b[35m',
17
+ green: '\x1b[32m',
18
+ cyan: '\x1b[36m',
19
+ yellow: '\x1b[33m',
20
+ red: '\x1b[31m',
21
+ }
22
+ const bold = s => `${c.bold}${s}${c.reset}`
23
+ const purple = s => `${c.purple}${s}${c.reset}`
24
+ const green = s => `${c.green}${s}${c.reset}`
25
+ const dim = s => `${c.dim}${s}${c.reset}`
26
+ const yellow = s => `${c.yellow}${s}${c.reset}`
27
+ const red = s => `${c.red}${s}${c.reset}`
28
+
29
+ // ─── Prompt helper ───────────────────────────────────────────────
30
+ function prompt(question) {
31
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
32
+ return new Promise(resolve => {
33
+ rl.question(question, answer => { rl.close(); resolve(answer.trim()) })
34
+ })
35
+ }
36
+
37
+ // ─── Copy template recursively ───────────────────────────────────
38
+ function copyDir(src, dest) {
39
+ fs.mkdirSync(dest, { recursive: true })
40
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
41
+ const srcPath = path.join(src, entry.name)
42
+ const destPath = path.join(dest, entry.name)
43
+ if (entry.isDirectory()) {
44
+ copyDir(srcPath, destPath)
45
+ } else {
46
+ // Rename _gitignore → .gitignore (npm strips dot-files from published packages)
47
+ const finalDest = destPath.replace(/_gitignore$/, '.gitignore')
48
+ fs.copyFileSync(srcPath, finalDest)
49
+ }
50
+ }
51
+ }
52
+
53
+ // ─── Main ─────────────────────────────────────────────────────────
54
+ async function main() {
55
+ console.log()
56
+ console.log(bold(purple(' ⚡ ViteFrame')))
57
+ console.log(dim(' File-based SPA framework powered by Vite\n'))
58
+
59
+ // Project name from arg or prompt
60
+ let projectName = process.argv[2]
61
+ if (!projectName) {
62
+ projectName = await prompt(` ${bold('Project name')} ${dim('(my-app)')} › `)
63
+ if (!projectName) projectName = 'my-app'
64
+ }
65
+
66
+ // Sanitize
67
+ const dirName = projectName.replace(/\s+/g, '-').toLowerCase()
68
+ const targetDir = path.resolve(process.cwd(), dirName)
69
+
70
+ // Check if dir exists
71
+ if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
72
+ const overwrite = await prompt(` ${yellow('!')} Directory "${dirName}" is not empty. Overwrite? ${dim('(y/N)')} › `)
73
+ if (!overwrite.toLowerCase().startsWith('y')) {
74
+ console.log(`\n ${red('Aborted.')}\n`)
75
+ process.exit(1)
76
+ }
77
+ fs.rmSync(targetDir, { recursive: true, force: true })
78
+ }
79
+
80
+ // Detect package manager
81
+ const agent = process.env.npm_config_user_agent || ''
82
+ const pm = agent.startsWith('pnpm') ? 'pnpm'
83
+ : agent.startsWith('yarn') ? 'yarn'
84
+ : 'npm'
85
+
86
+ // Copy template
87
+ console.log(`\n Creating ${bold(dirName)}...\n`)
88
+ copyDir(templateDir, targetDir)
89
+
90
+ // Write package.json with project name
91
+ const pkgPath = path.join(targetDir, 'package.json')
92
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
93
+ pkg.name = dirName
94
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
95
+
96
+ // Done
97
+ const rel = path.relative(process.cwd(), targetDir)
98
+ const cdCmd = rel === dirName ? dirName : rel
99
+
100
+ console.log(` ${green('✓')} Created ${bold(dirName)}\n`)
101
+ console.log(` Next steps:\n`)
102
+ console.log(` ${dim('$')} ${bold(`cd ${cdCmd}`)}`)
103
+ console.log(` ${dim('$')} ${bold(`${pm} install`)}`)
104
+ console.log(` ${dim('$')} ${bold(`${pm} run dev`)}`)
105
+ console.log()
106
+ console.log(` ${dim('Docs → src/pages/docs.page.html')}`)
107
+ console.log()
108
+ }
109
+
110
+ main().catch(e => {
111
+ console.error(red(`\n Error: ${e.message}\n`))
112
+ process.exit(1)
113
+ })
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "create-viteframe",
3
+ "version": "1.0.0",
4
+ "description": "Create a new ViteFrame app — file-based routing, reactive state, dynamic components",
5
+ "keywords": ["vite", "spa", "router", "framework", "scaffold"],
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "bin": {
9
+ "create-viteframe": "./bin/create.js"
10
+ },
11
+ "files": [
12
+ "bin",
13
+ "template"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18.0.0"
17
+ }
18
+ }
@@ -0,0 +1,3 @@
1
+ node_modules
2
+ dist
3
+ .DS_Store
@@ -0,0 +1,15 @@
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.0" />
6
+ <title>ViteFrame App</title>
7
+ <link rel="stylesheet" href="/src/styles/main.css" />
8
+ </head>
9
+ <body>
10
+ <div id="app">
11
+ <div id="router-outlet"></div>
12
+ </div>
13
+ <script type="module" src="/src/main.js"></script>
14
+ </body>
15
+ </html>
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "my-viteframe-app",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "vite build",
8
+ "preview": "vite preview"
9
+ },
10
+ "devDependencies": {
11
+ "vite": "^5.0.0"
12
+ }
13
+ }
@@ -0,0 +1,42 @@
1
+ <header class="vf-header">
2
+ <div class="container flex-between">
3
+ <a href="/" class="vf-logo">⚡ ViteFrame</a>
4
+ <nav class="flex gap-4">
5
+ <a href="/" class="vf-nav-link">Home</a>
6
+ <a href="/about" class="vf-nav-link">About</a>
7
+ <a href="/docs" class="vf-nav-link">Docs</a>
8
+ </nav>
9
+ <button class="btn btn-ghost" id="vf-theme-btn" style="padding:6px 12px;font-size:.8rem;">◑</button>
10
+ </div>
11
+ </header>
12
+
13
+ <style>
14
+ .vf-header {
15
+ position: sticky; top: 0; z-index: 100; height: 56px;
16
+ display: flex; align-items: center;
17
+ background: color-mix(in srgb, var(--bg) 80%, transparent);
18
+ backdrop-filter: blur(12px);
19
+ border-bottom: 1px solid var(--border);
20
+ }
21
+ .vf-logo { font-weight: 800; color: var(--text); font-size: .95rem; }
22
+ .vf-nav-link {
23
+ font-size: .875rem; font-weight: 500; color: var(--text-2);
24
+ padding: 4px 8px; border-radius: 6px; transition: all .15s;
25
+ }
26
+ .vf-nav-link:hover,
27
+ .vf-nav-link.active { color: var(--text); background: var(--surface-2); }
28
+ </style>
29
+
30
+ <script>
31
+ (function () {
32
+ // Theme toggle
33
+ document.getElementById('vf-theme-btn')?.addEventListener('click', () => {
34
+ const next = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'
35
+ window.__vf?.globalState.set('theme', next)
36
+ })
37
+ // Active link
38
+ document.querySelectorAll('.vf-nav-link').forEach(a => {
39
+ a.classList.toggle('active', a.getAttribute('href') === location.pathname)
40
+ })
41
+ })()
42
+ </script>
@@ -0,0 +1,12 @@
1
+ <div id="toast-container"></div>
2
+
3
+ <script>
4
+ // window.toast('Message', 'success' | 'error' | 'info', ms?)
5
+ window.toast = function (msg, type = 'info', ms = 3000) {
6
+ let el = document.getElementById('toast-container')
7
+ if (!el) { el = document.createElement('div'); el.id = 'toast-container'; document.body.append(el) }
8
+ const t = Object.assign(document.createElement('div'), { className: `toast toast-${type}`, textContent: msg })
9
+ el.append(t)
10
+ setTimeout(() => { t.classList.add('exit'); t.addEventListener('animationend', () => t.remove()) }, ms)
11
+ }
12
+ </script>
@@ -0,0 +1,134 @@
1
+ /**
2
+ * ComponentLoader — Dynamic HTML component loading system
3
+ *
4
+ * Naming: anything ending in .comp.html
5
+ *
6
+ * Usage in HTML:
7
+ * <component src="header"></component>
8
+ * <component src="card" props='{"title":"Hello"}'></component>
9
+ *
10
+ * Usage in JS:
11
+ * import { loadComponent, mountComponent } from './components.js'
12
+ * const html = await loadComponent('header', { user: 'Alice' })
13
+ * await mountComponent('#target', 'card', { title: 'Hello' })
14
+ */
15
+
16
+ const componentCache = new Map()
17
+
18
+ /**
19
+ * Fetch and parse a .comp.html file
20
+ * Props are injected as {{propName}} template variables
21
+ */
22
+ export async function loadComponent(name, props = {}) {
23
+ const cacheKey = name
24
+
25
+ if (!componentCache.has(cacheKey)) {
26
+ const path = `/src/components/${name}.comp.html`
27
+ const res = await fetch(path)
28
+ if (!res.ok) throw new Error(`Component not found: ${name} (${path})`)
29
+ const text = await res.text()
30
+ componentCache.set(cacheKey, text)
31
+ }
32
+
33
+ let html = componentCache.get(cacheKey)
34
+
35
+ // Inject props via {{propName}} syntax
36
+ html = interpolate(html, props)
37
+
38
+ return html
39
+ }
40
+
41
+ /**
42
+ * Mount a component into a DOM element
43
+ */
44
+ export async function mountComponent(selectorOrEl, name, props = {}) {
45
+ const el = typeof selectorOrEl === 'string'
46
+ ? document.querySelector(selectorOrEl)
47
+ : selectorOrEl
48
+
49
+ if (!el) throw new Error(`Mount target not found: ${selectorOrEl}`)
50
+
51
+ const html = await loadComponent(name, props)
52
+ el.innerHTML = html
53
+
54
+ // Auto-process any nested <component> tags
55
+ await processComponentTags(el)
56
+
57
+ // Run any inline <script> tags in the component
58
+ await executeComponentScripts(el)
59
+
60
+ return el
61
+ }
62
+
63
+ /**
64
+ * Process all <component src="name"> tags in a container
65
+ */
66
+ export async function processComponentTags(container = document) {
67
+ const tags = container.querySelectorAll('component[src]')
68
+ const tasks = Array.from(tags).map(async (tag) => {
69
+ const name = tag.getAttribute('src')
70
+ const propsAttr = tag.getAttribute('props')
71
+ const props = propsAttr ? JSON.parse(propsAttr) : {}
72
+
73
+ // Merge data-* attributes as props too
74
+ Object.entries(tag.dataset).forEach(([k, v]) => {
75
+ try { props[k] = JSON.parse(v) } catch { props[k] = v }
76
+ })
77
+
78
+ try {
79
+ const html = await loadComponent(name, props)
80
+ const wrapper = document.createElement('div')
81
+ wrapper.className = tag.className || ''
82
+ wrapper.innerHTML = html
83
+ tag.replaceWith(wrapper)
84
+ await processComponentTags(wrapper)
85
+ await executeComponentScripts(wrapper)
86
+ } catch (e) {
87
+ console.error(`Failed to load component <${name}>:`, e)
88
+ tag.replaceWith(createErrorPlaceholder(name, e.message))
89
+ }
90
+ })
91
+ await Promise.all(tasks)
92
+ }
93
+
94
+ /**
95
+ * Execute <script> tags inside a freshly-inserted HTML fragment
96
+ */
97
+ export async function executeComponentScripts(container) {
98
+ const scripts = container.querySelectorAll('script')
99
+ for (const script of scripts) {
100
+ const newScript = document.createElement('script')
101
+ newScript.type = script.type || 'text/javascript'
102
+ if (script.src) {
103
+ newScript.src = script.src
104
+ } else {
105
+ newScript.textContent = script.textContent
106
+ }
107
+ script.replaceWith(newScript)
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Simple {{key}} template interpolation
113
+ * Supports nested: {{user.name}}
114
+ */
115
+ function interpolate(template, data) {
116
+ return template.replace(/\{\{([\w.]+)\}\}/g, (_, path) => {
117
+ const value = path.split('.').reduce((obj, key) => obj?.[key], data)
118
+ return value !== undefined ? String(value) : ''
119
+ })
120
+ }
121
+
122
+ function createErrorPlaceholder(name, msg) {
123
+ const div = document.createElement('div')
124
+ div.style.cssText = 'border:1px dashed #ff4444;padding:8px;color:#ff4444;font-size:12px;font-family:monospace'
125
+ div.textContent = `⚠ Component "${name}": ${msg}`
126
+ return div
127
+ }
128
+
129
+ /**
130
+ * Clear the component cache (useful during HMR / dev)
131
+ */
132
+ export function clearComponentCache() {
133
+ componentCache.clear()
134
+ }