apptify 0.5.0 → 0.5.1

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 CHANGED
@@ -1,49 +1,18 @@
1
1
  # Apptify
2
2
 
3
- 面向桌面与移动端的个人 Vue 3 组件库。统一使用 `<apple-*>` 前缀,保留 Vuetify 的 Ripple 指令,而不继承它的整套 Material 组件外观。
3
+ 面向桌面与移动端的 Vue 3 组件库,采用 Apple 风格设计,统一使用 `<apple-*>` 标签。支持主题切换、减少动效、表单、图片预览与多层弹窗;支持 JavaScript、TypeScript 和 Options API。
4
4
 
5
- 设计借鉴 Apple 网站的排版、留白、层次与交互节奏;不是 Apple 官方产品,也不声称全部控件都与 Apple 网站逐像素一致。表格、级联选择、验证码、下拉刷新等能力是在同一套视觉语言下的扩展。
5
+ [npm](https://www.npmjs.com/package/apptify) · [组件 API](docs/COMPONENTS.md) · [应用模板](docs/PLAYGROUND-TEMPLATE.md)
6
6
 
7
- 当前导出并注册 **66 个组件**:基础 9 个、表单 18 个、内容与移动交互 28 个、弹层 9 个、动效 2 个。其中 `AppleOverlayHost` 是由 Provider 自动挂载的基础设施,通常直接使用其余 65 个即可。完整实际 API 见 [组件参考](docs/COMPONENTS.md)。
8
-
9
- ## 本地运行与安装
10
-
11
- 项目尚未发布到 npm。不要把 `npm install apptify` 当成已发布包的安装方式。
12
-
13
- 建议使用 Node.js 22 LTS 或 24 LTS;测试工具不支持奇数版本的 Node.js。
14
-
15
- 在本仓库执行:
7
+ ## 安装
16
8
 
17
9
  ```sh
18
- npm ci
19
- npm run dev
10
+ npm install apptify vue@^3.5 vuetify@^3.9
20
11
  ```
21
12
 
22
- 开发服务器地址以终端输出为准。随包提供完整可编辑 playground 应用模板,导航、布局、登录、设置和404均可复用;删掉首页和组件总览即可开始自己的业务页面。见[模板交付说明](docs/PLAYGROUND-TEMPLATE.md)。
23
-
24
- 在另一个项目中使用本地包:
25
-
26
- ```sh
27
- # 在本仓库构建并生成 apptify-0.4.0.tgz
28
- npm run build
29
- npm pack
30
-
31
- # 在消费项目执行,将路径替换为实际包路径
32
- npm install /absolute/path/apptify/apptify-0.4.0.tgz
33
- npm install vue@^3.5 vuetify@^3.9
34
- ```
35
-
36
- Vue 与 Vuetify 是 peer dependencies。图标使用 `lucide-vue-next`,桌面图片预览的缩放与手势使用 `@panzoom/panzoom`,手机端使用图片边界约束的 Pointer Events 手势。本库没有要求安装 Vuetify 插件或导入其完整样式,也不需要 `<v-app>`。
37
-
38
- ## 复制完整应用模板
39
-
40
- 安装本地 tgz 后执行 `npx --no-install apptify-playground ../my-app`,再在新目录执行 `npm install`、`npm run dev`。复制项目携带本地库归档与完整源码/资源,能独立安装、类型检查与构建。目标目录必须为空。
41
-
42
- 完整步骤、删除示例页面后的导航与路由行为,以及本地打包验收见[模板交付说明](docs/PLAYGROUND-TEMPLATE.md)。组件库的常规接入方式保持可用。
13
+ Vue 与 Vuetify 是 peer dependencies。Vuetify 仅用于 Ripple,无需安装其插件、导入完整样式或使用 `<v-app>`。
43
14
 
44
- ## 接入应用
45
-
46
- JavaScript 和 TypeScript 项目都可直接使用。消费项目不需要 TypeScript、`tsconfig.json` 或 `<script setup>`;普通 `<script>` 与 Options API 是完整支持的用法。随包提供的类型声明也可以为 JavaScript 编辑器提供补全。
15
+ ## 快速接入
47
16
 
48
17
  ```js
49
18
  // src/main.js
@@ -52,30 +21,23 @@ import { createAppleUI } from 'apptify'
52
21
  import 'apptify/style.css'
53
22
  import App from './App.vue'
54
23
 
55
- const apple = createAppleUI({
56
- theme: 'system',
57
- motion: 'auto',
58
- persist: true,
59
- storageKey: 'my-app:appearance',
60
- })
61
-
62
- createApp(App).use(apple).mount('#app')
24
+ createApp(App)
25
+ .use(createAppleUI({ theme: 'system', motion: 'auto', persist: true }))
26
+ .mount('#app')
63
27
  ```
64
28
 
65
29
  ```vue
66
30
  <!-- App.vue -->
67
31
  <template>
68
32
  <apple-provider>
69
- <main>
70
- <apple-container>
71
- <apple-card title="个人资料" subtitle="保持联系。">
72
- <apple-input v-model="name" label="姓名" autocomplete="name" />
73
- <template #actions>
74
- <apple-button @click="save">保存</apple-button>
75
- </template>
76
- </apple-card>
77
- </apple-container>
78
- </main>
33
+ <apple-container>
34
+ <apple-card title="个人资料">
35
+ <apple-input v-model="name" label="姓名" />
36
+ <template #actions>
37
+ <apple-button @click="save">保存</apple-button>
38
+ </template>
39
+ </apple-card>
40
+ </apple-container>
79
41
  </apple-provider>
80
42
  </template>
81
43
 
@@ -91,277 +53,43 @@ export default {
91
53
  </script>
92
54
  ```
93
55
 
94
- 这是 Options API 用法。插件注册 PascalCase 名称,Vue 模板同时支持 `<AppleCard>` 和 `<apple-card>`。所有组件也可命名导入并局部注册。
95
-
96
- `AppleProvider` 应包住业务 UI。它应用主题变量、监听系统偏好、提供同主题的 Teleport 目标,并自动渲染 `AppleOverlayHost`。**不要在同一上下文再额外渲染一份 Host。** 根 Provider 不传属性时复用插件上下文;嵌套 Provider 或显式指定 `theme` / `motion` 的 Provider 会建立局部上下文。嵌套 Provider 未指定的主题与动效属性跟随父级更新。
97
-
98
- `this.$apple` 始终指向插件级上下文。局部 Provider 内的组件如需使用局部主题、通知或弹层,使用 Options API 注入:
99
-
100
- ```js
101
- import { appleKey } from 'apptify'
102
-
103
- export default {
104
- inject: { apple: { from: appleKey } },
105
- methods: {
106
- showMessage() {
107
- this.apple.notify('使用当前 Provider 的通知')
108
- },
109
- },
110
- }
111
- ```
112
-
113
- 也导出 `useApple()` 供 Composition API 的 `setup()` 使用,但不需要为接入组件库重写现有 Options API 页面。
114
-
115
- ## 自动路由(与 Vuetify 项目相同)
116
-
117
- 采用 `vite-plugin-pages` + Vue Router 4,默认扫描 `src/views`。新页面自动生成路由,不需要维护 routes 数组;普通 JavaScript 和 Options API 页面同样适用。
118
-
119
- ```sh
120
- npm install vue-router@^4.5
121
- npm install -D vite-plugin-pages@^0.32.4
122
- ```
123
-
124
- ```js
125
- // vite.config.js
126
- import { defineConfig } from 'vite'
127
- import vue from '@vitejs/plugin-vue'
128
- import apptifyRoutes from 'apptify/vite'
129
-
130
- export default defineConfig({
131
- plugins: [apptifyRoutes(), vue()],
132
- })
133
- ```
134
-
135
- ```js
136
- // src/router/index.js
137
- import { createRouter, createWebHistory } from 'vue-router'
138
- import routes from 'virtual:generated-pages'
139
-
140
- export default createRouter({
141
- history: createWebHistory(import.meta.env.BASE_URL),
142
- routes,
143
- })
144
- ```
145
-
146
- ```js
147
- // src/main.js
148
- import { createApp } from 'vue'
149
- import { createAppleUI } from 'apptify'
150
- import 'apptify/style.css'
151
- import App from './App.vue'
152
- import router from './router'
153
-
154
- createApp(App).use(createAppleUI()).use(router).mount('#app')
155
- ```
156
-
157
- ```vue
158
- <!-- src/App.vue:全局主题和弹层跨页面保留 -->
159
- <template>
160
- <apple-provider>
161
- <router-view />
162
- </apple-provider>
163
- </template>
164
- ```
165
-
166
- | 页面文件 | 路由 |
167
- | --- | --- |
168
- | `src/views/index.vue` | `/` |
169
- | `src/views/about.vue` | `/about` |
170
- | `src/views/users/[id].vue` | `/users/:id` |
171
- | `src/views/[...all].vue` | 未匹配地址(404 页面) |
56
+ `AppleProvider` 提供主题与弹层宿主,无需额外添加 `AppleOverlayHost`。插件全局注册组件,也支持命名导入并局部注册。嵌套 Provider 内可用 `appleKey` 注入局部上下文,或在 `setup()` 中使用 `useApple()`。
172
57
 
173
- 页面内使用 `<router-link to="/about">关于</router-link>`,Options API 可通过 `this.$router.push('/about')` 跳转、通过 `this.$route.params.id` 读取参数。页面无需再包根 `apple-provider`。
174
-
175
- TypeScript 项目在 `src/vite-env.d.ts` 增加 `/// <reference types="vite-plugin-pages/client" />`。需要改扫描目录时传入 `apptifyRoutes({ dirs: [{ dir: 'src/pages', baseRoute: '' }] })`;支持其余 Pages 选项,包括排除文件和扩展路由。
176
-
177
- 已经接入 `Pages()` 的项目继续使用现有配置即可,避免重复添加生成插件。`apptify/vite` 只负责构建时生成路由,浏览器组件入口不加载此插件。
178
-
179
- 本仓库演示也使用同一套机制:`playground/App.vue` 放 Provider 与 RouterView,`playground/router/index.ts` 创建 Router,工作台页面位于 `playground/views/index.vue`。演示扫描目录设为 `playground/views`。
180
-
181
- 使用 history 模式部署时,服务器需要将页面 URL 回退到 `index.html`,并保留 `/api` 和静态资源自己的处理规则;你的 Spring Boot 项目同样需要配置该回退。无法配置回退时,可改用 `createWebHashHistory(import.meta.env.BASE_URL)`。
182
-
183
- ## 主题与动效
184
-
185
- 共享响应式状态统一使用 `ref`:`theme`、`motion`、`overlays.entries` 和 `portalTarget`。在 JavaScript 中通过 `.value` 访问;嵌套访问(如 `$apple.theme.value.name`)也需显式使用 `.value`。
186
-
187
- 内置主题为 `light`、`dark`、`graphite`、`rose`;`system` 表示跟随系统浅深色。主题即时生效,不需要刷新页面。设置 `persist: true` 才会保存主题名称、动效等级并同步其他标签页的同名存储设置;自定义主题定义应通过初始化 `themes` 配置或启动时的 `register()` 再次提供。
58
+ ## 常用 API
188
59
 
189
60
  ```js
190
61
  this.$apple.theme.value.set('dark')
191
- this.$apple.theme.value.register('forest', {
192
- accent: '#237047',
193
- 'accent-text': '#ffffff',
194
- bg: '#f4f7f5',
195
- surface: '#ffffff',
196
- }, 'light')
197
- this.$apple.theme.value.set('forest')
198
62
  this.$apple.motion.value.set('reduced')
199
- ```
200
-
201
- `register(name, tokens, scheme)` 自动补齐对应浅色或深色主题的默认 token。可覆盖 `bg`、`surface`、`surface-alt`、`text`、`secondary`、`border`、`accent`、`accent-text`、`danger`、`success`、`warning`、`shadow`、`radius`,对应 `--apple-*` CSS 变量。注册后先调用 `set()` 才会切换;`system` 是保留名称。
202
-
203
- | 动效值 | 行为 |
204
- | --- | --- |
205
- | `auto` | 自动遵循系统减少动态效果偏好。 |
206
- | `full` | 在全局策略与系统偏好允许时使用完整动效。 |
207
- | `reduced` | 减少位移、缩放和持续动态,保留必要状态反馈。 |
208
- | `none` | 关闭动效,包括按钮 Ripple。 |
209
- | `inherit` | 组件默认值,继承当前 Provider 的策略;不是全局配置值。 |
210
-
211
- 全局 `none` / `reduced` 是动效上限,安全优先。组件可以再降低动效,但 `motion="full"` 不能越过全局限制或系统的减少动态效果偏好。支持 `motion` 的组件以 [API 表](docs/COMPONENTS.md) 为准;静态布局组件不需要动效属性。
212
-
213
- ```vue
214
- <apple-card motion="none" zoom="none" title="静态内容" />
215
- <apple-button :ripple="false">仅关闭这个按钮的波纹</apple-button>
216
- ```
217
-
218
- ## Dialog 与反向通道
63
+ this.$apple.notify('保存成功', { tone: 'success' })
219
64
 
220
- ```js
221
- const dialog = this.$apple.dialog({
222
- title: '删除文件?',
223
- message: '此操作不能撤销。',
224
- confirmText: '删除',
225
- cancelText: '取消',
226
- tone: 'danger',
227
- })
228
-
229
- const confirmed = await dialog.result
230
- // 默认确认返回 true,取消返回 false,关闭按钮 / Escape 返回 undefined。
231
- if (confirmed === true) this.$apple.notify('文件已删除', { tone: 'success' })
65
+ const dialog = this.$apple.dialog({ title: '保存修改?', cancelText: '取消' })
66
+ const confirmed = await dialog.result // 确认 true,取消 false,关闭 undefined
232
67
  ```
233
68
 
234
- 每次调用创建独立层。`dialog` 句柄含 `id`、`close(value?)`、`update(patch)` 与 `result` Promise。`this.$apple.overlays.closeTop()` 只关闭最上面的非通知弹层;已有层不会被新层覆盖。声明式 `<apple-dialog v-model="opened">` 与命令式弹层共享焦点栈和滚动锁。
69
+ 内置主题:`light`、`dark`、`graphite`、`rose`;`system` 跟随系统。全局动效:`auto`、`full`、`reduced`、`none`。共享状态使用 ref,在 JavaScript 中通过 `.value` 访问。
235
70
 
236
- 自定义弹层组件接收 `close`、`sendMessage` 两个 prop。下面的编辑组件通过反向通道请求父调用方保存,父调用方再打开第二层确认。
237
-
238
- ```vue
239
- <!-- ProfileEditor.vue -->
240
- <template>
241
- <apple-input v-model="name" label="姓名" />
242
- <apple-button :loading="saving" @click="save">保存资料</apple-button>
243
- </template>
71
+ Props、事件、插槽、自定义主题与弹层消息回传见[组件 API](docs/COMPONENTS.md)。
244
72
 
245
- <script>
246
- export default {
247
- props: {
248
- initialName: { type: String, default: '' },
249
- close: { type: Function, required: true },
250
- sendMessage: { type: Function, required: true },
251
- },
252
- data() { return { name: this.initialName, saving: false } },
253
- methods: {
254
- async save() {
255
- this.saving = true
256
- try {
257
- const result = await this.sendMessage('profile:save', { name: this.name })
258
- if (result?.saved) this.close(result)
259
- } finally {
260
- this.saving = false
261
- }
262
- },
263
- },
264
- }
265
- </script>
266
- ```
267
-
268
- ```js
269
- import ProfileEditor from './ProfileEditor.vue'
270
-
271
- export default {
272
- methods: {
273
- async editProfile() {
274
- const editor = this.$apple.dialog({
275
- title: '编辑资料',
276
- component: ProfileEditor,
277
- props: { initialName: '小明' },
278
- confirmText: '',
279
- cancelText: '取消',
280
- onMessage: async (channel, payload) => {
281
- if (channel !== 'profile:save') return
282
- const confirmation = this.$apple.dialog({
283
- title: '保存修改?',
284
- message: `将姓名更新为 ${payload.name}。`,
285
- })
286
- if (await confirmation.result !== true) return { saved: false }
287
- // 在这里执行实际业务请求;该示例只返回结果。
288
- return { saved: true, name: payload.name }
289
- },
290
- })
291
- const result = await editor.result
292
- if (result?.saved) this.$apple.notify('资料已保存', { tone: 'success' })
293
- },
294
- },
295
- }
296
- ```
297
-
298
- 通用消息总线也有真实的返回值通道,而不只是单向通知:
299
-
300
- ```js
301
- const unsubscribe = this.$apple.onMessage('profile:read', () => ({ name: '小明' }))
302
- const responses = this.$apple.sendMessage('profile:read')
303
- console.log(responses[0].name)
304
- unsubscribe()
305
- ```
306
-
307
- `sendMessage()` 同步调用监听器,返回所有监听器返回值组成的数组;异步监听器返回 Promise,可用 `Promise.all()` 等待。动态弹层的 `sendMessage()` 若有专属 `onMessage`,直接返回该回调结果;未设置时才回退到当前上下文消息总线。长期监听请在 `beforeUnmount` 中调用取消订阅函数。
308
-
309
- ## 通知与图片
310
-
311
- ```js
312
- const notice = this.$apple.notify('正在同步', { duration: 0 })
313
- notice.update({ message: '同步完成', tone: 'success', duration: 2500 })
314
- // 或 notice.close()
315
- ```
316
-
317
- 通知独立堆叠,默认 4 秒,`duration: 0` 常驻,悬停或键盘焦点进入时暂停倒计时。需要动作按钮时直接使用 `<apple-snackbar action="撤销" @action="undo" />`;命令式 `notify()` 当前没有 action 配置。
318
-
319
- ```vue
320
- <apple-image
321
- :gallery="[{ src: '/images/workspace.webp', alt: '桌面工作区' }, { src: '/images/detail.webp', alt: '工作区细节' }]"
322
- v-model:index="imageIndex"
323
- gallery-layout="compact"
324
- />
73
+ ## 创建完整应用
325
74
 
75
+ ```sh
76
+ npm create apptify@latest ./my-app
77
+ cd my-app
78
+ npm install
79
+ npm run dev
326
80
  ```
327
81
 
328
- 示例图片路径需要替换为业务资源。图片来源仅使用 `gallery`:单图可传 `{ src, alt }` 或单元素数组,多图传数组;不再接受顶层 `src`、`alt`。`index` 从 0 开始,默认 0,可用 `v-model:index` 控制;非有限值归零,越界值限制在有效范围,空项忽略,空列表显示占位且不打开预览。单图的小窗与全屏均不显示翻页箭头。`AppleImage` 默认支持点击预览,可用 `:preview="false"` 关闭。手机端的图片组支持 `gallery-layout="compact"`(固定框内横滑,默认)和 `gallery-layout="tiled"`(横向平铺,溢出时显示随主题变化的圆角滚动条,支持拖动、轨道点击及键盘操作),组内每张预览图都会渲染。省空间(compact)模式桌面触控板支持自由连续横滑,一次手势可跨越多张图片,垂直滚动仍交给页面;快速连续点击翻页会沿当前画面的位置和速度继续。点击小点跨多张时,直接在当前画面和目标图之间滑动一屏,不快速扫过中间图片;小点保持选中目标,连续改选从当前动画画面衔接。预览支持双指缩放与同时平移、双击放大或恢复、松手边界回弹、左右滑动切图、单击或下滑退出,顶部显示序号。手机单击等待 200ms 区分双击;双击围绕点击位置放大至 2 倍,再次双击恢复;拖动、双指操作、切图及关闭会取消等待中的单击。按钮、键盘和静止松手后的切图使用接近小窗横滑的先加速、后减速曲线,时长随移动距离调整;滑动松手仍以带初速度的阻尼曲线延续手势,快速轻扫也能翻页,翻页、缩放和边界回弹均可由下一次触摸接住;打开和关闭通过固定尺寸图片的 transform 与裁剪连接原图位置,避免逐帧修改布局尺寸;退出前同步横滑预览的位置,当前缩略图保持黑色占位直到落回。`AppleImage` 统一使用同一组件入口,根据 `navigator.maxTouchPoints` 或 `(any-pointer: coarse)` 判断触屏能力,不按屏幕宽度区分;触屏设备使用手势预览,非触屏设备保留缩放、拖动、旋转、左右按钮与键盘控制。双端共用图片专用的阻尼节奏:打开 360ms、关闭 300ms、双击缩放 300ms,翻页根据剩余距离与松手速度调整;这些时长独立于通用 UI 过渡。桌面端按钮缩放、双击缩放与旋转平滑过渡,拖动可接住缩放中的实际位置,滚轮仍直接跟随输入。减少动态效果时不执行大幅位移或缩放,关闭动效时立即更新。双端预览按钮均使用半透明背景、backdrop blur 和 200% 饱和度。
329
-
330
- ## 移动端与边界
331
-
332
- - 弹层、抽屉、底部面板和通知考虑安全区;布局基于可用宽度,而不是把屏幕横竖方向等同于设备类型。
333
- - 提供下拉刷新、无限加载、侧滑操作、验证码、级联选择、ActionSheet 等组件,保留桌面按钮或键盘入口。
334
- - 日期、时间、颜色和基础 Select 使用浏览器原生选择能力;弹出界面随操作系统变化,不宣称它们在所有平台都长得完全一样。
335
- - Upload 负责本地文件选择、拖放与限制校验,不自动上传文件;Table 支持客户端排序、分页、固定行高虚拟滚动与列宽拖拽,不含服务端分页协议。
336
- - 导入阶段避免直接访问浏览器全局;这不等于已完成 SSR 全部组件的服务端渲染、hydration、跨请求隔离和主题首屏验证。SSR 接入时每个请求创建独立实例,并自行验证水合行为。
337
- - 本项目不是浏览器、屏幕阅读器和实体设备兼容矩阵认证。即使桌面/移动视口检查通过,也不代表已覆盖 iOS Safari、Android WebView、触控笔、软键盘与所有真机组合。
338
-
339
- ## 从旧项目迁移
82
+ 也可运行 `npx --package=apptify apptify-playground ./my-app`。
340
83
 
341
- | 旧方式 | 新方式与差异 |
342
- | --- | --- |
343
- | `<apple-card title subtitle text icon icon-color>` | 保留这些属性与默认 slot,并增加 `media`、`title`、`icon`、`actions` slots。 |
344
- | `zoom="big/small/none"`、`shadow="normal/static/focused/none"` | 继续支持;新默认 `zoom="small"`,不保证旧尺寸和缩放数值完全一致。 |
345
- | `icon="mdi-home"` | 不再识别 MDI 字符串。导入 Lucide 组件并通过 `:icon="Home"` 传入;Card 的字符串 icon 被当作图片 URL。 |
346
- | `globalTheme` / `fancyAnimation` 模块常量 | 改用响应式 `theme.value.set()` / `motion.value.set()`;不需要刷新,也不要再直接读写旧 localStorage 键。 |
347
- | `sendMessage('showSnackbar', { type, text })` | 在 `this.$apple.sendMessage()` 或当前注入上下文上兼容;`showSnackBar`、`showMessage` 别名也保留。 |
348
- | `sendMessage('showDiag', { title, message })` | 仍可通过当前上下文发送;推荐使用返回独立句柄与 Promise 的 `dialog()`。 |
349
- | `btn: [{ bgColor, text, clickEvent }]` | **不直接兼容**。简单对话框用 `confirmText` / `cancelText`,复杂交互用声明式 `#footer` 或动态组件。 |
350
- | `sendMessage('closeDiag')` | 关闭当前上下文最上面的非通知弹层;不会一次关闭全部层。 |
351
- | `<e-img src squared gallery index>` | 改为 `<apple-image :gallery="{ src, alt }">`,描述放在图片对象内;不注册旧 `e-img` 名称。 |
84
+ 目标目录需为空。模板含导航、设置、账号表单、404 和组件示例,可直接编辑;账号功能需接入自己的后端。复制项目携带库归档,可独立安装。页面开发与部署见[应用模板](docs/PLAYGROUND-TEMPLATE.md)。
352
85
 
353
- ## 验证命令
86
+ ## 本地开发
354
87
 
355
88
  ```sh
356
- npm run typecheck
357
- npm test
358
- npm run build
359
- npm run test:e2e
360
- npm run test:package
89
+ npm ci
90
+ npm run dev
361
91
  ```
362
92
 
363
- 命令是否通过以当前执行结果为准。`build` 生成组件库 `dist/`、类型声明与工作台 `site/`。源码中的行为测试不代替消费项目的业务测试与真机验证。
364
-
365
- ### 图片轮播
93
+ 建议 Node.js 22 或 24 LTS。检查使用 `npm run typecheck`、`npm test`、`npm run build`、`npm run test:e2e` 和 `npm run test:package`;`npm pack` 自动构建并准备模板。生成器位于 [`packages/create-apptify`](packages/create-apptify/README.md),使用 `npm run test:create` 验证。历史验证记录见[验证文档](docs/VALIDATION.md)。
366
94
 
367
- 轮播统一使用 `AppleImage` 的 `carousel` 属性、`gallery` 与 `v-model:index`,支持 `item` 插槽;独立 `AppleCarousel` 与 `AppleSwipeCell` 已移除。
95
+ MIT 许可。Apptify 不是 Apple 官方产品。
@@ -199,7 +199,7 @@ Modal slots:`title`、default `{close}`、footer `{close,confirm,cancel}`。
199
199
 
200
200
  `OverlayOptions` 实际字段为 `kind?`、`title?`、`message?`、`component?`、`props?`、`onMessage?`、`persistent?`、`confirmText?`、`cancelText?`、`tone?`、`duration?`。不要把声明式组件的 width、loading、action 等属性假定成当前服务参数;需要这些控制时使用声明式组件或自定义内容。
201
201
 
202
- 返回的 `OverlayHandle<T>` 为 `{ id, close(value?), update(patch), result: Promise<T | undefined> }`。`props` 传给动态内容组件;宿主额外注入 `close` 和 `sendMessage`。`onMessage(channel,payload)` 可返回普通值或 Promise,实现双向请求与响应,示例见 [README](../README.md#dialog-与反向通道)。
202
+ 返回的 `OverlayHandle<T>` 为 `{ id, close(value?), update(patch), result: Promise<T | undefined> }`。`props` 传给动态内容组件;宿主额外注入 `close` 和 `sendMessage`。`onMessage(channel,payload)` 可返回普通值或 Promise,实现双向请求与响应。
203
203
 
204
204
  ## 验证边界
205
205
 
@@ -1,74 +1,51 @@
1
- # 完整 playground 应用模板
1
+ # 应用模板
2
2
 
3
- apptify 包既提供组件库,也携带完整的可编辑应用模板。模板源码在包内 templates/playground/,包括导航与页面布局、账号菜单与登录/注册表单、主题/动效/玻璃设置、404 纸片故事、全部示例页面及图片资源。
3
+ npm 包包含完整可编辑的 Vue 应用:导航、账号表单、主题/动效/玻璃设置、404、组件示例与图片资源。
4
4
 
5
- 这些应用文件不增加组件库的公开 exports。复制出的项目通过 apptify 和 apptify/vite 使用已打包组件库,不能导入原仓库的 src。
5
+ ## 创建与运行
6
6
 
7
- ## 从本地包创建应用
8
-
9
- 当前尚未发布 npm。使用 Node.js 22 或 24 LTS,先在库仓库生成包:
10
-
11
- ~~~sh
12
- npm ci
13
- npm pack --pack-destination /tmp
14
- ~~~
15
-
16
- npm pack 会先执行构建与模板准备。将输出的实际 tgz 路径代入以下命令;在一个用于安装组件库的空目录执行:
17
-
18
- ~~~sh
19
- npm init -y
20
- npm install /tmp/apptify-0.4.0.tgz
21
- npx --no-install apptify-playground ../my-app
22
- cd ../my-app
7
+ ```sh
8
+ npm create apptify@latest ./my-app
9
+ cd my-app
23
10
  npm install
24
- npm run typecheck
25
- npm run build
26
11
  npm run dev
27
- ~~~
28
-
29
- 复制命令要求目标目录不存在或为空,拒绝覆盖已有项目。复制过程不会自动安装项目依赖。
12
+ ```
30
13
 
31
- my-app/vendor/ 会携带组件库 tgz,应用依赖使用相对 file: 依赖。可以把整个 my-app 移动到另一目录或发给其他使用者;后续安装无需原仓库或用于调用命令的安装目录。运行与构建所需依赖全部声明在应用的 package.json。更新库时可把新 tgz 放入 vendor/ 并调整该相对依赖。
14
+ 也可运行 `npx --package=apptify apptify-playground ./my-app`。
32
15
 
33
- 也可手工复制包内 templates/playground/,把 gitignore 重命名为 .gitignore,再将模板 package.json 中的 apptify 版本依赖改为本地 tgz;复制命令会自动完成这些步骤。仓库内 templates/playground 是打包时生成的目录,开发者应编辑 playground 源文件;使用者复制出的项目可以直接编辑。
16
+ 使用 Node.js 22 或 24 LTS。目标目录必须不存在或为空;复制命令不会安装依赖。
34
17
 
35
- ## 开始业务开发
18
+ 项目的 `vendor/` 保存组件库 tgz,`package.json` 使用相对 `file:` 依赖,移动整个项目后仍可独立安装。改用 npm 更新库时,在新项目执行 `npm install apptify@latest`。
36
19
 
37
- 可以只删除以下两页,然后编写自己的页面:
20
+ ## 开始开发
38
21
 
39
- ~~~text
40
- playground/views/index.vue
41
- playground/views/components.vue
42
- ~~~
43
-
44
- 文件路由从 playground/views/ 自动生成,不存在对这两页的硬导入。删除后重启开发服务即可;模板导航自动隐藏不存在的页面,品牌链接回到第一个保留的导航项,404 的“返回首页”在没有首页时回到设置。
45
-
46
- 新页面例如 playground/views/orders.vue 会得到 /orders 路由。将入口加入 playground/App.vue 的 navigation 数组即可出现在顶部导航。自己的 index.vue 仍对应首页。生产服务器需把 history 路由请求回退到 index.html。
22
+ | 位置 | 用途 |
23
+ | --- | --- |
24
+ | `playground/App.vue` | 布局、导航项与账号入口 |
25
+ | `playground/views/` | 页面与自动生成的路由 |
26
+ | `playground/router/index.ts` | 滚动恢复、hash 定位与页面标题 |
27
+ | `playground/components/UserMenu.vue` | 账号菜单与登录/注册表单 |
28
+ | `playground/views/settings.vue` | 主题、动效与玻璃设置 |
29
+ | `playground/views/[...all].vue` | 404 页面 |
30
+ | `playground/style.css` | 应用样式 |
31
+ | `public/images/` | 示例图片与来源说明 |
47
32
 
48
- 删除组件总览后,ComponentDemo.vue、catalog.ts 和 components/ComponentIndex.vue 可以留着,也可按需删除;设置、账号、404 与布局不依赖它们。不需要清理库组件依赖或维护手写路由列表。
33
+ 可删除 `views/index.vue` 和 `views/components.vue`,再创建业务页面。删除后重启开发服务;导航会隐藏不存在的页面。删除组件总览后,可一并删除 `ComponentDemo.vue`、`catalog.ts` 和 `components/ComponentIndex.vue`。
49
34
 
50
- | 可编辑位置 | 用途 |
51
- | --- | --- |
52
- | playground/App.vue | 公共导航、品牌、账号入口和页面进入动效 |
53
- | playground/router/index.ts | 滚动恢复、hash 定位和页面标题 |
54
- | playground/components/UserMenu.vue | 账号菜单、登录/注册、邮箱验证码和忘记密码入口 |
55
- | playground/views/settings.vue | 外观、动效和玻璃偏好 |
56
- | playground/views/[...all].vue 与 components/not-found-paper-* | 404 页面与故事 |
57
- | playground/style.css | 应用布局与页面样式 |
58
- | public/images/ | 示例图片与来源说明 |
35
+ 例如 `playground/views/orders.vue` 自动对应 `/orders`;在 `App.vue` 的 `navigation` 数组添加入口即可显示导航。`index.vue` 对应首页。
59
36
 
60
- 账户操作仍是 UI 演示,需要接入使用者自己的后端。图片来源说明随模板保留在 public/images/SOURCES.md。
37
+ 组件通过 `apptify` 导入,样式通过 `apptify/style.css` 导入;`apptify/vite` 提供文件路由插件。已有项目接入时需安装 `vue-router` 和 `vite-plugin-pages`,模板已配置这些依赖。
61
38
 
62
- ## 本地交付验证
39
+ 账号表单需要接入自己的后端。部署使用 history 路由,服务器需将页面请求回退到 `index.html`,保留 API 与静态资源路由。图片来源说明位于 `public/images/SOURCES.md`。
63
40
 
64
- 在库仓库执行:
41
+ ## 检查与本地打包
65
42
 
66
- ~~~sh
67
- npm run test:package
68
- ~~~
43
+ 复制出的应用使用 `npm run typecheck` 和 `npm run build` 检查。
69
44
 
70
- 此检查会构建原库消费者、生成真实 npm tarball、检查每份 playground 源码和静态资源都在包内,并在临时空目录安装包后调用复制命令。随后删除最初的安装目录,独立安装、类型检查、构建和启动复制项目,实测导航、图片、设置、登录及 404;再删除首页、组件总览及其可选演示文件,重复类型检查、构建和浏览器验证。原库消费测试仍单独执行。
45
+ 库开发者编辑原仓库的 `playground/`,模板目录在打包时生成。`npm pack` 自动构建组件库并准备模板;本地安装生成的 tgz 后,也可运行 `npx --no-install apptify-playground ../my-app`。
71
46
 
72
- source-snapshot.json 记录本次复制前各源码与资源的 SHA-256。构建前后与验收末尾都会比对库、模板、资源及配置;若用户在打包中途编辑,检查要求重新取最新快照,不会回写或恢复旧源码。命令打印临时目录与验收 JSON 路径,失败时也保留该目录用于检查。
47
+ `npm run test:package` 验证包内容、独立安装、模板复制、类型检查、构建与浏览器交互,并检查删除示例页面后的行为。命令输出临时目录与验收结果路径。验证指定归档:
73
48
 
74
- 已生成的具体 tgz 也可直接复验:运行 node tooling/test-playground-package.mjs /absolute/path/apptify-0.4.0.tgz。验收结果记录该文件 SHA-256,避免并行构建替换了被验证对象。
49
+ ```sh
50
+ node tooling/test-playground-package.mjs /absolute/path/apptify-<version>.tgz
51
+ ```
@@ -10,7 +10,7 @@
10
10
  - WebKit 390px、触屏/移动视口模拟验证了输入、叠层弹窗、滚动锁、图片加载与翻页以及系统减少动态效果。
11
11
  - `npm run test:package`:TypeScript 检查、展示站构建、ESM 库构建、声明生成,以及通过包名 `apptify` 和 `apptify/style.css` 导入的独立消费示例均通过。消费者使用纯 JavaScript Options API,不要求 TypeScript。
12
12
  - `npm audit`:本次锁文件检查为 0 项已知漏洞。此结果仅代表当时的软件漏洞数据库。
13
- - 未发布到 npm;旧 tgz 是整改前版本,当前源码需重新 `npm pack`。演示图片不进入组件库安装包。
13
+ - 此节为 2026-09-26 的历史记录。当前包已发布;模板与示例图片随包分发,最新源码需重新 `npm pack`。
14
14
 
15
15
  ## 回归修复
16
16
 
package/package.json CHANGED
@@ -1,7 +1,15 @@
1
1
  {
2
2
  "name": "apptify",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "An Apple-inspired, responsive Vue 3 component library.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/GARBAGELINUXGRAPHIC/apptify.git"
8
+ },
9
+ "homepage": "https://github.com/GARBAGELINUXGRAPHIC/apptify#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/GARBAGELINUXGRAPHIC/apptify/issues"
12
+ },
5
13
  "type": "module",
6
14
  "license": "MIT",
7
15
  "files": [
@@ -34,6 +42,7 @@
34
42
  "test": "vitest run",
35
43
  "test:e2e": "playwright test",
36
44
  "test:package": "npm run prepack && vite build --config vite.consumer.config.ts && node tooling/test-playground-package.mjs",
45
+ "test:create": "node tooling/test-create-apptify.mjs",
37
46
  "preview": "vite preview --host 127.0.0.1",
38
47
  "prepare:playground": "node tooling/prepare-playground.mjs",
39
48
  "prepack": "node tooling/prepare-package.mjs"
@@ -1,17 +1,21 @@
1
- # Apptify 可编辑应用模板
1
+ # Apptify 应用模板
2
2
 
3
- 完整源码位于 playground/,静态图片与来源说明位于 public/images/。导航、账号/登录、设置、404 和页面动效均可直接编辑复用。
3
+ 可编辑的 Vue 应用,包含导航、账号表单、外观设置、404 与组件示例。建议 Node.js 22 或 24 LTS。
4
4
 
5
- 通过 apptify-playground 复制命令创建时,vendor/ 包含本地组件库 tgz,package.json 使用相对 file: 依赖;移动整个项目后仍可独立安装。
5
+ ```sh
6
+ npm install
7
+ npm run dev
8
+ ```
6
9
 
7
- 运行 npm install、npm run typecheck、npm run build;开发使用 npm run dev。请使用 Node.js 22 或 24 LTS。
10
+ 检查:`npm run typecheck`、`npm run build`。
8
11
 
9
- 开始业务开发可直接删除 playground/views/index.vue 和 playground/views/components.vue,再添加自己的页面;文件路由自动生成,导航自动隐藏已删除页面,品牌链接回到第一个保留的导航页面。删除页面后重启开发服务。
12
+ - 页面:`playground/views/`,文件路由自动生成。
13
+ - 布局与导航:`playground/App.vue` 的 `navigation` 数组。
14
+ - 账号表单:`playground/components/UserMenu.vue`,需要接入自己的后端。
15
+ - 样式:`playground/style.css`;图片与来源:`public/images/`。
10
16
 
11
- 导航项在 playground/App.vue 的 navigation 数组;页面文件在 playground/views/;布局和账号入口在 App.vue 与 playground/components/UserMenu.vue。
17
+ 可删除 `views/index.vue` 和 `views/components.vue`,再添加业务页面;删除后重启开发服务。删除组件总览后,可一并删除 `ComponentDemo.vue`、`catalog.ts` 和 `components/ComponentIndex.vue`。
12
18
 
13
- 设置保留主题、动效与玻璃偏好。404 在 playground/views/[...all].vue;首页和组件总览没有被其他页面硬导入。ComponentDemo.vue、catalog.ts 和 ComponentIndex.vue 可以保留,也可在删除总览页后按需删除。
19
+ 复制命令在 `vendor/` 保存库归档,移动整个项目后仍可独立安装。使用 npm 更新库:`npm install apptify@latest`。手工复制模板时,将 `gitignore` 重命名为 `.gitignore`。
14
20
 
15
- 登录、注册、忘记密码、邮箱验证码仍为 UI 演示入口,需要接入自己的后端。部署采用 history 路由,服务器应把未知页面路径回退到 index.html。
16
-
17
- 若手工复制包内 templates/playground,先将 package.json 的 apptify 依赖替换为实际本地 tgz 路径,或安装自己的正式发布版本;当前项目尚未发布 npm。
21
+ 部署时将页面请求回退到 `index.html`,保留 API 与静态资源路由。
@@ -10,7 +10,7 @@
10
10
  "preview": "vite preview --host 127.0.0.1"
11
11
  },
12
12
  "dependencies": {
13
- "apptify": "^0.5.0",
13
+ "apptify": "^0.5.1",
14
14
  "vue": "^3.5.0",
15
15
  "vue-router": "^4.6.4",
16
16
  "vuetify": "^3.9.0",
@@ -1,6 +1,6 @@
1
1
  {
2
- "createdAt": "2026-10-03T06:52:26.901Z",
3
- "libraryVersion": "0.5.0",
2
+ "createdAt": "2026-10-03T07:22:12.858Z",
3
+ "libraryVersion": "0.5.1",
4
4
  "files": {
5
5
  "index.html": "75f6d93e3962ddca97a13b666a04d98ba9b263b6c288171383db764215034fea",
6
6
  "playground/App.vue": "ef3f06f80758a3d4198ee90eb05f766a89b6156b4899fd566f8dca9b42e0ffc9",
@@ -94,18 +94,18 @@
94
94
  "public/images/snow.jpg": "59fec6d2bbf9df11b7c697c57a253b7bd4b5a097489ce1017fb5209b9ee54b8e",
95
95
  "public/images/sunset.jpg": "4ee91e88f1e341143a807ef48fdfc24d63aa045d99351b4ea8ad7efcfef6ef9b",
96
96
  "public/images/waterfall.jpg": "cfd55ff5f41d3a58e91793d0e26a9ca90c86760aa401da033fbfa77d3364f238",
97
- "docs/COMPONENTS.md": "ca4a01cefaddd3e45a1cfdaef3bc0474fa6a831de9802fcd4c27e0926da00a70",
98
- "docs/PLAYGROUND-TEMPLATE.md": "b8a418d940cb99c53dd7f95c4f44da2682dbaf03201ec7eccfe7234b198f1408",
99
- "docs/VALIDATION.md": "fb73cf9a3ae78c87093c89627ad6fdd9edc516b87aefd7244849d731283a1d0f",
100
- "README.md": "0a4838e51c0d6bd2269411176ad4dbb89293ec77c332e9ed95b0f7f66f07ca9f",
101
- "package.json": "856dd5ba89db5b5ebe685f711f1c1851eb4869ffea94f860b5918325428c0f7f",
97
+ "docs/COMPONENTS.md": "975f33ddb8584c0cb2a12066316018a02a7bfd73014a0a79d89d8990ca06f496",
98
+ "docs/PLAYGROUND-TEMPLATE.md": "26b15d5d9ca939d765aa93ce1389018f1aa4ce32de11c368556344b81c6b8b09",
99
+ "docs/VALIDATION.md": "3c1755f4699d6b7d8febb44b2b0b00eb416b86559c26d8b69e46a1c98271af5f",
100
+ "README.md": "7923e6dd7f06996af0ac8fcafafc9c338f5700969fd080a8af31a08df9514e58",
101
+ "package.json": "ceb8c42a9140d21fa2d4dd20528a85922be94d99ad739aeec83519b471092a86",
102
102
  "index.html": "75f6d93e3962ddca97a13b666a04d98ba9b263b6c288171383db764215034fea",
103
103
  "vite.config.ts": "df2eae92f81c9b9007549de5721a2311dd9b918dea45a4c7b8b4979f247e49ae",
104
104
  "vite.lib.config.ts": "f877c1772da70c9fed5174118ca36a0fc25fd46cce49b71280a5f91c4f8003a3",
105
105
  "tsconfig.json": "09590870bfbf6e90614d8a34af38b9a72902e8747ecd1a76be47ab24c842f9ea",
106
106
  "tsconfig.lib.json": "c885806b003cee01192cddcaf32df3e7c6d369e645360398aa088c14b1c4792f",
107
107
  "tooling/create-playground.mjs": "c45480c5982a64efd06f69dfb4620c65fd8ea35d01877b8574e7e2b30c095606",
108
- "tooling/prepare-playground.mjs": "67e62e7e8bfd9a97f567474fe14e3776ce68166c97150c8d72509ba18efe4126",
108
+ "tooling/prepare-playground.mjs": "2e431ee93803d640f9b81fd8228a6b2edf0538e0eef8a8749f0622abf9f1a23e",
109
109
  "tooling/prepare-package.mjs": "cf57a112eec798412e2a68089d1b2d25b984600d6ffef99397ec253abf2fca57"
110
110
  }
111
111
  }
@@ -77,14 +77,27 @@ await writeFile(join(output, 'vite.config.ts'), [
77
77
  // npm excludes .gitignore from tarballs; the copy command restores its dot.
78
78
  await writeFile(join(output, 'gitignore'), 'node_modules/\ndist/\n.env.local\n')
79
79
  await writeFile(join(output, 'README.md'), [
80
- '# Apptify 可编辑应用模板', '',
81
- '完整源码位于 playground/,静态图片与来源说明位于 public/images/。导航、账号/登录、设置、404 和页面动效均可直接编辑复用。', '',
82
- '通过 apptify-playground 复制命令创建时,vendor/ 包含本地组件库 tgz,package.json 使用相对 file: 依赖;移动整个项目后仍可独立安装。', '',
83
- '运行 npm install、npm run typecheck、npm run build;开发使用 npm run dev。请使用 Node.js 22 或 24 LTS。', '',
84
- '开始业务开发可直接删除 playground/views/index.vue 和 playground/views/components.vue,再添加自己的页面;文件路由自动生成,导航自动隐藏已删除页面,品牌链接回到第一个保留的导航页面。删除页面后重启开发服务。', '',
85
- '导航项在 playground/App.vue 的 navigation 数组;页面文件在 playground/views/;布局和账号入口在 App.vue 与 playground/components/UserMenu.vue。', '',
86
- '设置保留主题、动效与玻璃偏好。404 在 playground/views/[...all].vue;首页和组件总览没有被其他页面硬导入。ComponentDemo.vue、catalog.ts 和 ComponentIndex.vue 可以保留,也可在删除总览页后按需删除。', '',
87
- '登录、注册、忘记密码、邮箱验证码仍为 UI 演示入口,需要接入自己的后端。部署采用 history 路由,服务器应把未知页面路径回退到 index.html。', '',
88
- '若手工复制包内 templates/playground,先将 package.json 的 apptify 依赖替换为实际本地 tgz 路径,或安装自己的正式发布版本;当前项目尚未发布 npm。', '',
80
+ "# Apptify 应用模板",
81
+ "",
82
+ "可编辑的 Vue 应用,包含导航、账号表单、外观设置、404 与组件示例。建议 Node.js 22 或 24 LTS。",
83
+ "",
84
+ "```sh",
85
+ "npm install",
86
+ "npm run dev",
87
+ "```",
88
+ "",
89
+ "检查:`npm run typecheck`、`npm run build`。",
90
+ "",
91
+ "- 页面:`playground/views/`,文件路由自动生成。",
92
+ "- 布局与导航:`playground/App.vue` 的 `navigation` 数组。",
93
+ "- 账号表单:`playground/components/UserMenu.vue`,需要接入自己的后端。",
94
+ "- 样式:`playground/style.css`;图片与来源:`public/images/`。",
95
+ "",
96
+ "可删除 `views/index.vue` 和 `views/components.vue`,再添加业务页面;删除后重启开发服务。删除组件总览后,可一并删除 `ComponentDemo.vue`、`catalog.ts` 和 `components/ComponentIndex.vue`。",
97
+ "",
98
+ "复制命令在 `vendor/` 保存库归档,移动整个项目后仍可独立安装。使用 npm 更新库:`npm install apptify@latest`。手工复制模板时,将 `gitignore` 重命名为 `.gitignore`。",
99
+ "",
100
+ "部署时将页面请求回退到 `index.html`,保留 API 与静态资源路由。",
101
+ "",
89
102
  ].join('\n'))
90
103
  console.log('Prepared editable playground template: ' + output)
@@ -0,0 +1,76 @@
1
+ import assert from 'node:assert/strict'
2
+ import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'
3
+ import { join } from 'node:path'
4
+ import { tmpdir } from 'node:os'
5
+ import { fileURLToPath } from 'node:url'
6
+ import { execFileSync, spawnSync } from 'node:child_process'
7
+
8
+ const root = fileURLToPath(new URL('../', import.meta.url))
9
+ const scratch = await mkdtemp(join(tmpdir(), 'create-apptify-test-'))
10
+ const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'
11
+ const env = { ...process.env, npm_config_cache: join(scratch, 'cache'), npm_config_offline: 'true' }
12
+ const installer = join(scratch, 'installer')
13
+ const creator = join(installer, 'node_modules/create-apptify')
14
+ const library = join(installer, 'node_modules/apptify')
15
+ const cli = join(creator, 'bin/create-apptify.mjs')
16
+
17
+ async function unpack(cwd, destination) {
18
+ const packed = JSON.parse(execFileSync(npm, ['pack', '--ignore-scripts', '--json', '--pack-destination', scratch], { cwd, env, encoding: 'utf8' }))[0]
19
+ await mkdir(destination, { recursive: true })
20
+ execFileSync('tar', ['-xzf', join(scratch, packed.filename), '--strip-components=1', '-C', destination])
21
+ return packed
22
+ }
23
+
24
+ try {
25
+ // Use real tarballs in an installed layout, without fetching dependencies.
26
+ // The generator only reads/copies the library; it never imports its Vue code.
27
+ const packed = await unpack(join(root, 'packages/create-apptify'), creator)
28
+ const files = new Set(packed.files.map(file => file.path))
29
+ for (const path of ['package.json', 'bin/create-apptify.mjs', 'README.md', 'LICENSE']) assert(files.has(path), 'Missing creator file: ' + path)
30
+ await unpack(root, library)
31
+ await mkdir(join(installer, 'node_modules/.bin'))
32
+ await symlink('../create-apptify/bin/create-apptify.mjs', join(installer, 'node_modules/.bin/create-apptify'))
33
+ await writeFile(join(installer, 'package.json'), '{"private":true,"dependencies":{"create-apptify":"0.1.0"}}\n')
34
+
35
+ const run = args => spawnSync(process.execPath, [cli, ...args], { cwd: installer, env, encoding: 'utf8' })
36
+ const help = run(['--help'])
37
+ assert.equal(help.status, 0, help.stderr)
38
+ assert.match(help.stdout, /npm create apptify@latest/)
39
+ assert.equal(run([]).status, 1)
40
+ assert.equal(run(['--unknown']).status, 1)
41
+ assert.equal(run(['one', 'two']).status, 1)
42
+
43
+ const app = join(scratch, 'my app')
44
+ const result = spawnSync(npm, ['create', 'apptify', '--offline', '--', app], { cwd: installer, env, encoding: 'utf8', timeout: 60_000 })
45
+ assert.equal(result.status, 0, result.stdout + result.stderr)
46
+ const manifest = JSON.parse(await readFile(join(app, 'package.json'), 'utf8'))
47
+ assert.match(manifest.dependencies.apptify, /^file:\.\/vendor\/apptify-/)
48
+ assert.match(await readFile(join(app, '.gitignore'), 'utf8'), /node_modules/)
49
+ await readFile(join(app, 'playground/App.vue'))
50
+ const archive = join(app, manifest.dependencies.apptify.slice('file:./'.length))
51
+ const portableManifest = JSON.parse(execFileSync('tar', ['-xOf', archive, 'package/package.json'], { encoding: 'utf8' }))
52
+ assert.equal(portableManifest.name, 'apptify')
53
+
54
+ const before = await readFile(join(app, 'package.json'), 'utf8')
55
+ const refused = run([app])
56
+ assert.equal(refused.status, 1)
57
+ assert.match(refused.stderr, /Destination must be empty/)
58
+ assert.equal(await readFile(join(app, 'package.json'), 'utf8'), before)
59
+ assert.equal(run([join(library, 'unsafe')]).status, 1)
60
+ const alias = join(scratch, 'library-alias')
61
+ await symlink(library, alias, 'dir')
62
+ assert.equal(run([join(alias, 'unsafe')]).status, 1)
63
+
64
+ const empty = join(scratch, 'existing-empty')
65
+ await mkdir(empty)
66
+ const copied = run([empty])
67
+ assert.equal(copied.status, 0, copied.stderr)
68
+ await readFile(join(empty, 'playground/App.vue'))
69
+ await rm(installer, { recursive: true, force: true })
70
+ execFileSync('tar', ['-t', '-f', archive])
71
+ console.log('create-apptify: packed CLI, npm create, portable archive, help, argument validation and overwrite protection passed.')
72
+ await rm(scratch, { recursive: true, force: true })
73
+ } catch (error) {
74
+ console.error('Acceptance workspace retained: ' + scratch)
75
+ throw error
76
+ }