dsh-design-studio 0.9.0 → 0.11.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/README.en.md +6 -4
- package/README.md +6 -4
- package/assets/demo.gif +0 -0
- package/assets/hero-sm.jpg +0 -0
- package/cordis.patch.yml +5 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/_shots/menu.jpg +0 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/app.css +64 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/cart.html +120 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/design.json +56 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/menu.html +200 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/order.html +135 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/tokens.css +43 -0
- package/gallery-seed/d/orderg-x__autumn-sale/08e9e40834f9/_shots/email.jpg +0 -0
- package/gallery-seed/d/orderg-x__autumn-sale/08e9e40834f9/design.json +20 -0
- package/gallery-seed/d/orderg-x__autumn-sale/08e9e40834f9/email.html +268 -0
- package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/_shots/courses.jpg +0 -0
- package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/courses.html +364 -0
- package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/design.json +49 -0
- package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/lesson.html +196 -0
- package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/tokens.css +55 -0
- package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/ui.css +255 -0
- package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/_shots/poster.jpg +0 -0
- package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/design.json +21 -0
- package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/poster.html +165 -0
- package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/tokens.css +35 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/_shots/home.jpg +0 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/app.css +155 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/booking.html +232 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/design.json +57 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/health.html +221 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/home.html +225 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/tokens.css +54 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/_shots/overview.jpg +0 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/app.css +79 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/design.json +51 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/entries.html +161 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/icons.svg +30 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/overview.html +181 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/tokens.css +50 -0
- package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/_shots/home.jpg +0 -0
- package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/book.html +279 -0
- package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/design.json +21 -0
- package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/home.html +348 -0
- package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/style.css +297 -0
- package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/tokens.css +49 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/01-cover.html +133 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/02-highlights.html +67 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/03-growth.html +89 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/04-milestones.html +92 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/05-next.html +99 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/06-thanks.html +61 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/_shots/01-cover.jpg +0 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/deck.css +67 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/design.json +25 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/tokens.css +30 -0
- package/gallery-seed/index.json +1 -0
- package/gallery-seed/thumbs/orderg-x__alley-noodles.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__autumn-sale.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__little-painter.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__moon-fair.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__paw-diary.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__pocket-ledger.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__time-bookshop.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__year-review.webp +0 -0
- package/lib/browser.js +23 -9
- package/lib/bundle.js +12 -3
- package/lib/client.js +3106 -111
- package/lib/core.js +44 -11
- package/lib/gallery/css.js +230 -0
- package/lib/gallery/html.js +313 -0
- package/lib/gallery/index.js +22 -0
- package/lib/gallery/market-routes.js +306 -0
- package/lib/gallery/market.js +623 -0
- package/lib/gallery/pack.js +311 -0
- package/lib/gallery/policy.js +1560 -0
- package/lib/gallery/qrfind.js +181 -0
- package/lib/gallery/render.js +148 -0
- package/lib/gallery/routes.js +360 -0
- package/lib/gallery/tar.js +118 -0
- package/lib/gallery/use.js +125 -0
- package/lib/gallery/zip.js +212 -0
- package/lib/handoff.js +291 -29
- package/lib/history.js +3 -1
- package/lib/index.js +118 -24
- package/lib/inspire.js +142 -0
- package/lib/offline.js +17 -0
- package/lib/picker.js +425 -32
- package/lib/serve.js +88 -11
- package/lib/works.js +238 -0
- package/locale/en.json +2 -1
- package/locale/offline/en.json +6 -0
- package/locale/offline/zh.json +6 -0
- package/locale/zh.json +2 -1
- package/package.json +10 -3
- package/skills/ui-design/SKILL.md +9 -0
package/lib/index.js
CHANGED
|
@@ -40,13 +40,19 @@
|
|
|
40
40
|
* 页面本身由 serve.js 开的独立端口提供(见那边的说明)。
|
|
41
41
|
*/
|
|
42
42
|
|
|
43
|
+
import crypto from 'node:crypto'
|
|
43
44
|
import fs from 'node:fs'
|
|
44
45
|
import path from 'node:path'
|
|
45
46
|
import { fileURLToPath } from 'node:url'
|
|
46
47
|
import os from 'node:os'
|
|
47
48
|
import { execFile } from 'node:child_process'
|
|
48
|
-
import { createHub, webOrigins, trustedOrigins, resolveInRoot, ERR_CATCHER } from './serve.js'
|
|
49
|
-
import { bundle, inlinePicker, MAX_TOTAL as BUNDLE_MAX } from './bundle.js'
|
|
49
|
+
import { createHub, webOrigins, trustedOrigins, resolveInRoot, ERR_CATCHER, defang } from './serve.js'
|
|
50
|
+
import { bundle, inlinePicker, strictMeta, MAX_TOTAL as BUNDLE_MAX } from './bundle.js'
|
|
51
|
+
import { createLibrary } from './works.js'
|
|
52
|
+
import { createInspiration } from './inspire.js'
|
|
53
|
+
import { createLibraryRoutes } from './gallery/routes.js'
|
|
54
|
+
import { createMarket } from './gallery/market.js'
|
|
55
|
+
import { createMarketRoutes } from './gallery/market-routes.js'
|
|
50
56
|
import { createTracker, parseTarget, findRule, KINDS } from './core.js'
|
|
51
57
|
import { createHistory } from './history.js'
|
|
52
58
|
import { createPhone } from './phone.js'
|
|
@@ -56,7 +62,7 @@ import { createBrowser } from './browser.js'
|
|
|
56
62
|
import { designOf, metaOfFile, sizeOf, pickScreens, measureAll, shootAll, cropIssues, formatMeasure, issueSnapshot, formatShots, whereOf, MAX_SCREENS, MAX_SIZES, MAX_SHOTS, MAX_CROPS } from './look.js'
|
|
57
63
|
import { SYSTEMS_DIR, extractSystem, listSystems, setDefault, skillSection } from './system.js'
|
|
58
64
|
import { readTweaks, applyTweaks, lintTweaks } from './tweaks.js'
|
|
59
|
-
import { FRAMEWORKS, detectProject, buildHandoff, compareScreens, implUrl, defaultDest, within } from './handoff.js'
|
|
65
|
+
import { FRAMEWORKS, detectProject, projectFor, defaultFramework, buildHandoff, compareScreens, implUrl, defaultDest, pickedDest, within } from './handoff.js'
|
|
60
66
|
|
|
61
67
|
export const name = 'dsh-design-studio'
|
|
62
68
|
export const inject = ['webServer']
|
|
@@ -82,7 +88,7 @@ const SKILL_RANK = 600 // 和 DSH 自带的技能同一档;用户 / 项目里
|
|
|
82
88
|
* 几行之间靠这个互通(同一个包里的模块 Node 只加载一次):工作台起来后放 tracker、无头浏览器(browser);
|
|
83
89
|
* 「自动打开预览」那行开着时 autoOpen > 0
|
|
84
90
|
*/
|
|
85
|
-
export const studio = { tracker: null, autoOpen: 0, check: null, browser: null, log: () => {} }
|
|
91
|
+
export const studio = { tracker: null, autoOpen: 0, offline: 0, check: null, browser: null, market: null, log: () => {} }
|
|
86
92
|
|
|
87
93
|
/** 设计技能:模型做界面设计时加载(DSH 的 skill 工具),也能让用户手动调 */
|
|
88
94
|
export const designSkill = {
|
|
@@ -439,11 +445,11 @@ export const handoffTool = {
|
|
|
439
445
|
name: 'handoff_design',
|
|
440
446
|
description:
|
|
441
447
|
'把右侧「设计工作台」里的设计(本地 HTML 那几屏)整理成交接包,准备做成真项目的代码:写好 <设计目录>/handoff/HANDOFF.md(每屏的文件、建议路由、跳转、设计变量、几屏共用该抽成组件的部分、交互和状态、调节滑块、素材、做法和验收),和每屏从上到下的截图。' +
|
|
442
|
-
'用户说「转成代码」「做成真的项目」「用 React / Vue
|
|
443
|
-
'framework:same
|
|
448
|
+
'用户说「转成代码」「做成真的项目」「用 React / Vue / uni-app 做出来」「做成小程序」时先调它,读 HANDOFF.md 照着做;每做完一屏用 compare_design 和设计稿比一遍,差异降到 3% 以下再做下一屏(原生小程序在浏览器里跑不起来,照 HANDOFF.md 说的请用户在微信开发者工具里看)。' +
|
|
449
|
+
'framework:same 跟目标目录现有项目一样(认得出框架时默认,uni-app(含 HBuilderX 建的、没有 package.json 的)、Taro、微信原生小程序也认得出)、react、vue、next、html、uniapp、weapp;dest:代码放哪个目录(不填:会话目录是项目就放进去,不是就新建 <设计名>-app)。要先用 preview_page 打开过设计。',
|
|
444
450
|
parameters: specToJsonSchema({
|
|
445
|
-
framework: { type: 'string', enum: ['same', ...Object.keys(FRAMEWORKS)], description: 'same 跟现有项目一样;react(React + Vite)、vue(Vue 3 + Vite)、next(Next.js)、html(纯 HTML / CSS
|
|
446
|
-
dest: { type: 'string', description: '
|
|
451
|
+
framework: { type: 'string', enum: ['same', ...Object.keys(FRAMEWORKS)], description: 'same 跟现有项目一样;react(React + Vite)、vue(Vue 3 + Vite)、next(Next.js)、html(纯 HTML / CSS)、uniapp(uni-app,出微信小程序和网页版)、weapp(微信原生小程序 WXML / WXSS)。不填:目标目录(或它所在的项目)认得出框架就 same,没有 package.json 的纯 HTML 站点 html,小程序的设计 uniapp,别的 react' },
|
|
452
|
+
dest: { type: 'string', description: '代码放哪:现有项目的目录(或它里面的子目录),或要新建的项目目录(绝对路径或相对会话目录)。不填:会话目录有 package.json 就放进去,没有就新建 <设计名>-app(设计目录在会话目录下就建在会话目录里,不然建在设计目录旁边)' },
|
|
447
453
|
screens: { type: 'array', items: { type: 'string' }, description: '只转哪几屏(文件名);不填全部' },
|
|
448
454
|
}),
|
|
449
455
|
output: {
|
|
@@ -469,16 +475,18 @@ export const handoffTool = {
|
|
|
469
475
|
// 先定下最终放哪(没给就和右侧栏建议的一样),再按真实路径查是不是在设计目录里
|
|
470
476
|
const dest = typeof a.dest === 'string' && a.dest.trim() ? resolvePath(a.dest, cwd) : defaultDest(t.root, cwd, detectProject(cwd))
|
|
471
477
|
if (within(dest, t.root)) return { text: `代码别放进设计目录(${t.root})里:设计稿留着做对比。换一个目录。` }
|
|
472
|
-
const project =
|
|
473
|
-
const framework = typeof a.framework === 'string' && (a.framework === 'same' || FRAMEWORKS[a.framework]) ? a.framework : project
|
|
474
|
-
if (framework === 'same' && !
|
|
478
|
+
const project = projectFor(dest)
|
|
479
|
+
const framework = typeof a.framework === 'string' && (a.framework === 'same' || FRAMEWORKS[a.framework]) ? a.framework : defaultFramework(project, kind)
|
|
480
|
+
if (framework === 'same' && !project.framework) return { text: `${dest} 里没有认得出的前端项目(package.json 里没有 React / Vue / Next / uni-app …,也不是 HBuilderX 或微信原生小程序的项目):framework 选 react、vue、next、html、uniapp 或 weapp。` }
|
|
475
481
|
let r
|
|
476
482
|
try {
|
|
477
483
|
r = await buildHandoff(studio.browser, { root: t.root, screens: pick.list, kind, size, framework, dest, project, system: systemOfDir(t.root) })
|
|
478
484
|
} catch (e) {
|
|
479
485
|
return { text: `没写成交接包:${String((e && e.message) || e).slice(0, 200)}` }
|
|
480
486
|
}
|
|
481
|
-
|
|
487
|
+
// 原生小程序跑不了网页版:比不了,照 HANDOFF.md 请用户在微信开发者工具里看
|
|
488
|
+
const native = framework === 'weapp' || (framework === 'same' && project.framework === 'weapp')
|
|
489
|
+
const head = `交接包写好了:${r.file}(${r.shots} 张截图在 handoff/shots/)。下面是全文,照「做法和验收」一屏一屏做,${native ? '每屏做完请用户在微信开发者工具里看' : '每屏做完用 compare_design 比'}。\n\n`
|
|
482
490
|
// 每屏顶上一张附上,直接看到要做成什么样
|
|
483
491
|
const firsts = r.rows.filter((row) => row.shots.length).slice(0, 4).map((row) => ({ key: row.key, jpg: fs.readFileSync(path.join(r.dir, 'shots', row.shots[0])) }))
|
|
484
492
|
const kept = firsts.length ? await keepImages(ctx, id, firsts) : {}
|
|
@@ -569,6 +577,8 @@ async function keepImages(ctx, id, list, toFile = false) {
|
|
|
569
577
|
return { files }
|
|
570
578
|
}
|
|
571
579
|
|
|
580
|
+
const VERSION = (() => { try { return JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version || '' } catch { return '' } })()
|
|
581
|
+
|
|
572
582
|
const fileStore = {
|
|
573
583
|
load: () => JSON.parse(fs.readFileSync(STORE_FILE, 'utf8')),
|
|
574
584
|
save: (data) => {
|
|
@@ -641,9 +651,16 @@ export function apply(ctx) {
|
|
|
641
651
|
const previewPorts = new Set()
|
|
642
652
|
let tracker = null
|
|
643
653
|
const history = createHistory({ base: HISTORY_DIR, log })
|
|
654
|
+
// 我的作品 + 哪些文件是别人做的(设计库来的):别人做的、没改过的页面预览时加硬规矩
|
|
655
|
+
const library = createLibrary({ log })
|
|
656
|
+
// 设计市场:目录和批次包从 npm 镜像来,验过签名、逐个文件核对过才用;市场上的页面原样预览时也加硬规矩
|
|
657
|
+
const seedDir = fileURLToPath(new URL('../gallery-seed', import.meta.url))
|
|
658
|
+
// 「设计库不联网」那行开着:市场不访问任何网址
|
|
659
|
+
const market = createMarket({ version: VERSION, log, seedDir: fs.existsSync(path.join(seedDir, 'index.json')) ? seedDir : '', online: () => studio.offline === 0 })
|
|
644
660
|
const hub = createHub({
|
|
645
661
|
log,
|
|
646
662
|
history,
|
|
663
|
+
strict: { html: (buf) => library.strictHtml(buf) || market.strictHtml(buf), root: (dir) => library.strictRoot(dir) || market.isStaging(dir), versions: (dir) => library.strictVersions(dir) },
|
|
647
664
|
// 桌面版的 dsh-app://app 是默认就认的;这里补上 DSH 网页端的地址,和 DSH 配置里声明的对外地址(反向代理 / 域名)
|
|
648
665
|
parents: () => {
|
|
649
666
|
let own = []
|
|
@@ -664,9 +681,12 @@ export function apply(ctx) {
|
|
|
664
681
|
open,
|
|
665
682
|
log,
|
|
666
683
|
history,
|
|
667
|
-
|
|
684
|
+
// 记会话的同时,在做的设计目录补进「我的作品」
|
|
685
|
+
store: { load: fileStore.load, save: (data) => { fileStore.save(data); try { library.sync(data) } catch (e) { log(e) } } },
|
|
668
686
|
alive: (origin) => hub.alive(origin),
|
|
669
687
|
autoOpen: () => studio.autoOpen > 0,
|
|
688
|
+
// 装好的别人的设计系统退回旧版:退回来的页面先记成别人做的(预览照样加规矩)
|
|
689
|
+
beforeRestore: (root, html) => { if (html.length) library.addPast(root, html) },
|
|
670
690
|
skipPorts: () => {
|
|
671
691
|
const s = new Set(previewPorts)
|
|
672
692
|
const own = ownHost(ctx).split(':')[1]
|
|
@@ -678,7 +698,11 @@ export function apply(ctx) {
|
|
|
678
698
|
ctx.effect(() => {
|
|
679
699
|
studio.tracker = tracker
|
|
680
700
|
studio.log = log
|
|
681
|
-
|
|
701
|
+
studio.market = market
|
|
702
|
+
return () => {
|
|
703
|
+
if (studio.tracker === tracker) { studio.tracker = null; studio.log = () => {} }
|
|
704
|
+
if (studio.market === market) { market.abort(); studio.market = null }
|
|
705
|
+
}
|
|
682
706
|
}, 'dsh-design-studio: tracker')
|
|
683
707
|
// check_design / look_design 用的无头浏览器:用到时才开,闲置自己关,这一行停用时关掉
|
|
684
708
|
ctx.effect(() => {
|
|
@@ -746,7 +770,33 @@ export function apply(ctx) {
|
|
|
746
770
|
const t = tracker.stateOf(session).target
|
|
747
771
|
let tweaks = null
|
|
748
772
|
if (t && t.kind === 'file') { try { tweaks = readTweaks(t.root) } catch (e) { log(e) } }
|
|
749
|
-
|
|
773
|
+
const snap = tracker.snapshot(session)
|
|
774
|
+
json(res, 200, { ...snap, strict: strictScreens(snap), phone: { on: ph.on, seen: ph.seen || 0 }, tweaks, ...(c ? { checkReq: c.id } : {}) })
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/** 正在看的几屏里,哪些是别人做的、没改过的(地址列表):右侧栏给它们的 iframe 加沙盒、不给剪贴板 */
|
|
778
|
+
const strictCache = new Map() // 文件 → { sig, strict }
|
|
779
|
+
function strictScreens(snap) {
|
|
780
|
+
const t = snap.target
|
|
781
|
+
if (!t || t.kind !== 'file' || !Array.isArray(snap.screens)) return []
|
|
782
|
+
const out = []
|
|
783
|
+
const files = new Set([...snap.screens.map((s) => s.key), snap.focus].filter(Boolean))
|
|
784
|
+
for (const key of files) {
|
|
785
|
+
const file = path.join(t.root, key)
|
|
786
|
+
let st
|
|
787
|
+
try { st = fs.statSync(file) } catch { continue }
|
|
788
|
+
const sig = `${st.mtimeMs}:${st.size}:${library.file && fs.existsSync(library.file) ? fs.statSync(library.file).mtimeMs : 0}`
|
|
789
|
+
let hit = strictCache.get(file)
|
|
790
|
+
if (!hit || hit.sig !== sig) {
|
|
791
|
+
let strict = false
|
|
792
|
+
try { strict = st.size <= 8 * 1024 * 1024 && library.strictHtml(fs.readFileSync(file)) } catch { strict = false }
|
|
793
|
+
hit = { sig, strict }
|
|
794
|
+
strictCache.set(file, hit)
|
|
795
|
+
if (strictCache.size > 500) strictCache.delete(strictCache.keys().next().value)
|
|
796
|
+
}
|
|
797
|
+
if (hit.strict) out.push(`${snap.origin}/${encodeURIComponent(key)}`)
|
|
798
|
+
}
|
|
799
|
+
return out
|
|
750
800
|
}
|
|
751
801
|
|
|
752
802
|
/** 调节滑块写回文件:values = { id: 值 } */
|
|
@@ -806,16 +856,24 @@ export function apply(ctx) {
|
|
|
806
856
|
}
|
|
807
857
|
|
|
808
858
|
/** 转成代码前:会话目录是不是个前端项目(右侧栏默认选「跟项目一样」、填好放哪) */
|
|
809
|
-
function projectRoute(req, res) {
|
|
859
|
+
async function projectRoute(req, res) {
|
|
810
860
|
if (!admit(req, res)) return
|
|
811
|
-
const
|
|
812
|
-
const
|
|
861
|
+
const q = new URL(req.url, 'http://x').searchParams
|
|
862
|
+
const session = q.get('session') || ''
|
|
863
|
+
// 新会话(还没跑过工具,比如从设计库「用这个开始」、浏览打开的)插件不知道目录:问 DSH,别拿家目录当项目找
|
|
864
|
+
const cwd = await sessionCwd(session)
|
|
813
865
|
const p = detectProject(cwd)
|
|
814
866
|
const t = tracker.stateOf(session).target
|
|
815
867
|
const root = t && t.kind === 'file' ? t.root : ''
|
|
816
868
|
// root 一起回去:右侧栏按它认这份建议是不是给现在这套设计的;project 按最终放的目录认(默认框架跟着它选)
|
|
817
|
-
|
|
818
|
-
|
|
869
|
+
// 带 dest:用户刚选了个文件夹,按它定最终放哪
|
|
870
|
+
const picked = (q.get('dest') || '').trim()
|
|
871
|
+
const pk = picked ? pickedDest(resolvePath(picked, cwd), root) : null
|
|
872
|
+
const dest = pk ? pk.dest : defaultDest(root, cwd, p)
|
|
873
|
+
const project = projectFor(dest)
|
|
874
|
+
// 设计类型跟着回去:小程序的设计右侧栏多两个选项(uni-app、原生小程序),默认 uni-app
|
|
875
|
+
const kind = root ? designFor(session).kind : ''
|
|
876
|
+
json(res, 200, { cwd, root, kind, project, fw: defaultFramework(project, kind), dest, why: pk ? pk.why : '', frameworks: Object.fromEntries(Object.entries(FRAMEWORKS).map(([k, v]) => [k, v.label])) })
|
|
819
877
|
}
|
|
820
878
|
|
|
821
879
|
async function checkRoute(req, res) {
|
|
@@ -843,7 +901,8 @@ export function apply(ctx) {
|
|
|
843
901
|
const session = String(body.session || '')
|
|
844
902
|
if (!session) return json(res, 400, { error: '缺 session' })
|
|
845
903
|
try {
|
|
846
|
-
|
|
904
|
+
// 输入框里写的相对路径按会话目录算(新会话也问 DSH 要)
|
|
905
|
+
const target = parseTarget(body.target, await sessionCwd(session))
|
|
847
906
|
await tracker.setTarget(session, target, { source: 'user', popup: false })
|
|
848
907
|
json(res, 200, tracker.snapshot(session))
|
|
849
908
|
} catch (e) {
|
|
@@ -969,9 +1028,13 @@ export function apply(ctx) {
|
|
|
969
1028
|
const page = read(key)
|
|
970
1029
|
if (!page) return json(res, 404, { error: 'no-screen' })
|
|
971
1030
|
const href = st.origin ? `${st.origin}/${v ? `__dsh_design__/v/${v}/` : ''}${key.split('/').map(encodeURIComponent).join('/')}` : ''
|
|
972
|
-
|
|
1031
|
+
// 别人做的设计(设计库来的、没改过):加硬规矩,右侧栏的沙盒也不放行表单
|
|
1032
|
+
const strict = library.strictHtml(page) || (v > 0 && library.strictVersions(t.root))
|
|
1033
|
+
const nonce = strict ? crypto.randomBytes(16).toString('base64') : ''
|
|
1034
|
+
const lead = strict ? strictMeta(nonce) + ERR_CATCHER.replace('<script>', `<script nonce="${nonce}">`) : ERR_CATCHER
|
|
1035
|
+
const html = bundle(strict ? defang(page.toString('utf8')) : page.toString('utf8'), { key, read, base: href, lead, tail: inlinePicker(fs.readFileSync(PICKER_FILE, 'utf8'), parent, href, nonce) })
|
|
973
1036
|
// 用到的文件太多就不列(null = 右侧栏不按文件筛,目录里一有改动就重新打包)
|
|
974
|
-
json(res, 200, { html, deps: deps.size > 200 ? null : [...deps] })
|
|
1037
|
+
json(res, 200, { html, deps: deps.size > 200 ? null : [...deps], strict })
|
|
975
1038
|
} catch (e) {
|
|
976
1039
|
json(res, 500, { error: e.message })
|
|
977
1040
|
}
|
|
@@ -1063,6 +1126,8 @@ export function apply(ctx) {
|
|
|
1063
1126
|
const ext = imageExt(buf)
|
|
1064
1127
|
if (!ext) return json(res, 400, { error: '不是图片' })
|
|
1065
1128
|
const st = tracker.stateOf(String(body.session || ''))
|
|
1129
|
+
// 画笔标注带着画的那份设计的目录来:这期间换了设计,别把旧图存进新设计
|
|
1130
|
+
if (typeof body.root === 'string' && !(st.target && st.target.kind === 'file' && st.target.root === body.root)) return json(res, 409, { error: 'moved' })
|
|
1066
1131
|
const dir = st.target && st.target.kind === 'file' ? path.join(st.target.root, 'refs') : REFS_DIR
|
|
1067
1132
|
const base = String(body.name || 'ref').replace(/\.[a-z0-9]{2,5}$/i, '').replace(/[^\w\u4e00-\u9fff-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60) || 'ref'
|
|
1068
1133
|
try {
|
|
@@ -1109,7 +1174,36 @@ export function apply(ctx) {
|
|
|
1109
1174
|
json(res, 200, st.on ? { ...st, qr: qrMatrix(st.url).map((row) => row.map((c) => (c ? 1 : 0)).join('')) } : st)
|
|
1110
1175
|
}
|
|
1111
1176
|
|
|
1112
|
-
|
|
1177
|
+
// 设计库:我的作品、灵感、打 zip、拖 zip 进来用
|
|
1178
|
+
const inspiration = createInspiration({ log })
|
|
1179
|
+
/**
|
|
1180
|
+
* 会话目录:跑过工具的会话插件自己记着;新会话(还没跑过工具)问 DSH——先看活着的会话,再查存下来的会话头。
|
|
1181
|
+
* 都拿不到给家目录(「用这个开始」就放到 ~/DSH 设计)
|
|
1182
|
+
*/
|
|
1183
|
+
async function sessionCwd(id) {
|
|
1184
|
+
const known = tracker.cwdOf(id)
|
|
1185
|
+
if (known && path.resolve(known) !== path.resolve(os.homedir())) return known
|
|
1186
|
+
const ok = (c) => typeof c === 'string' && path.isAbsolute(c) ? c : ''
|
|
1187
|
+
try {
|
|
1188
|
+
const ss = ctx && typeof ctx.get === 'function' ? ctx.get('sessions') : null
|
|
1189
|
+
const live = ss && typeof ss.list === 'function' ? [...ss.list()].find((s) => s && s.id === id) : null
|
|
1190
|
+
const c = live && ok(live.header && live.header.cwd)
|
|
1191
|
+
if (c) return c
|
|
1192
|
+
} catch (e) { log(e) }
|
|
1193
|
+
try {
|
|
1194
|
+
const q = ctx && typeof ctx.get === 'function' ? ctx.get('sessionQuery') : null
|
|
1195
|
+
if (q && typeof q.listSessions === 'function') {
|
|
1196
|
+
const rec = (await q.listSessions()).find((r) => r && r.header && r.header.id === id)
|
|
1197
|
+
const c = rec && ok(rec.header.cwd)
|
|
1198
|
+
if (c) return c
|
|
1199
|
+
}
|
|
1200
|
+
} catch (e) { log(e) }
|
|
1201
|
+
return known
|
|
1202
|
+
}
|
|
1203
|
+
const lib = createLibraryRoutes({ admit, readBody, json, tracker, library, inspiration, studio, open, storeLoad: fileStore.load, version: VERSION, exported, log, sessionCwd })
|
|
1204
|
+
const mk = createMarketRoutes({ admit, readBody, json, tracker, library, market, open, systemsDir: SYSTEMS_DIR, log, sessionCwd })
|
|
1205
|
+
|
|
1206
|
+
const routes = { works: lib.works, workthumb: lib.workthumb, pack: lib.pack, usezip: lib.usezip, inspire: lib.inspire, inspimg: lib.inspimg, ...mk, state, open: openRoute, view: viewRoute, close: closeRoute, history: historyRoute, restore: restoreRoute, edit: editRoute, style: styleRoute, bundle: bundleRoute, ref: refRoute, export: exportRoute, doc: docRoute, reveal: revealRoute, phone: phoneRoute, check: checkRoute, locate: locateRoute, tweak: tweakRoute, systems: systemsRoute, systhumb: systhumbRoute, project: projectRoute }
|
|
1113
1207
|
for (const [key, handler] of Object.entries(routes)) {
|
|
1114
1208
|
ctx.effect(() => ctx.webServer.register({
|
|
1115
1209
|
kind: 'exact',
|
package/lib/inspire.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 设计库 › 灵感:只存在用户电脑上,不上传、不统计
|
|
3
|
+
*
|
|
4
|
+
* <库>/inspiration.json { format: 1, items: [{ id, type, at, … }] },新的在前
|
|
5
|
+
* image { name, file, w, h } 拖进来的参考图(图在 <库>/inspiration/<file>)→「照这个做」走参考图流程
|
|
6
|
+
* url { url, name } 存下的网址 →「提取设计系统」
|
|
7
|
+
* style { name, style, mid? } 我的风格(从当前设计存下来的一组配色、字体、圆角;mid=从市场收藏的那张)→ 和内置风格卡放一起用
|
|
8
|
+
* market { mid, mtype, name } 收藏的市场设计(模板 / 设计系统):只记 id;离线看靠市场缓存,下架了界面标「已下架」
|
|
9
|
+
*
|
|
10
|
+
* 风格的字段和风格卡一样(policy.checkStyle 的白名单);本机存的不卡对比度(分享时才卡)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import crypto from 'node:crypto'
|
|
14
|
+
import fs from 'node:fs'
|
|
15
|
+
import path from 'node:path'
|
|
16
|
+
import { LIBRARY_DIR } from './works.js'
|
|
17
|
+
import { checkStyle, imageInfo } from './gallery/policy.js'
|
|
18
|
+
import { ID_RE } from './gallery/market.js'
|
|
19
|
+
|
|
20
|
+
const MAX_ITEMS = 300
|
|
21
|
+
const MAX_IMAGE = 6 * 1024 * 1024
|
|
22
|
+
const IMG_EXT = { png: 'png', jpg: 'jpg', webp: 'webp', gif: 'gif', avif: 'avif' }
|
|
23
|
+
const oneLine = (s, n) => [...String(s == null ? '' : s).replace(/[\x00-\x1f\x7f\u2028\u2029]+/g, ' ').replace(/\s+/g, ' ').trim()].slice(0, n).join('')
|
|
24
|
+
|
|
25
|
+
export function createInspiration({ dir = LIBRARY_DIR, log = () => {}, now = Date.now } = {}) {
|
|
26
|
+
const file = path.join(dir, 'inspiration.json')
|
|
27
|
+
const imgDir = path.join(dir, 'inspiration')
|
|
28
|
+
|
|
29
|
+
function load() {
|
|
30
|
+
try {
|
|
31
|
+
const raw = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
32
|
+
if (raw && Array.isArray(raw.items)) return { format: 1, items: raw.items.filter((x) => x && typeof x.id === 'string' && /^[a-z0-9]{8,32}$/.test(x.id)) }
|
|
33
|
+
} catch (e) {
|
|
34
|
+
if (e.code !== 'ENOENT') {
|
|
35
|
+
log(e)
|
|
36
|
+
try { fs.copyFileSync(file, `${file}.bad-${now()}`) } catch { /* 算了 */ }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { format: 1, items: [] }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function save(data) {
|
|
43
|
+
// 太多了:最旧的图片 / 网址先删(我的风格、收藏留着)
|
|
44
|
+
while (data.items.length > MAX_ITEMS) {
|
|
45
|
+
let i = data.items.length - 1
|
|
46
|
+
while (i >= 0 && (data.items[i].type === 'style' || data.items[i].type === 'market')) i--
|
|
47
|
+
if (i < 0) break
|
|
48
|
+
const [gone] = data.items.splice(i, 1)
|
|
49
|
+
if (gone.type === 'image' && gone.file) try { fs.rmSync(path.join(imgDir, gone.file), { force: true }) } catch { /* 算了 */ }
|
|
50
|
+
}
|
|
51
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
52
|
+
const tmp = `${file}.${process.pid}.tmp`
|
|
53
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 1), { mode: 0o600 })
|
|
54
|
+
fs.renameSync(tmp, file)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const newId = () => crypto.randomBytes(8).toString('hex')
|
|
58
|
+
|
|
59
|
+
function add(item) {
|
|
60
|
+
const data = load()
|
|
61
|
+
const it = { id: newId(), at: now(), ...item }
|
|
62
|
+
data.items.unshift(it)
|
|
63
|
+
save(data)
|
|
64
|
+
return it
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 参考图:data 是图片的 Buffer */
|
|
68
|
+
function addImage({ name, data }) {
|
|
69
|
+
if (!Buffer.isBuffer(data) || !data.length) throw new Error('不是图片')
|
|
70
|
+
if (data.length > MAX_IMAGE) throw new Error('图片太大(最多 6MB)')
|
|
71
|
+
const info = imageInfo(data)
|
|
72
|
+
const ext = info && IMG_EXT[info.type]
|
|
73
|
+
if (!ext) throw new Error('不是图片')
|
|
74
|
+
const id = newId()
|
|
75
|
+
fs.mkdirSync(imgDir, { recursive: true })
|
|
76
|
+
fs.writeFileSync(path.join(imgDir, `${id}.${ext}`), data, { mode: 0o600 })
|
|
77
|
+
const data2 = load()
|
|
78
|
+
const it = { id, at: now(), type: 'image', name: oneLine(name, 60) || 'image', file: `${id}.${ext}`, w: info.w, h: info.h }
|
|
79
|
+
data2.items.unshift(it)
|
|
80
|
+
save(data2)
|
|
81
|
+
return it
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 网址:只收 http(s),带不带协议都行 */
|
|
85
|
+
function addUrl({ url, name }) {
|
|
86
|
+
let s = String(url || '').trim()
|
|
87
|
+
if (!/^https?:\/\//i.test(s)) s = 'https://' + s
|
|
88
|
+
let u
|
|
89
|
+
try { u = new URL(s) } catch { throw new Error('不是网址') }
|
|
90
|
+
if (!/^https?:$/.test(u.protocol) || !u.hostname.includes('.') || s.length > 500) throw new Error('不是网址')
|
|
91
|
+
const data = load()
|
|
92
|
+
const href = u.href
|
|
93
|
+
const old = data.items.find((x) => x.type === 'url' && x.url === href)
|
|
94
|
+
if (old) return old
|
|
95
|
+
return add({ type: 'url', url: href, name: oneLine(name, 60) || u.hostname })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 我的风格:字段按风格卡的白名单查;对比度本机不卡 */
|
|
99
|
+
function addStyle({ name, style, mid }) {
|
|
100
|
+
const problems = checkStyle(style).filter((p) => p.code !== 'contrast')
|
|
101
|
+
if (problems.length) throw new Error(`风格不对:${problems.map((p) => p.x).join('、')}`)
|
|
102
|
+
const s = {}
|
|
103
|
+
for (const k of ['c', 'head', 'w', 'radius', 'shadow', 'nzh', 'nen', 'dzh', 'den', 'bgImage']) if (style[k] !== undefined) s[k] = style[k]
|
|
104
|
+
// 从市场收藏的:同一张只存一份
|
|
105
|
+
const from = typeof mid === 'string' && ID_RE.test(mid) ? mid : ''
|
|
106
|
+
if (from) {
|
|
107
|
+
const old = load().items.find((x) => x.type === 'style' && x.mid === from)
|
|
108
|
+
if (old) return old
|
|
109
|
+
}
|
|
110
|
+
return add({ type: 'style', name: oneLine(name, 24) || '我的风格', style: s, ...(from ? { mid: from } : {}) })
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 收藏市场上的一套:同一套只记一次 */
|
|
114
|
+
function addMarket({ mid, mtype, name }) {
|
|
115
|
+
const id = String(mid || '')
|
|
116
|
+
if (!ID_RE.test(id) || (mtype !== 'template' && mtype !== 'system')) throw new Error('不是市场上的设计')
|
|
117
|
+
const data = load()
|
|
118
|
+
const old = data.items.find((x) => x.type === 'market' && x.mid === id)
|
|
119
|
+
if (old) return old
|
|
120
|
+
return add({ type: 'market', mid: id, mtype, name: oneLine(name, 40) || id.split('__')[1] })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function remove(id) {
|
|
124
|
+
const data = load()
|
|
125
|
+
const i = data.items.findIndex((x) => x.id === id)
|
|
126
|
+
if (i < 0) return false
|
|
127
|
+
const [gone] = data.items.splice(i, 1)
|
|
128
|
+
if (gone.type === 'image' && gone.file) try { fs.rmSync(path.join(imgDir, path.basename(gone.file)), { force: true }) } catch { /* 算了 */ }
|
|
129
|
+
save(data)
|
|
130
|
+
return true
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** 图片文件的绝对路径(只认登记过的) */
|
|
134
|
+
function imageFile(id) {
|
|
135
|
+
const it = load().items.find((x) => x.id === id && x.type === 'image')
|
|
136
|
+
if (!it || !it.file || path.basename(it.file) !== it.file) return null
|
|
137
|
+
const p = path.join(imgDir, it.file)
|
|
138
|
+
return fs.existsSync(p) ? p : null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { list: () => load().items, addImage, addUrl, addStyle, addMarket, remove, imageFile, file }
|
|
142
|
+
}
|
package/lib/offline.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-design-studio/offline:设计库不联网(默认关)。
|
|
3
|
+
* 打开后设计库的「市场」一个网址都不访问,只显示已经下载过的;「我的作品」「灵感」照常用。
|
|
4
|
+
* 默认关、反着来:老版本升级上来插件页里还没有这一行时,市场照常联网
|
|
5
|
+
*/
|
|
6
|
+
import { studio } from './index.js'
|
|
7
|
+
|
|
8
|
+
export const name = 'dsh-design-studio-offline'
|
|
9
|
+
|
|
10
|
+
export function apply(ctx) {
|
|
11
|
+
ctx.effect(() => {
|
|
12
|
+
studio.offline++
|
|
13
|
+
// 正在下载的目录 / 批次包马上掐掉(不等下完)
|
|
14
|
+
try { if (studio.market) studio.market.abort() } catch { /* 市场还没起来 */ }
|
|
15
|
+
return () => { studio.offline-- }
|
|
16
|
+
}, 'dsh-design-studio: gallery offline')
|
|
17
|
+
}
|