@zackt/create-ztweb 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 techzt13
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # create-ztweb
2
+
3
+ Scaffold a new **ztweb** project — a Vue 3 + Vite framework using `.zweb` file extensions.
4
+
5
+ ## What is ztweb?
6
+
7
+ **ztweb** is a Vue 3 + Vite scaffolding tool that uses `.zweb` Single File Components instead of `.vue` files. `.zweb` files have the exact same syntax as Vue SFCs (`<template>`, `<script setup>`, `<style>` blocks), but use a custom file extension and are compiled by the included `vite-plugin-zweb` plugin.
8
+
9
+ ## Usage
10
+
11
+ ```bash
12
+ # Using npm
13
+ npm create ztweb@latest
14
+
15
+ # Or specify a project name directly
16
+ npm create ztweb@latest my-app
17
+
18
+ # Using pnpm
19
+ pnpm create ztweb
20
+
21
+ # Using yarn
22
+ yarn create ztweb
23
+ ```
24
+
25
+ Then follow the prompts!
26
+
27
+ Once scaffolded, run:
28
+
29
+ ```bash
30
+ cd my-app
31
+ npm install
32
+ npm run dev
33
+ ```
34
+
35
+ Your app will be running at `http://localhost:5173`
36
+
37
+ ## What Gets Scaffolded
38
+
39
+ The CLI creates a complete Vue 3 + Vite project with:
40
+
41
+ - **Vue 3** with Composition API (`<script setup>`)
42
+ - **Vite 5** for blazing fast dev server and builds
43
+ - **`.zweb` files** instead of `.vue` files
44
+ - **vite-plugin-zweb** — a custom Vite plugin that compiles `.zweb` SFCs using `@vue/compiler-sfc`
45
+ - Hot Module Replacement (HMR) for `.zweb` files
46
+ - Support for `<style scoped>`, template expressions, directives, etc.
47
+ - Sample components to get you started
48
+
49
+ ## How `.zweb` Files Work
50
+
51
+ `.zweb` files are **Single File Components** with identical syntax to `.vue` files:
52
+
53
+ ```html
54
+ <script setup>
55
+ import { ref } from 'vue'
56
+ const count = ref(0)
57
+ </script>
58
+
59
+ <template>
60
+ <button @click="count++">{{ count }}</button>
61
+ </template>
62
+
63
+ <style scoped>
64
+ button { color: #42b883; }
65
+ </style>
66
+ ```
67
+
68
+ The only difference is the file extension. The `vite-plugin-zweb` plugin hooks into Vite's transform pipeline and uses Vue's official `@vue/compiler-sfc` to compile `.zweb` files into JavaScript modules.
69
+
70
+ ## Project Structure
71
+
72
+ ```
73
+ my-app/
74
+ ├── index.html
75
+ ├── package.json
76
+ ├── vite.config.js
77
+ ├── jsconfig.json
78
+ ├── public/
79
+ │ └── favicon.ico
80
+ ├── src/
81
+ │ ├── main.js
82
+ │ ├── App.zweb # Main app component
83
+ │ ├── style.css
84
+ │ ├── assets/
85
+ │ │ └── logo.svg
86
+ │ └── components/
87
+ │ └── HelloWorld.zweb
88
+ └── vite-plugin-zweb/ # Custom Vite plugin (bundled locally)
89
+ ├── package.json
90
+ └── index.js
91
+ ```
92
+
93
+ ## Development Scripts
94
+
95
+ ```bash
96
+ npm run dev # Start dev server
97
+ npm run build # Build for production
98
+ npm run preview # Preview production build
99
+ ```
100
+
101
+ ## Contributing to create-ztweb
102
+
103
+ To develop and test the CLI locally:
104
+
105
+ ```bash
106
+ # Clone the repo
107
+ git clone https://github.com/techzt13/ztweb.git
108
+ cd ztweb
109
+
110
+ # Install dependencies
111
+ npm install
112
+
113
+ # Link it globally for testing
114
+ npm link
115
+
116
+ # Test scaffolding
117
+ create-ztweb my-test-app
118
+
119
+ # Test the scaffolded app
120
+ cd my-test-app
121
+ npm install
122
+ npm run dev
123
+ ```
124
+
125
+ ## License
126
+
127
+ MIT
128
+
129
+ ## Links
130
+
131
+ - [GitHub Repository](https://github.com/techzt13/ztweb)
132
+ - [Vue.js Documentation](https://vuejs.org/)
133
+ - [Vite Documentation](https://vitejs.dev/)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import('../src/index.js').then(({ default: init }) => {
4
+ init().catch((e) => {
5
+ console.error(e)
6
+ process.exit(1)
7
+ })
8
+ })
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@zackt/create-ztweb",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Scaffold a new ztweb (Vue + Vite) project with .zweb file support",
6
+ "keywords": [
7
+ "ztweb",
8
+ "vue",
9
+ "vite",
10
+ "scaffold",
11
+ "cli",
12
+ "zweb"
13
+ ],
14
+ "license": "MIT",
15
+ "bin": {
16
+ "create-ztweb": "bin/create-ztweb.js"
17
+ },
18
+ "files": [
19
+ "bin",
20
+ "src",
21
+ "template",
22
+ "vite-plugin-zweb"
23
+ ],
24
+ "dependencies": {
25
+ "fs-extra": "^11.2.0",
26
+ "handlebars": "^4.7.8",
27
+ "kolorist": "^1.8.0",
28
+ "minimist": "^1.2.8",
29
+ "prompts": "^2.4.2"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/techzt13/ztweb.git"
34
+ }
35
+ }
package/src/index.js ADDED
@@ -0,0 +1,636 @@
1
+ import fs from 'fs-extra'
2
+ import path from 'path'
3
+ import { fileURLToPath } from 'url'
4
+ import prompts from 'prompts'
5
+ import minimist from 'minimist'
6
+ import { red, green, bold, cyan } from 'kolorist'
7
+ import Handlebars from 'handlebars'
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
10
+
11
+ // Register Handlebars helper for equality checks
12
+ Handlebars.registerHelper('eq', (a, b) => a === b)
13
+
14
+ async function init() {
15
+ const argv = minimist(process.argv.slice(2))
16
+
17
+ let projectName = argv._[0]
18
+
19
+ const onCancel = () => {
20
+ console.log(red('✖') + ' Operation cancelled')
21
+ process.exit(1)
22
+ }
23
+
24
+ // If no project name provided, ask interactively
25
+ if (!projectName) {
26
+ const result = await prompts(
27
+ {
28
+ type: 'text',
29
+ name: 'projectName',
30
+ message: 'Project name:',
31
+ initial: 'ztweb-project',
32
+ },
33
+ { onCancel }
34
+ )
35
+ projectName = result.projectName
36
+ }
37
+
38
+ // Validate project name
39
+ if (!projectName || !isValidPackageName(projectName)) {
40
+ console.error(red('✖') + ' Invalid project name')
41
+ process.exit(1)
42
+ }
43
+
44
+ const targetDir = path.resolve(process.cwd(), projectName)
45
+
46
+ // Check if directory exists and handle confirmation
47
+ if (fs.existsSync(targetDir)) {
48
+ const files = fs.readdirSync(targetDir)
49
+ if (files.length > 0) {
50
+ const result = await prompts(
51
+ {
52
+ type: 'confirm',
53
+ name: 'overwrite',
54
+ message: `Directory ${cyan(projectName)} already exists and is not empty. Continue?`,
55
+ initial: false,
56
+ },
57
+ { onCancel }
58
+ )
59
+
60
+ if (!result.overwrite) {
61
+ console.log(red('✖') + ' Operation cancelled')
62
+ process.exit(1)
63
+ }
64
+ }
65
+ }
66
+
67
+ // Ask for optional features
68
+ const features = await prompts(
69
+ [
70
+ {
71
+ type: 'toggle',
72
+ name: 'useTypeScript',
73
+ message: 'Add TypeScript?',
74
+ initial: false,
75
+ active: 'Yes',
76
+ inactive: 'No',
77
+ },
78
+ {
79
+ type: 'toggle',
80
+ name: 'useJsx',
81
+ message: 'Add JSX Support?',
82
+ initial: false,
83
+ active: 'Yes',
84
+ inactive: 'No',
85
+ },
86
+ {
87
+ type: 'toggle',
88
+ name: 'useRouter',
89
+ message: 'Add ztweb Router for Single Page Application development?',
90
+ initial: false,
91
+ active: 'Yes',
92
+ inactive: 'No',
93
+ },
94
+ {
95
+ type: 'toggle',
96
+ name: 'usePinia',
97
+ message: 'Add Pinia for state management?',
98
+ initial: false,
99
+ active: 'Yes',
100
+ inactive: 'No',
101
+ },
102
+ {
103
+ type: 'toggle',
104
+ name: 'useVitest',
105
+ message: 'Add Vitest for Unit testing?',
106
+ initial: false,
107
+ active: 'Yes',
108
+ inactive: 'No',
109
+ },
110
+ {
111
+ type: 'select',
112
+ name: 'useE2e',
113
+ message: 'Add an End-to-End Testing Solution?',
114
+ choices: [
115
+ { title: 'No', value: false },
116
+ { title: 'Cypress', value: 'cypress' },
117
+ { title: 'Nightwatch', value: 'nightwatch' },
118
+ { title: 'Playwright', value: 'playwright' },
119
+ ],
120
+ initial: 0,
121
+ },
122
+ {
123
+ type: 'toggle',
124
+ name: 'useEslint',
125
+ message: 'Add ESLint for code quality?',
126
+ initial: false,
127
+ active: 'Yes',
128
+ inactive: 'No',
129
+ },
130
+ {
131
+ type: (prev, values) => (values.useEslint ? 'toggle' : null),
132
+ name: 'usePrettier',
133
+ message: 'Add Prettier for code formatting?',
134
+ initial: false,
135
+ active: 'Yes',
136
+ inactive: 'No',
137
+ },
138
+ ],
139
+ { onCancel }
140
+ )
141
+
142
+ const {
143
+ useTypeScript = false,
144
+ useJsx = false,
145
+ useRouter = false,
146
+ usePinia = false,
147
+ useVitest = false,
148
+ useE2e = false,
149
+ useEslint = false,
150
+ usePrettier = false,
151
+ } = features
152
+
153
+ const flags = {
154
+ projectName,
155
+ useTypeScript,
156
+ useJsx,
157
+ useRouter,
158
+ usePinia,
159
+ useVitest,
160
+ useE2e,
161
+ useEslint,
162
+ usePrettier,
163
+ }
164
+
165
+ // Create target directory
166
+ fs.ensureDirSync(targetDir)
167
+
168
+ console.log(green('✓') + ` Creating project in ${cyan(targetDir)}...`)
169
+
170
+ const templateDir = path.resolve(__dirname, '../template')
171
+ const pluginDir = path.resolve(__dirname, '../vite-plugin-zweb')
172
+
173
+ // Copy all template files (skip files we'll handle specially)
174
+ copyDirectory(templateDir, targetDir, flags)
175
+
176
+ // Copy vite-plugin-zweb directory
177
+ const targetPluginDir = path.join(targetDir, 'vite-plugin-zweb')
178
+ fs.copySync(pluginDir, targetPluginDir)
179
+
180
+ // Rename _gitignore to .gitignore
181
+ const gitignorePath = path.join(targetDir, '_gitignore')
182
+ if (fs.existsSync(gitignorePath)) {
183
+ fs.renameSync(gitignorePath, path.join(targetDir, '.gitignore'))
184
+ }
185
+
186
+ const ext = useTypeScript ? 'ts' : 'js'
187
+
188
+ // Generate vite.config (overwrite copied version with feature-aware version)
189
+ writeViteConfig(targetDir, flags)
190
+
191
+ // Generate src/main.js or main.ts
192
+ writeMainFile(targetDir, flags)
193
+
194
+ // Generate App.zweb (router vs non-router variant)
195
+ writeAppZweb(targetDir, useRouter)
196
+
197
+ // TypeScript config files
198
+ if (useTypeScript) {
199
+ writeTsConfig(targetDir)
200
+ fs.writeFileSync(
201
+ path.join(targetDir, 'env.d.ts'),
202
+ `/// <reference types="vite/client" />\n\ndeclare module '*.zweb' {\n import type { DefineComponent } from 'vue'\n const component: DefineComponent<{}, {}, any>\n export default component\n}\n`
203
+ )
204
+ // Remove main.js (replaced by main.ts)
205
+ const oldMain = path.join(targetDir, 'src', 'main.js')
206
+ if (fs.existsSync(oldMain)) fs.removeSync(oldMain)
207
+ }
208
+
209
+ // Router files
210
+ if (useRouter) {
211
+ fs.ensureDirSync(path.join(targetDir, 'src', 'router'))
212
+ fs.ensureDirSync(path.join(targetDir, 'src', 'views'))
213
+ fs.writeFileSync(path.join(targetDir, 'src', 'router', `index.${ext}`), routerIndexContent())
214
+ fs.writeFileSync(path.join(targetDir, 'src', 'views', 'HomeView.zweb'), homeViewContent())
215
+ fs.writeFileSync(path.join(targetDir, 'src', 'views', 'AboutView.zweb'), aboutViewContent())
216
+ }
217
+
218
+ // Pinia store
219
+ if (usePinia) {
220
+ fs.ensureDirSync(path.join(targetDir, 'src', 'stores'))
221
+ fs.writeFileSync(
222
+ path.join(targetDir, 'src', 'stores', `counter.${ext}`),
223
+ counterStoreContent(useTypeScript)
224
+ )
225
+ }
226
+
227
+ // Vitest
228
+ if (useVitest) {
229
+ fs.ensureDirSync(path.join(targetDir, 'src', 'components', '__tests__'))
230
+ fs.writeFileSync(
231
+ path.join(targetDir, 'src', 'components', '__tests__', `HelloWorld.spec.${ext}`),
232
+ vitestSpecContent()
233
+ )
234
+ }
235
+
236
+ // E2E testing
237
+ if (useE2e === 'cypress') {
238
+ fs.writeFileSync(path.join(targetDir, 'cypress.config.js'), cypressConfigContent())
239
+ fs.ensureDirSync(path.join(targetDir, 'cypress', 'e2e'))
240
+ fs.writeFileSync(path.join(targetDir, 'cypress', 'e2e', 'example.cy.js'), cypressExampleContent())
241
+ } else if (useE2e === 'playwright') {
242
+ fs.writeFileSync(path.join(targetDir, 'playwright.config.js'), playwrightConfigContent())
243
+ fs.ensureDirSync(path.join(targetDir, 'e2e'))
244
+ fs.writeFileSync(path.join(targetDir, 'e2e', 'example.spec.js'), playwrightExampleContent())
245
+ } else if (useE2e === 'nightwatch') {
246
+ fs.writeFileSync(path.join(targetDir, 'nightwatch.conf.js'), nightwatchConfigContent())
247
+ fs.ensureDirSync(path.join(targetDir, 'nightwatch', 'e2e'))
248
+ fs.writeFileSync(
249
+ path.join(targetDir, 'nightwatch', 'e2e', 'example.js'),
250
+ nightwatchExampleContent()
251
+ )
252
+ }
253
+
254
+ // ESLint
255
+ if (useEslint) {
256
+ fs.writeFileSync(path.join(targetDir, 'eslint.config.js'), eslintConfigContent())
257
+ }
258
+
259
+ // Prettier
260
+ if (usePrettier) {
261
+ fs.writeFileSync(
262
+ path.join(targetDir, '.prettierrc.json'),
263
+ JSON.stringify({ semi: false, singleQuote: true, printWidth: 100 }, null, 2) + '\n'
264
+ )
265
+ }
266
+
267
+ console.log()
268
+ console.log(green('✅') + ` ztweb project ${bold(cyan(projectName))} created successfully!`)
269
+ console.log()
270
+ console.log(bold('Next steps:'))
271
+ console.log(` ${cyan('cd ' + projectName)}`)
272
+ console.log(` ${cyan('npm install')}`)
273
+ console.log(` ${cyan('npm run dev')}`)
274
+ console.log()
275
+ }
276
+
277
+ // ---------------------------------------------------------------------------
278
+ // Template rendering helpers
279
+ // ---------------------------------------------------------------------------
280
+
281
+ function copyDirectory(src, dest, flags) {
282
+ const files = fs.readdirSync(src)
283
+
284
+ for (const file of files) {
285
+ const srcPath = path.join(src, file)
286
+ const destPath = path.join(dest, file)
287
+ const stat = fs.statSync(srcPath)
288
+
289
+ if (stat.isDirectory()) {
290
+ fs.ensureDirSync(destPath)
291
+ copyDirectory(srcPath, destPath, flags)
292
+ } else if (file === 'package.json.hbs') {
293
+ const template = fs.readFileSync(srcPath, 'utf-8')
294
+ const compiledTemplate = Handlebars.compile(template)
295
+ const output = compiledTemplate(flags)
296
+ fs.writeFileSync(path.join(dest, 'package.json'), output)
297
+ } else if (file === 'vite.config.js.hbs') {
298
+ // handled separately via writeViteConfig
299
+ } else if (file === 'main.js' || file === 'main.ts') {
300
+ // handled separately via writeMainFile
301
+ } else if (file === 'App.zweb') {
302
+ // handled separately via writeAppZweb
303
+ } else if (file === 'vite.config.js') {
304
+ // handled separately via writeViteConfig
305
+ } else {
306
+ fs.copyFileSync(srcPath, destPath)
307
+ }
308
+ }
309
+ }
310
+
311
+ function writeViteConfig(targetDir, flags) {
312
+ const { useJsx, useTypeScript } = flags
313
+ const ext = useTypeScript ? 'ts' : 'js'
314
+ const lines = [`import { defineConfig } from 'vite'`, `import zweb from 'vite-plugin-zweb'`]
315
+ if (useJsx) lines.push(`import vueJsx from '@vitejs/plugin-vue-jsx'`)
316
+ lines.push(``)
317
+ lines.push(`export default defineConfig({`)
318
+ const plugins = useJsx ? `zweb(), vueJsx()` : `zweb()`
319
+ lines.push(` plugins: [${plugins}],`)
320
+ lines.push(`})`)
321
+ lines.push(``)
322
+ fs.writeFileSync(path.join(targetDir, `vite.config.${ext}`), lines.join('\n'))
323
+ // Remove old .js if we wrote .ts
324
+ if (useTypeScript) {
325
+ const old = path.join(targetDir, 'vite.config.js')
326
+ if (fs.existsSync(old)) fs.removeSync(old)
327
+ }
328
+ }
329
+
330
+ function writeMainFile(targetDir, flags) {
331
+ const { useTypeScript, useRouter, usePinia } = flags
332
+ const ext = useTypeScript ? 'ts' : 'js'
333
+ const lines = [`import { createApp } from 'vue'`]
334
+ if (usePinia) lines.push(`import { createPinia } from 'pinia'`)
335
+ if (useRouter) lines.push(`import router from './router'`)
336
+ lines.push(`import './style.css'`)
337
+ lines.push(`import App from './App.zweb'`)
338
+ lines.push(``)
339
+ lines.push(`const app = createApp(App)`)
340
+ if (usePinia) lines.push(`app.use(createPinia())`)
341
+ if (useRouter) lines.push(`app.use(router)`)
342
+ lines.push(`app.mount('#app')`)
343
+ lines.push(``)
344
+ fs.ensureDirSync(path.join(targetDir, 'src'))
345
+ fs.writeFileSync(path.join(targetDir, 'src', `main.${ext}`), lines.join('\n'))
346
+ }
347
+
348
+ function writeAppZweb(targetDir, useRouter) {
349
+ let content
350
+ if (useRouter) {
351
+ content = `<script setup>
352
+ import HelloWorld from './components/HelloWorld.zweb'
353
+ </script>
354
+
355
+ <template>
356
+ <div>
357
+ <nav>
358
+ <RouterLink to="/">Home</RouterLink>
359
+ <span> | </span>
360
+ <RouterLink to="/about">About</RouterLink>
361
+ </nav>
362
+ <RouterView />
363
+ <HelloWorld msg="Welcome to ztweb!" />
364
+ </div>
365
+ </template>
366
+
367
+ <style scoped>
368
+ nav {
369
+ padding: 1em 0;
370
+ }
371
+ nav a {
372
+ font-weight: bold;
373
+ color: #42b883;
374
+ text-decoration: none;
375
+ }
376
+ nav a:hover {
377
+ text-decoration: underline;
378
+ }
379
+ </style>
380
+ `
381
+ } else {
382
+ content = `<script setup>
383
+ import HelloWorld from './components/HelloWorld.zweb'
384
+ </script>
385
+
386
+ <template>
387
+ <div>
388
+ <a href="https://github.com/techzt13/ztweb" target="_blank">
389
+ <img src="./assets/logo.svg" class="logo" alt="ztweb logo" />
390
+ </a>
391
+ <HelloWorld msg="Welcome to ztweb!" />
392
+ </div>
393
+ </template>
394
+
395
+ <style scoped>
396
+ .logo {
397
+ height: 6em;
398
+ padding: 1.5em;
399
+ will-change: filter;
400
+ transition: filter 300ms;
401
+ }
402
+ .logo:hover {
403
+ filter: drop-shadow(0 0 2em #42b883aa);
404
+ }
405
+ </style>
406
+ `
407
+ }
408
+ fs.ensureDirSync(path.join(targetDir, 'src'))
409
+ fs.writeFileSync(path.join(targetDir, 'src', 'App.zweb'), content)
410
+ }
411
+
412
+ function writeTsConfig(targetDir) {
413
+ const tsconfig = {
414
+ files: [],
415
+ references: [{ path: './tsconfig.app.json' }, { path: './tsconfig.node.json' }],
416
+ }
417
+ const tsconfigApp = {
418
+ extends: '@vue/tsconfig/tsconfig.dom.json',
419
+ include: ['env.d.ts', 'src/**/*', 'src/**/*.zweb'],
420
+ exclude: ['src/**/__tests__/*'],
421
+ compilerOptions: {
422
+ composite: true,
423
+ tsBuildInfoFile: './node_modules/.tmp/tsconfig.app.tsbuildinfo',
424
+ baseUrl: '.',
425
+ paths: { '@/*': ['./src/*'] },
426
+ },
427
+ }
428
+ const tsconfigNode = {
429
+ extends: '@tsconfig/node20/tsconfig.json',
430
+ include: ['vite.config.*', 'vitest.config.*', 'cypress/**/*', 'playwright/**/*'],
431
+ compilerOptions: {
432
+ composite: true,
433
+ tsBuildInfoFile: './node_modules/.tmp/tsconfig.node.tsbuildinfo',
434
+ module: 'ESNext',
435
+ moduleResolution: 'Bundler',
436
+ types: ['node'],
437
+ },
438
+ }
439
+ fs.writeFileSync(path.join(targetDir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2) + '\n')
440
+ fs.writeFileSync(
441
+ path.join(targetDir, 'tsconfig.app.json'),
442
+ JSON.stringify(tsconfigApp, null, 2) + '\n'
443
+ )
444
+ fs.writeFileSync(
445
+ path.join(targetDir, 'tsconfig.node.json'),
446
+ JSON.stringify(tsconfigNode, null, 2) + '\n'
447
+ )
448
+ }
449
+
450
+ function routerIndexContent(ext) {
451
+ return `import { createRouter, createWebHistory } from 'vue-router'
452
+ import HomeView from '../views/HomeView.zweb'
453
+
454
+ const router = createRouter({
455
+ history: createWebHistory(import.meta.env.BASE_URL),
456
+ routes: [
457
+ {
458
+ path: '/',
459
+ name: 'home',
460
+ component: HomeView,
461
+ },
462
+ {
463
+ path: '/about',
464
+ name: 'about',
465
+ component: () => import('../views/AboutView.zweb'),
466
+ },
467
+ ],
468
+ })
469
+
470
+ export default router
471
+ `
472
+ }
473
+
474
+ function homeViewContent() {
475
+ return `<template>
476
+ <main>
477
+ <h1>Home</h1>
478
+ <p>Welcome to the ztweb app!</p>
479
+ </main>
480
+ </template>
481
+ `
482
+ }
483
+
484
+ function aboutViewContent() {
485
+ return `<template>
486
+ <main>
487
+ <h1>About</h1>
488
+ <p>This is an About page built with ztweb.</p>
489
+ </main>
490
+ </template>
491
+ `
492
+ }
493
+
494
+ function counterStoreContent(useTypeScript) {
495
+ if (useTypeScript) {
496
+ return `import { ref, computed } from 'vue'
497
+ import { defineStore } from 'pinia'
498
+
499
+ export const useCounterStore = defineStore('counter', () => {
500
+ const count = ref<number>(0)
501
+ const doubleCount = computed(() => count.value * 2)
502
+ function increment() {
503
+ count.value++
504
+ }
505
+
506
+ return { count, doubleCount, increment }
507
+ })
508
+ `
509
+ }
510
+ return `import { ref, computed } from 'vue'
511
+ import { defineStore } from 'pinia'
512
+
513
+ export const useCounterStore = defineStore('counter', () => {
514
+ const count = ref(0)
515
+ const doubleCount = computed(() => count.value * 2)
516
+ function increment() {
517
+ count.value++
518
+ }
519
+
520
+ return { count, doubleCount, increment }
521
+ })
522
+ `
523
+ }
524
+
525
+ function vitestSpecContent() {
526
+ return `import { describe, it, expect } from 'vitest'
527
+ import { mount } from '@vue/test-utils'
528
+ import HelloWorld from '../HelloWorld.zweb'
529
+
530
+ describe('HelloWorld', () => {
531
+ it('renders properly', () => {
532
+ const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
533
+ expect(wrapper.text()).toContain('Hello Vitest')
534
+ })
535
+ })
536
+ `
537
+ }
538
+
539
+ function cypressConfigContent() {
540
+ return `import { defineConfig } from 'cypress'
541
+
542
+ export default defineConfig({
543
+ e2e: {
544
+ specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}',
545
+ baseUrl: 'http://localhost:4173',
546
+ },
547
+ })
548
+ `
549
+ }
550
+
551
+ function cypressExampleContent() {
552
+ return `describe('My App', () => {
553
+ it('visits the app', () => {
554
+ cy.visit('/')
555
+ cy.contains('ztweb')
556
+ })
557
+ })
558
+ `
559
+ }
560
+
561
+ function playwrightConfigContent() {
562
+ return `import { defineConfig, devices } from '@playwright/test'
563
+
564
+ export default defineConfig({
565
+ testDir: './e2e',
566
+ use: {
567
+ baseURL: 'http://localhost:4173',
568
+ },
569
+ projects: [
570
+ {
571
+ name: 'chromium',
572
+ use: { ...devices['Desktop Chrome'] },
573
+ },
574
+ ],
575
+ })
576
+ `
577
+ }
578
+
579
+ function playwrightExampleContent() {
580
+ return `import { test, expect } from '@playwright/test'
581
+
582
+ test('visits the app', async ({ page }) => {
583
+ await page.goto('/')
584
+ await expect(page).toHaveTitle(/ztweb/)
585
+ })
586
+ `
587
+ }
588
+
589
+ function nightwatchConfigContent() {
590
+ return `module.exports = {
591
+ src_folders: ['nightwatch/e2e'],
592
+ test_settings: {
593
+ default: {
594
+ launch_url: 'http://localhost:4173',
595
+ desiredCapabilities: {
596
+ browserName: 'chrome',
597
+ },
598
+ },
599
+ },
600
+ }
601
+ `
602
+ }
603
+
604
+ function nightwatchExampleContent() {
605
+ return `module.exports = {
606
+ 'visits the app': function (browser) {
607
+ browser
608
+ .url(browser.launchUrl)
609
+ .waitForElementVisible('body')
610
+ .assert.titleContains('ztweb')
611
+ .end()
612
+ },
613
+ }
614
+ `
615
+ }
616
+
617
+ function eslintConfigContent() {
618
+ return `import pluginVue from 'eslint-plugin-vue'
619
+ import js from '@eslint/js'
620
+
621
+ export default [
622
+ js.configs.recommended,
623
+ ...pluginVue.configs['flat/essential'],
624
+ {
625
+ files: ['**/*.{js,ts,zweb}'],
626
+ rules: {},
627
+ },
628
+ ]
629
+ `
630
+ }
631
+
632
+ function isValidPackageName(name) {
633
+ return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(name)
634
+ }
635
+
636
+ export default init
@@ -0,0 +1,4 @@
1
+ node_modules
2
+ dist
3
+ *.local
4
+ .DS_Store
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.ico" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>ztweb App</title>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.js"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,7 @@
1
+ {
2
+ "compilerOptions": {
3
+ "paths": {
4
+ "@/*": ["./src/*"]
5
+ }
6
+ }
7
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ {{#if useTypeScript}}
9
+ "build": "vue-tsc && vite build",
10
+ {{else}}
11
+ "build": "vite build",
12
+ {{/if}}
13
+ "preview": "vite preview"{{#if useVitest}},
14
+ "test": "vitest"{{/if}}{{#if useE2e}},
15
+ {{#if (eq useE2e "cypress")}}"test:e2e": "cypress open"{{else if (eq useE2e "playwright")}}"test:e2e": "playwright test"{{else if (eq useE2e "nightwatch")}}"test:e2e": "nightwatch"{{/if}}{{/if}}{{#if useEslint}},
16
+ "lint": "eslint ."{{/if}}{{#if usePrettier}},
17
+ "format": "prettier --write ."{{/if}}
18
+ },
19
+ "dependencies": {
20
+ "vue": "^3.4.0"{{#if useRouter}},
21
+ "vue-router": "^4.3.0"{{/if}}{{#if usePinia}},
22
+ "pinia": "^2.1.0"{{/if}}
23
+ },
24
+ "devDependencies": {
25
+ "@vue/compiler-sfc": "^3.4.0",
26
+ "vite": "^5.0.0",
27
+ "vite-plugin-zweb": "file:./vite-plugin-zweb"{{#if useTypeScript}},
28
+ "typescript": "^5.4.0",
29
+ "vue-tsc": "^2.0.0"{{/if}}{{#if useJsx}},
30
+ "@vitejs/plugin-vue-jsx": "^4.0.0"{{/if}}{{#if useVitest}},
31
+ "vitest": "^1.5.0",
32
+ "@vue/test-utils": "^2.4.0",
33
+ "jsdom": "^24.0.0"{{/if}}{{#if (eq useE2e "cypress")}},
34
+ "cypress": "^13.0.0"{{else if (eq useE2e "playwright")}},
35
+ "@playwright/test": "^1.44.0"{{else if (eq useE2e "nightwatch")}},
36
+ "nightwatch": "^3.5.0"{{/if}}{{#if useEslint}},
37
+ "eslint": "^9.0.0",
38
+ "eslint-plugin-vue": "^9.25.0",
39
+ "@eslint/js": "^9.0.0"{{/if}}{{#if usePrettier}},
40
+ "prettier": "^3.2.0"{{/if}}
41
+ }
42
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
+ <rect width="32" height="32" rx="4" fill="#42b883"/>
3
+ <text x="16" y="24" font-family="Arial" font-size="18" font-weight="bold" fill="white" text-anchor="middle">ZT</text>
4
+ </svg>
@@ -0,0 +1,24 @@
1
+ <script setup>
2
+ import HelloWorld from './components/HelloWorld.zweb'
3
+ </script>
4
+
5
+ <template>
6
+ <div>
7
+ <a href="https://github.com/techzt13/ztweb" target="_blank">
8
+ <img src="./assets/logo.svg" class="logo" alt="ztweb logo" />
9
+ </a>
10
+ <HelloWorld msg="Welcome to ztweb!" />
11
+ </div>
12
+ </template>
13
+
14
+ <style scoped>
15
+ .logo {
16
+ height: 6em;
17
+ padding: 1.5em;
18
+ will-change: filter;
19
+ transition: filter 300ms;
20
+ }
21
+ .logo:hover {
22
+ filter: drop-shadow(0 0 2em #42b883aa);
23
+ }
24
+ </style>
@@ -0,0 +1,10 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
2
+ <defs>
3
+ <linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
4
+ <stop offset="0%" style="stop-color:#42b883;stop-opacity:1" />
5
+ <stop offset="100%" style="stop-color:#35495e;stop-opacity:1" />
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="200" height="200" rx="20" fill="url(#grad)"/>
9
+ <text x="100" y="130" font-family="Arial, sans-serif" font-size="80" font-weight="bold" fill="white" text-anchor="middle">ZT</text>
10
+ </svg>
@@ -0,0 +1,39 @@
1
+ <script setup>
2
+ import { ref } from 'vue'
3
+
4
+ defineProps({
5
+ msg: String,
6
+ })
7
+
8
+ const count = ref(0)
9
+ </script>
10
+
11
+ <template>
12
+ <h1>{{ msg }}</h1>
13
+ <div class="card">
14
+ <button type="button" @click="count++">count is {{ count }}</button>
15
+ <p>
16
+ Edit <code>src/components/HelloWorld.zweb</code> to test HMR
17
+ </p>
18
+ </div>
19
+ <p>
20
+ Check out the
21
+ <a href="https://vuejs.org/guide/quick-start.html" target="_blank">Vue docs</a>,
22
+ and the
23
+ <a href="https://vitejs.dev/guide/" target="_blank">Vite docs</a>.
24
+ </p>
25
+ <p class="powered">Powered by <strong>ztweb</strong> — Vue + Vite with .zweb files</p>
26
+ </template>
27
+
28
+ <style scoped>
29
+ .card {
30
+ padding: 2em;
31
+ }
32
+ h1 {
33
+ color: #42b883;
34
+ }
35
+ .powered {
36
+ margin-top: 2em;
37
+ color: #888;
38
+ }
39
+ </style>
@@ -0,0 +1,5 @@
1
+ import { createApp } from 'vue'
2
+ import './style.css'
3
+ import App from './App.zweb'
4
+
5
+ createApp(App).mount('#app')
@@ -0,0 +1,79 @@
1
+ :root {
2
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3
+ line-height: 1.5;
4
+ font-weight: 400;
5
+
6
+ color-scheme: light dark;
7
+ color: rgba(255, 255, 255, 0.87);
8
+ background-color: #242424;
9
+
10
+ font-synthesis: none;
11
+ text-rendering: optimizeLegibility;
12
+ -webkit-font-smoothing: antialiased;
13
+ -moz-osx-font-smoothing: grayscale;
14
+ }
15
+
16
+ a {
17
+ font-weight: 500;
18
+ color: #646cff;
19
+ text-decoration: inherit;
20
+ }
21
+ a:hover {
22
+ color: #535bf2;
23
+ }
24
+
25
+ body {
26
+ margin: 0;
27
+ display: flex;
28
+ place-items: center;
29
+ min-width: 320px;
30
+ min-height: 100vh;
31
+ }
32
+
33
+ h1 {
34
+ font-size: 3.2em;
35
+ line-height: 1.1;
36
+ }
37
+
38
+ button {
39
+ border-radius: 8px;
40
+ border: 1px solid transparent;
41
+ padding: 0.6em 1.2em;
42
+ font-size: 1em;
43
+ font-weight: 500;
44
+ font-family: inherit;
45
+ background-color: #1a1a1a;
46
+ cursor: pointer;
47
+ transition: border-color 0.25s;
48
+ }
49
+ button:hover {
50
+ border-color: #646cff;
51
+ }
52
+ button:focus,
53
+ button:focus-visible {
54
+ outline: 4px auto -webkit-focus-ring-color;
55
+ }
56
+
57
+ .card {
58
+ padding: 2em;
59
+ }
60
+
61
+ #app {
62
+ max-width: 1280px;
63
+ margin: 0 auto;
64
+ padding: 2rem;
65
+ text-align: center;
66
+ }
67
+
68
+ @media (prefers-color-scheme: light) {
69
+ :root {
70
+ color: #213547;
71
+ background-color: #ffffff;
72
+ }
73
+ a:hover {
74
+ color: #747bff;
75
+ }
76
+ button {
77
+ background-color: #f9f9f9;
78
+ }
79
+ }
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from 'vite'
2
+ import zweb from 'vite-plugin-zweb'
3
+
4
+ export default defineConfig({
5
+ plugins: [zweb()],
6
+ })
@@ -0,0 +1,154 @@
1
+ import { parse, compileScript, compileTemplate, compileStyleAsync } from '@vue/compiler-sfc'
2
+ import { createHash } from 'crypto'
3
+
4
+ export default function zwebPlugin() {
5
+ const cache = new Map()
6
+
7
+ return {
8
+ name: 'vite-plugin-zweb',
9
+
10
+ config() {
11
+ return {
12
+ resolve: {
13
+ extensions: ['.zweb', '.js', '.json']
14
+ }
15
+ }
16
+ },
17
+
18
+ async transform(code, id) {
19
+ if (!id.endsWith('.zweb')) return null
20
+
21
+ const filename = id.split('?')[0]
22
+
23
+ // Parse the SFC
24
+ const { descriptor, errors } = parse(code, {
25
+ filename,
26
+ sourceMap: true
27
+ })
28
+
29
+ if (errors.length) {
30
+ throw errors[0]
31
+ }
32
+
33
+ // Generate a scoped ID for this component
34
+ const scopeId = `data-v-${generateId(filename)}`
35
+
36
+ let output = ''
37
+ const attachedProps = []
38
+
39
+ // 1. Compile script block
40
+ if (descriptor.script || descriptor.scriptSetup) {
41
+ const script = compileScript(descriptor, {
42
+ id: scopeId,
43
+ inlineTemplate: false,
44
+ templateOptions: {
45
+ id: scopeId,
46
+ scoped: descriptor.styles.some(s => s.scoped),
47
+ slotted: descriptor.slotted
48
+ }
49
+ })
50
+
51
+ output += script.content.replace('export default', 'const __component__ =')
52
+ output += '\n'
53
+ } else {
54
+ output += 'const __component__ = {}\n'
55
+ }
56
+
57
+ // 2. Compile template block
58
+ if (descriptor.template) {
59
+ const template = compileTemplate({
60
+ id: scopeId,
61
+ source: descriptor.template.content,
62
+ filename,
63
+ scoped: descriptor.styles.some(s => s.scoped),
64
+ slotted: descriptor.slotted,
65
+ compilerOptions: {
66
+ mode: 'module',
67
+ scopeId: descriptor.styles.some(s => s.scoped) ? scopeId : undefined
68
+ }
69
+ })
70
+
71
+ if (template.errors.length) {
72
+ throw template.errors[0]
73
+ }
74
+
75
+ output += template.code.replace('export function render', 'function render')
76
+ output += '\n__component__.render = render\n'
77
+ }
78
+
79
+ // 3. Compile style blocks
80
+ if (descriptor.styles.length) {
81
+ for (let i = 0; i < descriptor.styles.length; i++) {
82
+ const style = descriptor.styles[i]
83
+ const compiled = await compileStyleAsync({
84
+ source: style.content,
85
+ filename,
86
+ id: scopeId,
87
+ scoped: style.scoped,
88
+ modules: style.module != null
89
+ })
90
+
91
+ if (compiled.errors.length) {
92
+ throw compiled.errors[0]
93
+ }
94
+
95
+ // Inject the style
96
+ output += `
97
+ const __style${i}__ = document.createElement('style')
98
+ __style${i}__.innerHTML = ${JSON.stringify(compiled.code)}
99
+ document.head.appendChild(__style${i}__)
100
+ `
101
+ }
102
+ }
103
+
104
+ // Add scoped ID if needed
105
+ if (descriptor.styles.some(s => s.scoped)) {
106
+ output += `\n__component__.__scopeId = "${scopeId}"\n`
107
+ }
108
+
109
+ // HMR support
110
+ if (!process.env.VITEST) {
111
+ output += `
112
+ if (import.meta.hot) {
113
+ __component__.__hmrId = "${scopeId}"
114
+ import.meta.hot.accept(mod => {
115
+ if (!mod) return
116
+ const updated = mod.default
117
+ updated.__hmrId = "${scopeId}"
118
+ import.meta.hot.data.instances?.forEach(instance => {
119
+ if (instance.$.type.__hmrId === "${scopeId}") {
120
+ instance.$.type = updated
121
+ instance.$.proxy?.$forceUpdate()
122
+ }
123
+ })
124
+ })
125
+ }
126
+ `
127
+ }
128
+
129
+ output += '\n__component__.__file = ' + JSON.stringify(filename) + '\n'
130
+ output += 'export default __component__\n'
131
+
132
+ return {
133
+ code: output,
134
+ map: null
135
+ }
136
+ },
137
+
138
+ handleHotUpdate({ file, server }) {
139
+ if (file.endsWith('.zweb')) {
140
+ cache.delete(file)
141
+
142
+ const module = server.moduleGraph.getModuleById(file)
143
+ if (module) {
144
+ server.moduleGraph.invalidateModule(module)
145
+ return [module]
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ function generateId(filename) {
153
+ return createHash('md5').update(filename).digest('hex').substring(0, 8)
154
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "vite-plugin-zweb",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "index.js",
6
+ "dependencies": {
7
+ "@vue/compiler-sfc": "^3.4.0"
8
+ },
9
+ "peerDependencies": {
10
+ "vite": "^5.0.0"
11
+ }
12
+ }