@mlightcad/cad-simple-viewer-cli 1.7.4 → 1.7.6-preview.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.
@@ -14,13 +14,16 @@
14
14
  * extension, with path separators turned into underscores. Same-stem
15
15
  * `.dwg`/`.dxf` pairs get a `_dwg` / `_dxf` suffix so they do not collide.
16
16
  *
17
+ * Reuses one Chromium session across all drawings (WASM / fonts stay warm).
18
+ *
17
19
  * Usage (from packages/cad-simple-viewer-cli after build):
18
20
  * node examples/batch-export-html-multi-preview.mjs <inputDir> <outputDir>
19
21
  */
20
- import { spawn, spawnSync } from 'node:child_process'
22
+ import { spawnSync } from 'node:child_process'
21
23
  import { existsSync } from 'node:fs'
22
24
  import {
23
25
  copyFile,
26
+ cp,
24
27
  mkdir,
25
28
  mkdtemp,
26
29
  readdir,
@@ -29,11 +32,11 @@ import {
29
32
  } from 'node:fs/promises'
30
33
  import os from 'node:os'
31
34
  import path from 'node:path'
32
- import { fileURLToPath } from 'node:url'
35
+ import { fileURLToPath, pathToFileURL } from 'node:url'
33
36
 
34
37
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
35
38
  const packageRoot = path.resolve(__dirname, '..')
36
- const cliJs = path.join(packageRoot, 'dist', 'cli.js')
39
+ const runHeadlessJs = path.join(packageRoot, 'dist', 'runHeadless.js')
37
40
  const scriptPath = path.join(__dirname, 'export-html-multi-preview.scr')
38
41
 
39
42
  const DRAWING_EXT = new Set(['.dwg', '.dxf'])
@@ -89,34 +92,86 @@ function drawingFolderName(inputDir, drawingPath, used) {
89
92
  return name
90
93
  }
91
94
 
92
- function runCli(input, outputDir) {
93
- return new Promise((resolve, reject) => {
94
- const child = spawn(
95
- process.execPath,
96
- [
97
- cliJs,
98
- '-i',
99
- input,
100
- '-s',
101
- scriptPath,
102
- '-o',
103
- outputDir,
104
- '--mode',
105
- 'read'
106
- ],
107
- { stdio: 'inherit' }
95
+ function delay(ms) {
96
+ return new Promise(resolve => setTimeout(resolve, ms))
97
+ }
98
+
99
+ const REPLACE_DIR_RETRY = new Set([
100
+ 'EPERM',
101
+ 'EACCES',
102
+ 'EBUSY',
103
+ 'ENOTEMPTY',
104
+ 'EMFILE',
105
+ 'ENFILE'
106
+ ])
107
+
108
+ /**
109
+ * Move `src` onto `dest`. Windows often returns EPERM from rename while
110
+ * Defender or a file watcher still has a file inside a freshly unpacked
111
+ * directory open (sharing without delete). Retry, then copy into place.
112
+ */
113
+ async function replaceDir(src, dest) {
114
+ const waits = [0, 50, 100, 200, 400, 800, 1600]
115
+ let lastError
116
+ for (const wait of waits) {
117
+ if (wait) await delay(wait)
118
+ try {
119
+ await rm(dest, {
120
+ recursive: true,
121
+ force: true,
122
+ maxRetries: 5,
123
+ retryDelay: 100
124
+ })
125
+ await rename(src, dest)
126
+ return
127
+ } catch (error) {
128
+ lastError = error
129
+ const code = error && error.code
130
+ if (code === 'EXDEV') break
131
+ if (!REPLACE_DIR_RETRY.has(code)) throw error
132
+ }
133
+ }
134
+
135
+ let copyError
136
+ for (const wait of [0, 200, 500, 1000, 2000]) {
137
+ if (wait) await delay(wait)
138
+ try {
139
+ await rm(dest, {
140
+ recursive: true,
141
+ force: true,
142
+ maxRetries: 5,
143
+ retryDelay: 100
144
+ })
145
+ await cp(src, dest, { recursive: true })
146
+ copyError = undefined
147
+ break
148
+ } catch (error) {
149
+ copyError = error
150
+ const code = error && error.code
151
+ if (code !== 'EXDEV' && !REPLACE_DIR_RETRY.has(code)) break
152
+ }
153
+ }
154
+ if (copyError) {
155
+ await rm(dest, { recursive: true, force: true }).catch(() => {})
156
+ const renameMsg =
157
+ lastError instanceof Error ? lastError.message : String(lastError)
158
+ const copyMsg =
159
+ copyError instanceof Error ? copyError.message : String(copyError)
160
+ throw new Error(
161
+ `Failed to move ${src} to ${dest} (${renameMsg}); copy fallback failed: ${copyMsg}`
108
162
  )
109
- child.on('error', reject)
110
- child.on('exit', code => {
111
- if (code === 0) resolve()
112
- else
113
- reject(
114
- new Error(
115
- `cad-simple-viewer-cli exited with code ${code} for ${input}`
116
- )
117
- )
163
+ }
164
+ try {
165
+ await rm(src, {
166
+ recursive: true,
167
+ force: true,
168
+ maxRetries: 8,
169
+ retryDelay: 200
118
170
  })
119
- })
171
+ } catch (error) {
172
+ const message = error instanceof Error ? error.message : String(error)
173
+ console.warn(`Could not remove temporary directory ${src}: ${message}`)
174
+ }
120
175
  }
121
176
 
122
177
  function unzipWithTar(zipPath, destDir) {
@@ -162,10 +217,14 @@ async function packageDrawing(workDir, targetDir, sourceDrawing) {
162
217
  const destName = ext === '.dxf' ? 'drawing.dxf' : 'drawing.dwg'
163
218
  await copyFile(sourceDrawing, path.join(stageDir, destName))
164
219
 
165
- await rm(targetDir, { recursive: true, force: true })
166
- await rename(stageDir, targetDir)
220
+ await replaceDir(stageDir, targetDir)
167
221
  } catch (error) {
168
- await rm(stageDir, { recursive: true, force: true })
222
+ await rm(stageDir, {
223
+ recursive: true,
224
+ force: true,
225
+ maxRetries: 5,
226
+ retryDelay: 100
227
+ }).catch(() => {})
169
228
  throw error
170
229
  }
171
230
  }
@@ -181,7 +240,7 @@ async function main() {
181
240
  process.exitCode = 1
182
241
  return
183
242
  }
184
- if (!existsSync(cliJs)) {
243
+ if (!existsSync(runHeadlessJs)) {
185
244
  console.error(
186
245
  'CLI not built. Run: pnpm --filter @mlightcad/cad-simple-viewer-cli build'
187
246
  )
@@ -208,25 +267,36 @@ async function main() {
208
267
  `Found ${drawings.length} drawing(s). Output (demo-drawings layout): ${outputDir}`
209
268
  )
210
269
 
270
+ const { HeadlessCadSession } = await import(pathToFileURL(runHeadlessJs).href)
271
+ const session = await HeadlessCadSession.create()
211
272
  const usedFolders = new Set()
212
273
  let failed = 0
213
274
 
214
- for (const drawing of drawings) {
215
- const folder = drawingFolderName(inputDir, drawing, usedFolders)
216
- const targetDir = path.join(outputDir, folder)
217
- console.log(`\n=== ${drawing} → ${folder}/ ===`)
275
+ try {
276
+ for (const drawing of drawings) {
277
+ const folder = drawingFolderName(inputDir, drawing, usedFolders)
278
+ const targetDir = path.join(outputDir, folder)
279
+ console.log(`\n=== ${drawing} → ${folder}/ ===`)
218
280
 
219
- const workDir = await mkdtemp(path.join(os.tmpdir(), 'cad-cli-demo-'))
220
- try {
221
- await runCli(drawing, workDir)
222
- await packageDrawing(workDir, targetDir, drawing)
223
- console.log(`Packaged ${targetDir}`)
224
- } catch (error) {
225
- failed++
226
- console.error(error instanceof Error ? error.message : String(error))
227
- } finally {
228
- await rm(workDir, { recursive: true, force: true })
281
+ const workDir = await mkdtemp(path.join(os.tmpdir(), 'cad-cli-demo-'))
282
+ try {
283
+ await session.run({
284
+ inputPath: drawing,
285
+ scriptPath,
286
+ outputDir: workDir,
287
+ mode: 'read'
288
+ })
289
+ await packageDrawing(workDir, targetDir, drawing)
290
+ console.log(`Packaged ${targetDir}`)
291
+ } catch (error) {
292
+ failed++
293
+ console.error(error instanceof Error ? error.message : String(error))
294
+ } finally {
295
+ await rm(workDir, { recursive: true, force: true })
296
+ }
229
297
  }
298
+ } finally {
299
+ await session.close()
230
300
  }
231
301
 
232
302
  console.log(`\nDone. success=${drawings.length - failed} failed=${failed}`)
@@ -0,0 +1,2 @@
1
+ ; Open-only timing: wait for idle in the runner, then exit.
2
+ quit
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mlightcad/cad-simple-viewer-cli",
3
- "version": "1.7.4",
3
+ "version": "1.7.6-preview.0",
4
4
  "description": "AcCoreConsole-style headless CLI: run .scr command scripts against DXF/DWG drawings",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -42,17 +42,17 @@
42
42
  "script"
43
43
  ],
44
44
  "dependencies": {
45
- "@mlightcad/data-model": "^1.15.1",
46
- "@mlightcad/libredwg-converter": "^3.15.1",
47
- "@mlightcad/mtext-parser": "^1.5.3",
48
- "@mlightcad/mtext-renderer": "^0.13.2",
45
+ "@mlightcad/data-model": "^1.15.7",
46
+ "@mlightcad/libredwg-converter": "^3.15.7",
47
+ "@mlightcad/mtext-parser": "^1.5.5",
48
+ "@mlightcad/mtext-renderer": "^0.13.4",
49
49
  "commander": "^12.1.0",
50
50
  "playwright": "^1.49.1",
51
- "@mlightcad/cad-html-plugin": "1.7.4",
52
- "@mlightcad/pdf-renderer": "1.7.4",
53
- "@mlightcad/cad-simple-viewer": "1.7.4",
54
- "@mlightcad/cad-pdf-plugin": "1.7.4",
55
- "@mlightcad/cad-svg-plugin": "1.7.4"
51
+ "@mlightcad/cad-html-plugin": "1.7.6-preview.0",
52
+ "@mlightcad/cad-pdf-plugin": "1.7.6-preview.0",
53
+ "@mlightcad/cad-simple-viewer": "1.7.6-preview.0",
54
+ "@mlightcad/pdf-renderer": "1.7.6-preview.0",
55
+ "@mlightcad/cad-svg-plugin": "1.7.6-preview.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@types/node": "^20.14.9",