@open-press/core 0.8.0 → 1.1.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.
Files changed (77) hide show
  1. package/README.md +17 -5
  2. package/engine/cli.mjs +9 -9
  3. package/engine/commands/_shared.mjs +70 -18
  4. package/engine/commands/deploy.mjs +3 -3
  5. package/engine/commands/dev.mjs +13 -4
  6. package/engine/commands/image.mjs +29 -0
  7. package/engine/commands/inspect.mjs +3 -2
  8. package/engine/commands/pdf.mjs +2 -2
  9. package/engine/commands/preview.mjs +2 -2
  10. package/engine/commands/render.mjs +6 -4
  11. package/engine/commands/replace.mjs +1 -1
  12. package/engine/commands/search.mjs +1 -1
  13. package/engine/commands/skills-sync.mjs +71 -0
  14. package/engine/commands/typecheck.mjs +71 -1
  15. package/engine/commands/upgrade.mjs +3 -3
  16. package/engine/document-export.mjs +1 -1
  17. package/engine/output/chrome-pdf.mjs +92 -0
  18. package/engine/output/static-server.mjs +60 -17
  19. package/engine/react/comment-marker.mjs +13 -13
  20. package/engine/react/document-entry.mjs +35 -28
  21. package/engine/react/document-export.mjs +309 -170
  22. package/engine/react/mdx-compile.mjs +30 -0
  23. package/engine/react/measurement-css.mjs +21 -0
  24. package/engine/react/object-entities.mjs +85 -0
  25. package/engine/react/pagination/allocator.mjs +48 -3
  26. package/engine/react/pagination.mjs +1 -1
  27. package/engine/react/pipeline/allocate.mjs +31 -65
  28. package/engine/react/pipeline/frame-measurement.mjs +4 -0
  29. package/engine/react/press-tree-inspection.mjs +172 -0
  30. package/engine/react/sources/mdx-resolver.mjs +1 -1
  31. package/engine/react/style-discovery.mjs +22 -4
  32. package/engine/runtime/config.d.mts +8 -0
  33. package/engine/runtime/config.mjs +57 -60
  34. package/engine/runtime/file-utils.mjs +9 -1
  35. package/engine/runtime/page-geometry.mjs +131 -0
  36. package/engine/runtime/source-text-tools.mjs +1 -1
  37. package/engine/runtime/source-workspace.mjs +12 -3
  38. package/engine/runtime/validation.mjs +19 -10
  39. package/index.html +4 -0
  40. package/package.json +9 -12
  41. package/src/main.tsx +16 -0
  42. package/src/openpress/app/OpenPressApp.tsx +173 -17
  43. package/src/openpress/app/OpenPressRuntime.tsx +10 -2
  44. package/src/openpress/app/WorkspaceGalleryPage.tsx +219 -0
  45. package/src/openpress/core/Frame.tsx +20 -7
  46. package/src/openpress/core/FrameContext.tsx +2 -0
  47. package/src/openpress/core/Press.tsx +25 -4
  48. package/src/openpress/core/Workspace.tsx +36 -0
  49. package/src/openpress/core/index.tsx +10 -3
  50. package/src/openpress/core/primitives.tsx +48 -1
  51. package/src/openpress/core/types.ts +86 -41
  52. package/src/openpress/core/useSource.ts +1 -1
  53. package/src/openpress/document-model/documentTypes.ts +9 -0
  54. package/src/openpress/document-model/index.ts +1 -0
  55. package/src/openpress/document-model/objectEntityModel.ts +4 -0
  56. package/src/openpress/document-model/workspaceManifestModel.ts +57 -0
  57. package/src/openpress/mdx/index.ts +15 -7
  58. package/src/openpress/reader/PageThumbnailsPanel.tsx +168 -0
  59. package/src/openpress/reader/index.ts +1 -0
  60. package/src/openpress/workbench/Workbench.tsx +120 -21
  61. package/src/openpress/workbench/actions/ExportImageControl.tsx +96 -0
  62. package/src/openpress/workbench/actions/SearchControl.tsx +3 -3
  63. package/src/openpress/workbench/actions/index.ts +1 -0
  64. package/src/openpress/workbench/inspector/useInspectorComments.ts +7 -1
  65. package/src/openpress/workbench/panels/PendingCommentsPanel.tsx +5 -1
  66. package/src/openpress/workbench/project/ProjectEntryPanel.tsx +4 -2
  67. package/src/openpress/workbench/project/projectSourceModel.ts +2 -2
  68. package/src/openpress/workbench/workbenchFormatters.ts +2 -2
  69. package/src/styles/openpress/reader-runtime.css +9 -0
  70. package/src/styles/openpress/workbench-panels.css +113 -0
  71. package/src/styles/openpress/workspace-gallery.css +300 -0
  72. package/src/styles/openpress.css +1 -5
  73. package/src/vite-env.d.ts +8 -0
  74. package/tsconfig.json +1 -1
  75. package/vite.config.ts +6 -6
  76. package/engine/commands/init.mjs +0 -24
  77. package/engine/init.mjs +0 -90
@@ -0,0 +1,300 @@
1
+ /* Workspace gallery — the reader's landing page for multi-Press
2
+ workspaces. Shows a card per <Press> child; clicking enters that
3
+ document's reader. Single-Press workspaces skip the gallery and
4
+ load the document directly, so this CSS is dormant until users
5
+ add a second <Press> to their <Workspace>.
6
+
7
+ Layout intent: Figma-style file grid — uniform card width, fixed
8
+ thumbnail aspect ratio (4:3), filename + meta below. Each card
9
+ loads its first page asynchronously and renders it scaled-down
10
+ inside the thumbnail slot. */
11
+
12
+ .openpress-workspace-gallery {
13
+ --workspace-bg: #10110f;
14
+ --workspace-bg-soft: #171813;
15
+ --workspace-ink: #f4f1e8;
16
+ --workspace-muted: rgba(244, 241, 232, 0.52);
17
+ --workspace-line: rgba(244, 241, 232, 0.12);
18
+ --workspace-card: #f7f5ee;
19
+ --workspace-card-ink: #141411;
20
+ --workspace-card-muted: #65635d;
21
+ --workspace-card-line: rgba(20, 20, 17, 0.1);
22
+ --workspace-card-stage: #e8e5dc;
23
+ display: grid;
24
+ gap: 2rem;
25
+ min-height: 100vh;
26
+ max-width: none;
27
+ margin: 0;
28
+ padding: 3.6rem clamp(2rem, 4vw, 4.5rem) 6rem;
29
+ font-family: var(--openpress-font-body, system-ui, sans-serif);
30
+ color: var(--workspace-ink);
31
+ background:
32
+ linear-gradient(180deg, var(--workspace-bg-soft), var(--workspace-bg) 42rem),
33
+ var(--workspace-bg);
34
+ }
35
+
36
+ .openpress-workspace-gallery__header {
37
+ display: grid;
38
+ grid-template-columns: minmax(0, 1fr) auto;
39
+ align-items: end;
40
+ justify-content: space-between;
41
+ gap: 2.5rem;
42
+ padding: 0 0 1.45rem;
43
+ border-bottom: 1px solid var(--workspace-line);
44
+ }
45
+
46
+ .openpress-workspace-gallery__headline {
47
+ display: grid;
48
+ gap: 0.75rem;
49
+ }
50
+
51
+ .openpress-workspace-gallery__eyebrow {
52
+ margin: 0;
53
+ color: var(--workspace-muted);
54
+ font-family: var(--openpress-font-mono, ui-monospace, monospace);
55
+ font-size: 0.68rem;
56
+ font-weight: 600;
57
+ letter-spacing: 0.16em;
58
+ text-transform: uppercase;
59
+ }
60
+
61
+ .openpress-workspace-gallery__header h1 {
62
+ margin: 0;
63
+ font-family: var(--openpress-font-display, var(--openpress-font-body, system-ui));
64
+ font-size: clamp(2.6rem, 5.4vw, 5.2rem);
65
+ font-weight: 720;
66
+ line-height: 0.94;
67
+ letter-spacing: -0.035em;
68
+ color: var(--workspace-ink);
69
+ }
70
+
71
+ .openpress-workspace-gallery__count {
72
+ margin: 0;
73
+ display: grid;
74
+ justify-items: end;
75
+ gap: 0.25rem;
76
+ min-width: 4.5rem;
77
+ color: var(--workspace-ink);
78
+ font-family: var(--openpress-font-mono, ui-monospace, monospace);
79
+ line-height: 1;
80
+ }
81
+
82
+ .openpress-workspace-gallery__count span {
83
+ color: var(--workspace-ink);
84
+ font-size: 2rem;
85
+ font-weight: 500;
86
+ letter-spacing: -0.04em;
87
+ }
88
+
89
+ .openpress-workspace-gallery__count small {
90
+ color: var(--workspace-muted);
91
+ font-size: 0.62rem;
92
+ font-weight: 600;
93
+ letter-spacing: 0.14em;
94
+ text-transform: uppercase;
95
+ }
96
+
97
+ .openpress-workspace-gallery__grid {
98
+ list-style: none;
99
+ margin: 0;
100
+ padding: 0;
101
+ display: grid;
102
+ align-items: start;
103
+ /* Uniform card size — Figma-style. Outer thumb is fixed 4:3, the
104
+ inner page letterboxes to its own geometry. */
105
+ grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
106
+ gap: 1.5rem;
107
+ }
108
+
109
+ .openpress-workspace-gallery__item {
110
+ display: flex;
111
+ }
112
+
113
+ .openpress-workspace-gallery__card {
114
+ appearance: none;
115
+ display: grid;
116
+ grid-template-rows: auto minmax(6.75rem, auto);
117
+ align-self: start;
118
+ width: 100%;
119
+ padding: 0;
120
+ border: 1px solid rgba(255, 255, 255, 0.08);
121
+ border-radius: 8px;
122
+ background: var(--workspace-card);
123
+ color: var(--workspace-card-ink);
124
+ text-align: left;
125
+ cursor: pointer;
126
+ overflow: hidden;
127
+ transition:
128
+ transform 160ms ease,
129
+ box-shadow 160ms ease,
130
+ border-color 160ms ease;
131
+ }
132
+
133
+ .openpress-workspace-gallery__card:hover,
134
+ .openpress-workspace-gallery__card:focus-visible {
135
+ border-color: rgba(255, 255, 255, 0.28);
136
+ transform: translateY(-2px);
137
+ box-shadow: 0 18px 44px rgba(0, 0, 0, 0.34);
138
+ outline: none;
139
+ }
140
+
141
+ .openpress-workspace-gallery__thumb {
142
+ position: relative;
143
+ display: block;
144
+ width: 100%;
145
+ /* Uniform 4:3 outer slot across every card. The page itself
146
+ letterboxes inside via centered scale, so each Press shows at its
147
+ own true aspect against the gradient background. */
148
+ aspect-ratio: 4 / 3;
149
+ background:
150
+ linear-gradient(
151
+ 135deg,
152
+ color-mix(in srgb, var(--workspace-card-ink) 5%, var(--workspace-card-stage)),
153
+ var(--workspace-card-stage)
154
+ );
155
+ border-bottom: 1px solid var(--workspace-card-line);
156
+ overflow: hidden;
157
+ }
158
+
159
+ .openpress-workspace-gallery__thumb::before {
160
+ content: "";
161
+ position: absolute;
162
+ inset: 0;
163
+ pointer-events: none;
164
+ background-image:
165
+ linear-gradient(rgba(20, 20, 17, 0.05) 1px, transparent 1px),
166
+ linear-gradient(90deg, rgba(20, 20, 17, 0.05) 1px, transparent 1px);
167
+ background-size: 24px 24px;
168
+ opacity: 0.5;
169
+ }
170
+
171
+ .openpress-workspace-gallery__thumb-stage {
172
+ position: absolute;
173
+ inset: clamp(0.85rem, 6%, 1.45rem);
174
+ display: grid;
175
+ place-items: center;
176
+ }
177
+
178
+ .openpress-workspace-gallery__thumb-frame {
179
+ position: relative;
180
+ box-shadow:
181
+ 0 18px 36px rgba(20, 20, 17, 0.18),
182
+ 0 0 0 1px rgba(20, 20, 17, 0.08);
183
+ }
184
+
185
+ .openpress-workspace-gallery__thumb-stage .openpress-public-page {
186
+ display: block;
187
+ pointer-events: none;
188
+ user-select: none;
189
+ }
190
+
191
+ .openpress-workspace-gallery__thumb-placeholder {
192
+ position: absolute;
193
+ inset: clamp(0.85rem, 6%, 1.45rem);
194
+ display: grid;
195
+ place-items: center;
196
+ }
197
+
198
+ .openpress-workspace-gallery__thumb-skel {
199
+ display: block;
200
+ width: 70%;
201
+ height: 70%;
202
+ background:
203
+ repeating-linear-gradient(
204
+ 135deg,
205
+ rgba(20, 20, 17, 0.04) 0 6px,
206
+ transparent 6px 14px
207
+ ),
208
+ #fff;
209
+ border: 1px solid var(--workspace-card-line);
210
+ border-radius: 3px;
211
+ box-shadow: 0 14px 28px rgba(20, 20, 17, 0.14);
212
+ }
213
+
214
+ .openpress-workspace-gallery__thumb-placeholder[data-state="loading"] .openpress-workspace-gallery__thumb-skel {
215
+ animation: openpress-gallery-skel-pulse 1.4s ease-in-out infinite;
216
+ }
217
+
218
+ @keyframes openpress-gallery-skel-pulse {
219
+ 0%, 100% { opacity: 1; }
220
+ 50% { opacity: 0.55; }
221
+ }
222
+
223
+ .openpress-workspace-gallery__body {
224
+ display: grid;
225
+ align-content: space-between;
226
+ gap: 1.2rem;
227
+ min-height: 6.75rem;
228
+ padding: 1.1rem 1.22rem 1.15rem;
229
+ background: var(--workspace-card);
230
+ }
231
+
232
+ .openpress-workspace-gallery__title {
233
+ display: block;
234
+ color: var(--workspace-card-ink);
235
+ font-size: 1rem;
236
+ font-weight: 700;
237
+ line-height: 1.2;
238
+ white-space: nowrap;
239
+ overflow: hidden;
240
+ text-overflow: ellipsis;
241
+ }
242
+
243
+ .openpress-workspace-gallery__meta {
244
+ display: flex;
245
+ align-items: center;
246
+ justify-content: space-between;
247
+ gap: 0.7rem;
248
+ flex-wrap: wrap;
249
+ color: var(--workspace-card-muted);
250
+ font-family: var(--openpress-font-mono, ui-monospace, monospace);
251
+ font-size: 0.66rem;
252
+ letter-spacing: 0.03em;
253
+ }
254
+
255
+ .openpress-workspace-gallery__slug {
256
+ max-width: 13rem;
257
+ color: color-mix(in srgb, var(--workspace-card-ink) 72%, transparent);
258
+ font-weight: 500;
259
+ overflow: hidden;
260
+ text-overflow: ellipsis;
261
+ text-transform: uppercase;
262
+ white-space: nowrap;
263
+ }
264
+
265
+ .openpress-workspace-gallery__dot {
266
+ color: color-mix(in srgb, var(--workspace-card-muted) 55%, transparent);
267
+ }
268
+
269
+ .openpress-workspace-gallery__pages,
270
+ .openpress-workspace-gallery__geom {
271
+ color: var(--workspace-card-muted);
272
+ }
273
+
274
+ .openpress-workspace-gallery__geom {
275
+ display: inline-flex;
276
+ align-items: center;
277
+ min-height: 1.35rem;
278
+ padding: 0 0.48rem;
279
+ border: 1px solid var(--workspace-card-line);
280
+ border-radius: 4px;
281
+ background: rgba(255, 255, 255, 0.36);
282
+ color: color-mix(in srgb, var(--workspace-card-ink) 76%, transparent);
283
+ font-size: 0.62rem;
284
+ white-space: nowrap;
285
+ }
286
+
287
+ @media (max-width: 720px) {
288
+ .openpress-workspace-gallery {
289
+ padding: 2.25rem 1rem 4rem;
290
+ }
291
+
292
+ .openpress-workspace-gallery__header {
293
+ display: grid;
294
+ align-items: start;
295
+ }
296
+
297
+ .openpress-workspace-gallery__grid {
298
+ grid-template-columns: 1fr;
299
+ }
300
+ }
@@ -1,9 +1,5 @@
1
- @import url("/openpress/fonts.css");
2
- @import url("/openpress/tokens.css");
3
- @import url("/openpress/content.css");
4
- @import url("/openpress/components.css");
5
-
6
1
  @import "./openpress/app-shell.css";
2
+ @import "./openpress/workspace-gallery.css";
7
3
  @import "./openpress/workbench.css";
8
4
  @import "./openpress/workbench-panels.css";
9
5
  @import "./openpress/project-preview-panel.css";
@@ -0,0 +1,8 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ // Workspace path constants injected by vite.config.ts at build time.
4
+ // These come from package-owned workspace conventions and package.json config.
5
+ declare const __OPENPRESS_CONTENT_PATH__: string;
6
+ declare const __OPENPRESS_MEDIA_PATH__: string;
7
+ declare const __OPENPRESS_COMPONENTS_PATH__: string;
8
+ declare const __OPENPRESS_PDF_HREF__: string;
package/tsconfig.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "forceConsistentCasingInFileNames": true,
12
12
  "module": "ESNext",
13
13
  "moduleResolution": "Bundler",
14
- "types": ["node", "vite/client"],
14
+ "types": ["node"],
15
15
  "resolveJsonModule": true,
16
16
  "isolatedModules": true,
17
17
  "noEmit": true,
package/vite.config.ts CHANGED
@@ -54,8 +54,10 @@ const workspaceDefines = {
54
54
  };
55
55
 
56
56
  export default defineConfig({
57
+ root: frameworkRoot,
57
58
  base: "./",
58
59
  cacheDir: path.join(workspaceRoot, ".openpress", "vite-client"),
60
+ publicDir: path.join(workspaceRoot, "public"),
59
61
  plugins: [openpressLocalDeployPlugin(), react()],
60
62
  define: workspaceDefines,
61
63
  resolve: {
@@ -409,7 +411,7 @@ function isLocalDeployConfigured() {
409
411
  function localDeploySetupMessage() {
410
412
  if (isLocalDeployConfigured()) return undefined;
411
413
  if (openpressConfig.deploy.adapter === "cloudflare-pages") {
412
- return "Cloudflare Pages deployment requires `deploy.projectName` in openpress.config.mjs.";
414
+ return "Cloudflare Pages deployment requires `openpress.deploy.projectName` in package.json.";
413
415
  }
414
416
  return `Deployment adapter \`${openpressConfig.deploy.adapter}\` is not configured.`;
415
417
  }
@@ -472,18 +474,16 @@ function getLocalDeploymentSourcePaths() {
472
474
  openpressConfig.paths.designDoc,
473
475
  openpressConfig.paths.componentsDir,
474
476
  path.join(frameworkRoot, "src"),
475
- path.join(workspaceRoot, "index.html"),
477
+ path.join(frameworkRoot, "index.html"),
478
+ path.join(frameworkRoot, "vite.config.ts"),
476
479
  path.join(workspaceRoot, "package.json"),
477
480
  path.join(workspaceRoot, "openpress.config.mjs"),
478
481
  openpressConfig.configPath,
479
- path.join(workspaceRoot, "vite.config.ts"),
480
482
  ];
481
483
  }
482
484
 
483
485
  function openpressCliCommand(args: string[]) {
484
- const relativeCliPath = path.relative(workspaceRoot, openpressCliPath).replaceAll("\\", "/");
485
- const displayCliPath = relativeCliPath && !relativeCliPath.startsWith("../") ? relativeCliPath : openpressCliPath;
486
- return `node ${displayCliPath} ${args.join(" ")}`;
486
+ return `open-press ${args.join(" ")}`;
487
487
  }
488
488
 
489
489
  async function findNewestLocalSourceMtime(paths: string[]) {
@@ -1,24 +0,0 @@
1
- import { initWorkspace, listStylePackSkills } from "../init.mjs";
2
- import { formatDisplayPath, parseInitOptions } from "./_shared.mjs";
3
-
4
- export const needsWorkspace = false;
5
-
6
- export async function run({ argv }) {
7
- const options = parseInitOptions(argv);
8
- if (!options.target) {
9
- console.error("openpress init: target path is required");
10
- console.error("Usage: openpress init <target> [--skill <name>] [--force]");
11
- const available = await listStylePackSkills();
12
- if (available.length) console.error(`Style packs available: ${available.join(", ")}`);
13
- return 1;
14
- }
15
- const result = await initWorkspace(options);
16
- const displayPath = formatDisplayPath(result.targetPath);
17
- console.log(`OpenPress init: created ${displayPath} from style pack "${result.skill}".`);
18
- console.log("Next steps:");
19
- console.log(` cd ${displayPath}`);
20
- console.log(" # 填入 openpress.config.mjs 的 title / subtitle / organization");
21
- console.log(" # 改 document/index.tsx 與 document/chapters/**/*.mdx 為實際內容");
22
- console.log(" node engine/cli.mjs validate");
23
- return 0;
24
- }
package/engine/init.mjs DELETED
@@ -1,90 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
-
5
- const SELF_DIR = path.dirname(fileURLToPath(import.meta.url));
6
- const ENGINE_ROOT = path.resolve(SELF_DIR, "..");
7
- const SKILLS_DIR = path.join(ENGINE_ROOT, "skills");
8
-
9
- const DEFAULT_SKILL = "editorial-monograph";
10
-
11
- export async function initWorkspace({ target, skill = DEFAULT_SKILL, force = false }) {
12
- if (!target) throw new Error("openpress init: target path is required");
13
- const targetPath = path.resolve(target);
14
-
15
- const starterPath = path.join(SKILLS_DIR, skill, "starter");
16
- try {
17
- const stat = await fs.stat(starterPath);
18
- if (!stat.isDirectory()) {
19
- throw new Error(`openpress init: skill "${skill}" has no starter/ directory at ${starterPath}`);
20
- }
21
- } catch (error) {
22
- if (error?.code === "ENOENT") {
23
- const available = await listStylePackSkills();
24
- throw new Error(
25
- `openpress init: skill "${skill}" not found or has no starter. ` +
26
- `Available style packs: ${available.join(", ") || "(none)"}`,
27
- );
28
- }
29
- throw error;
30
- }
31
-
32
- if (!force) {
33
- try {
34
- const stat = await fs.stat(targetPath);
35
- if (stat.isDirectory()) {
36
- const entries = await fs.readdir(targetPath);
37
- if (entries.length > 0) {
38
- throw new Error(`openpress init: target ${targetPath} exists and is not empty. Pass --force to overwrite.`);
39
- }
40
- } else {
41
- throw new Error(`openpress init: target ${targetPath} exists and is not a directory.`);
42
- }
43
- } catch (error) {
44
- if (error?.code !== "ENOENT") throw error;
45
- }
46
- }
47
-
48
- await fs.mkdir(targetPath, { recursive: true });
49
- await copyDirectory(starterPath, targetPath);
50
-
51
- return { targetPath, skill };
52
- }
53
-
54
- export async function listStylePackSkills() {
55
- try {
56
- const entries = await fs.readdir(SKILLS_DIR, { withFileTypes: true });
57
- const names = [];
58
- for (const entry of entries) {
59
- if (!entry.isDirectory()) continue;
60
- const starter = path.join(SKILLS_DIR, entry.name, "starter");
61
- try {
62
- const stat = await fs.stat(starter);
63
- if (stat.isDirectory()) names.push(entry.name);
64
- } catch {
65
- // skill without starter/ is not a style pack — skip
66
- }
67
- }
68
- return names.sort();
69
- } catch (error) {
70
- if (error?.code === "ENOENT") return [];
71
- throw error;
72
- }
73
- }
74
-
75
- async function copyDirectory(source, destination) {
76
- await fs.mkdir(destination, { recursive: true });
77
- for (const entry of await fs.readdir(source, { withFileTypes: true })) {
78
- if (entry.name === ".DS_Store") continue;
79
- const sourcePath = path.join(source, entry.name);
80
- const destPath = path.join(destination, entry.name);
81
- if (entry.isDirectory()) {
82
- await copyDirectory(sourcePath, destPath);
83
- } else if (entry.isFile()) {
84
- await fs.copyFile(sourcePath, destPath);
85
- } else if (entry.isSymbolicLink()) {
86
- const link = await fs.readlink(sourcePath);
87
- await fs.symlink(link, destPath);
88
- }
89
- }
90
- }