apptify 0.2.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.
Files changed (73) hide show
  1. package/README.md +40 -239
  2. package/dist/apptify.css +1 -1
  3. package/dist/apptify.js +4153 -2177
  4. package/dist/components/button.d.ts +1 -1
  5. package/dist/components/content.d.ts +73 -362
  6. package/dist/components/date-picker.d.ts +10 -0
  7. package/dist/components/forms.d.ts +77 -18
  8. package/dist/components/foundation.d.ts +2061 -111
  9. package/dist/components/gallery-scrollbar.d.ts +46 -0
  10. package/dist/components/mobile-image-viewer.d.ts +166 -0
  11. package/dist/components/navibar.d.ts +80 -0
  12. package/dist/components/overlays.d.ts +127 -158
  13. package/dist/components/tabs.d.ts +117 -0
  14. package/dist/core/context.d.ts +573 -9
  15. package/dist/core/desktop-image-enter.d.ts +6 -0
  16. package/dist/core/desktop-image-return.d.ts +1 -0
  17. package/dist/core/device.d.ts +5 -0
  18. package/dist/core/image-gallery-motion.d.ts +2 -0
  19. package/dist/core/image-geometry.d.ts +28 -0
  20. package/dist/core/image-motion.d.ts +18 -0
  21. package/dist/core/layers.d.ts +2 -0
  22. package/dist/core/motion.d.ts +2 -0
  23. package/dist/core/popup-motion.d.ts +12 -0
  24. package/dist/core/popup-placement.d.ts +31 -0
  25. package/dist/core/preview-navigation.d.ts +5 -0
  26. package/dist/index.d.ts +1214 -339
  27. package/docs/COMPONENTS.md +18 -16
  28. package/docs/PLAYGROUND-TEMPLATE.md +51 -0
  29. package/docs/VALIDATION.md +25 -1
  30. package/package.json +35 -7
  31. package/templates/playground/README.md +21 -0
  32. package/templates/playground/gitignore +3 -0
  33. package/templates/playground/index.html +26 -0
  34. package/templates/playground/package.json +26 -0
  35. package/templates/playground/playground/App.vue +73 -0
  36. package/templates/playground/playground/ComponentDemo.vue +234 -0
  37. package/templates/playground/playground/catalog.ts +80 -0
  38. package/templates/playground/playground/components/ComponentIndex.vue +62 -0
  39. package/templates/playground/playground/components/PageFooter.vue +3 -0
  40. package/templates/playground/playground/components/UserMenu.vue +132 -0
  41. package/templates/playground/playground/components/not-found-paper-scene.vue +210 -0
  42. package/templates/playground/playground/components/not-found-paper-story.ts +149 -0
  43. package/templates/playground/playground/env.d.ts +1 -0
  44. package/templates/playground/playground/main.ts +8 -0
  45. package/templates/playground/playground/router/index.ts +24 -0
  46. package/templates/playground/playground/style.css +228 -0
  47. package/templates/playground/playground/views/[...all].vue +68 -0
  48. package/templates/playground/playground/views/components.vue +126 -0
  49. package/templates/playground/playground/views/index.vue +75 -0
  50. package/templates/playground/playground/views/settings.vue +98 -0
  51. package/templates/playground/public/images/SOURCES.md +21 -0
  52. package/templates/playground/public/images/airpods-max-orange.jpg +0 -0
  53. package/templates/playground/public/images/architecture.jpg +0 -0
  54. package/templates/playground/public/images/city.jpg +0 -0
  55. package/templates/playground/public/images/coast.jpg +0 -0
  56. package/templates/playground/public/images/desert.jpg +0 -0
  57. package/templates/playground/public/images/food.jpg +0 -0
  58. package/templates/playground/public/images/forest.jpg +0 -0
  59. package/templates/playground/public/images/lake.jpg +0 -0
  60. package/templates/playground/public/images/mountains.jpg +0 -0
  61. package/templates/playground/public/images/snow.jpg +0 -0
  62. package/templates/playground/public/images/sunset.jpg +0 -0
  63. package/templates/playground/public/images/waterfall.jpg +0 -0
  64. package/templates/playground/source-snapshot.json +111 -0
  65. package/templates/playground/tsconfig.json +24 -0
  66. package/templates/playground/vite.config.ts +9 -0
  67. package/tooling/create-playground.mjs +35 -0
  68. package/tooling/prepare-package.mjs +35 -0
  69. package/tooling/prepare-playground.mjs +103 -0
  70. package/tooling/test-create-apptify.mjs +76 -0
  71. package/tooling/test-playground-package.mjs +188 -0
  72. package/tooling/vite.d.ts +5 -0
  73. package/tooling/vite.js +10 -0
@@ -0,0 +1,188 @@
1
+ import assert from 'node:assert/strict'
2
+ import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
3
+ import { join, resolve } from 'node:path'
4
+ import { tmpdir } from 'node:os'
5
+ import { createHash } from 'node:crypto'
6
+ import { execFileSync, spawn } from 'node:child_process'
7
+ import { fileURLToPath } from 'node:url'
8
+ import { chromium } from '@playwright/test'
9
+
10
+ const root = fileURLToPath(new URL('../', import.meta.url))
11
+ 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
+ console.log('Package acceptance workspace: ' + scratch)
15
+ const archiveInput = process.argv[2] ? resolve(process.argv[2]) : undefined
16
+ const archiveSha256 = archiveInput ? createHash('sha256').update(await readFile(archiveInput)).digest('hex') : undefined
17
+ const packArgs = ['pack', '--ignore-scripts', '--json', '--loglevel=error', '--pack-destination', scratch]
18
+ if (archiveInput) packArgs.push(archiveInput)
19
+ const packed = JSON.parse(execFileSync(npm, packArgs, { cwd: root, encoding: 'utf8' }))[0]
20
+ const contents = new Set(packed.files.map(file => file.path))
21
+ const snapshot = archiveInput
22
+ ? JSON.parse(execFileSync('tar', ['-xOf', join(scratch, packed.filename), 'package/templates/playground/source-snapshot.json'], { encoding: 'utf8' }))
23
+ : JSON.parse(await readFile(join(root, 'templates/playground/source-snapshot.json'), 'utf8'))
24
+ for (const path of Object.keys(snapshot.files)) assert(contents.has('templates/playground/' + path), 'Missing template file: ' + path)
25
+ for (const path of ['package.json', 'vite.config.ts', 'tsconfig.json', 'README.md', 'gitignore', 'source-snapshot.json']) {
26
+ assert(contents.has('templates/playground/' + path), 'Missing template configuration: ' + path)
27
+ }
28
+ assert(contents.has('tooling/create-playground.mjs'))
29
+ assert(contents.has('dist/apptify.js'))
30
+ assert(contents.has('dist/apptify.css'))
31
+ console.log('Pack manifest includes every playground source and public resource (' + Object.keys(snapshot.files).length + ' files).')
32
+
33
+ const installer = join(scratch, 'installer')
34
+ await mkdir(installer)
35
+ await writeFile(join(installer, 'package.json'), '{"name":"template-installer","private":true,"type":"module"}\n')
36
+ run(['install', '--ignore-scripts', '--no-audit', '--no-fund', join(scratch, packed.filename)], installer)
37
+ const app = join(scratch, 'editable-app')
38
+ const cli = join(installer, 'node_modules/apptify/tooling/create-playground.mjs')
39
+ run(['exec', '--offline', '--', 'apptify-playground', app], installer)
40
+ let refused = false
41
+ try { execFileSync(process.execPath, [cli, app], { cwd: installer, stdio: 'pipe' }) } catch { refused = true }
42
+ assert(refused, 'Copy command must not overwrite a nonempty directory')
43
+ // Remove the installation used to invoke the command: the copied app must be portable.
44
+ await rm(installer, { recursive: true, force: true })
45
+ const appManifest = JSON.parse(await readFile(join(app, 'package.json'), 'utf8'))
46
+ assert(appManifest.dependencies.apptify.startsWith('file:./vendor/'))
47
+ assert((await readFile(join(app, '.gitignore'), 'utf8')).includes('node_modules/'))
48
+ for (const [path, hash] of Object.entries(snapshot.files)) {
49
+ if (path.startsWith('public/')) assert.equal(createHash('sha256').update(await readFile(join(app, path))).digest('hex'), hash)
50
+ }
51
+ run(['install', '--no-audit', '--no-fund'], app)
52
+ run(['run', 'typecheck'], app)
53
+ run(['run', 'build'], app)
54
+
55
+ // Verify the packed library's original consumer, without repository aliases.
56
+ const consumer = join(app, 'library-consumer')
57
+ await cp(join(root, 'tests/consumer'), consumer, { recursive: true })
58
+ await writeFile(join(consumer, 'vite.config.ts'), [
59
+ "import { defineConfig } from 'vite'",
60
+ "import vue from '@vitejs/plugin-vue'",
61
+ "import apptifyRoutes from 'apptify/vite'",
62
+ "import { fileURLToPath } from 'node:url'",
63
+ "export default defineConfig({ root: fileURLToPath(new URL('.', import.meta.url)), plugins: [apptifyRoutes(), vue()], build: { outDir: 'dist' } })", '',
64
+ ].join('\n'))
65
+ execFileSync(process.execPath, [join(app, 'node_modules/vite/bin/vite.js'), 'build', '--config', join(consumer, 'vite.config.ts')], { cwd: app, stdio: 'inherit' })
66
+ const hasConsumerTypes = await readFile(join(consumer, 'image-api-types.ts')).then(() => true).catch(error => { if (error.code === 'ENOENT') return false; throw error })
67
+ if (hasConsumerTypes) {
68
+ await writeFile(join(consumer, 'tsconfig.json'), JSON.stringify({ extends: '../tsconfig.json', include: ['image-api-types.ts'] }, null, 2) + '\n')
69
+ execFileSync(process.execPath, [join(app, 'node_modules/vue-tsc/bin/vue-tsc.js'), '--noEmit', '-p', join(consumer, 'tsconfig.json')], { cwd: app, stdio: 'inherit' })
70
+ }
71
+
72
+ 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' })
74
+ const stop = () => {
75
+ if (process.platform !== 'win32') { try { process.kill(-child.pid, 'SIGTERM') } catch {} }
76
+ else child.kill('SIGTERM')
77
+ }
78
+ let output = ''
79
+ const url = await new Promise((resolve, reject) => {
80
+ const timer = setTimeout(() => reject(new Error('Dev server did not start: ' + output)), 20_000)
81
+ const collect = chunk => {
82
+ output += chunk
83
+ const match = output.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '').match(/http:\/\/127\.0\.0\.1:\d+\//)
84
+ if (match) { clearTimeout(timer); resolve(match[0]) }
85
+ }
86
+ child.stdout.on('data', collect)
87
+ child.stderr.on('data', collect)
88
+ child.once('exit', code => { clearTimeout(timer); reject(new Error('Dev server exited ' + code + ': ' + output)) })
89
+ }).catch(error => { stop(); throw error })
90
+ return { url, stop }
91
+ }
92
+
93
+ const browser = await chromium.launch({ channel: 'chrome' })
94
+ const errors = []
95
+ async function settledScreenshot(page, name) {
96
+ await page.evaluate(async () => { await new Promise(requestAnimationFrame); await new Promise(requestAnimationFrame) })
97
+ await page.waitForFunction(() => !document.getAnimations().some(animation => animation.playState === 'running' && Number.isFinite(Number(animation.effect?.getComputedTiming().endTime))))
98
+ await page.screenshot({ path: join(scratch, name) })
99
+ }
100
+ async function checkPage(baseURL, path, check) {
101
+ const page = await browser.newPage()
102
+ page.on('pageerror', error => errors.push(error.message))
103
+ await page.goto(new URL(path, baseURL).href)
104
+ await check(page)
105
+ assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), 'Page overflow: ' + path)
106
+ await page.close()
107
+ }
108
+ let server
109
+ try {
110
+ server = await serve()
111
+ await checkPage(server.url, '/', async page => {
112
+ await page.locator('.site-nav').waitFor()
113
+ await page.locator('main').waitFor()
114
+ await page.locator('.site-nav').getByRole('link', { name: '设置', exact: true }).click()
115
+ await page.waitForURL(new URL('/settings', server.url).href)
116
+ await page.goBack()
117
+ await page.waitForURL(server.url)
118
+ await settledScreenshot(page, 'complete-home.png')
119
+ })
120
+ await checkPage(server.url, '/components', async page => {
121
+ const image = page.locator('#apple-image img').first()
122
+ await image.scrollIntoViewIfNeeded()
123
+ 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()
125
+ })
126
+ await checkPage(server.url, '/settings', async page => {
127
+ await page.setViewportSize({ width: 320, height: 900 })
128
+ await page.getByRole('button', { name: '深色', exact: true }).click()
129
+ await page.getByRole('slider', { name: '玻璃不透明度', exact: true }).waitFor()
130
+ await page.getByRole('button', { name: '打开用户菜单', exact: true }).click()
131
+ await page.getByRole('button', { name: /登录 Login/ }).click()
132
+ await page.getByLabel('电子邮箱', { exact: true }).fill('template@example.com')
133
+ await page.getByLabel('密码', { exact: true }).fill('template-password')
134
+ assert.equal(await page.locator('.user-auth-form input[type=password]').count(), 1)
135
+ await settledScreenshot(page, 'settings-login-320.png')
136
+ })
137
+ await checkPage(server.url, '/template-missing-page', async page => {
138
+ await page.emulateMedia({ reducedMotion: 'reduce' })
139
+ await page.getByRole('heading', { name: '页面不存在', exact: true }).waitFor()
140
+ await page.locator('.paper-scene__art').waitFor()
141
+ await settledScreenshot(page, '404-current.png')
142
+ await page.getByRole('link', { name: '返回首页', exact: true }).click()
143
+ await page.waitForURL(server.url)
144
+ })
145
+ for (const path of Object.keys(snapshot.files).filter(path => path.startsWith('public/'))) {
146
+ const response = await fetch(new URL(path.slice('public/'.length), server.url))
147
+ assert.equal(response.status, 200, 'Missing served resource: ' + path)
148
+ assert.equal(createHash('sha256').update(Buffer.from(await response.arrayBuffer())).digest('hex'), snapshot.files[path])
149
+ }
150
+ server.stop()
151
+ server = undefined
152
+
153
+ await rm(join(app, 'playground/views/index.vue'))
154
+ await rm(join(app, 'playground/views/components.vue'))
155
+ // These files only served the removed overview page. Deleting them must be optional.
156
+ await rm(join(app, 'playground/ComponentDemo.vue'))
157
+ await rm(join(app, 'playground/catalog.ts'))
158
+ await rm(join(app, 'playground/components/ComponentIndex.vue'))
159
+ run(['run', 'typecheck'], app)
160
+ run(['run', 'build'], app)
161
+ server = await serve()
162
+ await checkPage(server.url, '/settings', async page => {
163
+ assert.equal(await page.locator('.site-nav a.apple-navibar__item').count(), 1)
164
+ assert.equal(await page.locator('.site-nav .brand').getAttribute('href'), '/settings')
165
+ await page.getByRole('button', { name: '打开用户菜单', exact: true }).click()
166
+ await page.getByRole('dialog', { name: '用户菜单', exact: true }).waitFor()
167
+ await settledScreenshot(page, 'reduced-settings.png')
168
+ })
169
+ await checkPage(server.url, '/new-business-page', async page => {
170
+ await page.getByRole('heading', { name: '页面不存在', exact: true }).waitFor()
171
+ assert.equal(await page.getByRole('link', { name: '返回首页', exact: true }).getAttribute('href'), '/settings')
172
+ await page.getByRole('link', { name: '返回首页', exact: true }).click()
173
+ await page.waitForURL(new URL('/settings', server.url).href)
174
+ })
175
+ assert.deepEqual(errors, [], 'Browser runtime errors')
176
+ } finally {
177
+ server?.stop()
178
+ await browser.close()
179
+ }
180
+ for (const [path, hash] of Object.entries(snapshot.files)) {
181
+ assert.equal(createHash('sha256').update(await readFile(join(root, path))).digest('hex'), hash, 'Source changed during packing; rerun for its latest snapshot: ' + path)
182
+ }
183
+ for (const [path, hash] of Object.entries(snapshot.buildSources || {})) {
184
+ assert.equal(createHash('sha256').update(await readFile(join(root, path))).digest('hex'), hash, 'Build source changed after this archive was created; pack the latest version: ' + path)
185
+ }
186
+ if (archiveInput) assert.equal(createHash('sha256').update(await readFile(archiveInput)).digest('hex'), archiveSha256, 'Archive was overwritten during acceptance')
187
+ await writeFile(join(scratch, 'acceptance.json'), JSON.stringify({ package: packed.filename, archiveInput, archiveSha256, packageBytes: packed.size, packageFileCount: packed.files.length, templateSourceFiles: Object.keys(snapshot.files).length, completeTemplate: true, removedDemoPages: true, libraryConsumer: true, libraryConsumerTypes: hasConsumerTypes, browserErrors: errors, nodeVersion: process.version, sourceSnapshot: snapshot }, null, 2) + '\n')
188
+ console.log('Complete editable template, reduced app, routes/resources/account/settings/404, and packed library consumer all passed.\nEvidence: ' + scratch)
@@ -0,0 +1,5 @@
1
+ import type { Plugin } from 'vite'
2
+ import type { UserOptions } from 'vite-plugin-pages'
3
+
4
+ /** Generates Vue routes from src/views by default. */
5
+ export default function apptifyRoutes(options?: Omit<UserOptions, 'resolver'>): Plugin
@@ -0,0 +1,10 @@
1
+ import Pages from 'vite-plugin-pages'
2
+
3
+ /** File routing with the same src/views convention as vuetify-project. */
4
+ export default function apptifyRoutes(options = {}) {
5
+ return Pages({
6
+ dirs: [{ dir: 'src/views', baseRoute: '' }],
7
+ ...options,
8
+ resolver: 'vue',
9
+ })
10
+ }