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
8 changes: 8 additions & 0 deletions .changeset/curly-moose-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@getlang/parser": minor
"@getlang/utils": minor
"@getlang/get": minor
"@getlang/lib": minor
---

slice dependencies analysis
Binary file modified bun.lockb
Binary file not shown.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
"test"
],
"devDependencies": {
"@biomejs/biome": "^2.1.4",
"@biomejs/biome": "^2.2.0",
"@changesets/changelog-github": "^0.5.1",
"@changesets/cli": "^2.29.5",
"@changesets/cli": "^2.29.6",
"@types/bun": "^1.2.20",
"knip": "^5.62.0",
"sherif": "^1.6.1",
Expand Down
38 changes: 21 additions & 17 deletions packages/get/src/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const {
SliceError,
UnknownInputsError,
ValueReferenceError,
ValueTypeError,
} = errors

export async function execute(
Expand Down Expand Up @@ -64,25 +65,13 @@ export async function execute(
return els.join('')
},

IdentifierExpr(node) {
const value = scope.vars[node.value.value]
invariant(
value !== undefined,
new ValueReferenceError(node.value.value),
)
return value
},

SliceExpr: {
async enter(node, visit) {
return withContext(scope, node, visit, async context => {
const { slice } = node
try {
const value = await hooks.slice(
slice.value,
context ? toValue(context.value, context.typeInfo) : {},
context?.value ?? {},
)
const deps = context && toValue(context.value, context.typeInfo)
const value = await hooks.slice(slice.value, deps)
const ret =
value === undefined ? new NullSelection('<slice>') : value
const optional = node.typeInfo.type === Type.Maybe
Expand All @@ -94,13 +83,28 @@ export async function execute(
},
},

IdentifierExpr: {
async enter(node, visit) {
return withContext(scope, node, visit, async () => {
const id = node.id.value
const value = id ? scope.vars[id] : scope.context
invariant(
value !== undefined,
new ValueReferenceError(node.id.value),
)
return value
})
},
},

SelectorExpr: {
async enter(node, visit) {
return withContext(scope, node, visit, async context => {
const selector = await visit(node.selector)
if (typeof selector !== 'string') {
return selector
}
invariant(
typeof selector === 'string',
new ValueTypeError('Expected selector string'),
)
const args = [context!.value, selector, node.expand] as const

function select(typeInfo: TypeInfo) {
Expand Down
4 changes: 1 addition & 3 deletions packages/get/src/hooks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,8 @@ describe('hook', () => {

test('on slice', async () => {
const sliceHook = mock<SliceHook>(() => 3)

const result = await execute('extract `1 + 2`', {}, { slice: sliceHook })

expect(sliceHook).toHaveBeenCalledWith('return 1 + 2', {}, {})
expect(sliceHook).toHaveBeenCalledWith('return 1 + 2;;', undefined)
expect(result).toEqual(3)
})

Expand Down
49 changes: 29 additions & 20 deletions packages/get/src/modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,27 @@ type Entry = {

export type Execute = (entry: Entry, inputs: Inputs) => Promise<any>

function buildImportKey(module: string, typeInfo?: TypeInfo) {
function repr(ti: TypeInfo): string {
switch (ti.type) {
case Type.Maybe:
return `maybe<${repr(ti.option)}>`
case Type.List:
return `${repr(ti.of)}[]`
case Type.Struct: {
const fields = Object.entries(ti.schema)
.map(e => `${e[0]}: ${repr(e[1])};`)
.join(' ')
return `{ ${fields} }`
}
case Type.Context:
case Type.Never:
throw new ValueTypeError('Unsupported key type')
default:
return ti.type
function repr(ti: TypeInfo): string {
switch (ti.type) {
case Type.Maybe:
return `maybe<${repr(ti.option)}>`
case Type.List:
return `${repr(ti.of)}[]`
case Type.Struct: {
const fields = Object.entries(ti.schema)
.map(e => `${e[0]}: ${repr(e[1])};`)
.join(' ')
return `{ ${fields} }`
}
case Type.Context:
case Type.Never:
throw new ValueTypeError('Unsupported key type')
default:
return ti.type
}
}

function buildImportKey(module: string, typeInfo?: TypeInfo) {
let key = module
if (typeInfo) {
key += `<${repr(typeInfo)}>`
Expand Down Expand Up @@ -133,15 +133,24 @@ export class Modules {
}
await this.hooks.extract(module, inputs, extracted)

if (typeof extracted !== 'object' || entry.returnType.type !== Type.Value) {
function dropWarning(reason: string) {
if (attrArgs.length) {
const dropped = attrArgs.map(e => e[0]).join(', ')
const err = [
`Module '${module}' returned a primitive`,
`Module '${module}' ${reason}`,
`dropping view attributes: ${dropped}`,
].join(', ')
console.warn(err)
}
}

if (entry.returnType.type !== Type.Value) {
dropWarning(`returned ${repr(entry.returnType)}`)
return extracted
}

if (typeof extracted !== 'object') {
dropWarning('returned a primitive')
return extracted
}

Expand Down
6 changes: 4 additions & 2 deletions packages/lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ export * as html from './values/html.js'
export * as js from './values/js.js'
export * as json from './values/json.js'

function runSlice(slice: string, context: unknown = {}, raw: unknown = {}) {
return new Function('$', '$$', slice)(context, raw)
const AsyncFunction: any = (async () => {}).constructor

function runSlice(slice: string, context: unknown = {}) {
return new AsyncFunction('$', slice)(context)
}

export const slice = { runSlice }
6 changes: 0 additions & 6 deletions packages/lib/src/values/html/patch-dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,6 @@ function main() {
return ds(this)
}

Object.defineProperty(Element.prototype, 'outerHTML', {
get() {
return ds(this)
},
})

Object.defineProperty(Node.prototype, 'nodeName', {
get: function () {
return this.name
Expand Down
5 changes: 4 additions & 1 deletion packages/lib/src/values/js.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import esquery from 'esquery'

export const parse = (js: string): AnyNode => {
try {
return acorn(js, { ecmaVersion: 'latest' })
return acorn(js, {
ecmaVersion: 'latest',
allowAwaitOutsideFunction: true,
})
} catch (e) {
throw new SliceSyntaxError('Could not parse slice', { cause: e })
}
Expand Down
2 changes: 1 addition & 1 deletion packages/parser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"@types/moo": "^0.5.10",
"@types/nearley": "^2.11.5",
"acorn": "^8.15.0",
"acorn-globals": "^7.0.1",
"estree-toolkit": "^1.7.13",
"globals": "^16.3.0",
"lodash-es": "^4.17.21",
"moo": "^0.5.2",
Expand Down
24 changes: 13 additions & 11 deletions packages/parser/src/ast/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,10 @@ export type TemplateExpr = {

type IdentifierExpr = {
kind: NodeKind.IdentifierExpr
value: Token
id: Token
expand: boolean
isUrlComponent: boolean
context?: Expr
typeInfo: TypeInfo
}

Expand Down Expand Up @@ -228,11 +230,17 @@ const subqueryExpr = (body: Stmt[], context?: Expr): SubqueryExpr => ({
context,
})

const identifierExpr = (value: Token): IdentifierExpr => ({
const identifierExpr = (
id: Token,
expand = false,
context?: Expr,
): IdentifierExpr => ({
kind: NodeKind.IdentifierExpr,
value,
isUrlComponent: value.text.startsWith(':'),
id,
expand,
isUrlComponent: id.text.startsWith(':'),
typeInfo: { type: Type.Value },
context,
})

const selectorExpr = (
Expand Down Expand Up @@ -303,20 +311,14 @@ const templateExpr = (elements: (Expr | Token)[]): TemplateExpr => ({

export const t = {
program,

// STATEMENTS
assignmentStmt,
declInputsStmt,
inputDeclStmt,
extractStmt,
requestStmt,

// EXPRESSIONS
requestExpr,
identifierExpr,
templateExpr,

// CONTEXTUAL EXPRESSIONS
identifierExpr,
selectorExpr,
modifierExpr,
moduleExpr,
Expand Down
36 changes: 24 additions & 12 deletions packages/parser/src/ast/print.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { builders, printer } from 'prettier/doc'
import { render } from '../utils.js'
import type { InterpretVisitor } from '../visitor/visitor.js'
import { visit } from '../visitor/visitor.js'
import type { Node } from './ast.js'
Expand Down Expand Up @@ -82,7 +83,6 @@ const printVisitor: InterpretVisitor<Doc> = {
},

ObjectLiteralExpr(node, _path, orig) {
const shorthand: Doc[] = []
const shouldBreak = orig.entries.some(
e => e.value.kind === NodeKind.SelectorExpr,
)
Expand All @@ -91,6 +91,15 @@ const printVisitor: InterpretVisitor<Doc> = {
if (!origEntry) {
throw new Error('Unmatched object literal entry')
}

if (origEntry.value.kind === NodeKind.IdentifierExpr) {
const key = render(origEntry.key)
const value = origEntry.value.id.value
if (key === value || (key === '$' && value === '')) {
return entry.value
}
}

const keyGroup: Doc[] = [entry.key]
if (entry.optional) {
keyGroup.push('?')
Expand All @@ -110,14 +119,13 @@ const printVisitor: InterpretVisitor<Doc> = {
shValue = shValue[0]
}
if (typeof shValue === 'string' && entry.key === shValue) {
shorthand[i] = [value, entry.optional ? '?' : '']
return [value, entry.optional ? '?' : '']
}
return { ...entry, key: keyGroup, value }
return group([keyGroup, value])
})

const inner = entries.map((e, i) => shorthand[i] || group([e.key, e.value]))
const sep = ifBreak(line, [',', line])
const obj = group(['{', indent([line, join(sep, inner)]), line, '}'], {
const obj = group(['{', indent([line, join(sep, entries)]), line, '}'], {
shouldBreak,
})
return node.context ? [node.context, indent([line, '-> ', obj])] : obj
Expand All @@ -138,20 +146,24 @@ const printVisitor: InterpretVisitor<Doc> = {
throw new Error(`Unexpected template node: ${og?.kind}`)
}

// strip the leading `$` character
let ret = el.slice(1)

let id: Doc = [og.id.value]
const nextEl = node.elements[i + 1]
if (isToken(nextEl) && /^\w/.test(nextEl.value)) {
// use ${id} syntax to delineate against next element in template
ret = ['{', ret, '}']
id = ['{', id, '}']
}
return [og.isUrlComponent ? ':' : '$', ret]
return [og.isUrlComponent ? ':' : '$', id]
})
},

IdentifierExpr(node) {
return ['$', node.value.value]
const id = node.id.value
if (!node.context) {
const arrow = node.expand ? '=> ' : ''
return [arrow, '$', id]
}
const arrow = node.expand ? '=> ' : '-> '
return [node.context, indent([line, arrow, '$', node.id.value])]
},

SelectorExpr(node) {
Expand Down Expand Up @@ -181,7 +193,7 @@ const printVisitor: InterpretVisitor<Doc> = {

SliceExpr(node) {
const { value } = node.slice
const quot = value.includes('`') ? '```' : '`'
const quot = value.includes('`') ? '|' : '`'
const lines = value.split('\n')
const slice = group([
quot,
Expand Down
Loading