diff --git a/.github/workflows/ci-host.yaml b/.github/workflows/ci-host.yaml index 1346ea281fc..8d666387570 100644 --- a/.github/workflows/ci-host.yaml +++ b/.github/workflows/ci-host.yaml @@ -10,6 +10,7 @@ on: - "packages/base/**" - "packages/boxel-icons/**" - "packages/boxel-ui/**" + - "packages/bxl/**" - "packages/eslint-plugin-boxel/**" - "packages/realm-server/**" - "packages/runtime-common/**" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ae6ff4b60d0..635915a3968 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -75,6 +75,7 @@ jobs: - 'packages/base/**' - 'packages/boxel-icons/**' - 'packages/boxel-ui/**' + - 'packages/bxl/**' - 'packages/host/**' - 'packages/realm-server/**' - 'packages/test-realm-cards/**' @@ -119,6 +120,7 @@ jobs: - 'packages/billing/**' - 'packages/boxel-icons/**' - 'packages/boxel-ui/**' + - 'packages/bxl/**' - 'packages/host/**' - 'packages/eslint-plugin-boxel/**' - 'packages/postgres/**' diff --git a/packages/host/tests/helpers/cards/bxl-tracking.ts b/packages/host/tests/helpers/cards/bxl-tracking.ts index 074f283e75c..ae2367a1549 100644 --- a/packages/host/tests/helpers/cards/bxl-tracking.ts +++ b/packages/host/tests/helpers/cards/bxl-tracking.ts @@ -255,8 +255,11 @@ export const bxlTrackingCardSource = ` // realm's first-ever index pass the live index is empty, so POL-100's // claims aggregations bake in their empty-set values; the next visit of // the policy converges them. Tests that assert converged aggregations -// re-write POL-100 with `bxlTrackingPol100Renewal` to trigger that visit. -function pol100Doc(policyStatus: string) { +// re-write POL-100 with `bxlTrackingPol100Renewal` to trigger that visit; +// a test needing more than one such visit builds its own docs with +// `bxlTrackingPol100Doc`, since a re-write of identical content is a +// no-op. +export function bxlTrackingPol100Doc(policyStatus: string) { return { data: { type: 'card', @@ -277,7 +280,7 @@ function pol100Doc(policyStatus: string) { }; } -export const bxlTrackingPol100Renewal = pol100Doc('Renewed'); +export const bxlTrackingPol100Renewal = bxlTrackingPol100Doc('Renewed'); export const bxlTrackingRealmContents: Record< string, @@ -364,7 +367,7 @@ export const bxlTrackingRealmContents: Record< }, }, }, - 'Policy/pol-100.json': pol100Doc('Active'), + 'Policy/pol-100.json': bxlTrackingPol100Doc('Active'), 'Policy/pol-200.json': { data: { type: 'card', diff --git a/packages/host/tests/integration/bxl-indexing-test.gts b/packages/host/tests/integration/bxl-indexing-test.gts new file mode 100644 index 00000000000..e31473bf3b8 --- /dev/null +++ b/packages/host/tests/integration/bxl-indexing-test.gts @@ -0,0 +1,451 @@ +import { getService } from '@universal-ember/test-support'; +import { module, test } from 'qunit'; + +import { rri } from '@cardstack/runtime-common'; +import type { IndexedInstance, Realm, Query } from '@cardstack/runtime-common'; +import type { Loader } from '@cardstack/runtime-common/loader'; + +import { + testRealmURL, + setupCardLogs, + setupLocalIndexing, + setupIntegrationTestRealm, +} from '../helpers'; +import { setupBaseRealm } from '../helpers/base-realm'; +import { + bxlTrackingCardSource, + bxlTrackingPol100Doc, + bxlTrackingPol100Renewal, + bxlTrackingRealmContents, +} from '../helpers/cards/bxl-tracking'; +import { setupMockMatrix } from '../helpers/mock-matrix'; +import { searchCardsForTest } from '../helpers/search-cards'; +import { setupRenderingTest } from '../helpers/setup'; + +// A BXL `computeVia` runs whenever the field is read, and indexing is the +// read that persists: the search doc is what queries filter and sort on, +// and it is regenerated on every pass that touches the card. This suite +// covers that indexing dimension — BXL computeds land in the search doc in +// a shape the query engine can match on, and they recompute whenever an +// edit invalidates the card, whether the edit lands on the card itself, on +// a card it reaches through a link, or on the module that declares the +// formula. +// +// Recomputation is the part with real teeth. `expression()` memoizes each +// compute per card instance, so a memo that outlived its cycle would show +// up as a computed that quietly keeps a superseded value across an +// incremental pass — indistinguishable, from the outside, from an +// invalidation that never fired. +// +// The value each formula returns is pinned by the expression suite and the +// cycle contract by the cyclic-graph suite; all three read the same +// tracking-realm fixture. +module('Integration | bxl indexing', function (hooks) { + setupRenderingTest(hooks); + setupBaseRealm(hooks); + let loader: Loader; + let realm: Realm; + + setupLocalIndexing(hooks); + setupCardLogs( + hooks, + async () => await loader.import('@cardstack/base/card-api'), + ); + + let mockMatrixUtils = setupMockMatrix(hooks, { + loggedInAs: '@testuser:localhost', + activeRealms: [testRealmURL], + autostart: true, + }); + + hooks.beforeEach(async function () { + loader = getService('loader-service').loader; + ({ realm } = await setupIntegrationTestRealm({ + mockMatrixUtils, + contents: bxlTrackingRealmContents, + })); + // Query-backed inverses resolve against the live index at visit time, + // and the from-scratch pass above ran with an empty live index — the + // claims aggregations baked in their empty-set values. Re-visiting the + // policy converges them; every assertion below starts from that + // converged state. + await realm.write( + 'Policy/pol-100.json', + JSON.stringify(bxlTrackingPol100Renewal), + ); + }); + + async function indexedSearchDoc(id: string) { + let entry = await realm.realmIndexQueryEngine.instance(new URL(id)); + if (!entry || entry.type === 'instance-error') { + throw new Error( + `expected ${id} to index cleanly, got ${JSON.stringify(entry?.error)}`, + ); + } + return (entry as IndexedInstance).searchDoc ?? {}; + } + + async function matchingIds(query: Query) { + let { data } = await searchCardsForTest(realm.realmIndexQueryEngine, query); + return data.map((resource) => String(resource.id)); + } + + const policyRef = { module: rri(`${testRealmURL}tracking`), name: 'Policy' }; + const claimRef = { module: rri(`${testRealmURL}tracking`), name: 'Claim' }; + + async function writeClaim(path: string, attributes: Record) { + await realm.write( + path, + JSON.stringify({ + data: { + type: 'card', + attributes, + relationships: { + policy: { links: { self: '../Policy/pol-100' } }, + }, + meta: { adoptsFrom: { module: '../tracking', name: 'Claim' } }, + }, + }), + ); + } + + // Query-backed aggregations converge when the policy is next visited, and + // a write of byte-identical content is skipped — so each revisit has to + // carry a status no earlier one used. No formula reads policyStatus; the + // counter exists only to make the write land. + let revisitCount = 0; + async function revisitPolicy() { + await realm.write( + 'Policy/pol-100.json', + JSON.stringify(bxlTrackingPol100Doc(`Reviewed ${++revisitCount}`)), + ); + } + + // =========================================================================== + // The search doc the query engine sees + // =========================================================================== + + test('BXL computeds are matchable by the query engine', async function (assert) { + // A computed that only rendered correctly would still be invisible to + // search. Matching on one proves the indexer wrote the computed value + // into the search doc under its field name, typed the way the field + // declares it — a string field matched by equality, a number field by + // range. + assert.deepEqual( + await matchingIds({ + filter: { on: claimRef, eq: { severityBand: 'Standard' } }, + }), + [`${testRealmURL}Claim/clm-1`], + 'a string computed matches by equality', + ); + assert.deepEqual( + await matchingIds({ + filter: { on: claimRef, range: { incurredAmount: { gt: 1000 } } }, + }), + [`${testRealmURL}Claim/clm-1`], + 'a number computed matches by range, so it indexed as a number', + ); + // POL-200 has no claims, so its loss ratio is 0 and it stays out. + assert.deepEqual( + await matchingIds({ + filter: { on: policyRef, range: { lossRatio: { gt: 0.4 } } }, + }), + [`${testRealmURL}Policy/pol-100`], + 'a computed chained off other computeds is matchable too', + ); + }); + + test('{ as: FieldDef } computeds are matchable on their nested paths', async function (assert) { + // The materialized field instance has to survive serialization into the + // search doc as a nested object, not as an opaque blob, or the dotted + // path has nothing to match against. Both policies band as Low — POL-200 + // has no claims at all, so its loss ratio is 0 — and the score field + // separates them. + assert.deepEqual( + await matchingIds({ + filter: { on: policyRef, eq: { 'riskBand.label': 'Low' } }, + sort: [{ on: policyRef, by: 'policyId', direction: 'asc' }], + }), + [`${testRealmURL}Policy/pol-100`, `${testRealmURL}Policy/pol-200`], + 'the nested label of a single materialized instance', + ); + assert.deepEqual( + await matchingIds({ + filter: { on: policyRef, range: { 'riskBand.score': { gt: 1 } } }, + }), + [`${testRealmURL}Policy/pol-100`], + 'a nested number keeps its type through materialization', + ); + assert.deepEqual( + await matchingIds({ + filter: { on: policyRef, eq: { 'claimBands.label': 'Minor' } }, + }), + [`${testRealmURL}Policy/pol-100`], + 'an element of a materialized array', + ); + }); + + test('BXL computeds are sortable', async function (assert) { + assert.deepEqual( + await matchingIds({ + filter: { type: policyRef }, + sort: [{ on: policyRef, by: 'premiumWithTax', direction: 'desc' }], + }), + [`${testRealmURL}Policy/pol-100`, `${testRealmURL}Policy/pol-200`], + 'descending by a computed premium', + ); + assert.deepEqual( + await matchingIds({ + filter: { type: policyRef }, + sort: [{ on: policyRef, by: 'premiumWithTax', direction: 'asc' }], + }), + [`${testRealmURL}Policy/pol-200`, `${testRealmURL}Policy/pol-100`], + 'and ascending, so the order tracks the values rather than the ids', + ); + }); + + // =========================================================================== + // Incremental reindex + // =========================================================================== + + test("editing a card's own input recomputes its computeds", async function (assert) { + await writeClaim('Claim/clm-1.json', { + claimId: 'CLM-1', + claimStatus: 'Open', + paidAmount: 20000, + reserveAmount: 1500, + }); + + let searchDoc = await indexedSearchDoc(`${testRealmURL}Claim/clm-1`); + assert.strictEqual(searchDoc.incurredAmount, 21500, 'the sum recomputes'); + assert.strictEqual( + searchDoc.severityBand, + 'Large', + 'the band follows the new amount across its threshold', + ); + // The index moved the claim from one band to the other, rather than + // dropping it or keeping both: a memo surviving the pass would leave it + // matching Standard, and a lost row would match neither. + assert.deepEqual( + await matchingIds({ + filter: { on: claimRef, eq: { severityBand: 'Standard' } }, + }), + [], + 'the superseded value is out of the search doc', + ); + assert.deepEqual( + await matchingIds({ + filter: { on: claimRef, eq: { severityBand: 'Large' } }, + }), + [`${testRealmURL}Claim/clm-1`], + 'and the fresh value is in it', + ); + }); + + test('editing a linked card recomputes the cards that traverse to it', async function (assert) { + await realm.write( + 'Customer/acme.json', + JSON.stringify({ + data: { + type: 'card', + attributes: { name: 'Acme Logistics', tier: 'Platinum' }, + meta: { adoptsFrom: { module: '../tracking', name: 'Customer' } }, + }, + }), + ); + + let customerDoc = await indexedSearchDoc(`${testRealmURL}Customer/acme`); + assert.strictEqual( + customerDoc.displayLabel, + 'Acme Logistics (Platinum)', + "the edited card's own computed recomputes", + ); + let policyDoc = await indexedSearchDoc(`${testRealmURL}Policy/pol-100`); + assert.strictEqual( + policyDoc.customerName, + 'Acme Logistics', + 'the policy reads the renamed customer one hop away', + ); + let claimDoc = await indexedSearchDoc(`${testRealmURL}Claim/clm-1`); + assert.strictEqual( + claimDoc.customerName, + 'Acme Logistics', + 'and the claim reads it two hops away, through the policy', + ); + }); + + test("an edit to a claim converges into the aggregate on the policy's next visit", async function (assert) { + await writeClaim('Claim/clm-2.json', { + claimId: 'CLM-2', + claimStatus: 'Open', + paidAmount: 1000, + reserveAmount: 500, + }); + + // The only stored edge runs claim → policy; the policy's `claims` side + // is a query resolved against the live index when the policy is + // visited. A dependency read through a query context is not recorded as + // an invalidation edge, so writing the claim reindexes the claim alone + // and the policy keeps the aggregate from its last visit. These two + // assert that staleness deliberately — they are the contract as it + // stands, not the behavior anyone would want. + let searchDoc = await indexedSearchDoc(`${testRealmURL}Policy/pol-100`); + assert.strictEqual( + searchDoc.paidClaimsTotal, + 3980.75, + 'the claim edit does not reach the policy through the query inverse', + ); + assert.strictEqual( + searchDoc.openClaimCount, + 1, + 'nor does the status change it would have counted', + ); + + // That next visit recomputes against the now-current claims. + await revisitPolicy(); + + searchDoc = await indexedSearchDoc(`${testRealmURL}Policy/pol-100`); + assert.strictEqual( + searchDoc.paidClaimsTotal, + 4200.5, + 'the aggregate picks up the edited claim', + ); + assert.strictEqual(searchDoc.reservedClaimsTotal, 2000, 'and its reserve'); + assert.strictEqual( + searchDoc.openClaimCount, + 2, + 'the reopened claim counts toward the open tally', + ); + assert.strictEqual( + searchDoc.lossRatio, + 0.5167, + 'the chained computed follows the aggregate it reads', + ); + }); + + test('a claim joining or leaving the realm converges into the aggregate', async function (assert) { + await writeClaim('Claim/clm-4.json', { + claimId: 'CLM-4', + claimStatus: 'Open', + paidAmount: 19.25, + reserveAmount: 0, + }); + await revisitPolicy(); + + let policyId = `${testRealmURL}Policy/pol-100`; + let searchDoc = await indexedSearchDoc(policyId); + assert.strictEqual(searchDoc.paidClaimsTotal, 4000, 'the total grows'); + assert.strictEqual(searchDoc.openClaimCount, 2, 'and so does the tally'); + assert.deepEqual( + searchDoc.claimPolicyIds, + [policyId, policyId, policyId], + 'the new claim joins the inverse the cycle-walking formula reads', + ); + + await realm.delete('Claim/clm-4.json'); + await revisitPolicy(); + + assert.strictEqual( + await realm.realmIndexQueryEngine.instance( + new URL(`${testRealmURL}Claim/clm-4`), + ), + undefined, + 'the deleted claim leaves the index entirely', + ); + searchDoc = await indexedSearchDoc(policyId); + assert.strictEqual(searchDoc.paidClaimsTotal, 3980.75, 'the total shrinks'); + assert.strictEqual(searchDoc.openClaimCount, 1, 'and so does the tally'); + assert.deepEqual( + searchDoc.claimPolicyIds, + [policyId, policyId], + 'and the claim leaves the inverse when it is deleted', + ); + }); + + test('editing the formula in the module recomputes every instance', async function (assert) { + // Formulas live in card source, so a formula change arrives as a module + // edit. Every instance of the card has to be revisited, not just the one + // that happens to be open. + await realm.write( + 'tracking.gts', + bxlTrackingCardSource.replace( + '.annualPremium * 1.07', + '.annualPremium * 1.1', + ), + ); + + assert.strictEqual( + (await indexedSearchDoc(`${testRealmURL}Policy/pol-100`)).premiumWithTax, + 13200, + 'the new multiplier reaches the instance in hand', + ); + assert.strictEqual( + (await indexedSearchDoc(`${testRealmURL}Policy/pol-200`)).premiumWithTax, + 8800, + 'the instance nothing else touched is reindexed too', + ); + }); + + test("an edit that strips a card's inputs still yields a clean index entry", async function (assert) { + // Every computed on the policy now reads a missing link or a blank + // number. Two distinct tolerances keep the card indexable, and a gap in + // either one turns it into an instance-error entry that strands the + // computeds still perfectly well defined: arithmetic on a null or a + // zero divisor yields null inside the engine, while a function that + // raises an Excel sentinel — NA() — throws, and the factory catches + // that at the boundary. + await realm.write( + 'Policy/pol-100.json', + JSON.stringify({ + data: { + type: 'card', + attributes: { policyId: 'POL-100', policyStatus: 'Lapsed' }, + meta: { adoptsFrom: { module: '../tracking', name: 'Policy' } }, + }, + }), + ); + + let entry = await realm.realmIndexQueryEngine.instance( + new URL(`${testRealmURL}Policy/pol-100`), + ); + assert.strictEqual( + entry?.type, + 'instance', + 'a clean instance entry, not an instance-error', + ); + let searchDoc = (entry as IndexedInstance).searchDoc ?? {}; + let customerName = searchDoc.customerName ?? null; + assert.strictEqual( + customerName, + null, + 'the dropped link recomputes to null rather than keeping a stale name', + ); + let divByZero = searchDoc.divByZero ?? null; + assert.strictEqual( + divByZero, + null, + 'dividing a blank premium by zero yields null inside the engine', + ); + let notApplicable = searchDoc.notApplicable ?? null; + assert.strictEqual( + notApplicable, + null, + 'and the sentinel NA() throws is caught at the factory boundary', + ); + assert.strictEqual( + searchDoc.premiumWithTax, + 0, + 'the blank premium reads as 0 under Excel blank semantics', + ); + + // The degraded card also leaves the result sets it used to match, so a + // query never serves the values it computed before the edit. + assert.deepEqual( + await matchingIds({ + filter: { on: policyRef, range: { 'riskBand.score': { gt: 1 } } }, + }), + [], + 'the pre-edit risk score is out of the index', + ); + }); +}); diff --git a/packages/realm-server/package.json b/packages/realm-server/package.json index 123d46e5598..f76a0ac943b 100644 --- a/packages/realm-server/package.json +++ b/packages/realm-server/package.json @@ -9,6 +9,7 @@ "@cardstack/billing": "workspace:*", "@cardstack/boxel-icons": "workspace:*", "@cardstack/boxel-ui": "workspace:*", + "@cardstack/bxl": "workspace:*", "@cardstack/eslint-plugin-boxel": "workspace:*", "@cardstack/local-types": "workspace:*", "@cardstack/postgres": "workspace:*", diff --git a/packages/realm-server/tests/bxl-node-smoke-test.ts b/packages/realm-server/tests/bxl-node-smoke-test.ts new file mode 100644 index 00000000000..31329ad28f4 --- /dev/null +++ b/packages/realm-server/tests/bxl-node-smoke-test.ts @@ -0,0 +1,143 @@ +import QUnit from 'qunit'; +const { module, test } = QUnit; +import { basename } from 'path'; +import { + compileBxl, + evaluateBxl, + loadAllFormulaExtensions, + runNativeJqAsync, + BXL_BUILD_INFO, +} from '@cardstack/bxl'; + +// BXL is an isomorphic package: the host bundles it through Vite and serves +// it to card code, and it is meant to be importable from node so platform +// code can reach it directly. Nothing on the server does yet, which is +// exactly why this suite is here — it keeps that direction working before +// the first consumer depends on it. +// +// Two things can only break from outside the package. The bare specifier +// `@cardstack/bxl` has to resolve through the package's `exports` map from +// a sibling workspace package and land on raw `.ts` that node's native type +// stripping accepts, with no bundler to rewrite anything. And a formula +// family the caller never named has to arrive on its own through the async +// entry point's dynamic import. The package's own suite covers the library +// from the inside and holds the exhaustive function matrix; card-level +// behavior lives in the host suites. Cards can't run in node at all, so +// this stays a smoke suite over plain JS objects. +// +// The expressions are lifted from the host's BXL card fixtures +// (packages/host/tests/helpers/cards/bxl-tracking.ts) and asserted to the +// same answers, so the two environments can be compared by reading them +// side by side. The values are copies, not imports — nothing enforces that +// they stay in step. +module(basename(import.meta.filename), function () { + test('the package loads under node and reports its build identity', function (assert) { + assert.strictEqual( + typeof BXL_BUILD_INFO.version, + 'string', + 'a version string, so a consumer can tell which build answered', + ); + assert.true( + BXL_BUILD_INFO.features.includes('null-tolerance'), + 'the feature list is populated, not an empty placeholder', + ); + }); + + test('evaluates readable BXL over a plain object', function (assert) { + // Bare PascalCase identifiers resolve to camelCase field keys, the same + // fallback the card factory relies on when no schema is supplied. + assert.strictEqual( + evaluateBxl('ROUND((PaidAmount + ReserveAmount) * 100) / 100', { + paidAmount: 3200.5, + reserveAmount: 1500, + }).value, + 4700.5, + 'Claim.incurredAmount', + ); + assert.strictEqual( + evaluateBxl( + 'IFS(IncurredAmount < 1000, "Minor", IncurredAmount < 10000, "Standard", TRUE, "Large")', + { incurredAmount: 4700.5 }, + ).value, + 'Standard', + 'Claim.severityBand', + ); + // Excel blank semantics: absent numerics read as 0 rather than + // propagating null or throwing. + assert.strictEqual( + evaluateBxl('ROUND((PaidAmount + ReserveAmount) * 100) / 100', {}).value, + 0, + 'a blank input still computes', + ); + }); + + test('compiles readable BXL to canonical jq', function (assert) { + let compiled = compileBxl( + 'ROUND(ABS(PMT(FinancingApr / 12, 12, -AnnualPremium)) * 100) / 100', + ); + assert.strictEqual( + compiled.source, + 'ROUND(ABS(PMT(.financingApr / 12; 12; -.annualPremium)) * 100) / 100', + 'PascalCase labels become field paths and commas become jq semicolons', + ); + assert.true(compiled.changed, 'the compiler reports it rewrote the source'); + assert.deepEqual(compiled.warnings, [], 'no diagnostics on valid source'); + }); + + test('evaluates canonical jq handed straight to the engine', function (assert) { + // readableSyntax: false is what the `jq` tagged template selects — the + // source skips the readable-syntax compiler and reaches the jq parser + // unchanged. + assert.strictEqual( + evaluateBxl( + '[.claims[] | .paidAmount] | add // 0', + { claims: [{ paidAmount: 3200.5 }, { paidAmount: 780.25 }] }, + { readableSyntax: false }, + ).value, + 3980.75, + 'Policy.paidClaimsTotal', + ); + assert.strictEqual( + evaluateBxl( + '[.claims[] | .paidAmount] | add // 0', + { claims: [] }, + { + readableSyntax: false, + }, + ).value, + 0, + 'an empty aggregation falls back rather than yielding null', + ); + }); + + test('a lazy formula chunk resolves through dynamic import', async function (assert) { + // PMT lives in the financial family, which ships as its own chunk behind + // a dynamic `import('…/formula-financial.ts')`. The async entry point + // inspects the program, pulls in the families it names, and registers + // them before evaluating. + let run = await runNativeJqAsync( + 'ROUND(ABS(PMT(FinancingApr / 12, 12, -AnnualPremium)) * 100) / 100', + { financingApr: 0.06, annualPremium: 12000 }, + ); + assert.deepEqual(run.outputs, [1032.8], 'Policy.monthlyPayment'); + + // Auto-loading widens the library set for that one async call only; the + // default set a synchronous caller gets still holds just the eager + // core. Folding the chunks into that default set is a separate, + // explicit step, and it's what lets a host serve a `computeVia` that + // cannot await an import mid-compute. That the async path leaves the + // default set alone can only be observed in a fresh process, so it is + // pinned by the bxl package's own smoke suite rather than here — the + // fold below is global and irreversible, and this file shares a node + // process with every other realm-server test. + await loadAllFormulaExtensions(); + assert.strictEqual( + evaluateBxl( + 'ROUND(ABS(PMT(FinancingApr / 12, 12, -AnnualPremium)) * 100) / 100', + { financingApr: 0.06, annualPremium: 12000 }, + ).value, + 1032.8, + 'the folded-in family is visible to synchronous evaluation', + ); + }); +}); diff --git a/packages/realm-server/tests/index.ts b/packages/realm-server/tests/index.ts index 764eb557fa1..b3c54b43e37 100644 --- a/packages/realm-server/tests/index.ts +++ b/packages/realm-server/tests/index.ts @@ -280,6 +280,7 @@ const ALL_TEST_FILES: string[] = [ './clamp-serialized-error-test', './sanitize-for-jsonb-test', './is-json-content-type-test', + './bxl-node-smoke-test', './file-size-limit-test', './content-hash-test', './fitted-formats-parity-test', diff --git a/packages/realm-server/tsconfig.json b/packages/realm-server/tsconfig.json index 30daa9a1ce3..898d3c00e77 100644 --- a/packages/realm-server/tsconfig.json +++ b/packages/realm-server/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "target": "es2020", + "lib": ["ES2022", "DOM", "DOM.Iterable"], "allowJs": true, "module": "nodenext", "moduleResolution": "nodenext", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc3126ea939..b8c5478ba17 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2707,6 +2707,9 @@ importers: '@cardstack/boxel-ui': specifier: workspace:* version: link:../boxel-ui + '@cardstack/bxl': + specifier: workspace:* + version: link:../bxl '@cardstack/eslint-plugin-boxel': specifier: workspace:* version: link:../eslint-plugin-boxel