@opencraw/studio 0.0.22 → 0.0.24

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
@@ -3,7 +3,7 @@ import { createServer } from 'node:http';
3
3
  import { promisify, stripVTControlCharacters } from 'node:util';
4
4
  import { fileURLToPath, pathToFileURL } from 'node:url';
5
5
  import { z } from 'zod';
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
+ import { BODY_KINDS, HttpClient, BrowserClient, NODE_ID_ATTRIBUTE, sessionStateOf, rowsOfCells, parseXml, findDeckTables, findGridTables, analyzeTables, parseRegion, regionText, deckRegionText, isDeckDocument, readRecipeSource, parseInputRecipe, parseOutputRecipe, calloutLabel, bindRecipeSet, memorySink, createCrawler, traceLine, UnknownHookError, resolveMissingPolicy, isWorkbookDocument, RecipeBindingError, RecipeValidationError, countMatches, matchesOf, tryParseJson, isPdfDocument, BrowserProfiles } from '@opencraw/core';
7
7
  import { randomBytes, createHash } from 'node:crypto';
8
8
  import { execFile } from 'node:child_process';
9
9
  import { createRequire } from 'node:module';
@@ -56,7 +56,9 @@ const openWorkspaceCommandSchema = z.object({
56
56
  const runSampleCommandSchema = z.object({
57
57
  type: z.literal('run-sample'),
58
58
  recipeId: z.string().min(1),
59
- budget: sampleBudgetSchema.optional()
59
+ budget: sampleBudgetSchema.optional(),
60
+ /** Values to answer with instead of calling a hook, by hook name, for this run only: how a sample skips a slow or paid callout. */
61
+ stubs: z.record(z.string(), z.unknown()).optional()
60
62
  });
61
63
  /** Closes the crawler of the run in progress, if any; a no-op otherwise. */
62
64
  const stopRunCommandSchema = z.object({
@@ -503,10 +505,18 @@ const recipeListingSchema = z.object({
503
505
  /** The Steps tab's view of this file's `steps`, from `@opencraw/studio`'s `scope-outline` slice. `input` recipes only; `undefined` for an `output` or `unknown` file, which has no steps to show. */
504
506
  outline: outlineViewSchema.optional()
505
507
  });
508
+ /** A hook, captcha solver or access plugin that calls outside the process. */
509
+ const remoteCalloutSchema = z.object({
510
+ kind: z.enum(['hook', 'captcha', 'access']),
511
+ name: z.string(),
512
+ label: z.string()
513
+ });
506
514
  /** The hooks Studio was started with (`opencraw studio --hooks <file>`, issue #150): where they came from and what they are called, for the warning bar and the hook-name suggestions. */
507
515
  const loadedHooksSchema = z.object({
508
516
  source: z.string(),
509
- names: z.array(z.string())
517
+ names: z.array(z.string()),
518
+ /** Which hooks, captcha solvers and access plugins call outside the process (slow, and they may cost): the UI says so, and lets a hook be stubbed for a sample run. */
519
+ remote: z.array(remoteCalloutSchema).optional()
510
520
  });
511
521
  /** What `open-workspace` answers with: every recipe file directly inside the folder, and the hooks Studio runs them with, if any. */
512
522
  const workspaceViewSchema = z.object({
@@ -1987,6 +1997,39 @@ function idOf(content) {
1987
1997
  return content?.id;
1988
1998
  }
1989
1999
 
2000
+ /**
2001
+ * The hooks a sample run uses: the ones Studio was started with, except that
2002
+ *
2003
+ * - a hook the person stubbed returns the stub's value and calls nothing, and
2004
+ * - a hook that calls outside the process says so in the trace, once per call, because it may be slow
2005
+ * and it may cost money.
2006
+ *
2007
+ * @param hooks - The hooks Studio was started with.
2008
+ * @param stubs - Values to answer with instead of calling a hook, by hook name. A name that is not a hook is ignored.
2009
+ * @param announce - Where the trace lines go.
2010
+ * @returns The hooks to run the sample with.
2011
+ */
2012
+ function calloutHooks(hooks, stubs, announce) {
2013
+ if (hooks === undefined) return undefined;
2014
+ const prepared = {};
2015
+ for (const [name, hook] of Object.entries(hooks)) {
2016
+ if (stubs !== undefined && Object.hasOwn(stubs, name)) {
2017
+ const stub = stubs[name];
2018
+ prepared[name] = () => {
2019
+ announce(`☎ hook "${name}" is stubbed: answering ${JSON.stringify(stub) ?? 'undefined'} without calling it`);
2020
+ return stub;
2021
+ };
2022
+ continue;
2023
+ }
2024
+ const label = calloutLabel(hook);
2025
+ prepared[name] = label === undefined ? hook : async (input, args, context) => {
2026
+ announce(`☎ hook "${name}" calls ${label} (slow, and it may cost: stub it to skip the call)`);
2027
+ return await hook(input, args, context);
2028
+ };
2029
+ }
2030
+ return prepared;
2031
+ }
2032
+
1990
2033
  /**
1991
2034
  * Runs one input recipe of a workspace folder as a sample: `debug: true` (so
1992
2035
  * each `record:emit` carries its scope snapshot and mapping trace, and each
@@ -2005,10 +2048,11 @@ function idOf(content) {
2005
2048
  * @param callbacks - Where trace lines and records go as the run proceeds.
2006
2049
  * @param browser - Browser launch settings for a web-mode recipe (an executable path override, headless…); the server-wide setting `studio-http.use-case.ts`'s `StudioServerOptions.browser` carries in, e.g. `OPENCRAW_CHROMIUM` in a sandbox with no full Playwright install.
2007
2050
  * @param plugins - Hooks, access plugins and captcha solvers the person started Studio with (`--hooks`, issue #150); none when it was started without.
2051
+ * @param stubs - Values a hook answers with instead of being called, by hook name, for this run only (issue #201): how a sample skips a slow or paid callout.
2008
2052
  * @returns A handle: `stop` closes the crawler cleanly, `result` resolves when the run ends.
2009
2053
  * @throws RecipeValidationError, RecipeBindingError, Error when the recipe or its output cannot be loaded or bound.
2010
2054
  */
2011
- async function runSample(folder, recipeId, budget, callbacks, browser, plugins) {
2055
+ async function runSample(folder, recipeId, budget, callbacks, browser, plugins, stubs) {
2012
2056
  const {
2013
2057
  input,
2014
2058
  output
@@ -2023,7 +2067,7 @@ async function runSample(folder, recipeId, budget, callbacks, browser, plugins)
2023
2067
  debug: true,
2024
2068
  sample: budget,
2025
2069
  browser,
2026
- hooks: plugins?.hooks,
2070
+ hooks: calloutHooks(plugins?.hooks, stubs, callbacks.onTraceLine),
2027
2071
  accessPlugins: plugins?.accessPlugins,
2028
2072
  captchaSolvers: plugins?.captchaSolvers,
2029
2073
  onEvent: event => {
@@ -3816,6 +3860,49 @@ function handleInspectPage(state, command) {
3816
3860
  };
3817
3861
  }
3818
3862
 
3863
+ /**
3864
+ * Which of what Studio was started with calls outside the process, for the bar that warns the person and
3865
+ * for the stubs a sample run may use.
3866
+ *
3867
+ * @param plugins - What Studio was started with.
3868
+ * @returns The remote hooks, solvers and plugins, hooks first, each kind sorted by name.
3869
+ */
3870
+ function remoteCallouts(plugins) {
3871
+ const found = [];
3872
+ for (const [name, hook] of Object.entries(plugins.hooks)) {
3873
+ const label = calloutLabel(hook);
3874
+ if (label !== undefined) found.push({
3875
+ kind: 'hook',
3876
+ name,
3877
+ label
3878
+ });
3879
+ }
3880
+ const solvers = plugins.captchaSolvers ?? [];
3881
+ for (const solver of solvers) {
3882
+ const label = calloutLabel(solver);
3883
+ if (label !== undefined) found.push({
3884
+ kind: 'captcha',
3885
+ name: solver.name,
3886
+ label
3887
+ });
3888
+ }
3889
+ const access = plugins.accessPlugins ?? [];
3890
+ for (const plugin of access) {
3891
+ const label = calloutLabel(plugin);
3892
+ if (label !== undefined) found.push({
3893
+ kind: 'access',
3894
+ name: plugin.name,
3895
+ label
3896
+ });
3897
+ }
3898
+ const order = {
3899
+ hook: 0,
3900
+ captcha: 1,
3901
+ access: 2
3902
+ };
3903
+ return found.sort((a, b) => order[a.kind] - order[b.kind] || a.name.localeCompare(b.name));
3904
+ }
3905
+
3819
3906
  /** Where the studio's own recorder profile lives when `StudioServerOptions.recorderProfileDir` is not given: never a crawl's own `session.browserProfile` directory, never the person's default browser profile. */
3820
3907
  function defaultRecorderProfileDir() {
3821
3908
  return join(tmpdir(), 'opencraw-studio', 'recorder-profile');
@@ -3871,11 +3958,16 @@ async function handleOpenWorkspace(state, command) {
3871
3958
  if (switched) broadcast(state, {
3872
3959
  type: 'workspace-changed'
3873
3960
  });
3874
- return state.plugins === undefined ? view : {
3961
+ if (state.plugins === undefined) return view;
3962
+ const remote = remoteCallouts(state.plugins);
3963
+ return {
3875
3964
  ...view,
3876
3965
  hooks: {
3877
3966
  source: state.plugins.source,
3878
- names: Object.keys(state.plugins.hooks).sort((a, b) => a.localeCompare(b))
3967
+ names: Object.keys(state.plugins.hooks).sort((a, b) => a.localeCompare(b)),
3968
+ ...(remote.length > 0 && {
3969
+ remote
3970
+ })
3879
3971
  }
3880
3972
  };
3881
3973
  }
@@ -4727,7 +4819,7 @@ async function handleRunSample(state, command) {
4727
4819
  scope: rejected.scope
4728
4820
  });
4729
4821
  }
4730
- }, state.browser, state.plugins);
4822
+ }, state.browser, state.plugins, command.stubs);
4731
4823
  state.activeRun = handle;
4732
4824
  void handle.result.then(result => {
4733
4825
  if (state.activeRun === handle) state.activeRun = undefined;
@@ -0,0 +1,15 @@
1
+ import type { HookMap } from '@opencraw/core';
2
+ /**
3
+ * The hooks a sample run uses: the ones Studio was started with, except that
4
+ *
5
+ * - a hook the person stubbed returns the stub's value and calls nothing, and
6
+ * - a hook that calls outside the process says so in the trace, once per call, because it may be slow
7
+ * and it may cost money.
8
+ *
9
+ * @param hooks - The hooks Studio was started with.
10
+ * @param stubs - Values to answer with instead of calling a hook, by hook name. A name that is not a hook is ignored.
11
+ * @param announce - Where the trace lines go.
12
+ * @returns The hooks to run the sample with.
13
+ */
14
+ export declare function calloutHooks(hooks: HookMap | undefined, stubs: Record<string, unknown> | undefined, announce: (line: string) => void): HookMap | undefined;
15
+ //# sourceMappingURL=callout-hooks.use-case.d.ts.map
@@ -33,8 +33,9 @@ export interface SampleRunHandle {
33
33
  * @param callbacks - Where trace lines and records go as the run proceeds.
34
34
  * @param browser - Browser launch settings for a web-mode recipe (an executable path override, headless…); the server-wide setting `studio-http.use-case.ts`'s `StudioServerOptions.browser` carries in, e.g. `OPENCRAW_CHROMIUM` in a sandbox with no full Playwright install.
35
35
  * @param plugins - Hooks, access plugins and captcha solvers the person started Studio with (`--hooks`, issue #150); none when it was started without.
36
+ * @param stubs - Values a hook answers with instead of being called, by hook name, for this run only (issue #201): how a sample skips a slow or paid callout.
36
37
  * @returns A handle: `stop` closes the crawler cleanly, `result` resolves when the run ends.
37
38
  * @throws RecipeValidationError, RecipeBindingError, Error when the recipe or its output cannot be loaded or bound.
38
39
  */
39
- export declare function runSample(folder: string, recipeId: string, budget: SampleBudget | undefined, callbacks: SampleRunCallbacks, browser?: BrowserSessionConfig, plugins?: TrustedPlugins): Promise<SampleRunHandle>;
40
+ export declare function runSample(folder: string, recipeId: string, budget: SampleBudget | undefined, callbacks: SampleRunCallbacks, browser?: BrowserSessionConfig, plugins?: TrustedPlugins, stubs?: Record<string, unknown>): Promise<SampleRunHandle>;
40
41
  //# sourceMappingURL=run-sample.use-case.d.ts.map
@@ -0,0 +1,15 @@
1
+ import type { HookMap } from '@opencraw/core';
2
+ /**
3
+ * The hooks a sample run uses: the ones Studio was started with, except that
4
+ *
5
+ * - a hook the person stubbed returns the stub's value and calls nothing, and
6
+ * - a hook that calls outside the process says so in the trace, once per call, because it may be slow
7
+ * and it may cost money.
8
+ *
9
+ * @param hooks - The hooks Studio was started with.
10
+ * @param stubs - Values to answer with instead of calling a hook, by hook name. A name that is not a hook is ignored.
11
+ * @param announce - Where the trace lines go.
12
+ * @returns The hooks to run the sample with.
13
+ */
14
+ export declare function calloutHooks(hooks: HookMap | undefined, stubs: Record<string, unknown> | undefined, announce: (line: string) => void): HookMap | undefined;
15
+ //# sourceMappingURL=callout-hooks.use-case.d.ts.map
@@ -33,8 +33,9 @@ export interface SampleRunHandle {
33
33
  * @param callbacks - Where trace lines and records go as the run proceeds.
34
34
  * @param browser - Browser launch settings for a web-mode recipe (an executable path override, headless…); the server-wide setting `studio-http.use-case.ts`'s `StudioServerOptions.browser` carries in, e.g. `OPENCRAW_CHROMIUM` in a sandbox with no full Playwright install.
35
35
  * @param plugins - Hooks, access plugins and captcha solvers the person started Studio with (`--hooks`, issue #150); none when it was started without.
36
+ * @param stubs - Values a hook answers with instead of being called, by hook name, for this run only (issue #201): how a sample skips a slow or paid callout.
36
37
  * @returns A handle: `stop` closes the crawler cleanly, `result` resolves when the run ends.
37
38
  * @throws RecipeValidationError, RecipeBindingError, Error when the recipe or its output cannot be loaded or bound.
38
39
  */
39
- export declare function runSample(folder: string, recipeId: string, budget: SampleBudget | undefined, callbacks: SampleRunCallbacks, browser?: BrowserSessionConfig, plugins?: TrustedPlugins): Promise<SampleRunHandle>;
40
+ export declare function runSample(folder: string, recipeId: string, budget: SampleBudget | undefined, callbacks: SampleRunCallbacks, browser?: BrowserSessionConfig, plugins?: TrustedPlugins, stubs?: Record<string, unknown>): Promise<SampleRunHandle>;
40
41
  //# sourceMappingURL=run-sample.use-case.d.ts.map
@@ -19,6 +19,7 @@ export declare const runSampleCommandSchema: z.ZodObject<{
19
19
  maxPages: z.ZodOptional<z.ZodNumber>;
20
20
  maxMs: z.ZodOptional<z.ZodNumber>;
21
21
  }, z.core.$strip>>;
22
+ stubs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
22
23
  }, z.core.$strip>;
23
24
  /** Closes the crawler of the run in progress, if any; a no-op otherwise. */
24
25
  export declare const stopRunCommandSchema: z.ZodObject<{
@@ -372,6 +373,7 @@ export declare const studioCommandSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
372
373
  maxPages: z.ZodOptional<z.ZodNumber>;
373
374
  maxMs: z.ZodOptional<z.ZodNumber>;
374
375
  }, z.core.$strip>>;
376
+ stubs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
375
377
  }, z.core.$strip>, z.ZodObject<{
376
378
  type: z.ZodLiteral<"stop-run">;
377
379
  }, z.core.$strip>, z.ZodObject<{
@@ -32,6 +32,15 @@ export declare const recipeListingSchema: z.ZodObject<{
32
32
  export declare const loadedHooksSchema: z.ZodObject<{
33
33
  source: z.ZodString;
34
34
  names: z.ZodArray<z.ZodString>;
35
+ remote: z.ZodOptional<z.ZodArray<z.ZodObject<{
36
+ kind: z.ZodEnum<{
37
+ hook: "hook";
38
+ captcha: "captcha";
39
+ access: "access";
40
+ }>;
41
+ name: z.ZodString;
42
+ label: z.ZodString;
43
+ }, z.core.$strip>>>;
35
44
  }, z.core.$strip>;
36
45
  /** What `open-workspace` answers with: every recipe file directly inside the folder, and the hooks Studio runs them with, if any. */
37
46
  export declare const workspaceViewSchema: z.ZodObject<{
@@ -58,6 +67,15 @@ export declare const workspaceViewSchema: z.ZodObject<{
58
67
  hooks: z.ZodOptional<z.ZodObject<{
59
68
  source: z.ZodString;
60
69
  names: z.ZodArray<z.ZodString>;
70
+ remote: z.ZodOptional<z.ZodArray<z.ZodObject<{
71
+ kind: z.ZodEnum<{
72
+ hook: "hook";
73
+ captcha: "captcha";
74
+ access: "access";
75
+ }>;
76
+ name: z.ZodString;
77
+ label: z.ZodString;
78
+ }, z.core.$strip>>>;
61
79
  }, z.core.$strip>>;
62
80
  }, z.core.$strip>;
63
81
  export type RecipeIssue = z.infer<typeof recipeIssueSchema>;
@@ -1,2 +1,4 @@
1
1
  export type { TrustedPlugins } from './trusted-plugins.contract.js';
2
+ export { remoteCallouts } from './remote-callouts.mapper.js';
3
+ export type { RemoteCallout } from './remote-callouts.mapper.js';
2
4
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,17 @@
1
+ import type { TrustedPlugins } from './trusted-plugins.contract.js';
2
+ /** A hook, solver or plugin that is backed by a handler outside the process: it may be slow, and it may cost money. */
3
+ export interface RemoteCallout {
4
+ kind: 'hook' | 'captcha' | 'access';
5
+ name: string;
6
+ /** What it calls (`POST https://svc/price`, `command python3 slug.py`). */
7
+ label: string;
8
+ }
9
+ /**
10
+ * Which of what Studio was started with calls outside the process, for the bar that warns the person and
11
+ * for the stubs a sample run may use.
12
+ *
13
+ * @param plugins - What Studio was started with.
14
+ * @returns The remote hooks, solvers and plugins, hooks first, each kind sorted by name.
15
+ */
16
+ export declare function remoteCallouts(plugins: TrustedPlugins): RemoteCallout[];
17
+ //# sourceMappingURL=remote-callouts.mapper.d.ts.map
@@ -19,6 +19,7 @@ export declare const runSampleCommandSchema: z.ZodObject<{
19
19
  maxPages: z.ZodOptional<z.ZodNumber>;
20
20
  maxMs: z.ZodOptional<z.ZodNumber>;
21
21
  }, z.core.$strip>>;
22
+ stubs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
22
23
  }, z.core.$strip>;
23
24
  /** Closes the crawler of the run in progress, if any; a no-op otherwise. */
24
25
  export declare const stopRunCommandSchema: z.ZodObject<{
@@ -372,6 +373,7 @@ export declare const studioCommandSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
372
373
  maxPages: z.ZodOptional<z.ZodNumber>;
373
374
  maxMs: z.ZodOptional<z.ZodNumber>;
374
375
  }, z.core.$strip>>;
376
+ stubs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
375
377
  }, z.core.$strip>, z.ZodObject<{
376
378
  type: z.ZodLiteral<"stop-run">;
377
379
  }, z.core.$strip>, z.ZodObject<{
@@ -32,6 +32,15 @@ export declare const recipeListingSchema: z.ZodObject<{
32
32
  export declare const loadedHooksSchema: z.ZodObject<{
33
33
  source: z.ZodString;
34
34
  names: z.ZodArray<z.ZodString>;
35
+ remote: z.ZodOptional<z.ZodArray<z.ZodObject<{
36
+ kind: z.ZodEnum<{
37
+ access: "access";
38
+ captcha: "captcha";
39
+ hook: "hook";
40
+ }>;
41
+ name: z.ZodString;
42
+ label: z.ZodString;
43
+ }, z.core.$strip>>>;
35
44
  }, z.core.$strip>;
36
45
  /** What `open-workspace` answers with: every recipe file directly inside the folder, and the hooks Studio runs them with, if any. */
37
46
  export declare const workspaceViewSchema: z.ZodObject<{
@@ -58,6 +67,15 @@ export declare const workspaceViewSchema: z.ZodObject<{
58
67
  hooks: z.ZodOptional<z.ZodObject<{
59
68
  source: z.ZodString;
60
69
  names: z.ZodArray<z.ZodString>;
70
+ remote: z.ZodOptional<z.ZodArray<z.ZodObject<{
71
+ kind: z.ZodEnum<{
72
+ access: "access";
73
+ captcha: "captcha";
74
+ hook: "hook";
75
+ }>;
76
+ name: z.ZodString;
77
+ label: z.ZodString;
78
+ }, z.core.$strip>>>;
61
79
  }, z.core.$strip>>;
62
80
  }, z.core.$strip>;
63
81
  export type RecipeIssue = z.infer<typeof recipeIssueSchema>;
@@ -1,2 +1,4 @@
1
1
  export type { TrustedPlugins } from './trusted-plugins.contract';
2
+ export { remoteCallouts } from './remote-callouts.mapper';
3
+ export type { RemoteCallout } from './remote-callouts.mapper';
2
4
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,17 @@
1
+ import type { TrustedPlugins } from './trusted-plugins.contract';
2
+ /** A hook, solver or plugin that is backed by a handler outside the process: it may be slow, and it may cost money. */
3
+ export interface RemoteCallout {
4
+ kind: 'hook' | 'captcha' | 'access';
5
+ name: string;
6
+ /** What it calls (`POST https://svc/price`, `command python3 slug.py`). */
7
+ label: string;
8
+ }
9
+ /**
10
+ * Which of what Studio was started with calls outside the process, for the bar that warns the person and
11
+ * for the stubs a sample run may use.
12
+ *
13
+ * @param plugins - What Studio was started with.
14
+ * @returns The remote hooks, solvers and plugins, hooks first, each kind sorted by name.
15
+ */
16
+ export declare function remoteCallouts(plugins: TrustedPlugins): RemoteCallout[];
17
+ //# sourceMappingURL=remote-callouts.mapper.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencraw/studio",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "type": "module",
5
5
  "main": "./dist/index.esm.js",
6
6
  "module": "./dist/index.esm.js",
@@ -24,8 +24,8 @@
24
24
  "!**/*.js.map"
25
25
  ],
26
26
  "dependencies": {
27
- "@opencraw/core": "^0.1.32",
28
- "@opencraw/probe": "^0.0.15",
27
+ "@opencraw/core": "^0.1.34",
28
+ "@opencraw/probe": "^0.0.17",
29
29
  "cheerio": "^1.2.0",
30
30
  "playwright": "^1.63.0",
31
31
  "zod": "^4.6.5",