@opencraw/studio 0.0.12 → 0.0.14

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/dist/index.esm.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import { join, dirname, basename, extname, normalize, sep, resolve } from 'node:path';
2
2
  import { createServer } from 'node:http';
3
+ import { promisify, stripVTControlCharacters } from 'node:util';
3
4
  import { fileURLToPath, pathToFileURL } from 'node:url';
4
5
  import { z } from 'zod';
5
6
  import { BODY_KINDS, HttpClient, BrowserClient, NODE_ID_ATTRIBUTE, sessionStateOf, rowsOfCells, parseXml, findDeckTables, findGridTables, analyzeTables, parseRegion, regionText, deckRegionText, isDeckDocument, readRecipeSource, parseInputRecipe, parseOutputRecipe, bindRecipeSet, memorySink, createCrawler, traceLine, UnknownHookError, resolveMissingPolicy, isWorkbookDocument, RecipeBindingError, RecipeValidationError, countMatches, matchesOf, tryParseJson, isPdfDocument, BrowserProfiles } from '@opencraw/core';
6
7
  import { randomBytes, createHash } from 'node:crypto';
7
8
  import { execFile } from 'node:child_process';
8
9
  import { createRequire } from 'node:module';
9
- import { promisify } from 'node:util';
10
10
  import { load } from 'cheerio';
11
- import { readFile, writeFile, mkdir } from 'node:fs/promises';
11
+ import { readFile, writeFile, rm, rename, mkdir } from 'node:fs/promises';
12
12
  import { describeJson } from '@opencraw/probe';
13
13
  import { tmpdir } from 'node:os';
14
14
  import { existsSync, createReadStream } from 'node:fs';
@@ -2125,9 +2125,9 @@ async function runToResult(crawler, set, recipeId, records, rejectedRecords, ste
2125
2125
  }
2126
2126
  /** What a run that could not start reports. A hook is the author's own code, which Studio never loads (issue #150): say so, instead of leaving "unknown hook" to read as a typo. */
2127
2127
  function failureMessage(error) {
2128
- if (error instanceof UnknownHookError) return `${error.message}. Studio does not load hooks, so a recipe that uses one cannot be sample-run here; run it with the cli or the mcp server, or temporarily replace the hook step to preview the rest.`;
2129
- if (error instanceof Error) return error.message;
2130
- return String(error);
2128
+ if (error instanceof UnknownHookError) return `${stripVTControlCharacters(error.message)}. Studio does not load hooks, so a recipe that uses one cannot be sample-run here; run it with the cli or the mcp server, or temporarily replace the hook step to preview the rest.`;
2129
+ if (error instanceof Error) return stripVTControlCharacters(error.message);
2130
+ return stripVTControlCharacters(String(error));
2131
2131
  }
2132
2132
 
2133
2133
  /** @returns A fresh, empty cache. */
@@ -2877,11 +2877,40 @@ function parseOne(file, content, kind, parse) {
2877
2877
  * round-trip for a hand-formatted file is future work; see this file's test
2878
2878
  * for what is guaranteed today.
2879
2879
  *
2880
+ * The text goes to a sibling temporary file that is then renamed over the
2881
+ * recipe, so a reader (or a crash) never sees a half-written file. Windows
2882
+ * refuses a rename over a file another process has open (`EPERM`, `EBUSY`),
2883
+ * so the rename is retried briefly and then gives way to a plain write.
2884
+ *
2880
2885
  * @param path - The recipe file to write.
2881
2886
  * @param recipe - The whole recipe object.
2882
2887
  */
2883
2888
  async function saveRecipe(path, recipe) {
2884
- await writeFile(path, `${JSON.stringify(recipe, null, 2)}\n`, 'utf8');
2889
+ const temporary = `${path}.${String(process.pid)}.tmp`;
2890
+ const text = `${JSON.stringify(recipe, null, 2)}\n`;
2891
+ await writeFile(temporary, text, 'utf8');
2892
+ try {
2893
+ await renameWithRetry(temporary, path);
2894
+ } catch {
2895
+ await writeFile(path, text, 'utf8');
2896
+ } finally {
2897
+ await rm(temporary, {
2898
+ force: true
2899
+ });
2900
+ }
2901
+ }
2902
+ const RENAME_ATTEMPTS = 5;
2903
+ const RENAME_PAUSE_MS = 20;
2904
+ async function renameWithRetry(from, to) {
2905
+ for (let attempt = 1;; attempt += 1) {
2906
+ try {
2907
+ await rename(from, to);
2908
+ return;
2909
+ } catch (error) {
2910
+ if (attempt >= RENAME_ATTEMPTS) throw error;
2911
+ await new Promise(resolve => setTimeout(resolve, RENAME_PAUSE_MS));
2912
+ }
2913
+ }
2885
2914
  }
2886
2915
 
2887
2916
  /** How many `name-2.ext`, `name-3.ext`… are tried before giving up on a folder full of same-named copies. */
@@ -5090,7 +5119,7 @@ async function handlePdfBytesRequest(response, state, query) {
5090
5119
  response.end(Buffer.from(bytes));
5091
5120
  } catch (error) {
5092
5121
  respondJson(response, 500, {
5093
- error: error instanceof Error ? error.message : String(error)
5122
+ error: messageOf(error)
5094
5123
  });
5095
5124
  }
5096
5125
  }
@@ -5108,7 +5137,7 @@ async function handleCommand(request, response, state) {
5108
5137
  command = parsed.data;
5109
5138
  } catch (error) {
5110
5139
  respondJson(response, 400, {
5111
- error: error instanceof Error ? error.message : String(error)
5140
+ error: messageOf(error)
5112
5141
  });
5113
5142
  return;
5114
5143
  }
@@ -5116,7 +5145,7 @@ async function handleCommand(request, response, state) {
5116
5145
  respondJson(response, 200, await dispatch(command, state));
5117
5146
  } catch (error) {
5118
5147
  respondJson(response, 500, {
5119
- error: error instanceof Error ? error.message : String(error)
5148
+ error: messageOf(error)
5120
5149
  });
5121
5150
  }
5122
5151
  }
@@ -5246,6 +5275,10 @@ function handleUpgrade(request, socket, token, state) {
5246
5275
  state.sockets.delete(connection);
5247
5276
  });
5248
5277
  }
5278
+ /** An error's message as plain text: Playwright colours its call logs with ANSI codes, which a browser would show as raw escapes (issue #187). */
5279
+ function messageOf(error) {
5280
+ return stripVTControlCharacters(error instanceof Error ? error.message : String(error));
5281
+ }
5249
5282
 
5250
5283
  /**
5251
5284
  * Runs `opencraw-studio`: starts the server, opens `folder` (if given) as
@@ -9,6 +9,11 @@
9
9
  * round-trip for a hand-formatted file is future work; see this file's test
10
10
  * for what is guaranteed today.
11
11
  *
12
+ * The text goes to a sibling temporary file that is then renamed over the
13
+ * recipe, so a reader (or a crash) never sees a half-written file. Windows
14
+ * refuses a rename over a file another process has open (`EPERM`, `EBUSY`),
15
+ * so the rename is retried briefly and then gives way to a plain write.
16
+ *
12
17
  * @param path - The recipe file to write.
13
18
  * @param recipe - The whole recipe object.
14
19
  */