apptify 0.5.4 → 0.6.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,59 +1,23 @@
1
1
  # Apptify
2
2
 
3
- 面向桌面与移动端的 Vue 3 组件库,采用 Apple 风格设计,统一使用 `<apple-*>` 标签。支持主题切换、减少动效、表单、图片预览与多层弹窗;支持 JavaScript、TypeScript 和 Options API。
3
+ 面向桌面与移动端的 Vue 3 组件库,基于 Vuetify Material UI 融合 Apple 风格设计,统一使用 `<apple-*>` 标签。
4
4
 
5
5
  [npm](https://www.npmjs.com/package/apptify) · [组件 API](docs/COMPONENTS.md) · [应用模板](docs/PLAYGROUND-TEMPLATE.md)
6
6
 
7
- ## 安装
7
+ ## 快速开始
8
8
 
9
9
  ```sh
10
- npm install apptify vue@^3.5 vuetify@^3.9
10
+ npm create apptify
11
11
  ```
12
12
 
13
- Vue 与 Vuetify 是 peer dependencies。Vuetify 仅用于 Ripple,无需安装其插件、导入完整样式或使用 `<v-app>`。
14
-
15
- ## 快速接入
16
-
17
- ```js
18
- // src/main.js
19
- import { createApp } from 'vue'
20
- import { createAppleUI } from 'apptify'
21
- import 'apptify/style.css'
22
- import App from './App.vue'
23
-
24
- createApp(App)
25
- .use(createAppleUI({ theme: 'system', motion: 'auto', persist: true }))
26
- .mount('#app')
27
- ```
28
-
29
- ```vue
30
- <!-- App.vue -->
31
- <template>
32
- <apple-provider>
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>
41
- </apple-provider>
42
- </template>
43
-
44
- <script>
45
- export default {
46
- data: () => ({ name: '' }),
47
- methods: {
48
- save() {
49
- this.$apple.notify('资料已保存', { tone: 'success' })
50
- },
51
- },
52
- }
53
- </script>
54
- ```
55
-
56
- `AppleProvider` 提供主题与弹层宿主,无需额外添加 `AppleOverlayHost`。插件全局注册组件,也支持命名导入并局部注册。嵌套 Provider 内可用 `appleKey` 注入局部上下文,或在 `setup()` 中使用 `useApple()`。
13
+ 安装后会自带所有组件说明的样例站。Bundle 包含以下组件之外的内容:
14
+ - 自动根据 src/views 生成 routes
15
+ - lucide icons
16
+ - 竖屏、触屏布尔状态
17
+ - 全局广播通道 `sendMessage`
18
+ - 错误页
19
+ - 外观设置页
20
+ - 各组件示例与源码等,有问题可以直接抄
57
21
 
58
22
  ## 常用 API
59
23
 
@@ -73,26 +37,12 @@ const confirmed = await dialog.result // 确认 true,取消 false,关闭 und
73
37
 
74
38
  Props、事件、插槽、自定义主题与弹层消息回传见[组件 API](docs/COMPONENTS.md)。
75
39
 
76
- ## 创建完整应用
40
+ ## 自动发布
77
41
 
78
- ```sh
79
- npm create apptify@latest ./my-app
80
- cd my-app
81
- npm install
82
- npm run dev
83
- ```
84
-
85
- 也可运行 `npx --package=apptify apptify-playground ./my-app`。
86
-
87
- 目标目录需为空。模板含导航、设置、账号表单、404 和组件示例,可直接编辑;账号功能需接入自己的后端。复制项目携带库归档,可独立安装。页面开发与部署见[应用模板](docs/PLAYGROUND-TEMPLATE.md)。
42
+ 推送到 `main` 后,GitHub Actions 会将尚未发布的 `apptify` 和 `create-apptify` 版本发布到 npm。发布前更新对应的 `package.json` 版本;已发布版本自动跳过,预发布版本使用 `next` 标签。
88
43
 
89
- ## 本地开发
90
-
91
- ```sh
92
- npm ci
93
- npm run dev
94
- ```
44
+ 在两个 npm 包的 Settings → Trusted Publisher 中选择 GitHub Actions,填写用户 `GARBAGELINUXGRAPHIC`、仓库 `apptify`、工作流文件 `npm-publish.yml`,允许 `npm publish`,环境名留空。使用 OIDC,无需配置或保存 npm token。
95
45
 
96
- 建议 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)。
46
+ ## LICENSE
97
47
 
98
- MIT 许可。Apptify 不是 Apple 官方产品。
48
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apptify",
3
- "version": "0.5.4",
3
+ "version": "0.6.1",
4
4
  "description": "An Apple-inspired, responsive Vue 3 component library.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,7 +10,7 @@
10
10
  "preview": "vite preview --host 127.0.0.1"
11
11
  },
12
12
  "dependencies": {
13
- "apptify": "^0.5.4",
13
+ "apptify": "^0.6.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-04T09:39:13.558Z",
3
- "libraryVersion": "0.5.4",
2
+ "createdAt": "2026-10-04T16:10:51.852Z",
3
+ "libraryVersion": "0.6.1",
4
4
  "files": {
5
5
  "index.html": "75f6d93e3962ddca97a13b666a04d98ba9b263b6c288171383db764215034fea",
6
6
  "public/images/SOURCES.md": "24ccb6625b1168e48550b7651e9fc20cb747324d87666d39199608c7db20541b",
@@ -17,8 +17,8 @@
17
17
  "public/images/sunset.jpg": "4ee91e88f1e341143a807ef48fdfc24d63aa045d99351b4ea8ad7efcfef6ef9b",
18
18
  "public/images/waterfall.jpg": "cfd55ff5f41d3a58e91793d0e26a9ca90c86760aa401da033fbfa77d3364f238",
19
19
  "src/App.vue": "1c794c9a04e5830cde04e2884a7ab3116e36283a9def63fc4d16cf9eea7151e0",
20
- "src/ComponentDemo.vue": "0c3b65ad2bec0da36d01451b0fd2e4c4e6bdb34da0beb2f0b813427fa2eeaa3d",
21
- "src/catalog.ts": "3aecd5190d37533ecd9427da3a2c9ed3b18b6f2f04a51c543ff88dfe52070e59",
20
+ "src/ComponentDemo.vue": "7f8ba3f3fc5c0c4a212383c6bd9d390271f5d9cc058802393ebd18b06be2aaa6",
21
+ "src/catalog.ts": "688145825314fed7dfb41a22c121a895f8114e3abf96dc812bc7c5186a16fa0c",
22
22
  "src/components/ComponentIndex.vue": "55c021899113b7fe26dcf42075f188a49d996e04a992ec9c86410eadc29e68a0",
23
23
  "src/components/DirectoryDialog.vue": "ec6191422dad07dff2d710748a3f024bf7828be8b9b70fa16ab474f36ed878d2",
24
24
  "src/components/PageFooter.vue": "5e9c1b4ba8866c05216993fb8d605fa826a09739a5ced15068b24334e4814fac",
@@ -46,7 +46,7 @@
46
46
  "src/views/component-docs/[component].vue": "3f505550dacb1d8151417df61d0598940aa7d218518179e532ddc962b0632a5e",
47
47
  "src/views/components.vue": "a19c4ca87a6a2ed43c4a6dc6eb52d3071733ce787024e1ab305db99b6acc6333",
48
48
  "src/views/index.vue": "9afc860da88aebe4879668784da24c657ecba19e5bc3ddad9dbcdd72f84db245",
49
- "src/views/settings.vue": "f5f9ca38f99d80fafab75fd33053715faa4265b998f32604af432497215a7c09",
49
+ "src/views/settings.vue": "1d36c866d1d5b1ec426d1254b31649e54585d3f3b4313ec8eb924f6a0eba456a",
50
50
  "tooling/playground-editor.mjs": "128b1ffa1b5660b04f7652130d5474c1f31686c2c8e0e2866f1ebde2594d0b5b",
51
51
  "tooling/prepare-input-source.mjs": "91bea35dcfa09735dbaeb5fbed7a70abfe447c3d133f4b4381e6ba0d0d4f9f6a"
52
52
  },
@@ -135,8 +135,8 @@
135
135
  "src/styles/overlays.css": "bb3d5f7bfc1b3e7a0ecf5ab4f632a65be3f496cb5de0e8c885bd6b6e5dc0f97a",
136
136
  "src/styles/ripple.css": "df0a5f661a862b5232bac256958f4bae6f555dd1f4a9c1432f61e85de71bdfea",
137
137
  "playground/App.vue": "1c794c9a04e5830cde04e2884a7ab3116e36283a9def63fc4d16cf9eea7151e0",
138
- "playground/ComponentDemo.vue": "0c3b65ad2bec0da36d01451b0fd2e4c4e6bdb34da0beb2f0b813427fa2eeaa3d",
139
- "playground/catalog.ts": "3aecd5190d37533ecd9427da3a2c9ed3b18b6f2f04a51c543ff88dfe52070e59",
138
+ "playground/ComponentDemo.vue": "7f8ba3f3fc5c0c4a212383c6bd9d390271f5d9cc058802393ebd18b06be2aaa6",
139
+ "playground/catalog.ts": "688145825314fed7dfb41a22c121a895f8114e3abf96dc812bc7c5186a16fa0c",
140
140
  "playground/components/ComponentIndex.vue": "55c021899113b7fe26dcf42075f188a49d996e04a992ec9c86410eadc29e68a0",
141
141
  "playground/components/DirectoryDialog.vue": "ec6191422dad07dff2d710748a3f024bf7828be8b9b70fa16ab474f36ed878d2",
142
142
  "playground/components/PageFooter.vue": "5e9c1b4ba8866c05216993fb8d605fa826a09739a5ced15068b24334e4814fac",
@@ -164,7 +164,7 @@
164
164
  "playground/views/component-docs/[component].vue": "3f505550dacb1d8151417df61d0598940aa7d218518179e532ddc962b0632a5e",
165
165
  "playground/views/components.vue": "a19c4ca87a6a2ed43c4a6dc6eb52d3071733ce787024e1ab305db99b6acc6333",
166
166
  "playground/views/index.vue": "9afc860da88aebe4879668784da24c657ecba19e5bc3ddad9dbcdd72f84db245",
167
- "playground/views/settings.vue": "f5f9ca38f99d80fafab75fd33053715faa4265b998f32604af432497215a7c09",
167
+ "playground/views/settings.vue": "1d36c866d1d5b1ec426d1254b31649e54585d3f3b4313ec8eb924f6a0eba456a",
168
168
  "public/images/SOURCES.md": "24ccb6625b1168e48550b7651e9fc20cb747324d87666d39199608c7db20541b",
169
169
  "public/images/airpods-max-orange.jpg": "f365fe9e374e2cede88f3e68487f6a773a767873978bcf20e4b21dee08aca966",
170
170
  "public/images/architecture.jpg": "928ea321954f0edabc1eed3c908c6b1fc93b247badcbd89d8dc5ba237420c26b",
@@ -182,15 +182,16 @@
182
182
  "docs/INPUT-WORKSHOP.md": "82ea6db156d2d9be4cfee233675b107c837aaa30a849f92010e988e1df7ce3a0",
183
183
  "docs/PLAYGROUND-TEMPLATE.md": "e476d6622e89d8cb04d9acdd54c090e5d559d666bb49f266bcf36d3f3bf8fe67",
184
184
  "docs/VALIDATION.md": "3c1755f4699d6b7d8febb44b2b0b00eb416b86559c26d8b69e46a1c98271af5f",
185
- "README.md": "95a66c9be54623335683af80d48e2415ae195e3797d624dfa69d042d6ea14958",
186
- "package.json": "905ddd2741ce948047ae576fd6715f048cbda60218618617d703397c76df7d0b",
185
+ "README.md": "1e24796cf31d3d93f34e4edd32b8e1629b1dfdc175f74df573e14b4eb12186ed",
186
+ "package.json": "780f0e2ce21421eb0b71c16ec19ab4d1276e94293e8be0ab1d7f1248296e999f",
187
187
  "index.html": "75f6d93e3962ddca97a13b666a04d98ba9b263b6c288171383db764215034fea",
188
188
  "vite.config.ts": "e2fee737d17c721cefaa33c955c6a7fcd47b46d51a95365768208eb16b608585",
189
189
  "vite.lib.config.ts": "f877c1772da70c9fed5174118ca36a0fc25fd46cce49b71280a5f91c4f8003a3",
190
190
  "tsconfig.json": "09590870bfbf6e90614d8a34af38b9a72902e8747ecd1a76be47ab24c842f9ea",
191
191
  "tsconfig.lib.json": "c885806b003cee01192cddcaf32df3e7c6d369e645360398aa088c14b1c4792f",
192
- "tooling/create-playground.mjs": "c45480c5982a64efd06f69dfb4620c65fd8ea35d01877b8574e7e2b30c095606",
192
+ "tooling/create-playground.mjs": "1252ef947ec8c1b3e0ca5d8c76cadacf69485aed5a181b866815cc9ed4658cc1",
193
+ "tooling/npm-command.mjs": "51c08c53cd395e32ec9b71b258dafadcdad3e6b227090d5b7bfbe55b9d0a97ba",
193
194
  "tooling/prepare-playground.mjs": "3a624b4966613a14e4f44a25555f25c564ea0fe69e5c68922c2ff5d21e1e2769",
194
- "tooling/prepare-package.mjs": "cf57a112eec798412e2a68089d1b2d25b984600d6ffef99397ec253abf2fca57"
195
+ "tooling/prepare-package.mjs": "a663ad0343bee2e711cdbc1c497daa89edc20752c67f17fc45d5173c6807b66b"
195
196
  }
196
197
  }
@@ -123,7 +123,6 @@
123
123
  </apple-action-sheet>
124
124
  </template>
125
125
  <apple-pull-refresh v-else-if="name === 'ApplePullRefresh'" @refresh="refresh"><apple-list :items="[{label:'最新内容',value:'latest',description:refreshText},{label:'最近更新',value:'recent',description:'所有内容已同步'}]" /></apple-pull-refresh>
126
- <apple-infinite-scroll v-else-if="name === 'AppleInfiniteScroll'" :finished="itemsCount >= 12" @load="loadMore"><apple-list :items="Array.from({length:itemsCount}, (_, i) => ({label:`灵感收藏 ${i+1}`, value:i, description:'刚刚更新'}))" /></apple-infinite-scroll>
127
126
  <template v-else-if="name === 'AppleBackTop'"><apple-button :icon="icons.ArrowUp" variant="secondary" @click="toTop">回到顶部</apple-button><apple-back-top :threshold="0" :fixed="false" /></template>
128
127
  <template v-else-if="name === 'AppleFloatingGroup'"><apple-button variant="secondary" @click="notify('快捷操作已就绪')">快捷操作</apple-button><apple-card class="demo-floating-preview"><apple-provider><apple-floating-group :threshold="0" /></apple-provider></apple-card></template>
129
128
  <apple-stack v-else-if="name === 'AppleAutoSize'"><apple-switch v-model="checked" label="显示详情" /><apple-auto-size><apple-list :items="checked ? settings : settings.slice(0,1)" selectable /></apple-auto-size></apple-stack>
@@ -135,6 +134,7 @@
135
134
  </apple-transition>
136
135
  </div>
137
136
  </apple-stack>
137
+ <apple-infinite-scroll v-else-if="name === 'AppleInfiniteScroll'" :finished="itemsCount >= 12" @load="loadMore"><apple-list :items="Array.from({length:itemsCount}, (_, i) => ({label:`灵感收藏 ${i+1}`, value:i, description:'刚刚更新'}))" /></apple-infinite-scroll>
138
138
  </div>
139
139
  </template>
140
140
 
@@ -60,11 +60,11 @@ export const catalog: CatalogItem[] = [
60
60
  item('AppleMenu', '下拉菜单', '反馈', '按需展开的操作列表。', 'v-model · items[{label,value,disabled,danger,icon,description}] · label · selected | select(value,item)', '<apple-menu :items="actions" label="更多操作" @select="onAction" />'),
61
61
  item('AppleActionSheet', '动作面板', '移动交互', '熟悉、直接的移动操作。', 'v-model · title · message · items[{label,value,disabled,danger}] · cancelText | default slot {select(item),close(value)} | select(value,item)', '<apple-action-sheet v-model="open" title="选择操作" @select="onAction">\n <template #default="{ select }">\n <apple-button @click="select({ label: \'收藏\', value: \'favorite\' })">加入收藏</apple-button>\n </template>\n</apple-action-sheet>'),
62
62
  item('ApplePullRefresh', '下拉刷新', '移动交互', '轻轻下拉,获取新内容。', 'v-model(refreshing) · threshold · label · disabled | refresh(done)', '<apple-pull-refresh @refresh="async done => { await reload(); done() }">内容</apple-pull-refresh>'),
63
- item('AppleInfiniteScroll', '无限滚动', '移动交互', '浏览,不必被翻页打断。', 'loading · finished · error · disabled · distance | load(done), retry(done)', '<apple-infinite-scroll :finished="finished" @load="async done => { await loadMore(); done() }">内容</apple-infinite-scroll>'),
64
63
  item('AppleBackTop', '回到顶部', '移动交互', '回到开始,毫不费力。', 'threshold · label', '<apple-back-top :threshold="400" />'),
65
64
  item('AppleFloatingGroup', '悬浮操作组', '移动交互', '常用操作,触手可及。', 'backTop · threshold · target · label · motion | default', '<apple-floating-group :threshold="300" />'),
66
65
  item('AppleAutoSize', '自适应尺寸', '基础', '内容变化,布局随行。', 'axis(height|both) · motion | default', '<apple-auto-size><div v-if="expanded">详情内容</div></apple-auto-size>'),
67
66
  item('AppleTransition', '动效容器', '基础', '为页面保留连续感。', 'name(page|slide-x|slide-y|fade) · mode · appear · motion | default', '<apple-transition name="page"><router-view /></apple-transition>'),
67
+ item('AppleInfiniteScroll', '无限滚动', '移动交互', '浏览,不必被翻页打断。', 'loading · finished · error · disabled · distance | load(done), retry(done)', '<apple-infinite-scroll :finished="finished" @load="async done => { await loadMore(); done() }">内容</apple-infinite-scroll>'),
68
68
  ]
69
69
  export const groups = ['全部组件', '基础', '表单', '导航', '数据展示', '反馈', '移动交互']
70
70
 
@@ -38,7 +38,7 @@
38
38
  <div class="settings-section-heading"><div><h2 id="ripple-heading">点击波纹</h2><p>让每一次轻点,都有细微的回应。</p></div></div>
39
39
  <apple-switch :model-value="rippleAllowed && apple.ripple.value.enabled" :disabled="!rippleAllowed" label="点击波纹" :hint="rippleAllowed ? '减弱动效时自动关闭,可手动开启;偏好保存在这台设备上。' : '关闭动效时,无法开启点击波纹。'" @update:model-value="apple.ripple.value.set" />
40
40
  <div ref="rippleDemo" class="effect-demo ripple-sample" aria-label="波纹自动演示" @pointerenter="ripplePaused = true" @pointerleave="ripplePaused = false" @focusin="ripplePaused = true" @focusout="ripplePaused = false">
41
- <apple-button @click="rippleDemoClicks++">试试点击波纹</apple-button>
41
+ <apple-button variant="outline" @click="rippleDemoClicks++">试试点击波纹</apple-button>
42
42
  <span class="ripple-click-count">已点击 {{ rippleDemoClicks }} 次</span>
43
43
  </div>
44
44
  <div class="settings-card-foot">{{ rippleAllowed && apple.ripple.value.enabled ? '自动点击 · 也可以手动试一试' : '点击波纹已关闭 · 静态展示' }}</div>
@@ -127,7 +127,6 @@ const glassPreviewChoice = ref('1')
127
127
  const glassPreviewItems = [
128
128
  { label: 'Item 1', value: '1', description: 'Item 1' },
129
129
  { label: 'Item 2', value: '2', description: 'Item 2' },
130
- { label: 'Item 3', value: '3', description: 'Item 3' },
131
130
  ]
132
131
  function formatOpacity(value: number) { return `${Number(value.toFixed(1))}%` }
133
132
  function formatBlur(value: number) { return `${value}px` }
@@ -3,6 +3,7 @@ import { cp, mkdir, readFile, readdir, rename, writeFile } from 'node:fs/promise
3
3
  import { resolve, join, sep } from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import { execFileSync } from 'node:child_process'
6
+ import { npmCommand } from './npm-command.mjs'
6
7
 
7
8
  const root = fileURLToPath(new URL('../', import.meta.url))
8
9
  const destination = process.argv[2]
@@ -23,8 +24,8 @@ try {
23
24
  await rename(join(target, 'gitignore'), join(target, '.gitignore'))
24
25
  const vendor = join(target, 'vendor')
25
26
  await mkdir(vendor)
26
- const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'
27
- const packed = JSON.parse(execFileSync(npm, ['pack', '--ignore-scripts', '--json', '--loglevel=error', '--pack-destination', vendor], { cwd: root, encoding: 'utf8' }))
27
+ const npm = npmCommand()
28
+ const packed = JSON.parse(execFileSync(npm.command, [...npm.args, 'pack', '--ignore-scripts', '--json', '--loglevel=error', '--pack-destination', vendor], { cwd: root, encoding: 'utf8' }))
28
29
  const manifest = JSON.parse(await readFile(join(target, 'package.json'), 'utf8'))
29
30
  manifest.dependencies.apptify = 'file:./vendor/' + packed[0].filename
30
31
  await writeFile(join(target, 'package.json'), JSON.stringify(manifest, null, 2) + '\n')
@@ -0,0 +1,16 @@
1
+ import { existsSync, realpathSync } from 'node:fs'
2
+ import { delimiter, dirname, join } from 'node:path'
3
+
4
+ // Run npm through Node so Windows .cmd shims never need shell quoting.
5
+ export function npmCommand(env = process.env) {
6
+ const candidates = [env.npm_execpath, join(dirname(process.execPath), 'node_modules/npm/bin/npm-cli.js')]
7
+ for (const directory of (env.PATH ?? env.Path ?? '').split(delimiter)) {
8
+ if (!directory) continue
9
+ candidates.push(join(directory, 'node_modules/npm/bin/npm-cli.js'))
10
+ const executable = join(directory, 'npm')
11
+ if (existsSync(executable)) candidates.push(realpathSync(executable))
12
+ }
13
+ const cli = candidates.find(path => path?.endsWith('npm-cli.js') && existsSync(path))
14
+ if (!cli) throw new Error('Cannot find npm-cli.js. Install npm alongside Node.js or set npm_execpath to its entry point.')
15
+ return { command: process.execPath, args: [cli] }
16
+ }
@@ -4,6 +4,7 @@ import { join, relative } from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import { createHash } from 'node:crypto'
6
6
  import { execFileSync } from 'node:child_process'
7
+ import { npmCommand } from './npm-command.mjs'
7
8
 
8
9
  const root = fileURLToPath(new URL('../', import.meta.url))
9
10
  async function sourceHashes() {
@@ -20,12 +21,12 @@ async function sourceHashes() {
20
21
  }
21
22
  }
22
23
  for (const name of ['src', 'playground', 'public', 'docs']) await directory(join(root, name))
23
- for (const name of ['README.md', 'package.json', 'index.html', 'vite.config.ts', 'vite.lib.config.ts', 'tsconfig.json', 'tsconfig.lib.json', 'tooling/create-playground.mjs', 'tooling/prepare-playground.mjs', 'tooling/prepare-package.mjs']) await record(join(root, name))
24
+ for (const name of ['README.md', 'package.json', 'index.html', 'vite.config.ts', 'vite.lib.config.ts', 'tsconfig.json', 'tsconfig.lib.json', 'tooling/create-playground.mjs', 'tooling/npm-command.mjs', 'tooling/prepare-playground.mjs', 'tooling/prepare-package.mjs']) await record(join(root, name))
24
25
  return files
25
26
  }
26
27
  const before = await sourceHashes()
27
- const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'
28
- execFileSync(npm, ['run', 'build'], { cwd: root, stdio: 'inherit' })
28
+ const npm = npmCommand()
29
+ execFileSync(npm.command, [...npm.args, 'run', 'build'], { cwd: root, stdio: 'inherit' })
29
30
  execFileSync(process.execPath, [join(root, 'tooling/prepare-playground.mjs')], { cwd: root, stdio: 'inherit' })
30
31
  assert.deepEqual(await sourceHashes(), before, 'Sources changed during the build; rerun packing to take the latest snapshot. No source file was restored.')
31
32
  const snapshotPath = join(root, 'templates/playground/source-snapshot.json')
@@ -1,13 +1,14 @@
1
1
  import assert from 'node:assert/strict'
2
- import { mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from 'node:fs/promises'
2
+ import { chmod, mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from 'node:fs/promises'
3
3
  import { join } from 'node:path'
4
4
  import { tmpdir } from 'node:os'
5
5
  import { fileURLToPath } from 'node:url'
6
6
  import { execFileSync, spawnSync } from 'node:child_process'
7
+ import { npmCommand } from './npm-command.mjs'
7
8
 
8
9
  const root = fileURLToPath(new URL('../', import.meta.url))
9
10
  const scratch = await mkdtemp(join(tmpdir(), 'create-apptify-test-'))
10
- const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'
11
+ const npm = npmCommand()
11
12
  const env = { ...process.env, npm_config_cache: join(scratch, 'cache'), npm_config_offline: 'true' }
12
13
  const installer = join(scratch, 'installer')
13
14
  const creator = join(installer, 'node_modules/create-apptify')
@@ -15,7 +16,7 @@ const library = join(installer, 'node_modules/apptify')
15
16
  const cli = join(creator, 'bin/create-apptify.mjs')
16
17
 
17
18
  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
+ const packed = JSON.parse(execFileSync(npm.command, [...npm.args, 'pack', '--ignore-scripts', '--json', '--pack-destination', scratch], { cwd, env, encoding: 'utf8' }))[0]
19
20
  await mkdir(destination, { recursive: true })
20
21
  execFileSync('tar', ['-xzf', join(scratch, packed.filename), '--strip-components=1', '-C', destination])
21
22
  return packed
@@ -27,9 +28,15 @@ try {
27
28
  const packed = await unpack(join(root, 'packages/create-apptify'), creator)
28
29
  const files = new Set(packed.files.map(file => file.path))
29
30
  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
+ const libraryPacked = await unpack(root, library)
32
+ assert(libraryPacked.files.some(file => file.path === 'tooling/npm-command.mjs'), 'Missing npm launcher in library tarball')
31
33
  await mkdir(join(installer, 'node_modules/.bin'))
32
- await symlink('../create-apptify/bin/create-apptify.mjs', join(installer, 'node_modules/.bin/create-apptify'))
34
+ if (process.platform === 'win32') {
35
+ await writeFile(join(installer, 'node_modules/.bin/create-apptify.cmd'), '@echo off\r\n"' + process.execPath + '" "%~dp0\\..\\create-apptify\\bin\\create-apptify.mjs" %*\r\n')
36
+ } else {
37
+ await chmod(cli, 0o755)
38
+ await symlink('../create-apptify/bin/create-apptify.mjs', join(installer, 'node_modules/.bin/create-apptify'))
39
+ }
33
40
  await writeFile(join(installer, 'package.json'), JSON.stringify({ private: true, dependencies: { 'create-apptify': packed.version } }) + '\n')
34
41
 
35
42
  const run = (args, cwd = installer) => spawnSync(process.execPath, [cli, ...args], { cwd, env, encoding: 'utf8' })
@@ -41,8 +48,8 @@ try {
41
48
  assert.equal(run(['--unknown']).status, 1)
42
49
  assert.equal(run(['one', 'two']).status, 1)
43
50
 
44
- const app = join(scratch, 'my app')
45
- const result = spawnSync(npm, ['create', 'apptify', '--offline', '--', app], { cwd: installer, env, encoding: 'utf8', timeout: 60_000 })
51
+ const app = join(scratch, 'my app & (demo)')
52
+ const result = spawnSync(npm.command, [...npm.args, 'create', 'apptify', '--offline', '--', app], { cwd: installer, env, encoding: 'utf8', timeout: 60_000 })
46
53
  assert.equal(result.status, 0, result.stdout + result.stderr)
47
54
  const manifest = JSON.parse(await readFile(join(app, 'package.json'), 'utf8'))
48
55
  assert.match(manifest.dependencies.apptify, /^file:\.\/vendor\/apptify-/)
@@ -62,7 +69,7 @@ try {
62
69
  assert.equal(await readFile(join(app, 'package.json'), 'utf8'), before)
63
70
  assert.equal(run([join(library, 'unsafe')]).status, 1)
64
71
  const alias = join(scratch, 'library-alias')
65
- await symlink(library, alias, 'dir')
72
+ await symlink(library, alias, process.platform === 'win32' ? 'junction' : 'dir')
66
73
  assert.equal(run([join(alias, 'unsafe')]).status, 1)
67
74
 
68
75
  const empty = join(scratch, 'existing-empty')
@@ -2,21 +2,23 @@ import assert from 'node:assert/strict'
2
2
  import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
3
3
  import { join, resolve } from 'node:path'
4
4
  import { tmpdir } from 'node:os'
5
+ import { createServer } from 'node:net'
5
6
  import { createHash } from 'node:crypto'
6
7
  import { execFileSync, spawn } from 'node:child_process'
7
8
  import { fileURLToPath } from 'node:url'
8
9
  import { chromium, expect } from '@playwright/test'
10
+ import { npmCommand } from './npm-command.mjs'
9
11
 
10
12
  const root = fileURLToPath(new URL('../', import.meta.url))
11
13
  const scratch = await mkdtemp(join(tmpdir(), 'apptify-package-'))
12
- const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'
13
- const run = (args, cwd = root) => execFileSync(npm, args, { cwd, stdio: 'inherit', timeout: 240_000 })
14
+ const npm = npmCommand()
15
+ const run = (args, cwd = root) => execFileSync(npm.command, [...npm.args, ...args], { cwd, stdio: 'inherit', timeout: 240_000 })
14
16
  console.log('Package acceptance workspace: ' + scratch)
15
17
  const archiveInput = process.argv[2] ? resolve(process.argv[2]) : undefined
16
18
  const archiveSha256 = archiveInput ? createHash('sha256').update(await readFile(archiveInput)).digest('hex') : undefined
17
19
  const packArgs = ['pack', '--ignore-scripts', '--json', '--loglevel=error', '--pack-destination', scratch]
18
20
  if (archiveInput) packArgs.push(archiveInput)
19
- const packed = JSON.parse(execFileSync(npm, packArgs, { cwd: root, encoding: 'utf8' }))[0]
21
+ const packed = JSON.parse(execFileSync(npm.command, [...npm.args, ...packArgs], { cwd: root, encoding: 'utf8' }))[0]
20
22
  const contents = new Set(packed.files.map(file => file.path))
21
23
  const snapshot = archiveInput
22
24
  ? JSON.parse(execFileSync('tar', ['-xOf', join(scratch, packed.filename), 'package/templates/playground/source-snapshot.json'], { encoding: 'utf8' }))
@@ -70,7 +72,14 @@ if (hasConsumerTypes) {
70
72
  }
71
73
 
72
74
  async function serve() {
73
- const child = spawn(npm, ['run', 'dev', '--', '--host', '127.0.0.1', '--port', '0', '--strictPort'], { cwd: app, stdio: ['ignore', 'pipe', 'pipe'], detached: process.platform !== 'win32' })
75
+ const reservation = createServer()
76
+ await new Promise((resolve, reject) => {
77
+ reservation.once('error', reject)
78
+ reservation.listen(0, '127.0.0.1', resolve)
79
+ })
80
+ const port = reservation.address().port
81
+ await new Promise((resolve, reject) => reservation.close(error => error ? reject(error) : resolve()))
82
+ const child = spawn(npm.command, [...npm.args, 'run', 'dev', '--', '--host', '127.0.0.1', '--port', String(port), '--strictPort'], { cwd: app, stdio: ['ignore', 'pipe', 'pipe'], detached: process.platform !== 'win32' })
74
83
  const stop = () => {
75
84
  if (process.platform !== 'win32') { try { process.kill(-child.pid, 'SIGTERM') } catch {} }
76
85
  else child.kill('SIGTERM')
@@ -121,7 +130,7 @@ try {
121
130
  const image = page.locator('#apple-image img').first()
122
131
  await image.scrollIntoViewIfNeeded()
123
132
  assert.equal(await image.evaluate(async image => { await image.decode(); return image.naturalWidth > 0 }), true)
124
- await page.locator('.component-index [aria-current]').first().waitFor()
133
+ await page.locator('.component-index [role=treeitem][aria-selected=true]').first().waitFor()
125
134
  })
126
135
  await checkPage(server.url, '/component-docs/apple-input', async page => {
127
136
  const frame = page.frameLocator('[data-testid="input-playground"] .vue-repl iframe')
@@ -135,8 +144,8 @@ try {
135
144
  await page.getByRole('slider', { name: '玻璃不透明度', exact: true }).waitFor()
136
145
  await page.getByRole('button', { name: '打开用户菜单', exact: true }).click()
137
146
  await page.getByRole('button', { name: /登录 Login/ }).click()
138
- await page.getByLabel('电子邮箱', { exact: true }).fill('template@example.com')
139
- await page.getByLabel('密码', { exact: true }).fill('template-password')
147
+ await page.locator('.user-auth-form').getByRole('textbox', { name: /^电子邮箱/ }).fill('template@example.com')
148
+ await page.locator('.user-auth-form').getByLabel(/^密码/).fill('template-password')
140
149
  assert.equal(await page.locator('.user-auth-form input[type=password]').count(), 1)
141
150
  await settledScreenshot(page, 'settings-login-320.png')
142
151
  })