From 834673391e691157ef3bc5f3bbefd5b5c60a3413 Mon Sep 17 00:00:00 2001 From: cruldra Date: Mon, 28 Sep 2026 11:09:06 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(prototype):=20=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E9=98=85=E8=AF=BB=E5=8E=9F=E5=9E=8B=E5=8A=A0=20Docker?= =?UTF-8?q?file=20=E4=B8=80=E9=A1=B5=EF=BC=88=E9=98=B6=E6=AE=B5=E5=A5=97?= =?UTF-8?q?=E6=8C=87=E4=BB=A4=E3=80=81RUN=20=E6=8C=89=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E6=8B=86=E3=80=81=E8=BF=9E=E7=9D=80=E7=9A=84=20COPY=20/=20ENV?= =?UTF-8?q?=20=E5=B9=B6=E8=A1=A8=E3=80=81=E5=81=A5=E5=BA=B7=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E3=80=81=E5=90=AF=E5=8A=A8=EF=BC=89=EF=BC=9BShell=20?= =?UTF-8?q?=E5=92=8C=20Dockerfile=20=E5=85=B1=E7=94=A8=E4=B8=80=E5=A5=97?= =?UTF-8?q?=E7=94=BB=E6=B3=95=EF=BC=8C=E6=8F=92=E4=BB=B6=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E5=8A=A0=20Dockerfile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../code-reader/figma-plugin/README.md | 9 +- .../code-reader/figma-plugin/code.js | 196 +++++++++++++++--- .../code-reader/figma-plugin/manifest.json | 4 + .../code-reader/figma-plugin/test.js | 33 ++- 4 files changed, 206 insertions(+), 36 deletions(-) diff --git a/docs/prototype/code-reader/figma-plugin/README.md b/docs/prototype/code-reader/figma-plugin/README.md index c0eddfb..9f944db 100644 --- a/docs/prototype/code-reader/figma-plugin/README.md +++ b/docs/prototype/code-reader/figma-plugin/README.md @@ -47,11 +47,18 @@ | --- | --- | --- | | `Prototype / Shell · fetch-runtime.sh` | luffy 桌面端 `apps/desktop/scripts/fetch-runtime.sh` | bash 语言插件拆块,命令的意思由「命令规则」按命令名注册(rm、mkdir、curl、tar、find……),像库规则一样把选项读成中文。块:解释器(#! 用哪个解释器、怎么找);选项(set -euo pipefail 每个开关一行);变量(`${1:-}`、`$(…)`、`${x:-默认}` 读成「第 1 个参数(没给就是空)」「执行 … 的输出」「x(没设就用 …)」,大写的当常量上色);分情况(每个情况都只有几句时画成「情况 / 做什么」表,有长的就每个情况一块子块,`*.zip` 读成「以 .zip 结尾」);函数(紧贴的注释当说明;按 `local x="$1"` 认出第几个参数叫什么;输出写它打印的东西);命令(「删除 dest(连同里面的东西,不提示)」「下载 url 到 out.part(失败就报错、跟着跳转走、最多重试 3 次)」,`>&2` 写「打印到错误输出」);管道(a → b,后面的 while read 照常是子块);如果 / 遍历(`[ -f x ]`、`[[ x == *-y ]]`、`x && y \|\| continue` 读成中文) | +## Dockerfile(插件菜单「Dockerfile」,单独一页 `代码阅读器 · Dockerfile`) + +| 画板 | 对应源码 | 设计点 | +| --- | --- | --- | +| `Prototype / Dockerfile · Dockerfile` | luffy 的 `frontend/client-nextjs/Dockerfile` | 每个 FROM 开一个「阶段」(名称、基础镜像 = 镜像名 + 标签),后面的指令是它的子块;`# syntax=` 是「解析指令」;WORKDIR / USER / EXPOSE 各一行「设置」;ARG「构建参数」、ENV「环境变量」,`$X` 读成变量;连着写的 ENV 并成「名称 / 值」表;`COPY --from=deps` 写「从阶段 deps 复制 … → …」,阶段名 Cmd+单击跳过去;连着写的 COPY 并成「从 / 源 / 到」表,属主相同就写在头一行;CMD「启动」写命令和写法(数组不经过 shell) | +| `Prototype / Dockerfile · Dockerfile.backend` | luffy 的 `backend/docker/Dockerfile.backend` 几段 | LABEL「标签」表;`&&` 连起来的 RUN 每条命令一个子块,命令照 Shell 的命令规则读(apt-get、curl、gpg、uv……),管道写「a → b」;`COPY --from=node:22-slim` 写「从镜像」;12 条连着的 COPY 一张表;`uv sync` 一长串选项画成「选项 / 意思」表,重复的 `--no-install-package` 并成一行;HEALTHCHECK「健康检查」:间隔 / 超时 / 启动后宽限 / 失败几次算不健康 / 检查命令 | + ## 导入与运行 1. 打开 Figma Desktop,打开任意 Design 文件。 2. `Plugins > Development > Import plugin from manifest…`,选本目录的 `manifest.json`(已经导入过就跳过)。 -3. `Plugins > Development > 代码阅读器 Design Lab`,选「主画板」或「Shell 脚本」,各自新建一页。改过 `code.js` 后要重启 Figma 才会用新代码。 +3. `Plugins > Development > 代码阅读器 Design Lab`,选「主画板」「Shell 脚本」或「Dockerfile」,各自新建一页。改过 `code.js` 后要重启 Figma 才会用新代码。 ## 运行条件 diff --git a/docs/prototype/code-reader/figma-plugin/code.js b/docs/prototype/code-reader/figma-plugin/code.js index 7e86995..0eade00 100644 --- a/docs/prototype/code-reader/figma-plugin/code.js +++ b/docs/prototype/code-reader/figma-plugin/code.js @@ -917,6 +917,8 @@ const LUCIDE = { "signpost": [["path", { d: "M12 13v8" }], ["path", { d: "M12 3v3" }], ["path", { d: "M2.354 10.354a1.207 1.207 0 0 1 0-1.708l2.06-2.06A2 2 0 0 1 5.828 6h12.344a2 2 0 0 1 1.414.586l2.06 2.06a1.207 1.207 0 0 1 0 1.708l-2.06 2.06a2 2 0 0 1-1.414.586H5.828a2 2 0 0 1-1.414-.586z" }]], "sliders-horizontal": [["path", { d: "M10 5H3" }], ["path", { d: "M12 19H3" }], ["path", { d: "M14 3v4" }], ["path", { d: "M16 17v4" }], ["path", { d: "M21 12h-9" }], ["path", { d: "M21 19h-5" }], ["path", { d: "M21 5h-7" }], ["path", { d: "M8 10v4" }], ["path", { d: "M8 12H3" }]], "terminal": [["path", { d: "M12 19h8" }], ["path", { d: "m4 17 6-6-6-6" }]], + "layers": [["path", { d: "M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z" }], ["path", { d: "M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12" }], ["path", { d: "M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17" }]], + "activity": [["path", { d: "M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2" }]], "package": [["path", { d: "M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z" }], ["path", { d: "M12 22V12" }], ["polyline", { points: "3.29 7 12 12 20.71 7" }], ["path", { d: "m7.5 4.27 9 5.15" }]], "git-pull-request": [["circle", { cx: "18", cy: "18", r: "3" }], ["circle", { cx: "6", cy: "6", r: "3" }], ["path", { d: "M13 6h3a2 2 0 0 1 2 2v7" }], ["path", { d: "M6 9v12" }]], "files": [["path", { d: "M20 7h-3a2 2 0 0 1-2-2V2" }], ["path", { d: "M9 18a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h7l4 4v10a2 2 0 0 1-2 2Z" }], ["path", { d: "M3 7.6v12.8A1.6 1.6 0 0 0 4.6 22h9.8" }]], @@ -2635,10 +2637,9 @@ const SHELL_BLOCKS = [ { line: 130, kind: "command", text: `[c|统计占用空间] [v|dest][q|/git]、[v|dest][q|/python][d|(按人读的单位,只看总数)]${STDERR}` }, ]; -/** Shell / 块 — Line=<行号>:一块,按 spec 画一行译文、属性行、表格。 */ -function shellBlock(ctx, spec) { - const kind = SHELL_KINDS[spec.kind]; - const { node, content } = cardShell(ctx, `Line=${spec.line}`, kind, { kids: Boolean(spec.kids) }); +/** <页> / <文件> — Line=<行号>:一块,按 spec 画一行译文、属性行、表格(列数由 columns 定)。 */ +function scriptBlock(ctx, kinds, spec) { + const { node, content } = cardShell(ctx, `Line=${spec.line}`, kinds[spec.kind], { kids: Boolean(spec.kids) }); if (spec.text) richText(ctx, content, "Text", rich(spec.text)); const rows = spec.rows || []; rows.forEach(([label, value], index) => { @@ -2646,78 +2647,211 @@ function shellBlock(ctx, spec) { propRow(ctx, content, label, rich(value), { twisty }); }); if (spec.table) { - // 分情况的表跟在头一行下面;函数的参数表跟在「输入参数」下面,「输出 / 返回」挪到表后面 - propTable(ctx, content, "Table", spec.table.columns, spec.table.rows.map(([a, b], index) => [`Row / ${index + 1}`, [rich(a), rich(b)]])); + // 表跟在头一行 / 最后一行属性下面;函数的参数表跟在「输入参数」下面,「输出 / 返回」挪到表后面 + propTable(ctx, content, "Table", spec.table.columns, spec.table.rows.map((cells, index) => [`Row / ${index + 1}`, cells.map(rich)])); if (spec.kind === "function") content.appendChild(named(content, `Prop / ${rows[rows.length - 1][0]}`)); } return { node }; } -function shellSpecs(specs, visit) { +function scriptSpecs(specs, visit) { for (const spec of specs) { visit(spec); - if (spec.kids) shellSpecs(spec.kids, visit); + if (spec.kids) scriptSpecs(spec.kids, visit); } } -/** 一层 shell 块,和代码块树同样的竖线和缩进。 */ -function shellLevel(ctx, parent, lib, specs, name) { +/** 一层块,和代码块树同样的竖线和缩进。 */ +function scriptLevel(ctx, parent, blocks, specs, name) { const { level, list } = levelList(ctx, parent, name); for (const spec of specs) { - const instance = lib.shell.get(spec.line).node.createInstance(); + const instance = blocks.get(spec.line).node.createInstance(); instance.name = `Block / L${spec.line}`; append(list, instance, true); - if (spec.kids) shellLevel(ctx, list, lib, spec.kids, `Children / L${spec.line}`); + if (spec.kids) scriptLevel(ctx, list, blocks, spec.kids, `Children / L${spec.line}`); } return level; } -/** 「Shell 脚本」这一页:只建它用得到的组件和一张画板。 */ -function buildShell(ctx, api, page, stage) { +/** 插件菜单里 main 以外的几项:各自一页,只建用得到的组件,每个示例文件一张画板。 */ +function buildScriptPage(ctx, api, page, stage, key) { + const spec = SCRIPT_PAGES[key]; stage("生成组件"); const sets = []; const lib = {}; ctx.lib = lib; const make = setMaker(ctx, page, sets); lib.tools = make("Reader / BlockTools", MODES.slice().reverse(), 2, (state) => blockToolsComponent(ctx, state)); - const lines = []; - shellSpecs(SHELL_BLOCKS, (spec) => lines.push(spec.line)); - const byLine = new Map(); - shellSpecs(SHELL_BLOCKS, (spec) => byLine.set(spec.line, spec)); - lib.shell = make(`Shell / ${SHELL_FILE.split("/").pop()}`, lines, 2, (line) => shellBlock(ctx, byLine.get(line)), (item) => item.node); + const built = spec.boards.map((board) => { + const byLine = new Map(); + scriptSpecs(board.blocks, (block) => byLine.set(block.line, block)); + const name = board.file.split("/").pop(); + return { board, name, blocks: make(`${spec.prefix} / ${name}`, [...byLine.keys()], 2, (line) => scriptBlock(ctx, spec.kinds, byLine.get(line)), (item) => item.node) }; + }); + lib.blocks = built.map((entry) => entry.blocks); stage("生成演示画板"); - const board = frame(ctx, `Prototype / Shell · ${SHELL_FILE.split("/").pop()}`, { width: TREE_W + 2 * VIEW_PAD, pad: VIEW_PAD, fill: COLORS.editor }); - page.appendChild(board); - timed(board.name, () => shellLevel(ctx, board, lib, SHELL_BLOCKS, "Level 0")); + const screens = built.map(({ board, name, blocks }) => { + const screen = frame(ctx, `Prototype / ${spec.prefix} · ${name}`, { width: TREE_W + 2 * VIEW_PAD, pad: VIEW_PAD, fill: COLORS.editor }); + page.appendChild(screen); + timed(screen.name, () => scriptLevel(ctx, screen, blocks, board.blocks, "Level 0")); + return screen; + }); stage("排布画板"); - board.x = 0; - board.y = 0; + let x = 0; + for (const screen of screens) { + screen.x = x; + screen.y = 0; + x += screen.width + 120; + } let setY = 0; for (const set of sets) { - set.x = board.width + 160; + set.x = x + 40; set.y = setY; setY += set.height + 64; } for (const node of page.children) node.expanded = false; - api.viewport.scrollAndZoomIntoView([board]); - return { clock, page, components: { lib, sets }, board, screens: [board], start: board.name }; + api.viewport.scrollAndZoomIntoView([screens[0]]); + return { clock, page, components: { lib, sets }, board: screens[0], screens, start: screens[0].name }; } +// ─── Dockerfile ────────────────────────────────────────────────────────────── +// +// Dockerfile 语言插件按指令拆块(语法另外引入,插件现用的分析组件里没有 Dockerfile): +// - 解析指令:第一行 # syntax=…,写 BuildKit 按哪个语法版本读这份文件; +// - 阶段:每个 FROM 开一个阶段,后面的指令都是它的子块;卡片写阶段名、基础镜像(镜像名 + 标签)、平台(写了才有); +// - 执行:RUN 里的 shell 交给 Shell 语言插件和命令规则读(apt-get、pnpm、uv、corepack、useradd……); +// 只有一条命令写一行,&& 连起来的每条命令一个子块,管道照 Shell 那样写「a → b」; +// - 复制:COPY / ADD 写「从阶段 X / 从镜像 Y 复制 源 → 目标(属主 u:g)」,阶段名 Cmd+单击跳到那个阶段; +// - 变量:ARG 是构建参数(默认值写了才有),ENV 是环境变量,值里的 $X 读成变量; +// - 连着写的同种指令(几行 ENV、几行 COPY)并成一张表,中间隔了注释就断开; +// - 设置:WORKDIR「工作目录」、USER「之后以 … 身份运行」、EXPOSE「开放端口」、VOLUME、STOPSIGNAL、SHELL 各一行; +// - 标签:LABEL 画成「名称 / 值」表; +// - 健康检查:间隔、超时、启动后宽限、失败几次算不健康、检查命令(命令照 Shell 读); +// - 启动:CMD / ENTRYPOINT 写命令和写法(数组写法不经过 shell),命令照命令规则读。 +// 原型的译文按这份设计手写。示例:luffy 的 frontend/client-nextjs/Dockerfile(三个阶段)和 backend/docker/Dockerfile.backend 的几段。 +const DOCKER_PAGE_NAME = "代码阅读器 · Dockerfile"; +const DOCKER_KINDS = { + comment: BLOCK_KINDS.comment, + variable: BLOCK_KINDS.variable, + directive: { label: "解析指令", color: "#9d9d9d", icon: "terminal" }, + stage: { label: "阶段", color: "#56b6c2", icon: "layers" }, + run: { label: "执行", color: "#d7ba7d", icon: "square-terminal" }, + copy: { label: "复制", color: "#c586c0", icon: "copy" }, + setting: { label: "设置", color: "#e5c07b", icon: "sliders-horizontal" }, + label: { label: "标签", color: "#9cdcfe", icon: "tag" }, + health: { label: "健康检查", color: "#f48771", icon: "activity" }, + start: { label: "启动", color: "#89d185", icon: "play" }, +}; +const PNPM_ACTIVATE = [ + { line: 0, kind: "run", text: "[c|开启 corepack]" }, + { line: 0, kind: "run", text: "[c|准备 pnpm] [q|latest][d|(并设为当前用的版本)]" }, +]; +const withLines = (blocks, first) => blocks.map((block, index) => ({ ...block, line: first + index / 10 })); +const NEXT_DOCKERFILE = [ + { line: 1, kind: "directive", rows: [["语法", "[q|docker/dockerfile:1.7][d|(BuildKit 按这个版本读这份文件)]"]] }, + { line: 3, kind: "stage", rows: [["名称", "[v|deps]"], ["基础镜像", "[t|node][d|,标签 ][q|22-alpine]"]], kids: [ + { line: 4, kind: "setting", text: "[k|工作目录] [q|/app]" }, + { line: 5, kind: "run", text: "[k|执行]", kids: withLines(PNPM_ACTIVATE, 5.1) }, + { line: 6, kind: "copy", text: "[c|复制] [q|package.json]、[q|pnpm-lock.yaml] [k|→] [q|./]" }, + { line: 7, kind: "run", text: "[c|pnpm 装依赖][d|(严格按 lock 文件、不跑安装脚本)]" }, + ] }, + { line: 9, kind: "stage", rows: [["名称", "[v|builder]"], ["基础镜像", "[t|node][d|,标签 ][q|22-alpine]"]], kids: [ + { line: 10, kind: "setting", text: "[k|工作目录] [q|/app]" }, + { line: 11, kind: "run", text: "[k|执行]", kids: withLines(PNPM_ACTIVATE, 11.1) }, + { line: 12, kind: "copy", text: "[k|从阶段] [v|deps] [c|复制] [q|/app/node_modules] [k|→] [q|./node_modules]" }, + { line: 13, kind: "copy", text: "[c|复制] [d|整个构建目录] [k|→] [q|./]" }, + { line: 14, kind: "variable", rows: [["环境变量", "[u|NEXT_TELEMETRY_DISABLED]"], ["值", "[n|1]"]] }, + { line: 15, kind: "comment", text: "[d|部署 ID 烙进本次构建: Next 据此给静态资源加 ?dpl=、页内跳转时比对版本;\n浏览器包与服务端包都能读 process.env.NEXT_DEPLOYMENT_ID (见 src/lib/version-skew).]" }, + { line: 17, kind: "variable", rows: [["构建参数", "[u|DEPLOYMENT_ID]"], ["默认值", "[d|没写,构建时传]"]] }, + { line: 18, kind: "variable", rows: [["环境变量", "[u|NEXT_DEPLOYMENT_ID]"], ["值", "[u|DEPLOYMENT_ID][d|(构建参数)]"]] }, + { line: 19, kind: "run", text: "[c|pnpm 构建]" }, + ] }, + { line: 21, kind: "stage", rows: [["名称", "[v|runner]"], ["基础镜像", "[t|node][d|,标签 ][q|22-alpine]"]], kids: [ + { line: 22, kind: "setting", text: "[k|工作目录] [q|/app]" }, + { line: 23, kind: "variable", text: "[k|环境变量][d|(4 条连着写)]", table: { columns: [["名称", 260], ["值", 0]], rows: [ + ["[u|NODE_ENV]", "[q|production]"], ["[u|NEXT_TELEMETRY_DISABLED]", "[n|1]"], ["[u|PORT]", "[n|3000]"], ["[u|HOSTNAME]", "[q|0.0.0.0]"], + ] } }, + { line: 27, kind: "variable", rows: [["构建参数", "[u|DEPLOYMENT_ID]"], ["默认值", "[d|没写,构建时传]"]] }, + { line: 28, kind: "variable", rows: [["环境变量", "[u|NEXT_DEPLOYMENT_ID]"], ["值", "[u|DEPLOYMENT_ID][d|(构建参数)]"]] }, + { line: 30, kind: "run", text: "[k|执行]", kids: [ + { line: 30.1, kind: "run", text: "[c|新建系统用户组] [v|nodejs][d|(gid ][n|1001][d|)]" }, + { line: 30.2, kind: "run", text: "[c|新建系统用户] [v|nextjs][d|(uid ][n|1001][d|)]" }, + ] }, + { line: 33, kind: "copy", text: "[c|复制][d|(2 条连着写,属主都是 ][v|nextjs:nodejs][d|)]", table: { columns: [["从", 120], ["源", 280], ["到", 0]], rows: [ + ["[k|阶段] [v|builder]", "[q|/app/.next/standalone]", "[q|./]"], + ["[k|阶段] [v|builder]", "[q|/app/.next/static]", "[q|./.next/static]"], + ] } }, + { line: 35, kind: "comment", text: "[d|standalone 输出不含 public/, 必须显式拷贝 (字体等静态资源)]" }, + { line: 36, kind: "copy", text: "[k|从阶段] [v|builder] [c|复制] [q|/app/public] [k|→] [q|./public][d|(属主 ][v|nextjs:nodejs][d|)]" }, + { line: 38, kind: "setting", text: "[k|之后以] [v|nextjs] [k|身份运行]" }, + { line: 39, kind: "setting", text: "[k|开放端口] [n|3000]" }, + { line: 41, kind: "start", rows: [["命令", "[c|node] [q|server.js]"], ["写法", "[d|数组,不经过 shell]"]] }, + ] }, +]; +const BACKEND_DOCKERFILE = [ + { line: 4, kind: "stage", rows: [["名称", "[v|base]"], ["基础镜像", "[t|python][d|,标签 ][q|3.12-slim]"]], kids: [ + { line: 6, kind: "label", text: "[k|标签]", table: { columns: [["名称", 160], ["值", 0]], rows: [ + ["[v|maintainer]", "[q|Cruldra]"], ["[v|description]", "[q|luffy-agent-platform - 统一 Backend 镜像 (API/Worker/Beat/Flower)]"], + ] } }, + { line: 13, kind: "run", text: "[k|执行][d|(10 条命令,一条失败就停)]", kids: [ + { line: 13.1, kind: "run", text: "[c|更新软件包列表]" }, + { line: 13.2, kind: "run", text: "[c|安装] [q|ca-certificates]、[q|curl]、[q|gnupg]、[q|postgresql-client]、[q|git][d|(不装推荐的包、不用确认)]" }, + { line: 13.3, kind: "run", text: "[c|建目录] [q|/usr/share/keyrings]" }, + { line: 13.4, kind: "run", text: "[c|下载] [q|https://www.postgresql.org/media/keys/ACCC4CF8.asc][d|(不打印进度、失败就报错、跟着跳转走)] [k|→] [c|转成二进制密钥] [k|写到] [q|/usr/share/keyrings/postgresql.gpg]" }, + { line: 13.5, kind: "run", text: "[c|读入] [q|/etc/os-release] [d|里的变量]" }, + { line: 13.6, kind: "run", text: "[c|写入] [q|\"deb [signed-by=…] http://apt.postgresql.org/pub/repos/apt ][v|VERSION_CODENAME][q|-pgdg main\"] [k|到] [q|/etc/apt/sources.list.d/pgdg.list][d|(覆盖)]" }, + { line: 13.7, kind: "run", text: "[c|更新软件包列表]" }, + { line: 13.8, kind: "run", text: "[c|安装] [q|postgresql-16-repack][d|(不装推荐的包、不用确认)]" }, + { line: 13.9, kind: "run", text: "[c|打印 pg_repack 的版本]" }, + { line: 13.95, kind: "run", text: "[c|删除] [q|/var/lib/apt/lists/*][d|(连同里面的东西、不提示)]" }, + ] }, + { line: 32, kind: "copy", text: "[c|复制][d|(2 条连着写)]", table: { columns: [["从", 160], ["源", 300], ["到", 0]], rows: [ + ["[k|镜像] [t|node][d|:][q|22-slim]", "[q|/usr/local/bin/node]", "[q|/usr/local/bin/node]"], + ["[k|镜像] [t|node][d|:][q|22-slim]", "[q|/usr/local/lib/node_modules]", "[q|/usr/local/lib/node_modules]"], + ] } }, + { line: 54, kind: "copy", text: "[c|复制][d|(12 条连着写,属主都是 ][v|appuser:appuser][d|)]", table: { columns: [["源", 420], ["到", 0]], rows: [ + ["[q|backend/pyproject.toml]、[q|backend/uv.lock]", "[q|./]"], + ...["agent-dsl", "agent-compiler-deepagents", "vendor-tools", "code-sandbox-core", "code-sandbox-daytona", "code-sandbox-e2b", "code-sandbox-local", + "code-publish-core", "code-publish-compose-runner", "luffy-billing-sdk", "luffy-observability"].map((name) => [`[q|lib/${name}/pyproject.toml]`, `[q|/lib/${name}/]`]), + ] } }, + { line: 67, kind: "comment", text: "[d|三方依赖先装;本项目与 lib editable 包此时无源码,跳过安装留给第二次 sync]" }, + { line: 68, kind: "run", rows: [["命令", "[c|uv 按 lock 同步依赖]"], ["选项", "[d|4 个]"]], table: { columns: [["选项", 320], ["意思", 0]], rows: [ + ["[q|--frozen]", "[d|严格按 lock 文件,不改它]"], + ["[q|--no-dev]", "[d|不装开发依赖]"], + ["[q|--no-install-project]", "[d|不装本项目]"], + ["[q|--no-install-package] [d|× 11]", "[d|不装这些包:]agent-dsl、agent-compiler-deepagents、vendor-tools、code-sandbox-core、code-sandbox-daytona、code-sandbox-e2b、code-sandbox-local、code-publish-core、code-publish-compose-runner、luffy-billing-sdk、luffy-observability"], + ] } }, + { line: 104, kind: "setting", text: "[k|开放端口] [n|8000]" }, + { line: 107, kind: "health", rows: [["间隔", "[n|30] [d|秒]"], ["超时", "[n|5] [d|秒]"], ["启动后宽限", "[n|10] [d|秒]"], ["失败几次算不健康", "[n|3] [d|次]"], + ["检查命令", "[c|请求] [q|http://localhost:8000/health][d|(失败就报错)] [k|或者] [c|退出][d|,退出码 ][n|1]"]] }, + { line: 110, kind: "start", rows: [["命令", "[c|用 uv 运行] [c|uvicorn] [q|luffy_agent.main:app][d|,监听 ][q|0.0.0.0][d|:][n|8000]"], ["写法", "[d|数组,不经过 shell]"]] }, + ] }, +]; + +// 插件菜单里 main 以外的几项 +const SCRIPT_PAGES = { + shell: { page: SHELL_PAGE_NAME, prefix: "Shell", kinds: SHELL_KINDS, boards: [{ file: SHELL_FILE, blocks: SHELL_BLOCKS }] }, + docker: { page: DOCKER_PAGE_NAME, prefix: "Dockerfile", kinds: DOCKER_KINDS, boards: [ + { file: "frontend/client-nextjs/Dockerfile", blocks: NEXT_DOCKERFILE }, + { file: "backend/docker/Dockerfile.backend", blocks: BACKEND_DOCKERFILE }, + ] }, +}; + // ─── Entry ─────────────────────────────────────────────────────────────────── -/** command:插件菜单里点的哪一项,main = 主画板,shell = Shell 脚本那一页。 */ +/** command:插件菜单里点的哪一项,main = 主画板,其余见 SCRIPT_PAGES(shell、docker)。 */ async function build(api, stage = () => {}, command = "main") { clock = new Map(); stage("加载中文字体"); const ctx = { api, fonts: await chooseFonts(api) }; stage("新建独立页面"); - const name = nextPageName(api.root.children, command === "shell" ? SHELL_PAGE_NAME : PAGE_NAME); + const name = nextPageName(api.root.children, SCRIPT_PAGES[command] ? SCRIPT_PAGES[command].page : PAGE_NAME); const page = api.createPage(); page.name = name; await api.setCurrentPageAsync(page); page.backgrounds = paint("#111214"); page.setSharedPluginData(NAMESPACE, "generator", GENERATOR); - if (command === "shell") return buildShell(ctx, api, page, stage); + if (SCRIPT_PAGES[command]) return buildScriptPage(ctx, api, page, stage, command); stage("生成组件"); const components = createComponents(ctx, page); @@ -2911,6 +3045,6 @@ async function run(api) { } if (typeof module !== "undefined") { - module.exports = { SHELL_BLOCKS, SHELL_PAGE_NAME, NESTED_CALL_SAMPLE, parseCall, ROUTER_SAMPLE, ROUTE_SAMPLE, REVIEW, REVIEW_ISSUES, ASK_SAMPLE, BEAT_SCHEDULE, ENUM_SAMPLE, SQLA_MODEL, ITERATION_SAMPLES, iterationSegments, facetCounts, filteredOut, filterPresets, blockAttributes, FILTER_FILE, NOTES, NOTE_DRAFT, PLAIN_KINDS, parseImport, parseVariable, translateCode, build, run, nextPageName, PAGE_NAME, NAMESPACE, TREE, WORKSPACES, FILES, CODE_TREE, BLOCK_KINDS, SOURCES, blockCode, lineRange, walkTree, translateBlock, highlightCode, parseSignature, typeSegments }; + module.exports = { NEXT_DOCKERFILE, BACKEND_DOCKERFILE, DOCKER_PAGE_NAME, SHELL_BLOCKS, SHELL_PAGE_NAME, NESTED_CALL_SAMPLE, parseCall, ROUTER_SAMPLE, ROUTE_SAMPLE, REVIEW, REVIEW_ISSUES, ASK_SAMPLE, BEAT_SCHEDULE, ENUM_SAMPLE, SQLA_MODEL, ITERATION_SAMPLES, iterationSegments, facetCounts, filteredOut, filterPresets, blockAttributes, FILTER_FILE, NOTES, NOTE_DRAFT, PLAIN_KINDS, parseImport, parseVariable, translateCode, build, run, nextPageName, PAGE_NAME, NAMESPACE, TREE, WORKSPACES, FILES, CODE_TREE, BLOCK_KINDS, SOURCES, blockCode, lineRange, walkTree, translateBlock, highlightCode, parseSignature, typeSegments }; } if (typeof figma !== "undefined") void run(figma); diff --git a/docs/prototype/code-reader/figma-plugin/manifest.json b/docs/prototype/code-reader/figma-plugin/manifest.json index 27e55ee..5f07eec 100644 --- a/docs/prototype/code-reader/figma-plugin/manifest.json +++ b/docs/prototype/code-reader/figma-plugin/manifest.json @@ -19,6 +19,10 @@ { "name": "Shell 脚本", "command": "shell" + }, + { + "name": "Dockerfile", + "command": "docker" } ] } diff --git a/docs/prototype/code-reader/figma-plugin/test.js b/docs/prototype/code-reader/figma-plugin/test.js index c35f6ca..9bf24c1 100644 --- a/docs/prototype/code-reader/figma-plugin/test.js +++ b/docs/prototype/code-reader/figma-plugin/test.js @@ -2,7 +2,7 @@ const assert = require("node:assert/strict"); const { test } = require("node:test"); const fs = require("node:fs"); const path = require("node:path"); -const { SHELL_BLOCKS, SHELL_PAGE_NAME, NESTED_CALL_SAMPLE, parseCall, ROUTER_SAMPLE, ROUTE_SAMPLE, REVIEW, REVIEW_ISSUES, ASK_SAMPLE, BEAT_SCHEDULE, ENUM_SAMPLE, SQLA_MODEL, ITERATION_SAMPLES, iterationSegments, facetCounts, filteredOut, filterPresets, FILTER_FILE, NOTES, NOTE_DRAFT, PLAIN_KINDS, build, run, nextPageName, PAGE_NAME, NAMESPACE, WORKSPACES, FILES, CODE_TREE, BLOCK_KINDS, SOURCES, blockCode, lineRange, walkTree, translateBlock, highlightCode, parseSignature, parseImport, parseVariable, typeSegments } = require("./code.js"); +const { NEXT_DOCKERFILE, BACKEND_DOCKERFILE, DOCKER_PAGE_NAME, SHELL_BLOCKS, SHELL_PAGE_NAME, NESTED_CALL_SAMPLE, parseCall, ROUTER_SAMPLE, ROUTE_SAMPLE, REVIEW, REVIEW_ISSUES, ASK_SAMPLE, BEAT_SCHEDULE, ENUM_SAMPLE, SQLA_MODEL, ITERATION_SAMPLES, iterationSegments, facetCounts, filteredOut, filterPresets, FILTER_FILE, NOTES, NOTE_DRAFT, PLAIN_KINDS, build, run, nextPageName, PAGE_NAME, NAMESPACE, WORKSPACES, FILES, CODE_TREE, BLOCK_KINDS, SOURCES, blockCode, lineRange, walkTree, translateBlock, highlightCode, parseSignature, parseImport, parseVariable, typeSegments } = require("./code.js"); const { fakeFigma } = require("../../figma-lab/fake-figma"); function action(node) { return node.reactions[0].actions[0]; } @@ -100,8 +100,8 @@ test("manifest stays local, without a fabricated plugin ID", () => { assert.deepEqual(manifest.editorType, ["figma"]); assert.deepEqual(manifest.networkAccess.allowedDomains, ["none"]); assert.equal("id" in manifest, false); - // 插件菜单分两项:主画板、Shell 脚本,各自只生成自己那一页 - assert.deepEqual(manifest.menu.map((item) => item.command), ["main", "shell"]); + // 插件菜单:主画板、Shell 脚本、Dockerfile,各自只生成自己那一页 + assert.deepEqual(manifest.menu.map((item) => item.command), ["main", "shell", "docker"]); }); test("plugin source passes Figma's load-time scan", () => { @@ -531,7 +531,7 @@ test("the Shell menu item builds its own page: blocks, case tables, function par assert.equal(new Set(lines).size, lines.length, "one block per source line"); const board = result.board; assert.deepEqual(board.findAll((node) => node.type === "INSTANCE" && node.name.startsWith("Block / L")).map((node) => Number(node.name.slice(9))), lines); - const block = (line) => result.components.lib.shell.get(line).node; + const block = (line) => result.components.lib.blocks[0].get(line).node; // 情况都短的 case 画成表,情况长的每个情况一块子块 assert.equal(named(named(block(24), "Table"), "Table").children.length - 1, 3); assert.ok(!named(block(78), "Table")); @@ -548,6 +548,31 @@ test("the Shell menu item builds its own page: blocks, case tables, function par assert.ok(owned < 3000, `shell page owns ${owned} layers`); }); +test("the Dockerfile menu item: stages hold their instructions, RUN chains split into commands, runs of COPY / ENV become tables", async () => { + const api = fakeFigma(); + const result = await build(api, () => {}, "docker"); + assert.equal(result.page.name, DOCKER_PAGE_NAME); + assert.equal(result.screens.length, 2); + const [next, backend] = result.components.lib.blocks; + // 每个 FROM 一个阶段,指令是阶段的子块 + const stages = NEXT_DOCKERFILE.filter((spec) => spec.kind === "stage"); + assert.deepEqual(stages.map((spec) => spec.rows[0][1]), ["[v|deps]", "[v|builder]", "[v|runner]"]); + const board = result.screens[0]; + for (const stage of stages) assert.ok(named(board, `Children / L${stage.line}`), `stage L${stage.line} holds its instructions`); + assert.ok(named(next.get(3).node, "Kind / 阶段")); + // && 连起来的 RUN 每条命令一个子块 + assert.equal(BACKEND_DOCKERFILE[0].kids.find((spec) => spec.line === 13).kids.length, 10); + // 连着写的 ENV、COPY 并成一张表 + assert.equal(named(named(next.get(23).node, "Table"), "Table").children.length - 1, 4); + assert.equal(named(named(backend.get(54).node, "Table"), "Table").children.length - 1, 12); + assert.ok(named(backend.get(107).node, "Kind / 健康检查")); + assert.ok(named(backend.get(110).node, "Kind / 启动")); + let owned = 0; + const walk = (node) => { owned += 1; if (node.type !== "INSTANCE") node.children.forEach(walk); }; + result.page.children.forEach(walk); + assert.ok(owned < 3000, `Dockerfile page owns ${owned} layers`); +}); + test("block kinds show as icons, the legend lists every kind with its name", async () => { const api = fakeFigma(); const { kindBoard } = await build(api);