dsh-design-studio 0.9.0 → 0.10.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 +2 -1
- package/README.md +2 -1
- package/lib/client.js +1660 -81
- package/lib/core.js +22 -9
- package/lib/handoff.js +119 -11
- package/lib/index.js +16 -9
- package/lib/picker.js +392 -32
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +2 -2
package/lib/core.js
CHANGED
|
@@ -109,7 +109,10 @@ export function fromBashPath(p, platform = process.platform) {
|
|
|
109
109
|
* @throws Error 带中文说明
|
|
110
110
|
*/
|
|
111
111
|
export function parseTarget(input, cwd = os.homedir()) {
|
|
112
|
-
|
|
112
|
+
let s = String(input || '').trim()
|
|
113
|
+
// Windows「复制为路径」会带一对引号:"C:\a\b.html"
|
|
114
|
+
const q = /^(["'“])(.*)(["'”])$/.exec(s)
|
|
115
|
+
if (q && (q[1] === q[3] || (q[1] === '“' && q[3] === '”'))) s = q[2].trim()
|
|
113
116
|
if (!s) throw new Error('地址是空的')
|
|
114
117
|
if (/^\d{2,5}$/.test(s)) return proxyTarget(Number(s), '/')
|
|
115
118
|
const m = LOCAL_URL.exec(s)
|
|
@@ -118,21 +121,31 @@ export function parseTarget(input, cwd = os.homedir()) {
|
|
|
118
121
|
let p = null
|
|
119
122
|
// 标准的 file:// 地址交给 Node 转(Windows 上是 file:///C:/…,直接去掉前缀会变成 /C:/…)
|
|
120
123
|
if (/^file:\/\//i.test(s)) try { p = fileURLToPath(s) } catch { /* file://~/… 之类的写法,按下面的老办法 */ }
|
|
124
|
+
const full = (x) => {
|
|
125
|
+
x = fromBashPath(x)
|
|
126
|
+
if (x === '~' || x.startsWith('~/') || x.startsWith('~\\')) x = path.join(os.homedir(), x.slice(1))
|
|
127
|
+
return path.resolve(cwd, x)
|
|
128
|
+
}
|
|
121
129
|
if (p === null) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
130
|
+
const raw = s.replace(/^file:\/\//i, '')
|
|
131
|
+
p = raw
|
|
132
|
+
// %20 之类按网址解开;但文件名里本来就带 %(「浏览…」选的、拖进来的都是原样路径)且原样存在时,别解
|
|
133
|
+
if (raw.includes('%') && !fs.existsSync(full(raw))) {
|
|
134
|
+
try { p = decodeURIComponent(raw) } catch { /* 原样用 */ }
|
|
125
135
|
}
|
|
126
136
|
}
|
|
127
|
-
p =
|
|
128
|
-
if (p === '~' || p.startsWith('~/') || p.startsWith('~\\')) p = path.join(os.homedir(), p.slice(1))
|
|
129
|
-
p = path.resolve(cwd, p)
|
|
137
|
+
p = full(p)
|
|
130
138
|
let st
|
|
131
139
|
try { st = fs.statSync(p) } catch { throw new Error(`找不到:${p}`) }
|
|
132
140
|
if (st.isDirectory()) {
|
|
141
|
+
// 有 index.html 开它;没有就开最先做的那屏(设计目录一屏一个文件,画布上会把同目录的几屏都排出来)
|
|
133
142
|
const index = path.join(p, 'index.html')
|
|
134
|
-
|
|
135
|
-
|
|
143
|
+
const first = fs.existsSync(index) ? 'index.html' : listScreens(p)[0]
|
|
144
|
+
if (!first) {
|
|
145
|
+
try { fs.readdirSync(p) } catch (e) { throw new Error(`读不了这个文件夹(${e.code === 'EACCES' || e.code === 'EPERM' ? '没有权限' : e.code || e.message}):${p}`) }
|
|
146
|
+
throw new Error(`这个文件夹里没有 HTML 文件:${p}`)
|
|
147
|
+
}
|
|
148
|
+
p = path.join(p, first)
|
|
136
149
|
} else if (!HTML_EXT.test(p)) {
|
|
137
150
|
throw new Error(`只能预览 .html 文件:${p}`)
|
|
138
151
|
}
|
package/lib/handoff.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import fs from 'node:fs'
|
|
13
|
+
import os from 'node:os'
|
|
13
14
|
import path from 'node:path'
|
|
14
15
|
import { pathToFileURL } from 'node:url'
|
|
15
16
|
import { prepare } from './look.js'
|
|
@@ -28,24 +29,75 @@ export const FRAMEWORKS = {
|
|
|
28
29
|
html: { label: '纯 HTML / CSS(不用框架)', create: '', router: '' },
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
const FW_DEPS = [['next', 'next', 'Next.js'], ['nuxt', 'vue', 'Nuxt'], ['@sveltejs/kit', 'svelte', 'SvelteKit'], ['svelte', 'svelte', 'Svelte'], ['astro', 'astro', 'Astro'], ['@angular/core', 'angular', 'Angular'], ['solid-js', 'solid', 'Solid'], ['vue', 'vue', 'Vue'], ['react', 'react', 'React']]
|
|
32
|
+
const FW_DEPS = [['next', 'next', 'Next.js'], ['nuxt', 'vue', 'Nuxt'], ['@sveltejs/kit', 'svelte', 'SvelteKit'], ['svelte', 'svelte', 'Svelte'], ['astro', 'astro', 'Astro'], ['@angular/core', 'angular', 'Angular'], ['solid-js', 'solid', 'Solid'], ['@builder.io/qwik', 'qwik', 'Qwik'], ['ember-source', 'ember', 'Ember'], ['preact', 'preact', 'Preact'], ['lit', 'lit', 'Lit'], ['vue', 'vue', 'Vue'], ['react-native', 'react-native', 'React Native'], ['react', 'react', 'React']]
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
// 文件夹里只有这些就算空的
|
|
35
|
+
const JUNK = /^\.(DS_Store|git|gitignore)$|^(Thumbs\.db|desktop\.ini)$/i
|
|
36
|
+
|
|
37
|
+
/** 文件夹里有没有正经东西(不存在 / 读不了算没有) */
|
|
38
|
+
function hasStuff(dir) {
|
|
39
|
+
try { return fs.readdirSync(dir).some((f) => !JUNK.test(f)) } catch { return false }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 有这些就算网页入口(没有 package.json 的纯 HTML 站点也算个项目)
|
|
43
|
+
const HTML_ENTRIES = ['index.html', 'src/index.html', 'public/index.html', 'app/index.html']
|
|
44
|
+
|
|
45
|
+
/** workspaces 列表里有本目录以外的(只写 '.' 的不算 monorepo;'!' 开头是排除) */
|
|
46
|
+
const listsOthers = (xs) => xs.some((x) => typeof x === 'string' && !/^!|^\.\/?$/.test(x.trim()))
|
|
47
|
+
|
|
48
|
+
/** 读文本:去掉 BOM、换行统一成 \n(Windows 检出的文件常是 CRLF);读不了返回 null */
|
|
49
|
+
function readText(file) {
|
|
50
|
+
try { return fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n') } catch { return null }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 像是 monorepo 根目录:package.json 的 workspaces、lerna / rush / Nx 集成式(根上没有 project.json)、pnpm-workspace.yaml 的 packages 里列了别的包(只用来提醒模型放进子包,不拿来改放哪) */
|
|
54
|
+
function monoRoot(dir, pkg) {
|
|
55
|
+
const ws = pkg && pkg.workspaces
|
|
56
|
+
const list = Array.isArray(ws) ? ws : ws && Array.isArray(ws.packages) ? ws.packages : null
|
|
57
|
+
if (list && listsOthers(list)) return true
|
|
58
|
+
const has = (f) => fs.existsSync(path.join(dir, f))
|
|
59
|
+
if (has('lerna.json') || has('rush.json') || (has('nx.json') && !has('project.json'))) return true
|
|
60
|
+
let y = readText(path.join(dir, 'pnpm-workspace.yaml'))
|
|
61
|
+
if (y === null) return false
|
|
62
|
+
y = y.replace(/#.*$/gm, '')
|
|
63
|
+
// packages: ['a/*', 'b'] 或者下面一行一个 - a/*
|
|
64
|
+
const m = y.match(/^packages[ \t]*:(.*(?:\n(?:[ \t]+.*|-.*|[ \t]*))*)/m)
|
|
65
|
+
return !!m && listsOthers([...m[1].matchAll(/['"]?([^'"\s,[\]-][^'"\s,[\]]*)['"]?/g)].map((x) => x[1]))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 目录是不是个项目、用的什么:{ dir, pkg, site(有 index.html 入口), empty(不存在或没正经东西), mono(像 monorepo 根目录), name, framework, label, tailwind, ts, pm }
|
|
70
|
+
* 后端、monorepo 根目录、只用 CDN 的纯 HTML 站点也都算项目:是不是前端、页面该放进哪个子包,交接说明里让写代码的模型看清结构自己定(猜不准)
|
|
71
|
+
*/
|
|
34
72
|
export function detectProject(dir) {
|
|
35
|
-
const out = { dir, pkg: false, name: path.basename(dir || ''), framework: '', label: '', tailwind: false, ts: false, pm: 'npm' }
|
|
36
|
-
|
|
37
|
-
|
|
73
|
+
const out = { dir, pkg: false, site: false, empty: true, mono: false, name: path.basename(dir || ''), framework: '', label: '', tailwind: false, ts: false, pm: 'npm' }
|
|
74
|
+
out.site = !!dir && HTML_ENTRIES.some((f) => fs.existsSync(path.join(dir, f)))
|
|
75
|
+
out.empty = !dir || !hasStuff(dir)
|
|
76
|
+
let pkg = null
|
|
77
|
+
try { pkg = dir ? JSON.parse(readText(path.join(dir, 'package.json'))) : null } catch { /* 没有或写坏了 */ }
|
|
78
|
+
if (!pkg || typeof pkg !== 'object' || Array.isArray(pkg)) return out
|
|
38
79
|
out.pkg = true
|
|
39
80
|
if (typeof pkg.name === 'string' && pkg.name) out.name = pkg.name
|
|
40
81
|
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) }
|
|
41
82
|
const hit = FW_DEPS.find(([d]) => deps[d])
|
|
42
83
|
if (hit) { out.framework = hit[1]; out.label = hit[2] + (hit[0] === 'react' && deps.vite ? ' + Vite' : hit[0] === 'vue' && deps.vite ? ' + Vite' : '') }
|
|
43
84
|
out.tailwind = !!deps.tailwindcss
|
|
85
|
+
out.mono = monoRoot(dir, pkg)
|
|
44
86
|
out.ts = !!deps.typescript || fs.existsSync(path.join(dir, 'tsconfig.json'))
|
|
45
87
|
out.pm = fs.existsSync(path.join(dir, 'pnpm-lock.yaml')) ? 'pnpm' : fs.existsSync(path.join(dir, 'yarn.lock')) ? 'yarn' : fs.existsSync(path.join(dir, 'bun.lockb')) || fs.existsSync(path.join(dir, 'bun.lock')) ? 'bun' : 'npm'
|
|
46
88
|
return out
|
|
47
89
|
}
|
|
48
90
|
|
|
91
|
+
/** 是个项目(不用往里面再套一层):有 package.json、index.html 入口或 .git(后端、monorepo 根目录、刚建只有 README 的仓库都算) */
|
|
92
|
+
function isProject(dir) {
|
|
93
|
+
return ['package.json', '.git', ...HTML_ENTRIES].some((f) => fs.existsSync(path.join(dir, f)))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 不指定框架时用什么:认得出的项目就跟它一样,纯 HTML 站点(没有 package.json)就纯 HTML,别的用 React(右侧栏和 handoff_design 都按这个) */
|
|
97
|
+
export function defaultFramework(project) {
|
|
98
|
+
return project && project.pkg && project.framework ? 'same' : project && project.site && !project.pkg ? 'html' : 'react'
|
|
99
|
+
}
|
|
100
|
+
|
|
49
101
|
/** 真实路径:不存在的部分接在最近一级存在的上级目录的真实路径后面(新建的目录也能按真实位置比) */
|
|
50
102
|
export function realish(p) {
|
|
51
103
|
let cur = path.resolve(p)
|
|
@@ -70,16 +122,56 @@ export function within(p, root) {
|
|
|
70
122
|
}
|
|
71
123
|
|
|
72
124
|
/**
|
|
73
|
-
*
|
|
125
|
+
* 代码放进 dest 时算哪个项目:dest 自己就是项目就用它;不是、但已经在某个项目里面(比如选了它的 src/pages)→ 用往上最近的那个
|
|
126
|
+
* (往上找到家目录为止,家目录本身不算:有人在家目录放 package.json;还不存在的目录不往上找:那是要新建的项目)
|
|
127
|
+
* dest 里有 index.html 时:上面的项目认得出框架才算它的(Angular、webpack 项目的 src/index.html);
|
|
128
|
+
* 认不出的(Node 库仓库里的 docs/ 静态站点)还是当纯 HTML 站点
|
|
129
|
+
*/
|
|
130
|
+
export function projectFor(dest) {
|
|
131
|
+
const own = detectProject(dest)
|
|
132
|
+
if (own.pkg || !fs.existsSync(dest)) return own
|
|
133
|
+
const home = fold(realish(os.homedir()))
|
|
134
|
+
for (let cur = path.dirname(realish(dest)); fold(cur) !== home && path.dirname(cur) !== cur; cur = path.dirname(cur)) {
|
|
135
|
+
if (!fs.existsSync(path.join(cur, 'package.json'))) continue
|
|
136
|
+
const p = detectProject(cur)
|
|
137
|
+
if (p.pkg) return own.site && !p.framework ? own : p
|
|
138
|
+
}
|
|
139
|
+
return own
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 代码默认放哪:会话目录是个项目(有 package.json)、又不在设计目录里 → 就放会话目录;
|
|
74
144
|
* 否则新建 <设计名>-app:设计目录在会话目录下就建在会话目录里,不然建在设计目录旁边(别塞进设计目录)
|
|
75
145
|
*/
|
|
76
146
|
export function defaultDest(root, cwd, project) {
|
|
77
147
|
const inside = !!root && within(cwd, root)
|
|
78
148
|
if (project && project.pkg && !inside) return cwd
|
|
79
|
-
const base = (root ? path.basename(root) : 'app').replace(/[^\w-]+/g, '-').replace(/^-+|-+$/g, '').toLowerCase() || 'app'
|
|
80
|
-
const name = base === 'app' || base.endsWith('-app') ? `${base}-code` : `${base}-app`
|
|
81
149
|
const under = !!root && !inside && within(root, cwd)
|
|
82
|
-
return path.join(root && !under ? path.dirname(root) : cwd,
|
|
150
|
+
return path.join(root && !under ? path.dirname(root) : cwd, appDirName(root))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 新建的代码目录叫什么:<设计名>-app */
|
|
154
|
+
export function appDirName(root) {
|
|
155
|
+
const base = (root ? path.basename(root) : 'app').replace(/[^\w-]+/g, '-').replace(/^-+|-+$/g, '').toLowerCase() || 'app'
|
|
156
|
+
return base === 'app' || base.endsWith('-app') ? `${base}-code` : `${base}-app`
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 用户在右侧栏选了个文件夹 → { dest, why }:
|
|
161
|
+
* 是个项目(有 package.json、index.html 或 .git,后端、monorepo 根目录也算)、在某个项目里面(比如它的 src/pages)、空文件夹、还不存在、
|
|
162
|
+
* 或者本来就叫 <设计名>-app(上回放过的)→ 就放这里(why 空);
|
|
163
|
+
* 别的有东西的文件夹(比如 ~/Projects)→ 在它里面新建 <设计名>-app(why 'nested');
|
|
164
|
+
* 选到设计目录里面了 → 放到设计目录旁边(why 'beside':设计稿要留着做对比,handoff_design 也不收设计目录里的地方)
|
|
165
|
+
*/
|
|
166
|
+
export function pickedDest(dir, root) {
|
|
167
|
+
const name = appDirName(root)
|
|
168
|
+
const beside = { dest: path.join(path.dirname(root || dir), name), why: 'beside' }
|
|
169
|
+
if (root && within(dir, root)) return beside
|
|
170
|
+
let out = { dest: dir, why: '' }
|
|
171
|
+
if (!isProject(dir) && !projectFor(dir).pkg && path.basename(dir).toLowerCase() !== name) {
|
|
172
|
+
if (hasStuff(dir)) out = { dest: path.join(dir, name), why: 'nested' } // 还不存在 / 读不了 / 空的:就用它
|
|
173
|
+
}
|
|
174
|
+
return root && within(out.dest, root) ? beside : out
|
|
83
175
|
}
|
|
84
176
|
|
|
85
177
|
/** 文件名 → 建议路由:index / home → /,别的 /名字 */
|
|
@@ -193,7 +285,11 @@ export function handoffMd({ root, rows, kind, size, framework, dest, project, sy
|
|
|
193
285
|
lines.push(`> 给写代码的模型看:照这份把设计稿做成真页面。设计稿就是源码——每屏一个 HTML,样式在 tokens.css 等样式表里。先读源文件,截图只用来对照,别凭截图猜数值。`, '')
|
|
194
286
|
lines.push('## 做什么', '')
|
|
195
287
|
lines.push(`- 设计目录:\`${root}\`(${rows.length} 屏${more ? `,另有 ${more} 屏没列进来` : ''},${kind} ${size[0]}×${size[1]})`)
|
|
288
|
+
// 目录里已经有别的东西、又不是 Node 项目(别的语言的仓库、后端……):别在原地或旁边跑脚手架
|
|
289
|
+
// (纯 HTML 站点再做成纯 HTML 不算:就加在这个站点里,下面第 1 步提醒别覆盖现有页面)
|
|
290
|
+
const occupied = !!(project && !project.pkg && project.empty === false) && !(project.site && framework === 'html')
|
|
196
291
|
if (project && project.pkg) lines.push(`- 做到:\`${dest || project.dir}\`(现有项目 ${project.name}:${project.label || '没认出框架'}${project.tailwind ? ' + Tailwind' : ''}${project.ts ? ',TypeScript' : ''},包管理 ${project.pm})`)
|
|
292
|
+
else if (occupied) lines.push(`- 做到:\`${dest}\`(目录里已经有别的东西,不是 Node 项目:先看清结构,前端放进它里面新的子目录)`)
|
|
197
293
|
else if (fw && fw.create) lines.push(`- 做到:\`${dest}\`(新建项目:\`${fw.create.replace('{name}', path.basename(dest || name + '-app'))}\`,在 \`${path.dirname(dest || root)}\` 里跑)`)
|
|
198
294
|
else lines.push(`- 做到:\`${dest}\``)
|
|
199
295
|
lines.push(`- 框架:${fwLabel}`)
|
|
@@ -292,9 +388,21 @@ export function handoffMd({ root, rows, kind, size, framework, dest, project, sy
|
|
|
292
388
|
lines.push(assets.length ? `- ${assets.slice(0, 30).map((a) => `\`${a}\``).join('、')}${assets.length > 30 ? ' …' : ''}:照原样复制进项目的静态资源目录(public/ 或 src/assets/)` : '- 没有单独的图片、字体文件(图形都写在 HTML / SVG 里)。', '')
|
|
293
389
|
|
|
294
390
|
// 做法
|
|
295
|
-
|
|
391
|
+
// 在认不出前端框架的项目(后端)或 monorepo 里选了个空的新文件夹:就在这里新建前端项目,不用再看结构、另套一层
|
|
392
|
+
const fresh = !!(project && project.pkg && (!project.framework || project.mono) && dest && fold(realish(dest)) !== fold(realish(project.dir)) && !hasStuff(dest))
|
|
393
|
+
// 「跟项目一样」新建子包时照项目的框架;装依赖用项目的包管理器(create-next-app 自己装依赖,换成对应的 --use-xxx)
|
|
394
|
+
// 只认得出是普通 React / Vue / Next.js 时才套现成的脚手架命令(Nuxt 也算 vue,但不能用 Vue + Vite 的命令搭)
|
|
395
|
+
const PLAIN = { react: 'React', vue: 'Vue', next: 'Next.js' }
|
|
396
|
+
const fwNew = fw || (fresh && same && PLAIN[project.framework] === String(project.label || '').replace(/ \+ Vite$/, '') ? FRAMEWORKS[project.framework] : null)
|
|
397
|
+
const pm = fresh && project.pm && project.pm !== 'npm' ? project.pm : ''
|
|
398
|
+
const create = (!(project && project.pkg) || fresh) && !occupied && fwNew && fwNew.create ? fwNew.create.replace('{name}', path.basename(dest || name + '-app')).replace('--use-npm', pm ? `--use-${pm}` : '--use-npm') : ''
|
|
296
399
|
lines.push('## 做法和验收', '')
|
|
297
|
-
|
|
400
|
+
// 现有项目但没认出前端框架(后端、monorepo 根目录、别的语言的仓库)/ 像 monorepo 根目录:页面该放哪让模型看清结构再定,别直接往根目录塞
|
|
401
|
+
const bare = !create && framework !== 'html' && framework !== 'same' && !!(project && ((project.pkg && !project.framework) || occupied))
|
|
402
|
+
const sub = fw && fw.create ? fw.create.replace('{name}', 'web') : ''
|
|
403
|
+
const mono = !create && !fresh && !!(project && project.mono)
|
|
404
|
+
const keep = project && project.empty === false ? ';目录里已经有文件:别覆盖现有的页面,重名的先问用户' : ''
|
|
405
|
+
lines.push(`1. 搭骨架:${create ? `新建项目 \`${create}\`${fresh ? `(在 \`${path.dirname(dest)}\` 里跑)` : ''},${pm ? `用 ${pm} ` : ''}装好依赖${fwNew.router ? `和 ${fwNew.router}` : ''}` : framework === 'html' ? `整理成一个静态站点目录(每屏一个页面,公共样式一份)${keep}` : fresh ? `在这个空文件夹里新建一个 ${fwLabel.replace('(跟项目一样)', '')} 子项目,照仓库里别的子包怎么搭的来(monorepo 记得加进 workspace)${pm ? `,用 ${pm} 装依赖` : ''}` : bare ? `这个目录里已经有东西,但没认出前端框架(可能是后端、monorepo 根目录或别的语言的仓库):先看清它的结构和用途再定放哪——适合直接加页面就加;不适合就在里面另建一个前端(monorepo 放进 apps/ 之类的子包,别的放进新的子目录${sub ? `,比如在里面跑 \`${sub}\`` : ''}),用 ${fw ? fw.label : framework} 搭,别打乱现有代码` : `在现有项目里加页面和路由(看清它现有的目录结构、样式写法、组件库再动手)`}${mono && !bare ? '(这像是 monorepo 根目录:先看根目录和 apps/ 之类的子包里有没有现成的前端应用,有就加进去;没有再新建一个子包,别把页面散放在根目录)' : ''};把 tokens.css${sheets.length ? '(和公共样式表)' : ''}搬进去。`)
|
|
298
406
|
lines.push('2. 一屏一个页面,路由照上面的表;先抽几屏共用的组件,再拼页面。')
|
|
299
407
|
lines.push('3. 结构、文案、数字、图标照设计稿的 HTML 搬;class 名可以改成项目的习惯,样式值(间距、字号、颜色、圆角、阴影)一律用设计变量。')
|
|
300
408
|
lines.push('4. 每做完一屏:把开发服务器跑起来,调 `compare_design`(screen 填设计文件名,url 填这一屏在开发服务器上的地址),照差异图把不一样的地方改掉,直到差异在 3% 以内,或者剩下的差别说得出理由(比如真实数据、系统字体)。')
|
package/lib/index.js
CHANGED
|
@@ -56,7 +56,7 @@ import { createBrowser } from './browser.js'
|
|
|
56
56
|
import { designOf, metaOfFile, sizeOf, pickScreens, measureAll, shootAll, cropIssues, formatMeasure, issueSnapshot, formatShots, whereOf, MAX_SCREENS, MAX_SIZES, MAX_SHOTS, MAX_CROPS } from './look.js'
|
|
57
57
|
import { SYSTEMS_DIR, extractSystem, listSystems, setDefault, skillSection } from './system.js'
|
|
58
58
|
import { readTweaks, applyTweaks, lintTweaks } from './tweaks.js'
|
|
59
|
-
import { FRAMEWORKS, detectProject, buildHandoff, compareScreens, implUrl, defaultDest, within } from './handoff.js'
|
|
59
|
+
import { FRAMEWORKS, detectProject, projectFor, defaultFramework, buildHandoff, compareScreens, implUrl, defaultDest, pickedDest, within } from './handoff.js'
|
|
60
60
|
|
|
61
61
|
export const name = 'dsh-design-studio'
|
|
62
62
|
export const inject = ['webServer']
|
|
@@ -440,10 +440,10 @@ export const handoffTool = {
|
|
|
440
440
|
description:
|
|
441
441
|
'把右侧「设计工作台」里的设计(本地 HTML 那几屏)整理成交接包,准备做成真项目的代码:写好 <设计目录>/handoff/HANDOFF.md(每屏的文件、建议路由、跳转、设计变量、几屏共用该抽成组件的部分、交互和状态、调节滑块、素材、做法和验收),和每屏从上到下的截图。' +
|
|
442
442
|
'用户说「转成代码」「做成真的项目」「用 React / Vue 做出来」时先调它,读 HANDOFF.md 照着做;每做完一屏用 compare_design 和设计稿比一遍,差异降到 3% 以下再做下一屏。' +
|
|
443
|
-
'framework:same
|
|
443
|
+
'framework:same 跟目标目录现有项目一样(认得出框架时默认)、react、vue、next、html;dest:代码放哪个目录(不填:会话目录是项目就放进去,不是就新建 <设计名>-app)。要先用 preview_page 打开过设计。',
|
|
444
444
|
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: '
|
|
445
|
+
framework: { type: 'string', enum: ['same', ...Object.keys(FRAMEWORKS)], description: 'same 跟现有项目一样;react(React + Vite)、vue(Vue 3 + Vite)、next(Next.js)、html(纯 HTML / CSS)。不填:目标目录(或它所在的项目)认得出框架就 same,没有 package.json 的纯 HTML 站点 html,别的 react' },
|
|
446
|
+
dest: { type: 'string', description: '代码放哪:现有项目的目录(或它里面的子目录),或要新建的项目目录(绝对路径或相对会话目录)。不填:会话目录有 package.json 就放进去,没有就新建 <设计名>-app(设计目录在会话目录下就建在会话目录里,不然建在设计目录旁边)' },
|
|
447
447
|
screens: { type: 'array', items: { type: 'string' }, description: '只转哪几屏(文件名);不填全部' },
|
|
448
448
|
}),
|
|
449
449
|
output: {
|
|
@@ -469,8 +469,8 @@ export const handoffTool = {
|
|
|
469
469
|
// 先定下最终放哪(没给就和右侧栏建议的一样),再按真实路径查是不是在设计目录里
|
|
470
470
|
const dest = typeof a.dest === 'string' && a.dest.trim() ? resolvePath(a.dest, cwd) : defaultDest(t.root, cwd, detectProject(cwd))
|
|
471
471
|
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
|
|
472
|
+
const project = projectFor(dest)
|
|
473
|
+
const framework = typeof a.framework === 'string' && (a.framework === 'same' || FRAMEWORKS[a.framework]) ? a.framework : defaultFramework(project)
|
|
474
474
|
if (framework === 'same' && !(project.pkg && project.framework)) return { text: `${dest} 里没有认得出的前端项目(package.json 里没有 React / Vue / Next …):framework 选 react、vue、next 或 html。` }
|
|
475
475
|
let r
|
|
476
476
|
try {
|
|
@@ -808,14 +808,19 @@ export function apply(ctx) {
|
|
|
808
808
|
/** 转成代码前:会话目录是不是个前端项目(右侧栏默认选「跟项目一样」、填好放哪) */
|
|
809
809
|
function projectRoute(req, res) {
|
|
810
810
|
if (!admit(req, res)) return
|
|
811
|
-
const
|
|
811
|
+
const q = new URL(req.url, 'http://x').searchParams
|
|
812
|
+
const session = q.get('session') || ''
|
|
812
813
|
const cwd = tracker.cwdOf(session)
|
|
813
814
|
const p = detectProject(cwd)
|
|
814
815
|
const t = tracker.stateOf(session).target
|
|
815
816
|
const root = t && t.kind === 'file' ? t.root : ''
|
|
816
817
|
// root 一起回去:右侧栏按它认这份建议是不是给现在这套设计的;project 按最终放的目录认(默认框架跟着它选)
|
|
817
|
-
|
|
818
|
-
|
|
818
|
+
// 带 dest:用户刚选了个文件夹,按它定最终放哪
|
|
819
|
+
const picked = (q.get('dest') || '').trim()
|
|
820
|
+
const pk = picked ? pickedDest(resolvePath(picked, cwd), root) : null
|
|
821
|
+
const dest = pk ? pk.dest : defaultDest(root, cwd, p)
|
|
822
|
+
const project = projectFor(dest)
|
|
823
|
+
json(res, 200, { cwd, root, project, fw: defaultFramework(project), dest, why: pk ? pk.why : '', frameworks: Object.fromEntries(Object.entries(FRAMEWORKS).map(([k, v]) => [k, v.label])) })
|
|
819
824
|
}
|
|
820
825
|
|
|
821
826
|
async function checkRoute(req, res) {
|
|
@@ -1063,6 +1068,8 @@ export function apply(ctx) {
|
|
|
1063
1068
|
const ext = imageExt(buf)
|
|
1064
1069
|
if (!ext) return json(res, 400, { error: '不是图片' })
|
|
1065
1070
|
const st = tracker.stateOf(String(body.session || ''))
|
|
1071
|
+
// 画笔标注带着画的那份设计的目录来:这期间换了设计,别把旧图存进新设计
|
|
1072
|
+
if (typeof body.root === 'string' && !(st.target && st.target.kind === 'file' && st.target.root === body.root)) return json(res, 409, { error: 'moved' })
|
|
1066
1073
|
const dir = st.target && st.target.kind === 'file' ? path.join(st.target.root, 'refs') : REFS_DIR
|
|
1067
1074
|
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
1075
|
try {
|