6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
224 lines
8.1 KiB
TypeScript
224 lines
8.1 KiB
TypeScript
import { setRecordValue } from '@/lib/core/utils/records'
|
|
import type {
|
|
CodePlaceholderBinding,
|
|
CodePlaceholderCompilationContext,
|
|
CodePlaceholderOccurrence,
|
|
CodePlaceholderRuntimeBinding,
|
|
CompiledCodePlaceholders,
|
|
InternalCompileCodePlaceholdersInput,
|
|
ResolvedCodePlaceholderOccurrence,
|
|
ResolvedCodePlaceholderValueOccurrence,
|
|
} from '@/lib/execution/code-placeholders/types'
|
|
|
|
const MAX_PLACEHOLDERS = 10_000
|
|
const PLACEHOLDER_PATTERN = /\{\{([^}]+)\}\}/g
|
|
|
|
export class CodePlaceholderCompileError extends Error {
|
|
readonly line?: number
|
|
readonly column?: number
|
|
|
|
constructor(message: string, code?: string, offset?: number) {
|
|
super(message)
|
|
this.name = 'CodePlaceholderCompileError'
|
|
if (code !== undefined && offset !== undefined) {
|
|
const prefix = code.slice(0, offset)
|
|
this.line = prefix.split('\n').length
|
|
this.column = offset - prefix.lastIndexOf('\n')
|
|
}
|
|
}
|
|
}
|
|
|
|
export function collectCodePlaceholderOccurrences(code: string): CodePlaceholderOccurrence[] {
|
|
const occurrences: CodePlaceholderOccurrence[] = []
|
|
let match: RegExpExecArray | null
|
|
PLACEHOLDER_PATTERN.lastIndex = 0
|
|
while ((match = PLACEHOLDER_PATTERN.exec(code)) !== null) {
|
|
const name = match[1].trim()
|
|
if (!name) continue
|
|
occurrences.push({
|
|
start: match.index,
|
|
end: match.index + match[0].length,
|
|
raw: match[0],
|
|
name,
|
|
})
|
|
if (occurrences.length > MAX_PLACEHOLDERS) {
|
|
throw new CodePlaceholderCompileError(
|
|
`Code contains more than ${MAX_PLACEHOLDERS} variable placeholders`
|
|
)
|
|
}
|
|
}
|
|
return occurrences
|
|
}
|
|
|
|
function chooseNamespace(code: string, reservedNames: ReadonlySet<string>): string {
|
|
const occupied = new Set<number>()
|
|
const collectOccupiedNamespaces = (value: string): void => {
|
|
for (const match of value.matchAll(/__sim_code_(\d+)/g)) occupied.add(Number(match[1]))
|
|
}
|
|
collectOccupiedNamespaces(code)
|
|
const normalizedCode = code.normalize('NFKC')
|
|
if (normalizedCode !== code) collectOccupiedNamespaces(normalizedCode)
|
|
for (const name of reservedNames) {
|
|
const match = /^__sim_code_(\d+)(?:_|$)/.exec(name)
|
|
if (match) occupied.add(Number(match[1]))
|
|
}
|
|
for (let index = 0; ; index += 1) {
|
|
if (!occupied.has(index)) return `__sim_code_${index}`
|
|
}
|
|
}
|
|
|
|
function stringifyResolverValues(
|
|
params: Record<string, unknown>,
|
|
environmentVariables: Record<string, string>,
|
|
referencedNames: ReadonlySet<string>
|
|
): Record<string, string> {
|
|
const values: Record<string, string> = Object.create(null)
|
|
for (const [name, value] of Object.entries(params)) {
|
|
if (!referencedNames.has(name)) continue
|
|
if (value !== undefined && value !== null) setRecordValue(values, name, String(value))
|
|
}
|
|
for (const [name, value] of Object.entries(environmentVariables)) {
|
|
if (!referencedNames.has(name)) continue
|
|
if (value !== undefined && value !== null) setRecordValue(values, name, value)
|
|
}
|
|
return values
|
|
}
|
|
|
|
export function createCodePlaceholderCompilationContext(
|
|
input: InternalCompileCodePlaceholdersInput,
|
|
options: { identifierSuffix?: string } = {}
|
|
): CodePlaceholderCompilationContext {
|
|
const params = input.params ?? {}
|
|
const environmentVariables = input.environmentVariables ?? {}
|
|
const occurrences = collectCodePlaceholderOccurrences(input.code)
|
|
const referencedNames = new Set(occurrences.map((occurrence) => occurrence.name))
|
|
const values = stringifyResolverValues(params, environmentVariables, referencedNames)
|
|
const reservedNames = new Set(input.reservedNames ?? [])
|
|
for (const name of Object.keys(environmentVariables)) reservedNames.add(name)
|
|
for (const name of Object.keys(params)) reservedNames.add(name)
|
|
|
|
const namespace =
|
|
occurrences.length > 0 ? chooseNamespace(input.code, reservedNames) : '__sim_code_0'
|
|
const bindingByName = new Map<string, CodePlaceholderBinding>()
|
|
const runtimeBindingByKind = new Map<
|
|
CodePlaceholderRuntimeBinding['kind'],
|
|
CodePlaceholderRuntimeBinding
|
|
>()
|
|
const resolvedSecretNameOffsets = new Map<string, number>()
|
|
const privateInputs: Array<{ environmentVariable: string; content: string }> = []
|
|
const privateInputByContent = new Map<string, { environmentVariable: string; content: string }>()
|
|
const internalIdentifiers = new Set<string>()
|
|
|
|
const hasValue = (name: string): boolean =>
|
|
input.analysisOnly === true || Object.hasOwn(values, name)
|
|
|
|
const recordResolution = (occurrence: CodePlaceholderOccurrence): void => {
|
|
if (!input.analysisOnly && !Object.hasOwn(environmentVariables, occurrence.name)) return
|
|
const currentOffset = resolvedSecretNameOffsets.get(occurrence.name)
|
|
if (currentOffset === undefined || occurrence.start < currentOffset) {
|
|
resolvedSecretNameOffsets.set(occurrence.name, occurrence.start)
|
|
}
|
|
}
|
|
|
|
const resolveValue = (
|
|
occurrence: CodePlaceholderOccurrence
|
|
): ResolvedCodePlaceholderValueOccurrence | undefined => {
|
|
if (!hasValue(occurrence.name)) return undefined
|
|
recordResolution(occurrence)
|
|
return {
|
|
...occurrence,
|
|
value: Object.hasOwn(values, occurrence.name) ? values[occurrence.name] : '',
|
|
}
|
|
}
|
|
|
|
const ensureBinding = (name: string, value: string): CodePlaceholderBinding => {
|
|
const existing = bindingByName.get(name)
|
|
if (existing) return existing
|
|
|
|
const bindingName = `${namespace}_binding_${bindingByName.size}${options.identifierSuffix ?? ''}`
|
|
reservedNames.add(bindingName)
|
|
internalIdentifiers.add(bindingName)
|
|
const binding = { name: bindingName, value }
|
|
bindingByName.set(name, binding)
|
|
return binding
|
|
}
|
|
|
|
return {
|
|
occurrences,
|
|
hasValue,
|
|
resolveValue,
|
|
runtimeBindingFor(kind) {
|
|
const existing = runtimeBindingByKind.get(kind)
|
|
if (existing) return existing
|
|
|
|
const name = `${namespace}_runtime_${runtimeBindingByKind.size}${options.identifierSuffix ?? ''}`
|
|
reservedNames.add(name)
|
|
internalIdentifiers.add(name)
|
|
const binding = { name, kind }
|
|
runtimeBindingByKind.set(kind, binding)
|
|
return binding
|
|
},
|
|
registerInternalIdentifier(identifier) {
|
|
internalIdentifiers.add(identifier)
|
|
},
|
|
resolve(occurrence): ResolvedCodePlaceholderOccurrence | undefined {
|
|
const resolved = resolveValue(occurrence)
|
|
if (!resolved) return undefined
|
|
const binding = ensureBinding(occurrence.name, resolved.value)
|
|
return {
|
|
...resolved,
|
|
bindingName: binding.name,
|
|
}
|
|
},
|
|
createPrivateInput(content) {
|
|
const existing = privateInputByContent.get(content)
|
|
if (existing) return existing
|
|
const environmentVariable = `${namespace}_input_${privateInputs.length}${options.identifierSuffix ?? ''}`
|
|
reservedNames.add(environmentVariable)
|
|
internalIdentifiers.add(environmentVariable)
|
|
const privateInput = { environmentVariable, content }
|
|
privateInputs.push(privateInput)
|
|
privateInputByContent.set(content, privateInput)
|
|
return privateInput
|
|
},
|
|
finish(code): CompiledCodePlaceholders {
|
|
return {
|
|
code,
|
|
bindings: [...bindingByName.values()],
|
|
privateInputs,
|
|
runtimeBindings: [...runtimeBindingByKind.values()],
|
|
resolvedSecretNames: [...resolvedSecretNameOffsets]
|
|
.sort((left, right) => left[1] - right[1])
|
|
.map(([name]) => name),
|
|
internalIdentifiers: [...internalIdentifiers],
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
export interface SourceEdit {
|
|
start: number
|
|
end: number
|
|
text: string
|
|
}
|
|
|
|
export function applySourceEdits(code: string, edits: SourceEdit[]): string {
|
|
if (edits.length === 0) return code
|
|
const sorted = [...edits].sort((left, right) => left.start - right.start || left.end - right.end)
|
|
let cursor = 0
|
|
let output = ''
|
|
for (const edit of sorted) {
|
|
if (edit.start < cursor || edit.end < edit.start || edit.end > code.length) {
|
|
throw new CodePlaceholderCompileError('Overlapping code placeholder transformations')
|
|
}
|
|
output += code.slice(cursor, edit.start)
|
|
output += edit.text
|
|
cursor = edit.end
|
|
}
|
|
return output + code.slice(cursor)
|
|
}
|
|
|
|
export function isOffsetInRanges(offset: number, ranges: ReadonlyArray<[number, number]>): boolean {
|
|
return ranges.some(([start, end]) => offset >= start && offset < end)
|
|
}
|