Skip to content

Commit 9b84e04

Browse files
committed
Support repl decorations for proxy objects
1 parent 9c222ef commit 9b84e04

File tree

18 files changed

+447
-207
lines changed

18 files changed

+447
-207
lines changed

packages/jsrepl/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
},
5757
"devDependencies": {
5858
"@babel/parser": "^7.25.6",
59+
"@babel/types": "^7.26.3",
5960
"@chromatic-com/storybook": "^1.9.0",
6061
"@iconify-json/mdi": "^1.2.0",
6162
"@iconify-json/simple-icons": "^1.2.2",

packages/jsrepl/src/lib/bundler/babel/repl-plugin/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,9 @@ export function replPlugin({ types: t }: { types: typeof types }): PluginObj {
336336
let fnId =
337337
t.isFunctionDeclaration(path.node) || t.isFunctionExpression(path.node)
338338
? path.node.id
339-
: null
339+
: t.isObjectMethod(path.node) && !path.node.computed && t.isIdentifier(path.node.key)
340+
? path.node.key
341+
: null
340342

341343
// Infer function id from variable id
342344
if (
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import type Parser from '@babel/parser'
2+
import type {
3+
ArrowFunctionExpression,
4+
FunctionExpression,
5+
ObjectExpression,
6+
ObjectMethod,
7+
} from '@babel/types'
8+
import { identifierNameFunctionMeta } from '@jsrepl/shared-types'
9+
import { getBabel } from '../get-babel'
10+
11+
// Let babel to parse this madness.
12+
// - function (arg1, arg2) {}
13+
// - async function (arg1, arg2) {}
14+
// - function name(arg1, arg2) {}
15+
// - async function name(arg1, arg2) {}
16+
// - function name(arg1, arg2 = 123, ...args) {}
17+
// - () => {}
18+
// - async () => {}
19+
// - args1 => {}
20+
// - async args1 => {}
21+
// - (args1, args2) => {}
22+
// - async (args1, args2) => {}
23+
// - function ({ jhkhj, asdad = 123 } = {}) {}
24+
// - () => 7
25+
// - function (asd = adsasd({})) { ... }
26+
// - get() { return 123 } // method, obj property
27+
// - set(value) { this.value = value } // method, obj property
28+
// - foo(value) {} // method, obj property
29+
// - async foo(value) {} // async method, obj property
30+
// - var obj = { foo: async function bar () {} } // async method, obj property, but defined with function keyword and name
31+
export function parseFunction(
32+
str: string,
33+
_isOriginalSource = false
34+
): {
35+
name: string
36+
args: string
37+
isAsync: boolean
38+
isArrow: boolean
39+
isMethod: boolean
40+
origSource: string | null
41+
} | null {
42+
const babel = getBabel()[0].value!
43+
44+
// @ts-expect-error Babel standalone: https://babeljs.io/docs/babel-standalone#internal-packages
45+
const { parser } = babel.packages as { parser: typeof Parser }
46+
47+
let ast: ArrowFunctionExpression | FunctionExpression | ObjectMethod
48+
49+
try {
50+
// ArrowFunctionExpression | FunctionExpression
51+
ast = parser.parseExpression(str) as ArrowFunctionExpression | FunctionExpression
52+
} catch {
53+
try {
54+
// ObjectMethod?
55+
str = `{${str}}`
56+
const objExpr = parser.parseExpression(str) as ObjectExpression
57+
if (
58+
objExpr.type === 'ObjectExpression' &&
59+
objExpr.properties.length === 1 &&
60+
objExpr.properties[0]!.type === 'ObjectMethod'
61+
) {
62+
ast = objExpr.properties[0]! as ObjectMethod
63+
} else {
64+
return null
65+
}
66+
} catch {
67+
return null
68+
}
69+
}
70+
71+
let origSource: string | null = null
72+
73+
if (!_isOriginalSource) {
74+
origSource = getFunctionOriginalSource(ast)
75+
if (origSource) {
76+
return parseFunction(origSource, true)
77+
}
78+
} else {
79+
origSource = str
80+
}
81+
82+
if (ast.type === 'ArrowFunctionExpression') {
83+
return {
84+
name: '',
85+
args: ast.params.map((param) => str.slice(param.start!, param.end!)).join(', '),
86+
isAsync: ast.async,
87+
isArrow: true,
88+
isMethod: false,
89+
origSource,
90+
}
91+
}
92+
93+
if (ast.type === 'FunctionExpression') {
94+
return {
95+
name: ast.id?.name ?? '',
96+
args: ast.params.map((param) => str.slice(param.start!, param.end!)).join(', '),
97+
isAsync: ast.async,
98+
isArrow: false,
99+
isMethod: false,
100+
origSource,
101+
}
102+
}
103+
104+
if (ast.type === 'ObjectMethod') {
105+
return {
106+
name: ast.computed ? '' : ast.key.type === 'Identifier' ? ast.key.name : '',
107+
args: ast.params.map((param) => str.slice(param.start!, param.end!)).join(', '),
108+
isAsync: ast.async,
109+
isArrow: false,
110+
isMethod: true,
111+
origSource,
112+
}
113+
}
114+
115+
return null
116+
}
117+
118+
function getFunctionOriginalSource(
119+
ast: ArrowFunctionExpression | FunctionExpression | ObjectMethod
120+
): string | null {
121+
if (
122+
(ast.type === 'ArrowFunctionExpression' ||
123+
ast.type === 'FunctionExpression' ||
124+
ast.type === 'ObjectMethod') &&
125+
ast.body.type === 'BlockStatement'
126+
) {
127+
const node = ast.body.body[0]
128+
if (
129+
node?.type === 'ExpressionStatement' &&
130+
node.expression.type === 'CallExpression' &&
131+
node.expression.callee.type === 'Identifier' &&
132+
node.expression.callee.name === identifierNameFunctionMeta &&
133+
node.expression.arguments[0]?.type === 'StringLiteral'
134+
) {
135+
return node.expression.arguments[0].value
136+
}
137+
}
138+
139+
return null
140+
}

packages/jsrepl/src/lib/repl-payload/payload-utils.ts

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
MarshalledFunction,
44
MarshalledObject,
55
MarshalledPromise,
6+
MarshalledProxy,
67
MarshalledSymbol,
78
MarshalledType,
89
MarshalledWeakMap,
@@ -42,6 +43,10 @@ export function isMarshalledPromise(result: object): result is MarshalledPromise
4243
return getMarshalledType(result) === MarshalledType.Promise
4344
}
4445

46+
export function isMarshalledProxy(result: object): result is MarshalledProxy {
47+
return getMarshalledType(result) === MarshalledType.Proxy
48+
}
49+
4550
export function getMarshalledType(result: object): MarshalledType | null {
4651
return '__meta__' in result &&
4752
result.__meta__ !== null &&

packages/jsrepl/src/lib/repl-payload/render-json.ts

+7
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ function replacer(this: unknown, key: string, value: unknown): unknown {
4747
finally: 'function finally() { [native code] }',
4848
}
4949

50+
case utils.isMarshalledProxy(value): {
51+
const { target } = value.__meta__
52+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
53+
const { __meta__, ...props } = target
54+
return props
55+
}
56+
5057
case utils.isMarshalledObject(value): {
5158
// eslint-disable-next-line @typescript-eslint/no-unused-vars
5259
const { __meta__, ...props } = value

packages/jsrepl/src/lib/repl-payload/render-mock-object.ts

+13
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ function revive(
3434
return doc.head.firstChild ?? doc.body.firstChild
3535
}
3636

37+
// TODO: support original source, see parse-function.ts
3738
case utils.isMarshalledFunction(value): {
3839
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
3940
let fn: Function
@@ -42,6 +43,11 @@ function revive(
4243
fn = new Function(`return ${value.serialized}`)()
4344
} catch {}
4445

46+
try {
47+
// Object method? - e.g., get() {}, async foo(value) {}
48+
fn = new Function(`return Object.values({ ${value.serialized} })[0]`)()
49+
} catch {}
50+
4551
try {
4652
// Some reserved keywords are not allowed in function names,
4753
// for example `catch`, `finally`, `do`, etc.
@@ -70,6 +76,13 @@ function revive(
7076
case utils.isMarshalledPromise(value):
7177
return new Promise(() => {})
7278

79+
case utils.isMarshalledProxy(value): {
80+
const { target, handler } = value.__meta__
81+
const revivedTarget = revive(target, context) as object
82+
const revivedHandler = revive(handler, context) as ProxyHandler<object>
83+
return new Proxy(revivedTarget, revivedHandler)
84+
}
85+
7386
case utils.isMarshalledObject(value): {
7487
const { __meta__: meta, ...props } = value
7588

packages/jsrepl/src/lib/repl-payload/stringify.ts

+31-103
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1-
import {
2-
type MarshalledDomNode,
3-
type ReplPayload,
4-
identifierNameFunctionMeta,
5-
} from '@jsrepl/shared-types'
6-
import { getBabel } from '../get-babel'
1+
import { type MarshalledDomNode, type ReplPayload } from '@jsrepl/shared-types'
2+
import { parseFunction } from './parse-function'
73
import * as utils from './payload-utils'
84

95
const MAX_NESTING_LEVEL = 20
@@ -57,8 +53,12 @@ function _stringifyResult(
5753
return data?.caught ? `[ref *${data.index}] ` : ''
5854
}
5955

60-
function next(result: ReplPayload['result'], target: StringifyResultTarget): StringifyResult {
61-
return _stringifyResult(result, target, nestingLevel + 1, refs)
56+
function next(
57+
result: ReplPayload['result'],
58+
target: StringifyResultTarget,
59+
customNestingLevel?: number
60+
): StringifyResult {
61+
return _stringifyResult(result, target, customNestingLevel ?? nestingLevel + 1, refs)
6262
}
6363

6464
function t(str: string, relativeIndexLevel: number) {
@@ -331,6 +331,29 @@ ${t('', 0)}}`
331331
return { value, type: 'promise', lang: 'js' }
332332
}
333333

334+
if (utils.isMarshalledProxy(result)) {
335+
const { target: targetObj, handler } = result.__meta__
336+
let value: string
337+
if (target === 'decor') {
338+
const targetStr = next(targetObj, target, nestingLevel).value
339+
const handlerStr = next(handler, target).value
340+
value = `Proxy(${targetStr}) ${handlerStr}`
341+
} else {
342+
const targetStr = next(targetObj, target).value
343+
const handlerStr = next(handler, target).value
344+
value = `Proxy {
345+
${t('', 1)}[[Target]]: ${targetStr},
346+
${t('', 1)}[[Handler]]: ${handlerStr}
347+
${t('', 0)}}`
348+
}
349+
350+
return {
351+
value,
352+
type: 'proxy',
353+
lang: 'js',
354+
}
355+
}
356+
334357
if (utils.isMarshalledObject(result)) {
335358
const releaseRef = putRef(result)
336359
const { __meta__: meta, ...props } = result
@@ -388,101 +411,6 @@ ${t('', 0)}}`
388411
}
389412
}
390413

391-
// Let babel to parse this madness.
392-
// - function (arg1, arg2) {}
393-
// - async function (arg1, arg2) {}
394-
// - function name(arg1, arg2) {}
395-
// - async function name(arg1, arg2) {}
396-
// - function name(arg1, arg2 = 123, ...args) {}
397-
// - () => {}
398-
// - async () => {}
399-
// - args1 => {}
400-
// - async args1 => {}
401-
// - (args1, args2) => {}
402-
// - async (args1, args2) => {}
403-
// - function ({ jhkhj, asdad = 123 } = {}) {}
404-
// - () => 7
405-
// - function (asd = adsasd({})) { ... }
406-
function parseFunction(
407-
str: string,
408-
_isOriginalSource = false
409-
): {
410-
name: string
411-
args: string
412-
isAsync: boolean
413-
isArrow: boolean
414-
origSource: string | null
415-
} | null {
416-
const babel = getBabel()[0].value!
417-
418-
// @ts-expect-error Babel standalone: https://babeljs.io/docs/babel-standalone#internal-packages
419-
const { parser } = babel.packages as { parser: typeof import('@babel/parser') }
420-
421-
let ast: ReturnType<typeof parser.parseExpression>
422-
423-
try {
424-
// ArrowFunctionExpression | FunctionExpression
425-
ast = parser.parseExpression(str)
426-
} catch {
427-
return null
428-
}
429-
430-
let origSource: string | null = null
431-
432-
if (!_isOriginalSource) {
433-
origSource = getFunctionOriginalSource(ast)
434-
if (origSource) {
435-
return parseFunction(origSource, true)
436-
}
437-
} else {
438-
origSource = str
439-
}
440-
441-
if (ast.type === 'ArrowFunctionExpression') {
442-
return {
443-
name: '',
444-
args: ast.params.map((param) => str.slice(param.start!, param.end!)).join(', '),
445-
isAsync: ast.async,
446-
isArrow: true,
447-
origSource,
448-
}
449-
}
450-
451-
if (ast.type === 'FunctionExpression') {
452-
return {
453-
name: ast.id?.name ?? '',
454-
args: ast.params.map((param) => str.slice(param.start!, param.end!)).join(', '),
455-
isAsync: ast.async,
456-
isArrow: false,
457-
origSource,
458-
}
459-
}
460-
461-
return null
462-
}
463-
464-
function getFunctionOriginalSource(
465-
ast: ReturnType<typeof import('@babel/parser').parseExpression>
466-
): string | null {
467-
if (
468-
(ast.type === 'ArrowFunctionExpression' || ast.type === 'FunctionExpression') &&
469-
ast.body.type === 'BlockStatement'
470-
) {
471-
const node = ast.body.body[0]
472-
if (
473-
node?.type === 'ExpressionStatement' &&
474-
node.expression.type === 'CallExpression' &&
475-
node.expression.callee.type === 'Identifier' &&
476-
node.expression.callee.name === identifierNameFunctionMeta &&
477-
node.expression.arguments[0]?.type === 'StringLiteral'
478-
) {
479-
return node.expression.arguments[0].value
480-
}
481-
}
482-
483-
return null
484-
}
485-
486414
function stringifyDomNodeShort(result: MarshalledDomNode): string {
487415
const meta = result.__meta__
488416
const idAttr = meta.attributes.find((attr) => attr.name === 'id')

0 commit comments

Comments
 (0)