@opencraw/studio 0.0.8 → 0.0.9

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.md CHANGED
@@ -1,11 +1,37 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/russoedu/open.craw/main/docs/assets/opencraw-logo.svg" alt="OpenCraw" width="360">
3
+ </p>
4
+
1
5
  # @opencraw/studio
2
6
 
3
- Generated by [MoNecromanCI](https://github.com/russoedu/MoNecromanCi).
7
+ A visual editor for [OpenCraw](https://github.com/russoedu/open.craw) recipes: open a site or a document,
8
+ click the data you want, and get a recipe that runs — the same engine, the same JSON files, no hidden state.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ npm install -g @opencraw/cli @opencraw/studio
14
+ ```
15
+
16
+ ## Start
17
+
18
+ ```sh
19
+ opencraw studio recipes/ # the cli delegates to this package when it's installed
20
+ npx @opencraw/studio recipes/ # or run it directly, without the cli
21
+ ```
22
+
23
+ Either opens a local server (`127.0.0.1` only, behind a random access token) and prints the URL to open:
4
24
 
5
- ## Building
25
+ ```text
26
+ OpenCraw Studio: http://127.0.0.1:52341/?token=8f2a1c9e4b7d3f6a0e5c8b1d2a4f7e9c
27
+ Workspace: /home/you/project/recipes
28
+ ```
6
29
 
7
- Run `nx build @opencraw/studio` to build this project.
30
+ `recipes/` can already have `.input.json`/`.output.json` files in it, or be empty — the studio's own
31
+ **+ New recipe** writes a minimal, valid pair to start from.
8
32
 
9
- ## Running unit tests
33
+ ## Learn more
10
34
 
11
- Run `nx test @opencraw/studio` to execute the unit tests via [Jest](https://jestjs.io).
35
+ [**The OpenCraw Studio manual**](https://github.com/russoedu/open.craw/blob/main/docs/studio/manual.md)
36
+ builds one real recipe end to end, tab by tab: picking on a live page, the Record tab's mapping, the
37
+ document canvases (JSON, PDF, Excel/CSV, PowerPoint), recording a login, and the JSON editor.
package/dist/index.esm.js CHANGED
@@ -1,9 +1,12 @@
1
- import { join, normalize, sep, extname, dirname, resolve } from 'node:path';
1
+ import { join, dirname, normalize, sep, extname, resolve } from 'node:path';
2
2
  import { createServer } from 'node:http';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { z } from 'zod';
5
- import { BODY_KINDS, BrowserClient, HttpClient, NODE_ID_ATTRIBUTE, rowsOfCells, parseXml, findDeckTables, findGridTables, analyzeTables, isDeckDocument, readRecipeSource, parseInputRecipe, parseOutputRecipe, bindRecipeSet, memorySink, createCrawler, traceLine, resolveMissingPolicy, isWorkbookDocument, countMatches, matchesOf, tryParseJson, RecipeBindingError, RecipeValidationError, isPdfDocument, BrowserProfiles } from '@opencraw/core';
5
+ import { BODY_KINDS, HttpClient, BrowserClient, NODE_ID_ATTRIBUTE, rowsOfCells, parseXml, findDeckTables, findGridTables, analyzeTables, isDeckDocument, readRecipeSource, parseInputRecipe, parseOutputRecipe, bindRecipeSet, memorySink, createCrawler, traceLine, resolveMissingPolicy, isWorkbookDocument, countMatches, matchesOf, tryParseJson, RecipeBindingError, RecipeValidationError, isPdfDocument, BrowserProfiles } from '@opencraw/core';
6
6
  import { randomBytes, createHash } from 'node:crypto';
7
+ import { execFile } from 'node:child_process';
8
+ import { createRequire } from 'node:module';
9
+ import { promisify } from 'node:util';
7
10
  import { load } from 'cheerio';
8
11
  import { readFile, writeFile } from 'node:fs/promises';
9
12
  import { describeJson } from '@opencraw/probe';
@@ -786,6 +789,85 @@ function cookieToken(header) {
786
789
  return undefined;
787
790
  }
788
791
 
792
+ const execFileAsync = promisify(execFile);
793
+ /** `playwright`'s own `exports` map doesn't whitelist a `./cli` subpath, so its CLI is resolved off `playwright/package.json`'s real location — the same technique `@opencraw/captcha-tesseract`'s `bundledLangPath()` uses for a bundled data path. */
794
+ function playwrightCliPath() {
795
+ const require = createRequire(import.meta.url);
796
+ return join(dirname(require.resolve('playwright/package.json')), 'cli.js');
797
+ }
798
+ async function runPlaywrightInstall() {
799
+ await execFileAsync(process.execPath, [playwrightCliPath(), 'install', 'chromium']);
800
+ }
801
+ let installing;
802
+ /**
803
+ * Runs `playwright install chromium` at most once per process. A failed install clears the memo,
804
+ * so the next browser launch attempt retries instead of remembering failure forever.
805
+ */
806
+ async function runOnce(run) {
807
+ try {
808
+ await run();
809
+ console.log('@opencraw/studio: Chromium installed.');
810
+ } catch (error) {
811
+ installing = undefined;
812
+ console.log('@opencraw/studio: "playwright install chromium" failed.');
813
+ throw error;
814
+ }
815
+ }
816
+ function installChromiumOnce(run = runPlaywrightInstall) {
817
+ if (installing === undefined) {
818
+ console.log('@opencraw/studio: Playwright\'s Chromium is not downloaded yet — running "playwright install chromium" once...');
819
+ installing = runOnce(run);
820
+ }
821
+ return installing;
822
+ }
823
+
824
+ /** Recognises Playwright's own "no browser downloaded" failure so a launch can be retried after installing one, instead of surfacing Playwright's raw ASCII-art error. */
825
+ function isMissingBrowserError(error) {
826
+ return error instanceof Error && /Executable doesn't exist at/.test(error.message);
827
+ }
828
+
829
+ /**
830
+ * Runs a browser-launching function, and — only when it fails with Playwright's own "no browser
831
+ * downloaded" error, only for a default chromium launch with no explicit binary configured — installs
832
+ * Chromium once and retries, instead of letting Playwright's raw error reach the studio's caller.
833
+ *
834
+ * An explicit `executablePath` or `OPENCRAW_CHROMIUM` means the person (or this repo's own sandbox)
835
+ * already chose a specific binary; a missing-browser failure there is their setup to fix, not
836
+ * something to silently install over.
837
+ */
838
+ async function ensureBrowserLaunch(browser, launch, installChromium = installChromiumOnce) {
839
+ try {
840
+ return await launch();
841
+ } catch (error) {
842
+ if (!isMissingBrowserError(error)) throw error;
843
+ if (browser?.executablePath !== undefined || process.env.OPENCRAW_CHROMIUM !== undefined) {
844
+ throw new Error('@opencraw/studio: no browser found at the configured executable path. Check it, or unset it and let the studio install Chromium itself.', {
845
+ cause: error
846
+ });
847
+ }
848
+ if ((browser?.browserType ?? 'chromium') !== 'chromium') {
849
+ throw new Error(`@opencraw/studio: no ${browser?.browserType} browser found. Run "npx playwright install ${browser?.browserType}" and try again.`, {
850
+ cause: error
851
+ });
852
+ }
853
+ try {
854
+ await installChromium();
855
+ } catch (installError) {
856
+ throw new Error('@opencraw/studio: could not install Chromium automatically. Run "npx playwright install chromium" yourself and try again.', {
857
+ cause: installError
858
+ });
859
+ }
860
+ try {
861
+ return await launch();
862
+ } catch (retryError) {
863
+ if (!isMissingBrowserError(retryError)) throw retryError;
864
+ throw new Error('@opencraw/studio: Chromium was installed but is still not found. Run "npx playwright install chromium" yourself and try again.', {
865
+ cause: retryError
866
+ });
867
+ }
868
+ }
869
+ }
870
+
789
871
  /**
790
872
  * Fetches an input recipe's start page as the engine would see it: a fresh
791
873
  * Playwright page in web mode, core's HTTP client in api mode. Returns plain
@@ -809,7 +891,7 @@ async function fetchStartPage(input, browser) {
809
891
  return input.mode === 'web' ? fetchWeb(point.url, browser) : fetchApi(point.url);
810
892
  }
811
893
  async function fetchWeb(url, browser) {
812
- const client = await BrowserClient.launch(browser);
894
+ const client = await ensureBrowserLaunch(browser, () => BrowserClient.launch(browser));
813
895
  try {
814
896
  const session = await client.newSession();
815
897
  try {
@@ -985,7 +1067,7 @@ async function takeSnapshot(input, stepPath, browser) {
985
1067
  return input.mode === 'web' ? snapshotWeb(point.url, browser) : snapshotApi(point.url);
986
1068
  }
987
1069
  async function snapshotWeb(url, browser) {
988
- const client = await BrowserClient.launch(browser);
1070
+ const client = await ensureBrowserLaunch(browser, () => BrowserClient.launch(browser));
989
1071
  try {
990
1072
  const session = await client.newSession();
991
1073
  try {
@@ -2969,7 +3051,7 @@ async function responsesSeen(input, stepPath, browser) {
2969
3051
  if (input.mode !== 'web') return [];
2970
3052
  const point = input.start[0];
2971
3053
  if (point === undefined) throw new Error(`recipe "${input.id}" has no start point`);
2972
- const client = await BrowserClient.launch(browser);
3054
+ const client = await ensureBrowserLaunch(browser, () => BrowserClient.launch(browser));
2973
3055
  try {
2974
3056
  const session = await client.newSession();
2975
3057
  try {
@@ -3957,7 +4039,7 @@ async function openRecorderSession(options, callbacks) {
3957
4039
  ...options.browser,
3958
4040
  headless: false
3959
4041
  });
3960
- const session = await profiles.open(RECORDER_PROFILE_NAME, {}, {});
4042
+ const session = await ensureBrowserLaunch(options.browser, () => profiles.open(RECORDER_PROFILE_NAME, {}, {}));
3961
4043
  const context = session.context;
3962
4044
  const steps = [];
3963
4045
  let closed = false;
@@ -0,0 +1,12 @@
1
+ import type { BrowserSessionConfig } from '@opencraw/core';
2
+ /**
3
+ * Runs a browser-launching function, and — only when it fails with Playwright's own "no browser
4
+ * downloaded" error, only for a default chromium launch with no explicit binary configured — installs
5
+ * Chromium once and retries, instead of letting Playwright's raw error reach the studio's caller.
6
+ *
7
+ * An explicit `executablePath` or `OPENCRAW_CHROMIUM` means the person (or this repo's own sandbox)
8
+ * already chose a specific binary; a missing-browser failure there is their setup to fix, not
9
+ * something to silently install over.
10
+ */
11
+ export declare function ensureBrowserLaunch<T>(browser: BrowserSessionConfig | undefined, launch: () => Promise<T>, installChromium?: (run?: () => Promise<void>) => Promise<void>): Promise<T>;
12
+ //# sourceMappingURL=ensure-browser-launch.use-case.d.ts.map
@@ -0,0 +1,2 @@
1
+ export { ensureBrowserLaunch } from './ensure-browser-launch.use-case.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,2 @@
1
+ export declare function installChromiumOnce(run?: () => Promise<void>): Promise<void>;
2
+ //# sourceMappingURL=install-chromium.use-case.d.ts.map
@@ -0,0 +1,3 @@
1
+ /** Recognises Playwright's own "no browser downloaded" failure so a launch can be retried after installing one, instead of surfacing Playwright's raw ASCII-art error. */
2
+ export declare function isMissingBrowserError(error: unknown): boolean;
3
+ //# sourceMappingURL=missing-browser.policy.d.ts.map