@proveanything/smartlinks 2.0.33 → 2.0.34
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/dist/docs/API_SUMMARY.md +1 -1
- package/docs/API_SUMMARY.md +1 -1
- package/package.json +1 -1
- package/scripts/publish.mjs +147 -99
package/dist/docs/API_SUMMARY.md
CHANGED
package/docs/API_SUMMARY.md
CHANGED
package/package.json
CHANGED
package/scripts/publish.mjs
CHANGED
|
@@ -1,99 +1,147 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* publish — the FAST dev publish for a SmartLinks app. Builds nothing itself: it takes an already-
|
|
4
|
-
* built `dist/`, uploads it to the SmartLinks publish endpoint, which writes it straight to the CDN
|
|
5
|
-
* (dev = no-store, instantly fresh) and registers the release. One call = host + register, no Cloud
|
|
6
|
-
* Build, no Lovable. Works from anywhere: local, Claude, CI, or a build step.
|
|
7
|
-
*
|
|
8
|
-
* smartlinks-publish # build dist/ yourself first, then this
|
|
9
|
-
* smartlinks-publish --watch # re-publish on file change (save → live)
|
|
10
|
-
*
|
|
11
|
-
* Env:
|
|
12
|
-
* SMARTLINKS_APP_ID the platform app id (required)
|
|
13
|
-
* SMARTLINKS_DEPLOY_KEY channel-scoped deploy key (dev key is dev-only, safe to keep locally)
|
|
14
|
-
* SMARTLINKS_API API host (default https://smartlinks.app)
|
|
15
|
-
* SMARTLINKS_CHANNEL channel (default dev; only dev is supported by this fast path today)
|
|
16
|
-
* Flags: --dir <dist> --watch --channel <dev>
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { readFileSync, readdirSync, statSync, watch } from 'node:fs'
|
|
20
|
-
import { join, relative, sep } from 'node:path'
|
|
21
|
-
|
|
22
|
-
const args = process.argv.slice(2)
|
|
23
|
-
const flag = (name, def) => { const i = args.indexOf(name); return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : true) : def }
|
|
24
|
-
|
|
25
|
-
const API = process.env.SMARTLINKS_API || 'https://smartlinks.app'
|
|
26
|
-
const KEY = process.env.SMARTLINKS_DEPLOY_KEY
|
|
27
|
-
const APP_ID = process.env.SMARTLINKS_APP_ID
|
|
28
|
-
const CHANNEL = flag('--channel', process.env.SMARTLINKS_CHANNEL || 'dev')
|
|
29
|
-
const DIR = flag('--dir', 'dist')
|
|
30
|
-
const WATCH = args.includes('--watch')
|
|
31
|
-
|
|
32
|
-
// Mirror of the server's write allowlist — a bundle is static web assets only.
|
|
33
|
-
const ALLOWED = new Set(['.js', '.mjs', '.css', '.json', '.map', '.wasm', '.html', '.txt',
|
|
34
|
-
'.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.ico', '.woff', '.woff2', '.ttf', '.otf'])
|
|
35
|
-
const extOf = (p) => { const i = p.lastIndexOf('.'); return i < 0 ? '' : p.slice(i).toLowerCase() }
|
|
36
|
-
|
|
37
|
-
function fail(msg) { console.error(`❌ ${msg}`); process.exit(1) }
|
|
38
|
-
if (!APP_ID) fail('SMARTLINKS_APP_ID is required')
|
|
39
|
-
if (!KEY) fail('SMARTLINKS_DEPLOY_KEY is required')
|
|
40
|
-
|
|
41
|
-
function collectFiles(dir) {
|
|
42
|
-
const out = {}
|
|
43
|
-
const walk = (d) => {
|
|
44
|
-
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
45
|
-
const full = join(d, e.name)
|
|
46
|
-
if (e.isDirectory()) { walk(full); continue }
|
|
47
|
-
if (!ALLOWED.has(extOf(e.name))) continue
|
|
48
|
-
const rel = relative(dir, full).split(sep).join('/') // forward slashes for the bucket
|
|
49
|
-
out[rel] = readFileSync(full)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
walk(dir)
|
|
53
|
-
return out
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* publish — the FAST dev publish for a SmartLinks app. Builds nothing itself: it takes an already-
|
|
4
|
+
* built `dist/`, uploads it to the SmartLinks publish endpoint, which writes it straight to the CDN
|
|
5
|
+
* (dev = no-store, instantly fresh) and registers the release. One call = host + register, no Cloud
|
|
6
|
+
* Build, no Lovable. Works from anywhere: local, Claude, CI, or a build step.
|
|
7
|
+
*
|
|
8
|
+
* smartlinks-publish # build dist/ yourself first, then this
|
|
9
|
+
* smartlinks-publish --watch # re-publish on file change (save → live)
|
|
10
|
+
*
|
|
11
|
+
* Env:
|
|
12
|
+
* SMARTLINKS_APP_ID the platform app id (required)
|
|
13
|
+
* SMARTLINKS_DEPLOY_KEY channel-scoped deploy key (dev key is dev-only, safe to keep locally)
|
|
14
|
+
* SMARTLINKS_API API host (default https://smartlinks.app)
|
|
15
|
+
* SMARTLINKS_CHANNEL channel (default dev; only dev is supported by this fast path today)
|
|
16
|
+
* Flags: --dir <dist> --watch --channel <dev>
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync, readdirSync, statSync, watch } from 'node:fs'
|
|
20
|
+
import { join, relative, sep } from 'node:path'
|
|
21
|
+
|
|
22
|
+
const args = process.argv.slice(2)
|
|
23
|
+
const flag = (name, def) => { const i = args.indexOf(name); return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : true) : def }
|
|
24
|
+
|
|
25
|
+
const API = process.env.SMARTLINKS_API || 'https://smartlinks.app'
|
|
26
|
+
const KEY = process.env.SMARTLINKS_DEPLOY_KEY
|
|
27
|
+
const APP_ID = process.env.SMARTLINKS_APP_ID
|
|
28
|
+
const CHANNEL = flag('--channel', process.env.SMARTLINKS_CHANNEL || 'dev')
|
|
29
|
+
const DIR = flag('--dir', 'dist')
|
|
30
|
+
const WATCH = args.includes('--watch')
|
|
31
|
+
|
|
32
|
+
// Mirror of the server's write allowlist — a bundle is static web assets only.
|
|
33
|
+
const ALLOWED = new Set(['.js', '.mjs', '.css', '.json', '.map', '.wasm', '.html', '.txt',
|
|
34
|
+
'.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.ico', '.woff', '.woff2', '.ttf', '.otf'])
|
|
35
|
+
const extOf = (p) => { const i = p.lastIndexOf('.'); return i < 0 ? '' : p.slice(i).toLowerCase() }
|
|
36
|
+
|
|
37
|
+
function fail(msg) { console.error(`❌ ${msg}`); process.exit(1) }
|
|
38
|
+
if (!APP_ID) fail('SMARTLINKS_APP_ID is required')
|
|
39
|
+
if (!KEY) fail('SMARTLINKS_DEPLOY_KEY is required')
|
|
40
|
+
|
|
41
|
+
function collectFiles(dir) {
|
|
42
|
+
const out = {}
|
|
43
|
+
const walk = (d) => {
|
|
44
|
+
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
45
|
+
const full = join(d, e.name)
|
|
46
|
+
if (e.isDirectory()) { walk(full); continue }
|
|
47
|
+
if (!ALLOWED.has(extOf(e.name))) continue
|
|
48
|
+
const rel = relative(dir, full).split(sep).join('/') // forward slashes for the bucket
|
|
49
|
+
out[rel] = readFileSync(full)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
walk(dir)
|
|
53
|
+
return out
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// One request carries at most this much file content (decoded). The server caps a request at 8 MB of
|
|
57
|
+
// content and ~10 MB of JSON; base64 adds a third, so 6 MB keeps every request comfortably under both.
|
|
58
|
+
const BATCH_BYTES = 6 * 1024 * 1024
|
|
59
|
+
const BATCH_FILES = 150
|
|
60
|
+
|
|
61
|
+
async function post(path, body) {
|
|
62
|
+
const res = await fetch(`${API}/api/v1/apps/${APP_ID}${path}`, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: { 'content-type': 'application/json', 'x-smartlinks-deploy-key': KEY },
|
|
65
|
+
body: JSON.stringify(body),
|
|
66
|
+
}).catch((e) => ({ ok: false, status: 0, json: async () => ({ errors: [{ message: `request failed — ${e.message}` }] }) }))
|
|
67
|
+
const b = await res.json().catch(() => ({}))
|
|
68
|
+
return { res, b }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function reportFailure(res, b) {
|
|
72
|
+
console.error(`❌ publish failed (${res.status}):`)
|
|
73
|
+
for (const e of b.errors || []) console.error(` • ${e.path || ''}: ${e.message}`)
|
|
74
|
+
if (!b.errors && (b.message || b.error)) console.error(` • ${b.message || b.error}`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const toB64 = (entries) => Object.fromEntries(entries.map(([p, buf]) => [p, buf.toString('base64')]))
|
|
78
|
+
|
|
79
|
+
async function publishOnce() {
|
|
80
|
+
let stat
|
|
81
|
+
try { stat = statSync(DIR) } catch { fail(`build dir not found: ${DIR} (build first)`) }
|
|
82
|
+
if (!stat.isDirectory()) fail(`${DIR} is not a directory`)
|
|
83
|
+
|
|
84
|
+
const files = collectFiles(DIR)
|
|
85
|
+
if (!files['app.manifest.json']) fail(`${DIR}/app.manifest.json not found — is this a built app bundle?`)
|
|
86
|
+
|
|
87
|
+
const t0 = Date.now()
|
|
88
|
+
const entries = Object.entries(files)
|
|
89
|
+
const totalBytes = entries.reduce((n, [, buf]) => n + buf.length, 0)
|
|
90
|
+
const qs = `?channel=${encodeURIComponent(CHANNEL)}`
|
|
91
|
+
let finalBody
|
|
92
|
+
|
|
93
|
+
if (totalBytes <= BATCH_BYTES && entries.length <= BATCH_FILES) {
|
|
94
|
+
finalBody = { files: toB64(entries) } // small bundle: one request, as before
|
|
95
|
+
} else {
|
|
96
|
+
// Large bundle: upload everything except the manifest in batches, then send the manifest last —
|
|
97
|
+
// the server only registers the release once every referenced file is confirmed uploaded.
|
|
98
|
+
const rest = entries.filter(([p]) => p !== 'app.manifest.json').sort((a, b) => b[1].length - a[1].length)
|
|
99
|
+
const batches = []
|
|
100
|
+
let cur = [], curBytes = 0
|
|
101
|
+
for (const e of rest) {
|
|
102
|
+
if (cur.length && (curBytes + e[1].length > BATCH_BYTES || cur.length >= BATCH_FILES)) { batches.push(cur); cur = []; curBytes = 0 }
|
|
103
|
+
if (e[1].length > BATCH_BYTES) console.warn(` ⚠︎ ${e[0]} is ${(e[1].length / 1048576).toFixed(1)} MB — larger than one request allows; the server will likely reject it`)
|
|
104
|
+
cur.push(e); curBytes += e[1].length
|
|
105
|
+
}
|
|
106
|
+
if (cur.length) batches.push(cur)
|
|
107
|
+
console.log(`📦 ${APP_ID}: ${(totalBytes / 1048576).toFixed(1)} MB in ${entries.length} files — uploading in ${batches.length} batch(es)`)
|
|
108
|
+
for (let i = 0; i < batches.length; i++) {
|
|
109
|
+
const { res, b } = await post(`/publish/files${qs}`, { files: toB64(batches[i]) })
|
|
110
|
+
if (!res.ok || !b.ok) {
|
|
111
|
+
reportFailure(res, b)
|
|
112
|
+
if (WATCH) return false
|
|
113
|
+
process.exit(1)
|
|
114
|
+
}
|
|
115
|
+
console.log(` ↑ batch ${i + 1}/${batches.length} (${batches[i].length} files)`)
|
|
116
|
+
}
|
|
117
|
+
finalBody = { files: toB64([['app.manifest.json', files['app.manifest.json']]]), uploaded: rest.map(([p]) => p) }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const { res, b } = await post(`/publish${qs}`, finalBody)
|
|
121
|
+
if (!res.ok || !b.ok) {
|
|
122
|
+
reportFailure(res, b)
|
|
123
|
+
if (WATCH) return false // keep watching; don't exit
|
|
124
|
+
process.exit(1)
|
|
125
|
+
}
|
|
126
|
+
for (const w of b.warnings || []) console.warn(` ⚠︎ ${w.path}: ${w.message}`)
|
|
127
|
+
console.log(`✅ ${APP_ID}@${b.version} → ${CHANNEL} (${entries.length} files, ${Date.now() - t0}ms) — functions: ${(b.functions || []).join(', ') || 'none'}`)
|
|
128
|
+
console.log(` ${b.bundleBaseUrl}`)
|
|
129
|
+
return true
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!WATCH) {
|
|
133
|
+
await publishOnce()
|
|
134
|
+
} else {
|
|
135
|
+
console.log(`👀 watching ${DIR} — publishing ${APP_ID} to ${CHANNEL} on change (Ctrl+C to stop)`)
|
|
136
|
+
await publishOnce()
|
|
137
|
+
let timer = null
|
|
138
|
+
let publishing = false
|
|
139
|
+
watch(DIR, { recursive: true }, () => {
|
|
140
|
+
clearTimeout(timer)
|
|
141
|
+
timer = setTimeout(async () => {
|
|
142
|
+
if (publishing) return
|
|
143
|
+
publishing = true
|
|
144
|
+
try { await publishOnce() } finally { publishing = false }
|
|
145
|
+
}, 300) // debounce a burst of file writes from one build
|
|
146
|
+
})
|
|
147
|
+
}
|