Compare commits

...

2 Commits

6 changed files with 398 additions and 28 deletions

View File

@@ -31,7 +31,12 @@ import {
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import type { WandControlHandlers } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block'
import { restoreCursorAfterInsertion } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/utils'
import {
restoreCursorAfterInsertion,
sanitizeForParsing,
validateJavaScript,
validatePython,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/utils'
import { WandPromptBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/wand-prompt-bar/wand-prompt-bar'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import { useWand } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand'
@@ -166,7 +171,7 @@ interface CodeProps {
defaultCollapsed?: boolean
defaultValue?: string | number | boolean | Record<string, unknown> | Array<unknown>
showCopyButton?: boolean
onValidationChange?: (isValid: boolean) => void
onValidationChange?: (isValid: boolean, errorMessage?: string | null) => void
wandConfig: {
enabled: boolean
prompt: string
@@ -250,6 +255,18 @@ export const Code = memo(function Code({
}
}, [shouldValidateJson, trimmedCode])
const syntaxError = useMemo(() => {
if (effectiveLanguage === 'json' || !trimmedCode) return null
const sanitized = sanitizeForParsing(trimmedCode)
if (effectiveLanguage === 'javascript') {
return validateJavaScript(sanitized)
}
if (effectiveLanguage === 'python') {
return validatePython(sanitized)
}
return null
}, [effectiveLanguage, trimmedCode])
const gutterWidthPx = useMemo(() => {
const lineCount = code.split('\n').length
return calculateGutterWidth(lineCount)
@@ -341,19 +358,21 @@ export const Code = memo(function Code({
useEffect(() => {
if (!onValidationChange) return
const isValid = !shouldValidateJson || isValidJson
const isValid = (!shouldValidateJson || isValidJson) && !syntaxError
if (isValid) {
onValidationChange(true)
onValidationChange(true, null)
return
}
const errorMessage = !isValidJson ? 'Invalid JSON' : syntaxError
const timeoutId = setTimeout(() => {
onValidationChange(false)
onValidationChange(false, errorMessage)
}, 150)
return () => clearTimeout(timeoutId)
}, [isValidJson, onValidationChange, shouldValidateJson])
}, [isValidJson, syntaxError, onValidationChange, shouldValidateJson])
useEffect(() => {
handleStreamStartRef.current = () => {

View File

@@ -189,7 +189,7 @@ const getPreviewValue = (
* Renders the label with optional validation and description tooltips.
*
* @param config - The sub-block configuration defining the label content
* @param isValidJson - Whether the JSON content is valid (for code blocks)
* @param codeValidation - Validation state for code blocks (valid flag + optional error message)
* @param subBlockValues - Current values of all subblocks for evaluating conditional requirements
* @param wandState - State and handlers for the inline AI generate feature
* @param canonicalToggle - Metadata and handlers for the basic/advanced mode toggle
@@ -200,7 +200,7 @@ const getPreviewValue = (
*/
const renderLabel = (
config: SubBlockConfig,
isValidJson: boolean,
codeValidation: { isValid: boolean; errorMessage: string | null },
subBlockValues?: Record<string, any>,
wandState?: {
isSearchActive: boolean
@@ -250,21 +250,18 @@ const renderLabel = (
{config.title}
{required && <span className='ml-0.5'>*</span>}
{labelSuffix}
{config.type === 'code' &&
config.language === 'json' &&
!isValidJson &&
!wandState?.isStreaming && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<span className='inline-flex'>
<AlertTriangle className='h-3 w-3 flex-shrink-0 cursor-pointer text-destructive' />
</span>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
<p>Invalid JSON</p>
</Tooltip.Content>
</Tooltip.Root>
)}
{config.type === 'code' && !codeValidation.isValid && !wandState?.isStreaming && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<span className='inline-flex'>
<AlertTriangle className='h-3 w-3 flex-shrink-0 cursor-pointer text-destructive' />
</span>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
<p>{codeValidation.errorMessage ?? 'Syntax error'}</p>
</Tooltip.Content>
</Tooltip.Root>
)}
</Label>
<div className='flex min-w-0 flex-1 items-center justify-end gap-[6px]'>
{showCopy && (
@@ -466,7 +463,8 @@ function SubBlockComponent({
const params = useParams()
const workspaceId = params.workspaceId as string
const [isValidJson, setIsValidJson] = useState(true)
const [isValidCode, setIsValidCode] = useState(true)
const [codeErrorMessage, setCodeErrorMessage] = useState<string | null>(null)
const [isSearchActive, setIsSearchActive] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [copied, setCopied] = useState(false)
@@ -484,8 +482,9 @@ function SubBlockComponent({
e.stopPropagation()
}
const handleValidationChange = (isValid: boolean): void => {
setIsValidJson(isValid)
const handleValidationChange = (isValid: boolean, errorMessage?: string | null): void => {
setIsValidCode(isValid)
setCodeErrorMessage(errorMessage ?? null)
}
const isWandEnabled = config.wandConfig?.enabled ?? false
@@ -1151,7 +1150,7 @@ function SubBlockComponent({
<div onMouseDown={handleMouseDown} className='subblock-content flex flex-col gap-[10px]'>
{renderLabel(
config,
isValidJson,
{ isValid: isValidCode, errorMessage: codeErrorMessage },
subBlockValues,
{
isSearchActive,

View File

@@ -0,0 +1,197 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { sanitizeForParsing, validateJavaScript, validatePython } from './utils'
describe('sanitizeForParsing', () => {
it('replaces <Block.output> references with valid identifiers', () => {
const result = sanitizeForParsing('const x = <Block.output>')
expect(result).not.toContain('<')
expect(result).not.toContain('>')
expect(result).toContain('__placeholder_')
})
it('replaces {{ENV_VAR}} with valid identifiers', () => {
const result = sanitizeForParsing('const url = {{API_URL}}')
expect(result).not.toContain('{{')
expect(result).not.toContain('}}')
expect(result).toContain('__placeholder_')
})
it('replaces nested path references like <Block.output[0].field>', () => {
const result = sanitizeForParsing('const x = <Agent.response.choices[0].text>')
expect(result).not.toContain('<Agent')
})
it('replaces loop/parallel context references', () => {
const result = sanitizeForParsing('const item = <loop.currentItem>')
expect(result).not.toContain('<loop')
})
it('replaces variable references', () => {
const result = sanitizeForParsing('const v = <variable.myVar>')
expect(result).not.toContain('<variable')
})
it('handles multiple references in one string', () => {
const code = 'const a = <Block1.out>; const b = {{SECRET}}; const c = <Block2.value>'
const result = sanitizeForParsing(code)
expect(result).not.toContain('<Block1')
expect(result).not.toContain('{{SECRET}}')
expect(result).not.toContain('<Block2')
expect(result.match(/__placeholder_/g)?.length).toBe(3)
})
it('does not replace regular JS comparison operators', () => {
const code = 'if (a < b && c > d) {}'
const result = sanitizeForParsing(code)
expect(result).toBe(code)
})
it('does not replace HTML tags that are not references', () => {
const code = 'const html = "<div>hello</div>"'
const result = sanitizeForParsing(code)
expect(result).toBe(code)
})
})
describe('validateJavaScript', () => {
it('returns null for valid JavaScript', () => {
expect(validateJavaScript('const x = 1')).toBeNull()
expect(validateJavaScript('function foo() { return 42 }')).toBeNull()
expect(validateJavaScript('const arr = [1, 2, 3].map(x => x * 2)')).toBeNull()
})
it('returns null for valid async/await code', () => {
expect(validateJavaScript('async function foo() { await bar() }')).toBeNull()
})
it('returns null for bare return statements (function block wraps in async fn)', () => {
expect(validateJavaScript('return 42')).toBeNull()
expect(validateJavaScript(sanitizeForParsing('return <Block.output>'))).toBeNull()
expect(validateJavaScript('const x = 1\nreturn x')).toBeNull()
})
it('returns null for await at top level (wrapped in async fn)', () => {
expect(validateJavaScript('const res = await fetch("url")')).toBeNull()
})
it('returns null for valid ES module syntax', () => {
expect(validateJavaScript('import { foo } from "bar"')).toBeNull()
expect(validateJavaScript('export default function() {}')).toBeNull()
})
it('detects missing closing brace', () => {
const result = validateJavaScript('function foo() {')
expect(result).not.toBeNull()
expect(result).toContain('Syntax error')
})
it('detects missing closing paren', () => {
const result = validateJavaScript('console.log("hello"')
expect(result).not.toBeNull()
expect(result).toContain('Syntax error')
})
it('detects unexpected token', () => {
const result = validateJavaScript('const = 5')
expect(result).not.toBeNull()
expect(result).toContain('Syntax error')
})
it('includes adjusted line and column in error message', () => {
const result = validateJavaScript('const x = 1\nconst = 5')
expect(result).toMatch(/line 2/)
expect(result).toMatch(/col \d+/)
})
it('returns null for empty code', () => {
expect(validateJavaScript('')).toBeNull()
})
it('does not error on sanitized references', () => {
const code = sanitizeForParsing('const x = <Block.output> + {{ENV_VAR}}')
expect(validateJavaScript(code)).toBeNull()
})
})
describe('validatePython', () => {
it('returns null for valid Python', () => {
expect(validatePython('x = 1')).toBeNull()
expect(validatePython('def foo():\n return 42')).toBeNull()
expect(validatePython('arr = [1, 2, 3]')).toBeNull()
})
it('returns null for Python with comments', () => {
expect(validatePython('x = 1 # this is a comment')).toBeNull()
expect(validatePython('# full line comment\nx = 1')).toBeNull()
})
it('returns null for Python with strings containing brackets', () => {
expect(validatePython('x = "hello (world)"')).toBeNull()
expect(validatePython("x = 'brackets [here] {too}'")).toBeNull()
})
it('returns null for triple-quoted strings', () => {
expect(validatePython('x = """hello\nworld"""')).toBeNull()
expect(validatePython("x = '''multi\nline\nstring'''")).toBeNull()
})
it('returns null for triple-quoted strings with brackets', () => {
expect(validatePython('x = """has { and ( inside"""')).toBeNull()
})
it('detects unmatched opening paren', () => {
const result = validatePython('foo(1, 2')
expect(result).not.toBeNull()
expect(result).toContain("'('")
})
it('detects unmatched closing paren', () => {
const result = validatePython('foo)')
expect(result).not.toBeNull()
expect(result).toContain("')'")
})
it('detects unmatched bracket', () => {
const result = validatePython('arr = [1, 2')
expect(result).not.toBeNull()
expect(result).toContain("'['")
})
it('detects unterminated string', () => {
const result = validatePython('x = "hello')
expect(result).not.toBeNull()
expect(result).toContain('Unterminated string')
})
it('detects unterminated triple-quoted string', () => {
const result = validatePython('x = """hello')
expect(result).not.toBeNull()
expect(result).toContain('Unterminated triple-quoted string')
})
it('includes line number in error', () => {
const result = validatePython('x = 1\ny = (2')
expect(result).toMatch(/line 2/)
})
it('handles escaped quotes in strings', () => {
expect(validatePython('x = "hello \\"world\\""')).toBeNull()
expect(validatePython("x = 'it\\'s fine'")).toBeNull()
})
it('handles brackets inside comments', () => {
expect(validatePython('x = 1 # unmatched ( here')).toBeNull()
})
it('returns null for empty code', () => {
expect(validatePython('')).toBeNull()
})
it('does not error on sanitized references', () => {
const code = sanitizeForParsing('x = <Block.output> + {{ENV_VAR}}')
expect(validatePython(code)).toBeNull()
})
})

View File

@@ -1,3 +1,17 @@
import { parse } from 'acorn'
/**
* Matches Sim block references: `<word.path>`, `<word.path[0].nested>`, `<loop.index>`, etc.
* Must contain a dot (.) to distinguish from HTML tags or comparison operators.
*/
const REFERENCE_PATTERN = /<[a-zA-Z]\w*(?:\.\w+(?:\[\d+\])?)+>/g
/**
* Matches Sim env-var placeholders: `{{WORD}}`, `{{MY_VAR}}`.
* Only allows word characters (no spaces, special chars).
*/
const ENV_VAR_PATTERN = /\{\{\w+\}\}/g
/**
* Restores the cursor position in a textarea after a dropdown insertion.
* Schedules a macrotask (via setTimeout) that runs after React's controlled-component commit
@@ -18,3 +32,132 @@ export function restoreCursorAfterInsertion(
}
}, 0)
}
/**
* Replaces `<Block.output>` references and `{{ENV_VAR}}` placeholders with
* valid JS/Python identifiers so the code can be parsed without false errors.
*/
export function sanitizeForParsing(code: string): string {
let counter = 0
let sanitized = code.replace(ENV_VAR_PATTERN, () => `__placeholder_${counter++}__`)
sanitized = sanitized.replace(REFERENCE_PATTERN, () => `__placeholder_${counter++}__`)
return sanitized
}
/**
* Validates JavaScript code for syntax errors using acorn.
*
* Tries two parse strategies to match the Function block's runtime behavior:
* 1. As a module (`import`/`export` are valid at top level)
* 2. Wrapped in `async () => { ... }` (bare `return`/`await` are valid)
*
* Only reports an error if both strategies fail, using the wrapped error
* since that's the primary execution context.
*
* @returns Error message string, or null if valid.
*/
export function validateJavaScript(code: string): string | null {
try {
parse(code, { ecmaVersion: 'latest', sourceType: 'module' })
return null
} catch {
// Module parse failed — try as function body (allows bare return/await)
}
const wrapped = `(async () => {\n${code}\n})()`
try {
parse(wrapped, { ecmaVersion: 'latest', sourceType: 'script' })
return null
} catch (e: unknown) {
if (e instanceof SyntaxError) {
const msg = e.message
const match = msg.match(/\((\d+):(\d+)\)/)
if (match) {
const adjustedLine = Number(match[1]) - 1
if (adjustedLine < 1) return null
return `Syntax error at line ${adjustedLine}, col ${match[2]}: ${msg.replace(/\s*\(\d+:\d+\)/, '')}`
}
return `Syntax error: ${msg}`
}
return null
}
}
/**
* Validates Python code for common syntax errors: unmatched brackets/parens,
* unterminated strings (single-line and triple-quoted).
* Processes the entire code string as a stream to correctly handle
* multiline triple-quoted strings.
*
* @returns Error message string, or null if no issues detected.
*/
export function validatePython(code: string): string | null {
const stack: { char: string; line: number }[] = []
const openers: Record<string, string> = { ')': '(', ']': '[', '}': '{' }
const closers = new Set([')', ']', '}'])
const openChars = new Set(['(', '[', '{'])
let line = 1
let i = 0
while (i < code.length) {
const ch = code[i]
if (ch === '\n') {
line++
i++
continue
}
if (ch === '#') {
const newline = code.indexOf('\n', i)
i = newline === -1 ? code.length : newline
continue
}
if (ch === '"' || ch === "'") {
const tripleQuote = ch.repeat(3)
if (code.slice(i, i + 3) === tripleQuote) {
const startLine = line
const endIdx = code.indexOf(tripleQuote, i + 3)
if (endIdx === -1) {
return `Unterminated triple-quoted string starting at line ${startLine}`
}
for (let k = i; k < endIdx + 3; k++) {
if (code[k] === '\n') line++
}
i = endIdx + 3
continue
}
const startLine = line
i++
while (i < code.length && code[i] !== ch && code[i] !== '\n') {
if (code[i] === '\\') i++
i++
}
if (i >= code.length || code[i] === '\n') {
return `Unterminated string at line ${startLine}`
}
i++
continue
}
if (openChars.has(ch)) {
stack.push({ char: ch, line })
} else if (closers.has(ch)) {
if (stack.length === 0 || stack[stack.length - 1].char !== openers[ch]) {
return `Unmatched '${ch}' at line ${line}`
}
stack.pop()
}
i++
}
if (stack.length > 0) {
const unmatched = stack[stack.length - 1]
return `Unmatched '${unmatched.char}' opened at line ${unmatched.line}`
}
return null
}

View File

@@ -82,6 +82,7 @@
"@trigger.dev/sdk": "4.1.2",
"@types/react-window": "2.0.0",
"@types/three": "0.177.0",
"acorn": "8.16.0",
"better-auth": "1.3.12",
"binary-extensions": "^2.0.0",
"browser-image-compression": "^2.0.2",

View File

@@ -115,6 +115,7 @@
"@trigger.dev/sdk": "4.1.2",
"@types/react-window": "2.0.0",
"@types/three": "0.177.0",
"acorn": "8.16.0",
"better-auth": "1.3.12",
"binary-extensions": "^2.0.0",
"browser-image-compression": "^2.0.2",
@@ -1641,7 +1642,7 @@
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="],
@@ -3825,6 +3826,8 @@
"@langchain/core/uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
"@mdx-js/mdx/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"@modelcontextprotocol/sdk/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
@@ -4079,6 +4082,8 @@
"engine.io-client/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
"esast-util-from-js/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
@@ -4139,6 +4144,8 @@
"imapflow/pino": ["pino@10.1.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w=="],
"import-in-the-middle/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"inquirer/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"inquirer/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="],
@@ -4173,8 +4180,12 @@
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"micromark-extension-mdxjs/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"mlly/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"neo4j-driver-bolt-connection/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],