tywrap 0.2.1 → 0.4.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 (144) hide show
  1. package/README.md +100 -32
  2. package/dist/cli.js +70 -5
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config/index.d.ts +11 -7
  5. package/dist/config/index.d.ts.map +1 -1
  6. package/dist/config/index.js +62 -20
  7. package/dist/config/index.js.map +1 -1
  8. package/dist/core/analyzer.d.ts +1 -0
  9. package/dist/core/analyzer.d.ts.map +1 -1
  10. package/dist/core/analyzer.js +46 -12
  11. package/dist/core/analyzer.js.map +1 -1
  12. package/dist/core/annotation-parser.d.ts +8 -0
  13. package/dist/core/annotation-parser.d.ts.map +1 -0
  14. package/dist/core/annotation-parser.js +409 -0
  15. package/dist/core/annotation-parser.js.map +1 -0
  16. package/dist/core/discovery.d.ts +1 -0
  17. package/dist/core/discovery.d.ts.map +1 -1
  18. package/dist/core/discovery.js +9 -9
  19. package/dist/core/discovery.js.map +1 -1
  20. package/dist/core/generator.d.ts +9 -1
  21. package/dist/core/generator.d.ts.map +1 -1
  22. package/dist/core/generator.js +738 -104
  23. package/dist/core/generator.js.map +1 -1
  24. package/dist/core/mapper.d.ts +9 -7
  25. package/dist/core/mapper.d.ts.map +1 -1
  26. package/dist/core/mapper.js +129 -44
  27. package/dist/core/mapper.js.map +1 -1
  28. package/dist/dev.d.ts +57 -0
  29. package/dist/dev.d.ts.map +1 -0
  30. package/dist/dev.js +743 -0
  31. package/dist/dev.js.map +1 -0
  32. package/dist/index.d.ts +8 -7
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +4 -4
  35. package/dist/index.js.map +1 -1
  36. package/dist/runtime/bridge-core.d.ts +1 -0
  37. package/dist/runtime/bridge-core.d.ts.map +1 -1
  38. package/dist/runtime/bridge-core.js +52 -17
  39. package/dist/runtime/bridge-core.js.map +1 -1
  40. package/dist/runtime/bridge-protocol.d.ts +17 -0
  41. package/dist/runtime/bridge-protocol.d.ts.map +1 -1
  42. package/dist/runtime/bridge-protocol.js +97 -0
  43. package/dist/runtime/bridge-protocol.js.map +1 -1
  44. package/dist/runtime/errors.d.ts +10 -0
  45. package/dist/runtime/errors.d.ts.map +1 -1
  46. package/dist/runtime/errors.js +9 -0
  47. package/dist/runtime/errors.js.map +1 -1
  48. package/dist/runtime/http-io.d.ts.map +1 -1
  49. package/dist/runtime/http-io.js.map +1 -1
  50. package/dist/runtime/http.d.ts +1 -0
  51. package/dist/runtime/http.d.ts.map +1 -1
  52. package/dist/runtime/http.js +6 -0
  53. package/dist/runtime/http.js.map +1 -1
  54. package/dist/runtime/node.d.ts.map +1 -1
  55. package/dist/runtime/node.js +206 -32
  56. package/dist/runtime/node.js.map +1 -1
  57. package/dist/runtime/optimized-node.d.ts +6 -5
  58. package/dist/runtime/optimized-node.d.ts.map +1 -1
  59. package/dist/runtime/optimized-node.js +6 -5
  60. package/dist/runtime/optimized-node.js.map +1 -1
  61. package/dist/runtime/pooled-transport.d.ts +4 -0
  62. package/dist/runtime/pooled-transport.d.ts.map +1 -1
  63. package/dist/runtime/pooled-transport.js +2 -0
  64. package/dist/runtime/pooled-transport.js.map +1 -1
  65. package/dist/runtime/process-io.d.ts +5 -0
  66. package/dist/runtime/process-io.d.ts.map +1 -1
  67. package/dist/runtime/process-io.js +64 -30
  68. package/dist/runtime/process-io.js.map +1 -1
  69. package/dist/runtime/pyodide-io.d.ts +4 -5
  70. package/dist/runtime/pyodide-io.d.ts.map +1 -1
  71. package/dist/runtime/pyodide-io.js +99 -37
  72. package/dist/runtime/pyodide-io.js.map +1 -1
  73. package/dist/runtime/safe-codec.d.ts +25 -3
  74. package/dist/runtime/safe-codec.d.ts.map +1 -1
  75. package/dist/runtime/safe-codec.js +231 -57
  76. package/dist/runtime/safe-codec.js.map +1 -1
  77. package/dist/runtime/transport.d.ts +6 -1
  78. package/dist/runtime/transport.d.ts.map +1 -1
  79. package/dist/runtime/transport.js.map +1 -1
  80. package/dist/runtime/validators.d.ts +0 -11
  81. package/dist/runtime/validators.d.ts.map +1 -1
  82. package/dist/runtime/validators.js +13 -3
  83. package/dist/runtime/validators.js.map +1 -1
  84. package/dist/runtime/worker-pool.d.ts +17 -0
  85. package/dist/runtime/worker-pool.d.ts.map +1 -1
  86. package/dist/runtime/worker-pool.js +97 -22
  87. package/dist/runtime/worker-pool.js.map +1 -1
  88. package/dist/types/index.d.ts +51 -8
  89. package/dist/types/index.d.ts.map +1 -1
  90. package/dist/tywrap.d.ts +6 -0
  91. package/dist/tywrap.d.ts.map +1 -1
  92. package/dist/tywrap.js +237 -331
  93. package/dist/tywrap.js.map +1 -1
  94. package/dist/utils/cache.d.ts.map +1 -1
  95. package/dist/utils/cache.js +53 -5
  96. package/dist/utils/cache.js.map +1 -1
  97. package/dist/utils/codec.d.ts.map +1 -1
  98. package/dist/utils/codec.js +10 -16
  99. package/dist/utils/codec.js.map +1 -1
  100. package/dist/utils/ir-cache.d.ts +28 -0
  101. package/dist/utils/ir-cache.d.ts.map +1 -0
  102. package/dist/utils/ir-cache.js +29 -0
  103. package/dist/utils/ir-cache.js.map +1 -0
  104. package/dist/utils/parallel-processor.d.ts +1 -1
  105. package/dist/utils/parallel-processor.d.ts.map +1 -1
  106. package/dist/utils/parallel-processor.js +111 -40
  107. package/dist/utils/parallel-processor.js.map +1 -1
  108. package/dist/utils/runtime.d.ts +5 -1
  109. package/dist/utils/runtime.d.ts.map +1 -1
  110. package/dist/utils/runtime.js +112 -18
  111. package/dist/utils/runtime.js.map +1 -1
  112. package/package.json +47 -21
  113. package/runtime/python_bridge.py +78 -12
  114. package/src/cli.ts +85 -5
  115. package/src/config/index.ts +91 -32
  116. package/src/core/analyzer.ts +51 -15
  117. package/src/core/annotation-parser.ts +500 -0
  118. package/src/core/discovery.ts +26 -12
  119. package/src/core/generator.ts +956 -116
  120. package/src/core/mapper.ts +158 -51
  121. package/src/dev.ts +983 -0
  122. package/src/index.ts +35 -6
  123. package/src/runtime/bridge-core.ts +58 -18
  124. package/src/runtime/bridge-protocol.ts +164 -0
  125. package/src/runtime/errors.ts +14 -0
  126. package/src/runtime/http-io.ts +2 -1
  127. package/src/runtime/http.ts +7 -0
  128. package/src/runtime/node.ts +312 -46
  129. package/src/runtime/optimized-node.ts +9 -5
  130. package/src/runtime/pooled-transport.ts +12 -1
  131. package/src/runtime/process-io.ts +83 -55
  132. package/src/runtime/pyodide-io.ts +110 -44
  133. package/src/runtime/safe-codec.ts +290 -82
  134. package/src/runtime/transport.ts +6 -1
  135. package/src/runtime/validators.ts +14 -3
  136. package/src/runtime/worker-pool.ts +120 -24
  137. package/src/types/global.d.ts +11 -1
  138. package/src/types/index.ts +63 -8
  139. package/src/tywrap.ts +296 -361
  140. package/src/utils/cache.ts +59 -5
  141. package/src/utils/codec.ts +11 -15
  142. package/src/utils/ir-cache.ts +51 -0
  143. package/src/utils/parallel-processor.ts +119 -43
  144. package/src/utils/runtime.ts +129 -18
@@ -7,7 +7,8 @@
7
7
 
8
8
  import { randomUUID } from 'node:crypto';
9
9
  import { existsSync } from 'node:fs';
10
- import { readFile, rm, writeFile } from 'node:fs/promises';
10
+ import { tmpdir } from 'node:os';
11
+ import { readFile, rm, writeFile, mkdtemp, symlink } from 'node:fs/promises';
11
12
  import { createRequire } from 'node:module';
12
13
  import { dirname, extname, resolve } from 'node:path';
13
14
  import { pathToFileURL } from 'node:url';
@@ -16,15 +17,9 @@ import type {
16
17
  OutputConfig,
17
18
  RuntimeConfig,
18
19
  PerformanceConfig,
19
- DevelopmentConfig,
20
20
  } from '../types/index.js';
21
21
  import { getDefaultPythonPath } from '../utils/python.js';
22
22
 
23
- /**
24
- * Public configuration type. Currently identical to {@link TywrapOptions}.
25
- */
26
- export type TywrapConfig = TywrapOptions;
27
-
28
23
  type DeepPartial<T> = {
29
24
  [K in keyof T]?: T[K] extends Array<infer U>
30
25
  ? Array<U>
@@ -33,19 +28,35 @@ type DeepPartial<T> = {
33
28
  : T[K];
34
29
  };
35
30
 
31
+ /**
32
+ * User-authored configuration shape accepted by defineConfig() and config files.
33
+ */
34
+ export type TywrapConfig = DeepPartial<TywrapOptions>;
35
+
36
+ /**
37
+ * Fully resolved configuration shape returned by createConfig() and resolveConfig().
38
+ */
39
+ export type ResolvedTywrapConfig = TywrapOptions;
40
+
36
41
  /**
37
42
  * Default configuration values used when options are not supplied.
38
43
  */
39
- const DEFAULT_CONFIG: TywrapConfig = {
44
+ const DEFAULT_CONFIG: ResolvedTywrapConfig = {
40
45
  pythonModules: {},
46
+ pythonImportPath: [],
41
47
  output: { dir: './generated', format: 'esm', declaration: false, sourceMap: false },
42
48
  runtime: { node: { pythonPath: getDefaultPythonPath(), timeout: 30000 } },
43
49
  performance: { caching: false, batching: false, compression: 'none' },
44
- development: { hotReload: false, sourceMap: false, validation: 'none' },
45
50
  types: { presets: [] },
46
51
  debug: false,
47
52
  };
48
53
 
54
+ const LEGACY_DEVELOPMENT_MESSAGE =
55
+ 'Legacy config field "development" is no longer supported. Use createBridgeReloader() or startNodeWatchSession() from "tywrap/dev" instead.';
56
+
57
+ const LEGACY_MODULE_WATCH_MESSAGE =
58
+ 'Legacy config field "pythonModules.<module>.watch" is no longer supported. Use startNodeWatchSession() from "tywrap/dev" instead.';
59
+
49
60
  /**
50
61
  * Recursively merge two configuration objects. Arrays are overwritten by
51
62
  * overrides instead of concatenated.
@@ -82,16 +93,38 @@ function safeExists(path: string): boolean {
82
93
  return existsSync(path);
83
94
  }
84
95
 
96
+ function detectLegacyFields(config: TywrapConfig): void {
97
+ const rawConfig = config as unknown as Record<string, unknown>;
98
+ if (Object.prototype.hasOwnProperty.call(rawConfig, 'development')) {
99
+ throw new Error(LEGACY_DEVELOPMENT_MESSAGE);
100
+ }
101
+
102
+ const pythonModules = rawConfig.pythonModules;
103
+ if (!isPlainObject(pythonModules)) {
104
+ return;
105
+ }
106
+
107
+ for (const moduleConfig of Object.values(pythonModules)) {
108
+ if (
109
+ isPlainObject(moduleConfig) &&
110
+ Object.prototype.hasOwnProperty.call(moduleConfig, 'watch')
111
+ ) {
112
+ throw new Error(LEGACY_MODULE_WATCH_MESSAGE);
113
+ }
114
+ }
115
+ }
116
+
85
117
  /**
86
118
  * Validate configuration values and throw user-friendly errors when invalid.
87
119
  */
88
- function validateConfig(config: TywrapConfig): void {
120
+ function validateConfig(config: ResolvedTywrapConfig): void {
121
+ detectLegacyFields(config);
89
122
  const allowedTopLevel = new Set([
90
123
  'pythonModules',
124
+ 'pythonImportPath',
91
125
  'output',
92
126
  'runtime',
93
127
  'performance',
94
- 'development',
95
128
  'types',
96
129
  'debug',
97
130
  ]);
@@ -105,6 +138,15 @@ function validateConfig(config: TywrapConfig): void {
105
138
  if (typeof out.dir !== 'string') {
106
139
  throw new Error('output.dir must be a string');
107
140
  }
141
+
142
+ if (config.pythonImportPath !== undefined) {
143
+ if (
144
+ !Array.isArray(config.pythonImportPath) ||
145
+ config.pythonImportPath.some(p => typeof p !== 'string')
146
+ ) {
147
+ throw new Error('pythonImportPath must be an array of strings');
148
+ }
149
+ }
108
150
  if (!['esm', 'cjs', 'both'].includes(out.format)) {
109
151
  throw new Error('output.format must be one of "esm", "cjs" or "both"');
110
152
  }
@@ -136,18 +178,6 @@ function validateConfig(config: TywrapConfig): void {
136
178
  throw new Error(`performance.compression must be one of ${validCompression.join(', ')}`);
137
179
  }
138
180
 
139
- const dev: DevelopmentConfig = config.development;
140
- if (typeof dev.hotReload !== 'boolean') {
141
- throw new Error('development.hotReload must be a boolean');
142
- }
143
- if (typeof dev.sourceMap !== 'boolean') {
144
- throw new Error('development.sourceMap must be a boolean');
145
- }
146
- const validValidation = ['runtime', 'compile', 'both', 'none'];
147
- if (!validValidation.includes(dev.validation)) {
148
- throw new Error(`development.validation must be one of ${validValidation.join(', ')}`);
149
- }
150
-
151
181
  if (config.types) {
152
182
  const presets = config.types.presets;
153
183
  const validPresets = new Set([
@@ -183,7 +213,7 @@ function validateConfig(config: TywrapConfig): void {
183
213
  *
184
214
  * @param overrides CLI or programmatic overrides
185
215
  */
186
- export function createConfig(overrides: DeepPartial<TywrapOptions> = {}): TywrapConfig {
216
+ export function createConfig(overrides: DeepPartial<TywrapOptions> = {}): ResolvedTywrapConfig {
187
217
  const merged = merge(DEFAULT_CONFIG, overrides);
188
218
  validateConfig(merged);
189
219
  return merged;
@@ -200,7 +230,9 @@ export interface ResolveConfigOptions {
200
230
  * Resolve configuration by loading a config file (JSON/JS/TS) and merging
201
231
  * defaults with any overrides.
202
232
  */
203
- export async function resolveConfig(options: ResolveConfigOptions = {}): Promise<TywrapConfig> {
233
+ export async function resolveConfig(
234
+ options: ResolveConfigOptions = {}
235
+ ): Promise<ResolvedTywrapConfig> {
204
236
  const cwd = options.cwd ?? process.cwd();
205
237
  const overrides = options.overrides ?? {};
206
238
  const configFile = options.configFile ? resolve(cwd, options.configFile) : undefined;
@@ -272,6 +304,20 @@ export async function loadConfigFile(configFile: string): Promise<Partial<Tywrap
272
304
  fileName: resolved,
273
305
  });
274
306
 
307
+ let transpiledOutput = output.outputText;
308
+ if (!emitCommonJs) {
309
+ try {
310
+ // Preserve support for `import { defineConfig } from 'tywrap'` when ESM config
311
+ // is evaluated from an OS temp directory outside the package scope.
312
+ const tywrapEntryHref = await import.meta.resolve('tywrap');
313
+ transpiledOutput = transpiledOutput
314
+ .replaceAll("'tywrap'", `'${tywrapEntryHref}'`)
315
+ .replaceAll('\"tywrap\"', `\"${tywrapEntryHref}\"`);
316
+ } catch {
317
+ // Best-effort: leave source as-is when tywrap cannot be resolved.
318
+ }
319
+ }
320
+
275
321
  if (emitCommonJs) {
276
322
  // Why: `.cts` is explicitly CommonJS. We evaluate the transpiled output in-memory using
277
323
  // Node's Module internals (`Module._compile` / `_nodeModulePaths`) to avoid writing an extra
@@ -309,18 +355,31 @@ export async function loadConfigFile(configFile: string): Promise<Partial<Tywrap
309
355
  return ensureConfigObject(loaded ?? {}, resolved);
310
356
  }
311
357
 
312
- // Why: Node can't import ESM from a string without a custom loader. We write the transpiled
313
- // output to a temporary `.mjs` file next to the source config so relative imports and Node
314
- // resolution behave naturally, then clean it up immediately after loading.
315
- const tmpPath = resolve(dirname(resolved), `.tywrap.config.${randomUUID()}.mjs`);
358
+ // Why: Node can't import ESM from a string without a custom loader. We write transpiled
359
+ // output to a temporary `.mjs` file under the OS temp directory (not next to user config),
360
+ // then clean up both file and temporary directory after loading.
361
+ const tempDir = await mkdtemp(resolve(tmpdir(), 'tywrap-config-'));
362
+ const tmpPath = resolve(tempDir, `.tywrap.config.${randomUUID()}.mjs`);
316
363
  try {
317
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- temp path is derived from config location
318
- await writeFile(tmpPath, output.outputText, 'utf-8');
364
+ const localNodeModules = resolve(process.cwd(), 'node_modules');
365
+ if (safeExists(localNodeModules)) {
366
+ const tempNodeModules = resolve(tempDir, 'node_modules');
367
+ try {
368
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- temp path is derived from OS temp directory
369
+ await symlink(localNodeModules, tempNodeModules, 'dir');
370
+ } catch {
371
+ // Best-effort only; regular resolution may still succeed without a symlink.
372
+ }
373
+ }
374
+
375
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- temp path is derived from OS temp directory
376
+ await writeFile(tmpPath, transpiledOutput, 'utf-8');
319
377
  const mod = (await import(pathToFileURL(tmpPath).href)) as Record<string, unknown>;
320
378
  const loaded = mod.default ?? mod;
321
379
  return ensureConfigObject(loaded ?? {}, resolved);
322
380
  } finally {
323
381
  await rm(tmpPath, { force: true });
382
+ await rm(tempDir, { recursive: true, force: true });
324
383
  }
325
384
  }
326
385
 
@@ -335,6 +394,6 @@ async function safeReadFileAsync(path: string): Promise<string> {
335
394
  /**
336
395
  * Type helper for authoring tywrap configs with full type inference.
337
396
  */
338
- export function defineConfig(config: TywrapConfig): TywrapConfig {
397
+ export function defineConfig<T extends TywrapConfig>(config: T): T {
339
398
  return config;
340
399
  }
@@ -26,6 +26,7 @@ import { getComponentLogger } from '../utils/logger.js';
26
26
  const log = getComponentLogger('Analyzer');
27
27
 
28
28
  const UNKNOWN_TYPE: PythonType = { kind: 'custom', name: 'Any', module: 'typing' };
29
+ const PYTHON_LANGUAGE = Python as unknown as Parameters<Parser['setLanguage']>[0];
29
30
 
30
31
  export class PyAnalyzer {
31
32
  private parser: Parser;
@@ -44,7 +45,7 @@ export class PyAnalyzer {
44
45
  }
45
46
 
46
47
  try {
47
- await this.parser.setLanguage(Python);
48
+ await this.parser.setLanguage(PYTHON_LANGUAGE);
48
49
  this.initialized = true;
49
50
  } catch (error) {
50
51
  throw new Error(`Failed to initialize Python parser: ${error}`);
@@ -141,7 +142,7 @@ export class PyAnalyzer {
141
142
  async extractFunctions(node: SyntaxNode): Promise<PythonFunction[]> {
142
143
  const functions: PythonFunction[] = [];
143
144
 
144
- const functionNodes = this.findNodesByType(node, 'function_definition');
145
+ const functionNodes = this.findDirectDefinitions(node, 'function_definition');
145
146
 
146
147
  for (const funcNode of functionNodes) {
147
148
  try {
@@ -161,7 +162,7 @@ export class PyAnalyzer {
161
162
  async extractClasses(node: SyntaxNode): Promise<PythonClass[]> {
162
163
  const classes: PythonClass[] = [];
163
164
 
164
- const classNodes = this.findNodesByType(node, 'class_definition');
165
+ const classNodes = this.findDirectDefinitions(node, 'class_definition');
165
166
 
166
167
  for (const classNode of classNodes) {
167
168
  try {
@@ -238,7 +239,7 @@ export class PyAnalyzer {
238
239
  const properties: Property[] = [];
239
240
 
240
241
  if (bodyNode) {
241
- const methodNodes = this.findNodesByType(bodyNode, 'function_definition');
242
+ const methodNodes = this.findDirectDefinitions(bodyNode, 'function_definition');
242
243
  for (const methodNode of methodNodes) {
243
244
  try {
244
245
  const method = await this.extractFunction(methodNode);
@@ -249,9 +250,18 @@ export class PyAnalyzer {
249
250
  }
250
251
 
251
252
  // Extract properties (assignments with type annotations or @property decorators)
252
- const assignmentNodes = this.findNodesByType(bodyNode, 'assignment');
253
- for (const assignNode of assignmentNodes) {
254
- const prop = this.extractProperty(assignNode);
253
+ for (const stmt of bodyNode.namedChildren) {
254
+ // Only consider class-body statements, never descend into method bodies.
255
+ const assignmentNode =
256
+ stmt.type === 'assignment'
257
+ ? stmt
258
+ : stmt.type === 'expression_statement'
259
+ ? (stmt.namedChildren.find(c => c.type === 'assignment') ?? null)
260
+ : null;
261
+ if (!assignmentNode) {
262
+ continue;
263
+ }
264
+ const prop = this.extractProperty(assignmentNode);
255
265
  if (prop) {
256
266
  properties.push(prop);
257
267
  }
@@ -641,21 +651,45 @@ export class PyAnalyzer {
641
651
  */
642
652
  private findNodesByType(node: SyntaxNode, type: string): SyntaxNode[] {
643
653
  const results: SyntaxNode[] = [];
644
-
645
- const traverse = (n: SyntaxNode): void => {
654
+ const stack: SyntaxNode[] = [node];
655
+ while (stack.length > 0) {
656
+ const n = stack.pop();
657
+ if (!n) {
658
+ break;
659
+ }
646
660
  if (n.type === type) {
647
661
  results.push(n);
648
662
  }
649
-
650
- for (const child of n.children) {
651
- traverse(child);
663
+ for (let i = n.children.length - 1; i >= 0; i--) {
664
+ const child = n.children[i];
665
+ if (child) {
666
+ stack.push(child);
667
+ }
652
668
  }
653
- };
654
-
655
- traverse(node);
669
+ }
656
670
  return results;
657
671
  }
658
672
 
673
+ private findDirectDefinitions(
674
+ node: SyntaxNode,
675
+ type: 'function_definition' | 'class_definition'
676
+ ): SyntaxNode[] {
677
+ const out: SyntaxNode[] = [];
678
+ for (const child of node.namedChildren) {
679
+ if (child.type === type) {
680
+ out.push(child);
681
+ continue;
682
+ }
683
+ if (child.type === 'decorated_definition') {
684
+ const def = child.namedChildren.find(c => c.type === type) ?? null;
685
+ if (def) {
686
+ out.push(def);
687
+ }
688
+ }
689
+ }
690
+ return out;
691
+ }
692
+
659
693
  private extractModuleName(path?: string): string {
660
694
  if (!path) {
661
695
  return 'unknown';
@@ -683,6 +717,8 @@ export class PyAnalyzer {
683
717
  const expr = firstChild.child(0);
684
718
  if (expr?.type === 'string') {
685
719
  let docstring = expr.text;
720
+ // Strip docstring prefixes like r/u/f/b and combos (rf/fr/br/...).
721
+ docstring = docstring.replace(/^(?:[rRuUbBfF]{1,2})(?=(\"\"\"|'''|\"|'))/, '');
686
722
  // Remove outer quotes and any remaining inner quotes
687
723
  if (docstring.startsWith('"""') && docstring.endsWith('"""')) {
688
724
  docstring = docstring.slice(3, -3);