Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/jsrepl/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
},
"devDependencies": {
"@babel/parser": "^7.25.6",
"@babel/types": "^7.26.3",
"@chromatic-com/storybook": "^1.9.0",
"@iconify-json/mdi": "^1.2.0",
"@iconify-json/simple-icons": "^1.2.2",
Expand Down
4 changes: 3 additions & 1 deletion packages/jsrepl/src/lib/bundler/babel/repl-plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,9 @@ export function replPlugin({ types: t }: { types: typeof types }): PluginObj {
let fnId =
t.isFunctionDeclaration(path.node) || t.isFunctionExpression(path.node)
? path.node.id
: null
: t.isObjectMethod(path.node) && !path.node.computed && t.isIdentifier(path.node.key)
? path.node.key
: null

// Infer function id from variable id
if (
Expand Down
140 changes: 140 additions & 0 deletions packages/jsrepl/src/lib/repl-payload/parse-function.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import type Parser from '@babel/parser'
import type {
ArrowFunctionExpression,
FunctionExpression,
ObjectExpression,
ObjectMethod,
} from '@babel/types'
import { identifierNameFunctionMeta } from '@jsrepl/shared-types'
import { getBabel } from '../get-babel'

// Let babel to parse this madness.
// - function (arg1, arg2) {}
// - async function (arg1, arg2) {}
// - function name(arg1, arg2) {}
// - async function name(arg1, arg2) {}
// - function name(arg1, arg2 = 123, ...args) {}
// - () => {}
// - async () => {}
// - args1 => {}
// - async args1 => {}
// - (args1, args2) => {}
// - async (args1, args2) => {}
// - function ({ jhkhj, asdad = 123 } = {}) {}
// - () => 7
// - function (asd = adsasd({})) { ... }
// - get() { return 123 } // method, obj property
// - set(value) { this.value = value } // method, obj property
// - foo(value) {} // method, obj property
// - async foo(value) {} // async method, obj property
// - var obj = { foo: async function bar () {} } // async method, obj property, but defined with function keyword and name
export function parseFunction(
str: string,
_isOriginalSource = false
): {
name: string
args: string
isAsync: boolean
isArrow: boolean
isMethod: boolean
origSource: string | null
} | null {
const babel = getBabel()[0].value!

// @ts-expect-error Babel standalone: https://babeljs.io/docs/babel-standalone#internal-packages
const { parser } = babel.packages as { parser: typeof Parser }

let ast: ArrowFunctionExpression | FunctionExpression | ObjectMethod

try {
// ArrowFunctionExpression | FunctionExpression
ast = parser.parseExpression(str) as ArrowFunctionExpression | FunctionExpression
} catch {
try {
// ObjectMethod?
str = `{${str}}`
const objExpr = parser.parseExpression(str) as ObjectExpression
if (
objExpr.type === 'ObjectExpression' &&
objExpr.properties.length === 1 &&
objExpr.properties[0]!.type === 'ObjectMethod'
) {
ast = objExpr.properties[0]! as ObjectMethod
} else {
return null
}
} catch {
return null
}
}

let origSource: string | null = null

if (!_isOriginalSource) {
origSource = getFunctionOriginalSource(ast)
if (origSource) {
return parseFunction(origSource, true)
}
} else {
origSource = str
}

if (ast.type === 'ArrowFunctionExpression') {
return {
name: '',
args: ast.params.map((param) => str.slice(param.start!, param.end!)).join(', '),
isAsync: ast.async,
isArrow: true,
isMethod: false,
origSource,
}
}

if (ast.type === 'FunctionExpression') {
return {
name: ast.id?.name ?? '',
args: ast.params.map((param) => str.slice(param.start!, param.end!)).join(', '),
isAsync: ast.async,
isArrow: false,
isMethod: false,
origSource,
}
}

if (ast.type === 'ObjectMethod') {
return {
name: ast.computed ? '' : ast.key.type === 'Identifier' ? ast.key.name : '',
args: ast.params.map((param) => str.slice(param.start!, param.end!)).join(', '),
isAsync: ast.async,
isArrow: false,
isMethod: true,
origSource,
}
}

return null
}

function getFunctionOriginalSource(
ast: ArrowFunctionExpression | FunctionExpression | ObjectMethod
): string | null {
if (
(ast.type === 'ArrowFunctionExpression' ||
ast.type === 'FunctionExpression' ||
ast.type === 'ObjectMethod') &&
ast.body.type === 'BlockStatement'
) {
const node = ast.body.body[0]
if (
node?.type === 'ExpressionStatement' &&
node.expression.type === 'CallExpression' &&
node.expression.callee.type === 'Identifier' &&
node.expression.callee.name === identifierNameFunctionMeta &&
node.expression.arguments[0]?.type === 'StringLiteral'
) {
return node.expression.arguments[0].value
}
}

return null
}
5 changes: 5 additions & 0 deletions packages/jsrepl/src/lib/repl-payload/payload-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
MarshalledFunction,
MarshalledObject,
MarshalledPromise,
MarshalledProxy,
MarshalledSymbol,
MarshalledType,
MarshalledWeakMap,
Expand Down Expand Up @@ -42,6 +43,10 @@ export function isMarshalledPromise(result: object): result is MarshalledPromise
return getMarshalledType(result) === MarshalledType.Promise
}

export function isMarshalledProxy(result: object): result is MarshalledProxy {
return getMarshalledType(result) === MarshalledType.Proxy
}

export function getMarshalledType(result: object): MarshalledType | null {
return '__meta__' in result &&
result.__meta__ !== null &&
Expand Down
7 changes: 7 additions & 0 deletions packages/jsrepl/src/lib/repl-payload/render-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ function replacer(this: unknown, key: string, value: unknown): unknown {
finally: 'function finally() { [native code] }',
}

case utils.isMarshalledProxy(value): {
const { target } = value.__meta__
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { __meta__, ...props } = target
return props
}

case utils.isMarshalledObject(value): {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { __meta__, ...props } = value
Expand Down
13 changes: 13 additions & 0 deletions packages/jsrepl/src/lib/repl-payload/render-mock-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ function revive(
return doc.head.firstChild ?? doc.body.firstChild
}

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

try {
// Object method? - e.g., get() {}, async foo(value) {}
fn = new Function(`return Object.values({ ${value.serialized} })[0]`)()
} catch {}

try {
// Some reserved keywords are not allowed in function names,
// for example `catch`, `finally`, `do`, etc.
Expand Down Expand Up @@ -70,6 +76,13 @@ function revive(
case utils.isMarshalledPromise(value):
return new Promise(() => {})

case utils.isMarshalledProxy(value): {
const { target, handler } = value.__meta__
const revivedTarget = revive(target, context) as object
const revivedHandler = revive(handler, context) as ProxyHandler<object>
return new Proxy(revivedTarget, revivedHandler)
}

case utils.isMarshalledObject(value): {
const { __meta__: meta, ...props } = value

Expand Down
134 changes: 31 additions & 103 deletions packages/jsrepl/src/lib/repl-payload/stringify.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import {
type MarshalledDomNode,
type ReplPayload,
identifierNameFunctionMeta,
} from '@jsrepl/shared-types'
import { getBabel } from '../get-babel'
import { type MarshalledDomNode, type ReplPayload } from '@jsrepl/shared-types'
import { parseFunction } from './parse-function'
import * as utils from './payload-utils'

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

function next(result: ReplPayload['result'], target: StringifyResultTarget): StringifyResult {
return _stringifyResult(result, target, nestingLevel + 1, refs)
function next(
result: ReplPayload['result'],
target: StringifyResultTarget,
customNestingLevel?: number
): StringifyResult {
return _stringifyResult(result, target, customNestingLevel ?? nestingLevel + 1, refs)
}

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

if (utils.isMarshalledProxy(result)) {
const { target: targetObj, handler } = result.__meta__
let value: string
if (target === 'decor') {
const targetStr = next(targetObj, target, nestingLevel).value
const handlerStr = next(handler, target).value
value = `Proxy(${targetStr}) ${handlerStr}`
} else {
const targetStr = next(targetObj, target).value
const handlerStr = next(handler, target).value
value = `Proxy {
${t('', 1)}[[Target]]: ${targetStr},
${t('', 1)}[[Handler]]: ${handlerStr}
${t('', 0)}}`
}

return {
value,
type: 'proxy',
lang: 'js',
}
}

if (utils.isMarshalledObject(result)) {
const releaseRef = putRef(result)
const { __meta__: meta, ...props } = result
Expand Down Expand Up @@ -388,101 +411,6 @@ ${t('', 0)}}`
}
}

// Let babel to parse this madness.
// - function (arg1, arg2) {}
// - async function (arg1, arg2) {}
// - function name(arg1, arg2) {}
// - async function name(arg1, arg2) {}
// - function name(arg1, arg2 = 123, ...args) {}
// - () => {}
// - async () => {}
// - args1 => {}
// - async args1 => {}
// - (args1, args2) => {}
// - async (args1, args2) => {}
// - function ({ jhkhj, asdad = 123 } = {}) {}
// - () => 7
// - function (asd = adsasd({})) { ... }
function parseFunction(
str: string,
_isOriginalSource = false
): {
name: string
args: string
isAsync: boolean
isArrow: boolean
origSource: string | null
} | null {
const babel = getBabel()[0].value!

// @ts-expect-error Babel standalone: https://babeljs.io/docs/babel-standalone#internal-packages
const { parser } = babel.packages as { parser: typeof import('@babel/parser') }

let ast: ReturnType<typeof parser.parseExpression>

try {
// ArrowFunctionExpression | FunctionExpression
ast = parser.parseExpression(str)
} catch {
return null
}

let origSource: string | null = null

if (!_isOriginalSource) {
origSource = getFunctionOriginalSource(ast)
if (origSource) {
return parseFunction(origSource, true)
}
} else {
origSource = str
}

if (ast.type === 'ArrowFunctionExpression') {
return {
name: '',
args: ast.params.map((param) => str.slice(param.start!, param.end!)).join(', '),
isAsync: ast.async,
isArrow: true,
origSource,
}
}

if (ast.type === 'FunctionExpression') {
return {
name: ast.id?.name ?? '',
args: ast.params.map((param) => str.slice(param.start!, param.end!)).join(', '),
isAsync: ast.async,
isArrow: false,
origSource,
}
}

return null
}

function getFunctionOriginalSource(
ast: ReturnType<typeof import('@babel/parser').parseExpression>
): string | null {
if (
(ast.type === 'ArrowFunctionExpression' || ast.type === 'FunctionExpression') &&
ast.body.type === 'BlockStatement'
) {
const node = ast.body.body[0]
if (
node?.type === 'ExpressionStatement' &&
node.expression.type === 'CallExpression' &&
node.expression.callee.type === 'Identifier' &&
node.expression.callee.name === identifierNameFunctionMeta &&
node.expression.arguments[0]?.type === 'StringLiteral'
) {
return node.expression.arguments[0].value
}
}

return null
}

function stringifyDomNodeShort(result: MarshalledDomNode): string {
const meta = result.__meta__
const idAttr = meta.attributes.find((attr) => attr.name === 'id')
Expand Down
Loading
Loading