@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 +21 -0
- package/README.md +133 -0
- package/bin/create-ztweb.js +8 -0
- package/package.json +35 -0
- package/src/index.js +636 -0
- package/template/_gitignore +4 -0
- package/template/index.html +13 -0
- package/template/jsconfig.json +7 -0
- package/template/package.json.hbs +42 -0
- package/template/public/favicon.ico +4 -0
- package/template/src/App.zweb +24 -0
- package/template/src/assets/logo.svg +10 -0
- package/template/src/components/HelloWorld.zweb +39 -0
- package/template/src/main.js +5 -0
- package/template/src/style.css +79 -0
- package/template/vite.config.js +6 -0
- package/vite-plugin-zweb/index.js +154 -0
- package/vite-plugin-zweb/package.json +12 -0
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/)
|
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,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,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,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,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,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
|
+
}
|