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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.2.0] - 2026-08-01

### Changed

- Pinned Airtable blocks to "interface-alpha" because that is what most consumers have.

## [0.1.1] - 2026-07-30

### Added
Expand Down
6 changes: 5 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ npm workspaces monorepo:

## Known constraints

- Pinned/validated SDK build: `@airtable/blocks@interface-alpha-next` = `0.0.0-experimental-dc9f9a979-20260605`. A new experimental build can move internals; the library fails loudly at import if the dist files vanish. Update the README's validated-version note when bumping.
- Pinned/validated SDK build: `@airtable/blocks@interface-alpha` = `0.0.0-experimental-8575f0e0d-20260428` — the tag Airtable's own extension templates install, so it's what most consumers have. The suite also passes against `interface-alpha-next`. A new experimental build can move internals; the library fails loudly at import if the dist files vanish. Update the README's validated-version note when bumping.
- **The two builds differ in their `exports` map:** `interface-alpha-next` exports `./package.json`, `interface-alpha` does not. `resolveSdkRoot()` in `src/sdk_internals.ts` therefore tries `./package.json` first and falls back to resolving `@airtable/blocks/interface/ui` and walking up to the owning directory. Don't "simplify" it back to a single `require.resolve('@airtable/blocks/package.json')` — that breaks `interface-alpha` entirely.
- Changing the SDK dist-tag needs the lockfile entry dropped to take effect: npm won't re-resolve (or downgrade) a dist-tag spec on its own. Edit both workspaces' `package.json`, delete `packages['node_modules/@airtable/blocks']` from `package-lock.json`, then `npm install`.
- The SDK's published ESM uses extensionless relative imports → **only runs under a transforming runner** (Jest+babel validated; Vitest plausible, unvalidated; plain Node impossible). Don't chase plain-Node support.
- Consumers use the bundled Jest preset (`packages/testing/jest-preset.cjs`, exported as `./jest-preset`): jsdom + inject setupFile + hermetic babel transform allowed to transform the ESM-only SDK + jest-dom. The toolchain (jest, babel, testing-library) ships in the testing package's `dependencies`; react/react-dom/@airtable/blocks stay peers. The examples workspace proves the preset. The testing package's OWN jest config stays hand-rolled because its tests run against `src/` and need `babel-plugin-transform-import-meta`; consumers of the built dist don't.
- Neither package is on npm. Distribution is GitHub Release tarballs, because npm cannot install a workspace subdirectory from a git URL and GitHub Packages would force every consumer to authenticate. Inside this repo the CLI bin resolves via `npx`; elsewhere, install the fixtures tarball.
Expand All @@ -64,6 +66,8 @@ Security rules adapted from [TikiTribe/claude-secure-coding-rules](https://githu

## Work log

- **2026-07-23** — Switched the pinned SDK from `interface-alpha-next` to `interface-alpha` (what Airtable's templates install). Required a real fix: `interface-alpha` doesn't export `./package.json`, so `sdk_internals.ts` gained `resolveSdkRoot()` with an entry-point fallback. Full suite verified green against both dist-tags.

- **2026-07-23** — Releases now trigger on merge: `.github/workflows/tag-on-merge.yml` tags `main` with the version from `packages/testing/package.json` (no-op when that tag exists) and invokes `release.yml`, which gained a `workflow_call` trigger. Documented the GITHUB_TOKEN-doesn't-trigger-workflows constraint that forces the direct call.
- **2026-07-23** — Added the release process: root `pack:release` script (build → mkdir → pack both packages into `release/`) and `.github/workflows/release.yml`, which on a `v*` tag verifies the tag matches both package versions, runs lint/build/test, packs, and publishes a GitHub Release with the tarballs attached. Consumer install instructions (release URLs and how to find them) added to the root README, both package READMEs, and docs/getting-started.md; maintainer release steps in the root README.
- **2026-07-23** — Fixed the CI build failure: the root `build` script now builds the testing package before the fixture generator explicitly, instead of relying on `--workspaces` ordering (see Commands). Verification lesson: check build exit codes directly — piping the build through `grep -c "Build success"` masked the non-zero status and made a clean-clone check look green.
Expand Down
2 changes: 1 addition & 1 deletion examples/todo-list/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"types": "tsc"
},
"dependencies": {
"@airtable/blocks": "interface-alpha-next",
"@airtable/blocks": "interface-alpha",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "airtable-interface-extension-testing",
"private": true,
"version": "0.1.0",
"version": "0.2.0",
"description": "Testing library and fixture tooling for Airtable interface extensions (interface-alpha Blocks SDK)",
"type": "module",
"workspaces": [
Expand Down
8 changes: 5 additions & 3 deletions packages/testing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ The [example extension's test suite](../../examples/todo-list/test/app.test.tsx)
This package isn't published to npm. Install the tarball from a [GitHub Release](https://github.com/usdigitalresponse/airtable-interface-extension-testing/releases):

```bash
npm install --save-dev https://github.com/usdigitalresponse/airtable-interface-extension-testing/releases/download/v0.1.0/usdr-airtable-interface-testing-0.1.0.tgz @airtable/blocks@interface-alpha-next
npm install --save-dev https://github.com/usdigitalresponse/airtable-interface-extension-testing/releases/download/v0.1.0/usdr-airtable-interface-testing-0.1.0.tgz @airtable/blocks@interface-alpha
```

**Finding the URL ---** on the [releases page](https://github.com/usdigitalresponse/airtable-interface-extension-testing/releases), open a release and copy the link to `usdr-airtable-interface-testing-<version>.tgz` under **Assets**. Every release follows the same URL shape, so bumping the version in the command above works too:
Expand All @@ -38,7 +38,9 @@ https://github.com/usdigitalresponse/airtable-interface-extension-testing/releas

The toolchain — **Jest**, jsdom, babel, and **Testing Library** — ships as this package's dependencies, so that one install is enough. **React 19** and `react-dom` are peer dependencies your extension project already has.

This library was validated against `@airtable/blocks@0.0.0-experimental-dc9f9a979-20260605` (the `interface-alpha-next` dist-tag as of July 2026). It reaches into the SDK's `dist/` for a few modules the package doesn't export publicly, so a new SDK build can break it — if that happens you'll get a descriptive error at import time rather than silent misbehavior.
This library is developed and tested against the `interface-alpha` dist-tag — the same build [Airtable's extension templates](https://github.com/Airtable/interface-extensions-hello-world-typescript) install — currently `0.0.0-experimental-8575f0e0d-20260428`. The suite also passes against `interface-alpha-next` (`0.0.0-experimental-dc9f9a979-20260605`), so either works.

It reaches into the SDK's `dist/` for a few modules the package doesn't export publicly, so a new SDK build can break it — if that happens you'll get a descriptive error at import time rather than silent misbehavior.

## Jest configuration

Expand Down Expand Up @@ -182,7 +184,7 @@ Record and globalConfig testing carry over almost unchanged — you drive writes

**"attempted to communicate with the host application outside of a simulated environment" ---** your extension rendered (or called SDK functions) outside `testDriver.Container`. Wrap the render, and access state through the driver.

**`ERR_PACKAGE_PATH_NOT_EXPORTED` or missing-module errors mentioning `dist/esm` ---** the installed `@airtable/blocks` isn't an interface-alpha build. Install `@airtable/blocks@interface-alpha-next`.
**`ERR_PACKAGE_PATH_NOT_EXPORTED` or missing-module errors mentioning `dist/esm` ---** the installed `@airtable/blocks` isn't an interface-alpha build. Install `@airtable/blocks@interface-alpha`.

**`SyntaxError: Cannot use import statement outside a module` pointing into `@airtable/blocks` ---** Jest isn't transforming the SDK; check `transformIgnorePatterns`.

Expand Down
2 changes: 1 addition & 1 deletion packages/testing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"jest-environment-jsdom": "^29.7.0"
},
"devDependencies": {
"@airtable/blocks": "interface-alpha-next",
"@airtable/blocks": "interface-alpha",
"@types/jest": "^29.5.14",
"@types/node": "^22.0.0",
"@types/react": "^19.1.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/testing/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ for (const relativePath of Object.values(sdkInternalPaths)) {
throw spawnError(
'The installed @airtable/blocks package (%s) is missing %s. This ' +
'testing library supports the interface-alpha builds of the SDK ' +
'(`npm install --save-dev @airtable/blocks@interface-alpha-next`).',
'(`npm install --save-dev @airtable/blocks@interface-alpha`).',
sdkPackageRoot,
relativePath,
);
Expand Down
58 changes: 46 additions & 12 deletions packages/testing/src/sdk_internals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
* Access to interface-mode SDK modules that ship in `@airtable/blocks`'s
* `dist/` but are not listed in its `exports` map (only `./base/*`,
* `./interface/models`, `./interface/ui`, and the base-mode testing utils
* are). We resolve the package root through `./package.json` (which IS
* exported) and load the files by absolute path, which bypasses the exports
* map in every resolver we care about.
* are). We locate the package root (see `resolveSdkRoot`) and load the files
* by absolute path, which bypasses the exports map in every resolver we care
* about.
*
* Module identity is preserved in both runtimes that matter:
* - Under Jest (babel CJS transform), the ambient `require` is Jest's, so
Expand All @@ -14,6 +14,7 @@
* - Under real Node ESM/CJS, `createRequire`'s require(esm) shares Node's ESM
* module cache with `import` (Node >= 20.19).
*/
import * as fs from 'node:fs';
import {createRequire} from 'node:module';
import * as path from 'node:path';
import {type ReactNode} from 'react';
Expand All @@ -34,16 +35,49 @@ const nodeRequire = createRequire(
typeof __filename !== 'undefined' ? __filename : import.meta.url,
);

let sdkRoot: string;
try {
sdkRoot = path.dirname(nodeRequire.resolve('@airtable/blocks/package.json'));
} catch {
throw spawnError(
'@airtable/blocks could not be resolved. Install the interface-alpha ' +
'build of the SDK, e.g. `npm install --save-dev @airtable/blocks@interface-alpha-next`.',
);
/**
* Locate the installed SDK package directory.
*
* The `interface-alpha-next` build exports `./package.json`, which makes this
* a one-liner. The `interface-alpha` build does not, so fall back to an entry
* point both builds export and walk up to the directory that owns it.
*/
function resolveSdkRoot(): string {
try {
return path.dirname(nodeRequire.resolve('@airtable/blocks/package.json'));
} catch {
// Falls through to the entry-point strategy below.
}

let entryPath: string;
try {
entryPath = nodeRequire.resolve('@airtable/blocks/interface/ui');
} catch {
throw spawnError(
'@airtable/blocks could not be resolved. Install an interface-alpha ' +
'build of the SDK, e.g. `npm install --save-dev @airtable/blocks@interface-alpha`.',
);
}

// e.g. <root>/dist/esm/interface/ui/ui.js -> <root>
for (let dir = path.dirname(entryPath); ; ) {
if (fs.existsSync(path.join(dir, 'package.json'))) {
return dir;
}
const parent = path.dirname(dir);
/* istanbul ignore next -- only reachable if the install is corrupt */
if (parent === dir) {
throw spawnError(
'Could not locate the @airtable/blocks package directory from %s.',
entryPath,
);
}
dir = parent;
}
}

const sdkRoot = resolveSdkRoot();

function loadSdkModule(relativePath: string): any {
const absolutePath = path.join(sdkRoot, relativePath);
try {
Expand All @@ -62,7 +96,7 @@ function loadSdkModule(relativePath: string): any {
throw spawnError(
'Failed to load %s from the installed @airtable/blocks package (%s). ' +
'This testing library requires the interface-alpha build of the SDK ' +
'(`npm install --save-dev @airtable/blocks@interface-alpha-next`). ' +
'(`npm install --save-dev @airtable/blocks@interface-alpha`). ' +
'Original error: %s',
relativePath,
sdkRoot,
Expand Down
Loading