From a40578181cd9f0a8968f35350dcd1bb92a17a72c Mon Sep 17 00:00:00 2001 From: "release-bot-allow-prs-and-push[bot]" <173871997+release-bot-allow-prs-and-push[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 10:47:10 +1000 Subject: [PATCH 1/3] Version Packages (#6852) This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @tinacms/app@2.4.8 ### Patch Changes - Updated dependencies \[]: - tinacms@3.8.1 ## @tinacms/cli@2.3.1 ### Patch Changes - [#6850](https://github.com/tinacms/tinacms/pull/6850) [`629af08`](https://github.com/tinacms/tinacms/commit/629af08dc4b6dbe7ed652cbc5dfd55699b0fffa9) Thanks [@18-th](https://github.com/18-th)! - Fix the generated `client.ts` / `databaseClient.ts` `./types` import so it satisfies both TypeScript strict mode and Node native ESM. The generated import is now `import { queries } from "./types.js"` unconditionally, and the CLI emits a co-resident `types.js` alongside `types.ts` for TypeScript projects. Modern TS module resolution (`bundler` / `node16` / `nodenext`) rewrites the `.js` import back to `types.ts` at compile time, so type checking still sees the `.ts` source and `allowImportingTsExtensions` is not required, while Node ESM consumers resolve the on-disk `.js` file at runtime. - Updated dependencies \[[`890108d`](https://github.com/tinacms/tinacms/commit/890108dd6c1a88a1c5531cf397514c34712d13bd)]: - @tinacms/graphql@2.4.1 - @tinacms/search@1.2.15 - tinacms@3.8.1 - @tinacms/app@2.4.8 ## @tinacms/datalayer@2.0.21 ### Patch Changes - Updated dependencies \[[`890108d`](https://github.com/tinacms/tinacms/commit/890108dd6c1a88a1c5531cf397514c34712d13bd)]: - @tinacms/graphql@2.4.1 ## @tinacms/graphql@2.4.1 ### Patch Changes - [#6853](https://github.com/tinacms/tinacms/pull/6853) [`890108d`](https://github.com/tinacms/tinacms/commit/890108dd6c1a88a1c5531cf397514c34712d13bd) Thanks [@kulesy](https://github.com/kulesy)! - fix(graphql): preserve absolute external URLs in image-type resolvers ## @tinacms/search@1.2.15 ### Patch Changes - Updated dependencies \[[`890108d`](https://github.com/tinacms/tinacms/commit/890108dd6c1a88a1c5531cf397514c34712d13bd)]: - @tinacms/graphql@2.4.1 ## @tinacms/vercel-previews@0.2.8 ### Patch Changes - Updated dependencies \[]: - tinacms@3.8.1 ## next-tinacms-azure@13.0.1 ### Patch Changes - Updated dependencies \[]: - tinacms@3.8.1 ## next-tinacms-cloudinary@25.0.1 ### Patch Changes - Updated dependencies \[]: - tinacms@3.8.1 ## next-tinacms-dos@22.0.1 ### Patch Changes - Updated dependencies \[]: - tinacms@3.8.1 ## next-tinacms-s3@22.0.1 ### Patch Changes - Updated dependencies \[]: - tinacms@3.8.1 ## tinacms@3.8.1 ### Patch Changes - Updated dependencies \[]: - @tinacms/search@1.2.15 ## tinacms-authjs@22.0.1 ### Patch Changes - Updated dependencies \[]: - tinacms@3.8.1 ## tinacms-clerk@22.0.1 ### Patch Changes - Updated dependencies \[]: - tinacms@3.8.1 ## tinacms-gitprovider-github@4.1.8 ### Patch Changes - Updated dependencies \[]: - @tinacms/datalayer@2.0.21 Co-authored-by: Tina Release Bot --- .changeset/fix-generated-client-types-import.md | 5 ----- .changeset/fresh-tomatoes-look.md | 5 ----- packages/@tinacms/app/CHANGELOG.md | 7 +++++++ packages/@tinacms/app/package.json | 2 +- packages/@tinacms/cli/CHANGELOG.md | 12 ++++++++++++ packages/@tinacms/cli/package.json | 2 +- packages/@tinacms/datalayer/CHANGELOG.md | 7 +++++++ packages/@tinacms/datalayer/package.json | 2 +- packages/@tinacms/graphql/CHANGELOG.md | 6 ++++++ packages/@tinacms/graphql/package.json | 2 +- packages/@tinacms/search/CHANGELOG.md | 7 +++++++ packages/@tinacms/search/package.json | 2 +- packages/@tinacms/vercel-previews/CHANGELOG.md | 7 +++++++ packages/@tinacms/vercel-previews/package.json | 2 +- packages/next-tinacms-azure/CHANGELOG.md | 7 +++++++ packages/next-tinacms-azure/package.json | 2 +- packages/next-tinacms-cloudinary/CHANGELOG.md | 7 +++++++ packages/next-tinacms-cloudinary/package.json | 2 +- packages/next-tinacms-dos/CHANGELOG.md | 7 +++++++ packages/next-tinacms-dos/package.json | 2 +- packages/next-tinacms-s3/CHANGELOG.md | 7 +++++++ packages/next-tinacms-s3/package.json | 2 +- packages/tinacms-authjs/CHANGELOG.md | 7 +++++++ packages/tinacms-authjs/package.json | 2 +- packages/tinacms-clerk/CHANGELOG.md | 7 +++++++ packages/tinacms-clerk/package.json | 2 +- packages/tinacms-gitprovider-github/CHANGELOG.md | 7 +++++++ packages/tinacms-gitprovider-github/package.json | 2 +- packages/tinacms/CHANGELOG.md | 7 +++++++ packages/tinacms/package.json | 2 +- 30 files changed, 116 insertions(+), 24 deletions(-) delete mode 100644 .changeset/fix-generated-client-types-import.md delete mode 100644 .changeset/fresh-tomatoes-look.md diff --git a/.changeset/fix-generated-client-types-import.md b/.changeset/fix-generated-client-types-import.md deleted file mode 100644 index 3f8d3de563..0000000000 --- a/.changeset/fix-generated-client-types-import.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@tinacms/cli": patch ---- - -Fix the generated `client.ts` / `databaseClient.ts` `./types` import so it satisfies both TypeScript strict mode and Node native ESM. The generated import is now `import { queries } from "./types.js"` unconditionally, and the CLI emits a co-resident `types.js` alongside `types.ts` for TypeScript projects. Modern TS module resolution (`bundler` / `node16` / `nodenext`) rewrites the `.js` import back to `types.ts` at compile time, so type checking still sees the `.ts` source and `allowImportingTsExtensions` is not required, while Node ESM consumers resolve the on-disk `.js` file at runtime. diff --git a/.changeset/fresh-tomatoes-look.md b/.changeset/fresh-tomatoes-look.md deleted file mode 100644 index 4eb57a9021..0000000000 --- a/.changeset/fresh-tomatoes-look.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@tinacms/graphql": patch ---- - -fix(graphql): preserve absolute external URLs in image-type resolvers diff --git a/packages/@tinacms/app/CHANGELOG.md b/packages/@tinacms/app/CHANGELOG.md index 0aa9fd0d5d..5faaf67f69 100644 --- a/packages/@tinacms/app/CHANGELOG.md +++ b/packages/@tinacms/app/CHANGELOG.md @@ -1,5 +1,12 @@ # @tinacms/app +## 2.4.8 + +### Patch Changes + +- Updated dependencies []: + - tinacms@3.8.1 + ## 2.4.7 ### Patch Changes diff --git a/packages/@tinacms/app/package.json b/packages/@tinacms/app/package.json index e62984cc52..2ff8546ec2 100644 --- a/packages/@tinacms/app/package.json +++ b/packages/@tinacms/app/package.json @@ -1,6 +1,6 @@ { "name": "@tinacms/app", - "version": "2.4.7", + "version": "2.4.8", "main": "src/main.tsx", "license": "Apache-2.0", "devDependencies": { diff --git a/packages/@tinacms/cli/CHANGELOG.md b/packages/@tinacms/cli/CHANGELOG.md index 40bac760cc..2547c62d44 100644 --- a/packages/@tinacms/cli/CHANGELOG.md +++ b/packages/@tinacms/cli/CHANGELOG.md @@ -1,5 +1,17 @@ # tinacms-cli +## 2.3.1 + +### Patch Changes + +- [#6850](https://github.com/tinacms/tinacms/pull/6850) [`629af08`](https://github.com/tinacms/tinacms/commit/629af08dc4b6dbe7ed652cbc5dfd55699b0fffa9) Thanks [@18-th](https://github.com/18-th)! - Fix the generated `client.ts` / `databaseClient.ts` `./types` import so it satisfies both TypeScript strict mode and Node native ESM. The generated import is now `import { queries } from "./types.js"` unconditionally, and the CLI emits a co-resident `types.js` alongside `types.ts` for TypeScript projects. Modern TS module resolution (`bundler` / `node16` / `nodenext`) rewrites the `.js` import back to `types.ts` at compile time, so type checking still sees the `.ts` source and `allowImportingTsExtensions` is not required, while Node ESM consumers resolve the on-disk `.js` file at runtime. + +- Updated dependencies [[`890108d`](https://github.com/tinacms/tinacms/commit/890108dd6c1a88a1c5531cf397514c34712d13bd)]: + - @tinacms/graphql@2.4.1 + - @tinacms/search@1.2.15 + - tinacms@3.8.1 + - @tinacms/app@2.4.8 + ## 2.3.0 ### Minor Changes diff --git a/packages/@tinacms/cli/package.json b/packages/@tinacms/cli/package.json index 2f9592b97f..14ccc6a409 100644 --- a/packages/@tinacms/cli/package.json +++ b/packages/@tinacms/cli/package.json @@ -1,7 +1,7 @@ { "name": "@tinacms/cli", "type": "module", - "version": "2.3.0", + "version": "2.3.1", "main": "dist/index.js", "typings": "dist/index.d.ts", "files": [ diff --git a/packages/@tinacms/datalayer/CHANGELOG.md b/packages/@tinacms/datalayer/CHANGELOG.md index dc5140d5ce..a78ac62ee4 100644 --- a/packages/@tinacms/datalayer/CHANGELOG.md +++ b/packages/@tinacms/datalayer/CHANGELOG.md @@ -1,5 +1,12 @@ # tina-graphql +## 2.0.21 + +### Patch Changes + +- Updated dependencies [[`890108d`](https://github.com/tinacms/tinacms/commit/890108dd6c1a88a1c5531cf397514c34712d13bd)]: + - @tinacms/graphql@2.4.1 + ## 2.0.20 ### Patch Changes diff --git a/packages/@tinacms/datalayer/package.json b/packages/@tinacms/datalayer/package.json index f4e1f527f0..bdcfb84bb5 100644 --- a/packages/@tinacms/datalayer/package.json +++ b/packages/@tinacms/datalayer/package.json @@ -1,7 +1,7 @@ { "name": "@tinacms/datalayer", "type": "module", - "version": "2.0.20", + "version": "2.0.21", "main": "dist/index.js", "types": "dist/index.d.ts", "files": [ diff --git a/packages/@tinacms/graphql/CHANGELOG.md b/packages/@tinacms/graphql/CHANGELOG.md index f0d8e22095..48ebe37bb7 100644 --- a/packages/@tinacms/graphql/CHANGELOG.md +++ b/packages/@tinacms/graphql/CHANGELOG.md @@ -1,5 +1,11 @@ # tina-graphql +## 2.4.1 + +### Patch Changes + +- [#6853](https://github.com/tinacms/tinacms/pull/6853) [`890108d`](https://github.com/tinacms/tinacms/commit/890108dd6c1a88a1c5531cf397514c34712d13bd) Thanks [@kulesy](https://github.com/kulesy)! - fix(graphql): preserve absolute external URLs in image-type resolvers + ## 2.4.0 ### Minor Changes diff --git a/packages/@tinacms/graphql/package.json b/packages/@tinacms/graphql/package.json index 401f316ac8..920fb5677d 100644 --- a/packages/@tinacms/graphql/package.json +++ b/packages/@tinacms/graphql/package.json @@ -1,7 +1,7 @@ { "name": "@tinacms/graphql", "type": "module", - "version": "2.4.0", + "version": "2.4.1", "main": "dist/index.js", "module": "./dist/index.js", "files": [ diff --git a/packages/@tinacms/search/CHANGELOG.md b/packages/@tinacms/search/CHANGELOG.md index 22cc098142..c20804e119 100644 --- a/packages/@tinacms/search/CHANGELOG.md +++ b/packages/@tinacms/search/CHANGELOG.md @@ -1,5 +1,12 @@ # @tinacms/search +## 1.2.15 + +### Patch Changes + +- Updated dependencies [[`890108d`](https://github.com/tinacms/tinacms/commit/890108dd6c1a88a1c5531cf397514c34712d13bd)]: + - @tinacms/graphql@2.4.1 + ## 1.2.14 ### Patch Changes diff --git a/packages/@tinacms/search/package.json b/packages/@tinacms/search/package.json index 71cfa4c7b1..5782c90d6a 100644 --- a/packages/@tinacms/search/package.json +++ b/packages/@tinacms/search/package.json @@ -1,7 +1,7 @@ { "name": "@tinacms/search", "type": "module", - "version": "1.2.14", + "version": "1.2.15", "main": "dist/index.js", "types": "dist/index.d.ts", "files": [ diff --git a/packages/@tinacms/vercel-previews/CHANGELOG.md b/packages/@tinacms/vercel-previews/CHANGELOG.md index c822dcbae5..388392481b 100644 --- a/packages/@tinacms/vercel-previews/CHANGELOG.md +++ b/packages/@tinacms/vercel-previews/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## 0.2.8 + +### Patch Changes + +- Updated dependencies []: + - tinacms@3.8.1 + ## 0.2.7 ### Patch Changes diff --git a/packages/@tinacms/vercel-previews/package.json b/packages/@tinacms/vercel-previews/package.json index 978186a73f..36ecb6ac5b 100644 --- a/packages/@tinacms/vercel-previews/package.json +++ b/packages/@tinacms/vercel-previews/package.json @@ -1,7 +1,7 @@ { "name": "@tinacms/vercel-previews", "type": "module", - "version": "0.2.7", + "version": "0.2.8", "main": "dist/index.js", "types": "dist/index.d.ts", "keywords": [ diff --git a/packages/next-tinacms-azure/CHANGELOG.md b/packages/next-tinacms-azure/CHANGELOG.md index abf2726669..c2b72dae58 100644 --- a/packages/next-tinacms-azure/CHANGELOG.md +++ b/packages/next-tinacms-azure/CHANGELOG.md @@ -1,5 +1,12 @@ # next-tinacms-azure +## 13.0.1 + +### Patch Changes + +- Updated dependencies []: + - tinacms@3.8.1 + ## 13.0.0 ### Patch Changes diff --git a/packages/next-tinacms-azure/package.json b/packages/next-tinacms-azure/package.json index 12bd69c0a1..06c46b71af 100644 --- a/packages/next-tinacms-azure/package.json +++ b/packages/next-tinacms-azure/package.json @@ -1,7 +1,7 @@ { "name": "next-tinacms-azure", "type": "module", - "version": "13.0.0", + "version": "13.0.1", "description": "", "main": "dist/index.js", "module": "./dist/index.js", diff --git a/packages/next-tinacms-cloudinary/CHANGELOG.md b/packages/next-tinacms-cloudinary/CHANGELOG.md index 8f252b710a..0ddf9f7a65 100644 --- a/packages/next-tinacms-cloudinary/CHANGELOG.md +++ b/packages/next-tinacms-cloudinary/CHANGELOG.md @@ -1,5 +1,12 @@ # next-tinacms-cloudinary +## 25.0.1 + +### Patch Changes + +- Updated dependencies []: + - tinacms@3.8.1 + ## 25.0.0 ### Patch Changes diff --git a/packages/next-tinacms-cloudinary/package.json b/packages/next-tinacms-cloudinary/package.json index 765946b298..99b9fbe9c8 100644 --- a/packages/next-tinacms-cloudinary/package.json +++ b/packages/next-tinacms-cloudinary/package.json @@ -1,7 +1,7 @@ { "name": "next-tinacms-cloudinary", "type": "module", - "version": "25.0.0", + "version": "25.0.1", "main": "dist/index.js", "module": "./dist/index.js", "files": [ diff --git a/packages/next-tinacms-dos/CHANGELOG.md b/packages/next-tinacms-dos/CHANGELOG.md index 2194eb3a11..f21a862c9f 100644 --- a/packages/next-tinacms-dos/CHANGELOG.md +++ b/packages/next-tinacms-dos/CHANGELOG.md @@ -1,5 +1,12 @@ # next-tinacms-cloudinary +## 22.0.1 + +### Patch Changes + +- Updated dependencies []: + - tinacms@3.8.1 + ## 22.0.0 ### Patch Changes diff --git a/packages/next-tinacms-dos/package.json b/packages/next-tinacms-dos/package.json index 89da6bc799..dace838b6b 100644 --- a/packages/next-tinacms-dos/package.json +++ b/packages/next-tinacms-dos/package.json @@ -1,7 +1,7 @@ { "name": "next-tinacms-dos", "type": "module", - "version": "22.0.0", + "version": "22.0.1", "main": "dist/index.js", "module": "./dist/index.js", "files": [ diff --git a/packages/next-tinacms-s3/CHANGELOG.md b/packages/next-tinacms-s3/CHANGELOG.md index 4572d76e7d..b40c5025d8 100644 --- a/packages/next-tinacms-s3/CHANGELOG.md +++ b/packages/next-tinacms-s3/CHANGELOG.md @@ -1,5 +1,12 @@ # next-tinacms-s3 +## 22.0.1 + +### Patch Changes + +- Updated dependencies []: + - tinacms@3.8.1 + ## 22.0.0 ### Patch Changes diff --git a/packages/next-tinacms-s3/package.json b/packages/next-tinacms-s3/package.json index 60e9c17eaa..cae3f96fe0 100644 --- a/packages/next-tinacms-s3/package.json +++ b/packages/next-tinacms-s3/package.json @@ -1,7 +1,7 @@ { "name": "next-tinacms-s3", "type": "module", - "version": "22.0.0", + "version": "22.0.1", "main": "dist/index.js", "module": "./dist/index.js", "files": [ diff --git a/packages/tinacms-authjs/CHANGELOG.md b/packages/tinacms-authjs/CHANGELOG.md index a4353ca639..fe6256fc64 100644 --- a/packages/tinacms-authjs/CHANGELOG.md +++ b/packages/tinacms-authjs/CHANGELOG.md @@ -1,5 +1,12 @@ # tinacms-authjs +## 22.0.1 + +### Patch Changes + +- Updated dependencies []: + - tinacms@3.8.1 + ## 22.0.0 ### Patch Changes diff --git a/packages/tinacms-authjs/package.json b/packages/tinacms-authjs/package.json index 03694ac96b..0208145b83 100644 --- a/packages/tinacms-authjs/package.json +++ b/packages/tinacms-authjs/package.json @@ -1,7 +1,7 @@ { "name": "tinacms-authjs", "type": "module", - "version": "22.0.0", + "version": "22.0.1", "main": "dist/index.js", "module": "./dist/index.js", "files": [ diff --git a/packages/tinacms-clerk/CHANGELOG.md b/packages/tinacms-clerk/CHANGELOG.md index 8457a284f3..4a249a038e 100644 --- a/packages/tinacms-clerk/CHANGELOG.md +++ b/packages/tinacms-clerk/CHANGELOG.md @@ -1,5 +1,12 @@ # tinacms-clerk +## 22.0.1 + +### Patch Changes + +- Updated dependencies []: + - tinacms@3.8.1 + ## 22.0.0 ### Patch Changes diff --git a/packages/tinacms-clerk/package.json b/packages/tinacms-clerk/package.json index 45397d8fa6..06c4e5e6e2 100644 --- a/packages/tinacms-clerk/package.json +++ b/packages/tinacms-clerk/package.json @@ -1,7 +1,7 @@ { "name": "tinacms-clerk", "type": "module", - "version": "22.0.0", + "version": "22.0.1", "main": "dist/index.js", "module": "./dist/index.js", "files": [ diff --git a/packages/tinacms-gitprovider-github/CHANGELOG.md b/packages/tinacms-gitprovider-github/CHANGELOG.md index 0e6dbcd59d..fc7313722b 100644 --- a/packages/tinacms-gitprovider-github/CHANGELOG.md +++ b/packages/tinacms-gitprovider-github/CHANGELOG.md @@ -1,5 +1,12 @@ # tinacms-gitprovider-github +## 4.1.8 + +### Patch Changes + +- Updated dependencies []: + - @tinacms/datalayer@2.0.21 + ## 4.1.7 ### Patch Changes diff --git a/packages/tinacms-gitprovider-github/package.json b/packages/tinacms-gitprovider-github/package.json index 6adc1e40a6..977855dd69 100644 --- a/packages/tinacms-gitprovider-github/package.json +++ b/packages/tinacms-gitprovider-github/package.json @@ -1,7 +1,7 @@ { "name": "tinacms-gitprovider-github", "type": "module", - "version": "4.1.7", + "version": "4.1.8", "main": "dist/index.js", "module": "./dist/index.js", "files": [ diff --git a/packages/tinacms/CHANGELOG.md b/packages/tinacms/CHANGELOG.md index 8f852bd86a..d5797b0c4f 100644 --- a/packages/tinacms/CHANGELOG.md +++ b/packages/tinacms/CHANGELOG.md @@ -1,5 +1,12 @@ # tinacms +## 3.8.1 + +### Patch Changes + +- Updated dependencies []: + - @tinacms/search@1.2.15 + ## 3.8.0 ### Minor Changes diff --git a/packages/tinacms/package.json b/packages/tinacms/package.json index d7e72bdd0a..d4b9f1f6e2 100644 --- a/packages/tinacms/package.json +++ b/packages/tinacms/package.json @@ -2,7 +2,7 @@ "name": "tinacms", "type": "module", "typings": "dist/index.d.ts", - "version": "3.8.0", + "version": "3.8.1", "main": "dist/index.js", "module": "./dist/index.js", "exports": { From 542c781b4f7a6ff5b5481bd88329f60c9bf3b57d Mon Sep 17 00:00:00 2001 From: "Ben Neoh [SSW]" <87377903+Ben0189@users.noreply.github.com> Date: Thu, 14 May 2026 15:53:24 +1000 Subject: [PATCH 2/3] Fix native SQLite crash in ESM database build (#6790) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #6675 Part of #6750 ## Problem TinaCMS v3's CLI bundles `tina/database.ts` with esbuild and dynamically imports the result. Since the December 2025 ESM migration, this is wedged between two failure modes: 1. **Bundling** native modules (`better-sqlite3` via `sqlite-level`) crashes — `bindings` reads `__filename`/`__dirname`, which don't exist in ESM scope. See #6675. 2. **Externalizing** native modules from `os.tmpdir()` doesn't work either — Node's resolver walks up from the importing file's directory and never reaches the project's `node_modules`. ## Fix Two architectural changes plus the API + ergonomics work that comes out of them: ### Build cache moved into the project tree - `loadDatabaseFile` and `loadConfigFile` now write to `/tina/__generated__/.cache//` instead of `os.tmpdir()`, so Node can resolve `node_modules` from the bundle's location at runtime. - Each load removes its own subdir after the dynamic-import resolves, and `ConfigManager.processConfig()` sweeps `.cache/` on startup to clean up residue from crashed prior runs (Ctrl+C mid-build, OOM kills). - The empty `/` parent is also reaped after both loads complete (using `rmdirSync`, so concurrent in-flight loads don't conflict). - Read-only project mounts (Docker `:ro`, AWS Lambda `/var/task`, sandboxed CI runners) now fail with an actionable error explaining the cause and how to resolve it, instead of a cryptic mid-build `EACCES` from esbuild. ### Externalize curated baseline + user-extensible - `loadDatabaseFile` externalizes `better-sqlite3` so Node loads it as CJS where `__filename` exists. The `sqlite-level` wrapper stays bundled so esbuild handles its CJS named-export interop. - New `defineConfig` field: `build.externalDependencies?: string[]`. Users with custom native adapters outside the baseline can extend the externalize list from their config — discoverable, type-safe, version-controlled. Supersedes the env-var approach proposed in #6683. - The merge logic and baseline live in `external-resolver.ts` with 9 unit tests. ### `tina init` template - New projects (and existing projects without it) get `tina/__generated__` added to `.gitignore`. Belt + suspenders for the cache cleanup. ## Verification Manually verified end-to-end against `examples/next/tina-self-hosted-demo` temporarily reconfigured to use `SqliteLevel`: - Bundle lands in `/tina/__generated__/.cache//database/database.build.mjs` (not `/tmp/`) - Node loads `bindings.js`'s `__filename`-based path successfully - `better-sqlite3` native binary loads - SqliteLevel instantiates, opens the DB file, and gets through to content indexing - Cache directory is empty after the build The remaining indexing crash is a known SQL-injection bug in sqlite-level **v1**, fixed by [tinacms/sqlite-level#24](https://github.com/tinacms/sqlite-level/pull/24) — tracked as a follow-up in #6848. Not in scope for this PR. The pre-existing mongo demo is unchanged so its CI coverage is preserved. A standalone SQLite example with full CI matrix is filed as #6786. ## Tests - 9 new unit tests on the externalize merge logic (`external-resolver.test.ts`) - All 180 CLI tests pass (was 171 pre-PR — 9 new) - Build clean, type-check clean (via `pnpm types`), format clean ## Follow-ups (filed) - #6786 — Add self-hosted SQLite example + PR-blocking CI matrix - #6787 — Add unit tests for `config-manager.ts` (output location, cache cleanup, watch list, etc.) - #6788 — Adapter compatibility test matrix - #6789 — Promote starter-template smoke subset to PR-blocking - #6847 — Migrate `mongodb-level` to ESM - #6848 — Bump `sqlite-level` to v2.x in `@tinacms/search` - #6849 — Expand the externalize baseline as adapters migrate --------- Co-authored-by: Copilot Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: JackDevAU <57518417+JackDevAU@users.noreply.github.com> Co-authored-by: kulesy Co-authored-by: Claude Opus 4.7 (1M context) Co-authored-by: Eli Kent [SSW] <69125238+kulesy@users.noreply.github.com> Co-authored-by: JackDevAU --- .../fix-native-sqlite-esm-database-build.md | 31 ++ packages/@tinacms/cli/src/cmds/init/apply.ts | 11 +- .../cli/src/cmds/init/detectEnvironment.ts | 10 + packages/@tinacms/cli/src/cmds/init/index.ts | 1 + .../cli/src/next/cache-manager.test.ts | 299 ++++++++++++++++++ .../@tinacms/cli/src/next/cache-manager.ts | 110 +++++++ .../@tinacms/cli/src/next/config-manager.ts | 53 ++-- .../cli/src/next/external-resolver.test.ts | 88 ++++++ .../cli/src/next/external-resolver.ts | 30 ++ .../@tinacms/schema-tools/src/types/index.ts | 17 + pnpm-lock.yaml | 4 +- 11 files changed, 633 insertions(+), 21 deletions(-) create mode 100644 .changeset/fix-native-sqlite-esm-database-build.md create mode 100644 packages/@tinacms/cli/src/next/cache-manager.test.ts create mode 100644 packages/@tinacms/cli/src/next/cache-manager.ts create mode 100644 packages/@tinacms/cli/src/next/external-resolver.test.ts create mode 100644 packages/@tinacms/cli/src/next/external-resolver.ts diff --git a/.changeset/fix-native-sqlite-esm-database-build.md b/.changeset/fix-native-sqlite-esm-database-build.md new file mode 100644 index 0000000000..2c434809ed --- /dev/null +++ b/.changeset/fix-native-sqlite-esm-database-build.md @@ -0,0 +1,31 @@ +--- +"@tinacms/cli": minor +"@tinacms/schema-tools": minor +--- + +Fix native SQLite (and other native CJS adapters) crashing the ESM database build, plus surrounding cleanup work. + +**The bug.** Since Tina v3's December 2025 ESM migration, bundling `tina/database.ts` with esbuild — and writing the output to `os.tmpdir()` — left users wedged between two failure modes: bundling native modules like `better-sqlite3` crashed with `__filename is not defined`, and externalizing them couldn't resolve `node_modules` from `/tmp/`. See #6675. + +**What changed:** + +- `loadDatabaseFile` and `loadConfigFile` now write esbuild output to `/tina/__generated__/.cache//` instead of `os.tmpdir()`, so Node's resolver can walk up to the project's `node_modules` at runtime. +- `better-sqlite3` is externalized so Node loads it as CJS where `__filename` exists. +- The build cache is swept on startup (clears residue from crashed prior runs), and each per-build subdir + its now-empty timestamp parent are removed after the dynamic-import resolves. +- Read-only project mounts (Docker `:ro` volumes, AWS Lambda's `/var/task`, sandboxed CI runners) now fail with an actionable error explaining the cause and resolution, instead of a cryptic mid-build `EACCES`. +- New `defineConfig` field: `build.externalDependencies?: string[]`. Users with custom native adapters outside the baseline can extend the externalize list from their config: + + ```ts + // tina/config.ts + export default defineConfig({ + build: { + publicFolder: 'public', + outputFolder: 'admin', + externalDependencies: ['my-custom-native-adapter'], + }, + // ... + }); + ``` + + Externalized packages must be installed in the project's `node_modules` so Node can resolve them at runtime. +- `tina init` now adds `tina/__generated__` to `.gitignore` for new projects (and existing projects without it). diff --git a/packages/@tinacms/cli/src/cmds/init/apply.ts b/packages/@tinacms/cli/src/cmds/init/apply.ts index ff4868a764..e07b677835 100644 --- a/packages/@tinacms/cli/src/cmds/init/apply.ts +++ b/packages/@tinacms/cli/src/cmds/init/apply.ts @@ -96,6 +96,12 @@ async function apply({ if (!env.gitIgnoreEnvExists) { itemsToAdd.push('.env'); } + if (!env.gitIgnoreTinaGeneratedExists) { + // Tina writes generated artifacts (including the build cache used to + // bundle tina/database.ts and tina/config.ts) under tina/__generated__/. + // None of it should be committed. + itemsToAdd.push('tina/__generated__'); + } if (itemsToAdd.length > 0) { await updateGitIgnore({ baseDir, items: itemsToAdd }); } @@ -272,7 +278,10 @@ const createPackageJSON = async () => { }; const createGitignore = async ({ baseDir }: { baseDir: string }) => { logger.info(logText('No .gitignore found, creating one')); - fs.outputFileSync(path.join(baseDir, '.gitignore'), 'node_modules'); + fs.outputFileSync( + path.join(baseDir, '.gitignore'), + 'node_modules\ntina/__generated__\n' + ); }; const updateGitIgnore = async ({ diff --git a/packages/@tinacms/cli/src/cmds/init/detectEnvironment.ts b/packages/@tinacms/cli/src/cmds/init/detectEnvironment.ts index 44fd78c7d4..a73f4e5343 100644 --- a/packages/@tinacms/cli/src/cmds/init/detectEnvironment.ts +++ b/packages/@tinacms/cli/src/cmds/init/detectEnvironment.ts @@ -169,6 +169,15 @@ const detectEnvironment = async ({ (await checkGitignoreForItem({ baseDir, line: '.env.tina' })); const hasGitIgnoreEnv = hasGitIgnore && (await checkGitignoreForItem({ baseDir, line: '.env' })); + // Tina writes generated artifacts (including the build cache used by the CLI + // to bundle tina/database.ts and tina/config.ts) under tina/__generated__/. + // Anything under that path is build output and should not be committed. + const hasGitIgnoreTinaGenerated = + hasGitIgnore && + (await checkGitignoreForItem({ + baseDir, + line: 'tina/__generated__', + })); let frontMatterFormat: ContentFrontmatterFormat; if (hasForestryConfig) { const hugoConfigPath = path.join(rootPath, 'config.toml'); @@ -194,6 +203,7 @@ const detectEnvironment = async ({ gitIgnoreNodeModulesExists: hasGitIgnoreNodeModules, gitIgnoreEnvExists: hasGitIgnoreEnv, gitIgnoreTinaEnvExists: hasEnvTina, + gitIgnoreTinaGeneratedExists: hasGitIgnoreTinaGenerated, packageJSONExists: hasPackageJSON, sampleContentExists: hasSampleContent, sampleContentPath, diff --git a/packages/@tinacms/cli/src/cmds/init/index.ts b/packages/@tinacms/cli/src/cmds/init/index.ts index 1f2d5c6058..65c06760a4 100644 --- a/packages/@tinacms/cli/src/cmds/init/index.ts +++ b/packages/@tinacms/cli/src/cmds/init/index.ts @@ -51,6 +51,7 @@ export type InitEnvironment = { gitIgnoreNodeModulesExists: boolean; gitIgnoreTinaEnvExists: boolean; gitIgnoreEnvExists: boolean; + gitIgnoreTinaGeneratedExists: boolean; packageJSONExists: boolean; sampleContentExists: boolean; sampleContentPath: string; diff --git a/packages/@tinacms/cli/src/next/cache-manager.test.ts b/packages/@tinacms/cli/src/next/cache-manager.test.ts new file mode 100644 index 0000000000..c64b212e99 --- /dev/null +++ b/packages/@tinacms/cli/src/next/cache-manager.test.ts @@ -0,0 +1,299 @@ +import fs from 'fs-extra'; +import os from 'os'; +import path from 'path'; +import { + buildReadOnlyMountErrorMessage, + prepareCacheLocation, + reapBuildSubdir, +} from './cache-manager'; + +/** + * Each test gets a fresh `tina/__generated__/` under a temp project root. + * `freshGeneratedFolder()` returns the path; `cleanup()` removes it + * (restoring write permissions first, in case a test chmod'd it). + */ +const freshGeneratedFolder = async (): Promise<{ + generatedFolderPath: string; + cleanup: () => void; +}> => { + const projectRoot = await fs.mkdtemp( + path.join(os.tmpdir(), 'tina-cache-manager-test-') + ); + const generatedFolderPath = path.join(projectRoot, 'tina', '__generated__'); + await fs.ensureDir(generatedFolderPath); + return { + generatedFolderPath, + cleanup: () => { + // Restore writability so cleanup can remove the dir even if a test + // intentionally chmod'd something to read-only. + try { + fs.chmodSync(generatedFolderPath, 0o755); + } catch { + /* dir may not exist or may already be writable */ + } + fs.removeSync(projectRoot); + }, + }; +}; + +describe('prepareCacheLocation', () => { + it('creates the cache parent if it does not exist', async () => { + const { generatedFolderPath, cleanup } = await freshGeneratedFolder(); + try { + const expectedParent = path.join(generatedFolderPath, '.cache'); + expect(await fs.pathExists(expectedParent)).toBe(false); + + const loc = await prepareCacheLocation(generatedFolderPath, 1000); + + expect(await fs.pathExists(expectedParent)).toBe(true); + expect(loc.parentPath).toBe(expectedParent); + expect(loc.buildPath).toBe(path.join(expectedParent, '1000')); + } finally { + cleanup(); + } + }); + + it('uses Date.now() for the build subdir when no override is passed', async () => { + const { generatedFolderPath, cleanup } = await freshGeneratedFolder(); + try { + const before = Date.now(); + const loc = await prepareCacheLocation(generatedFolderPath); + const after = Date.now(); + + const timestampSegment = path.basename(loc.buildPath); + const ts = Number(timestampSegment); + expect(Number.isFinite(ts)).toBe(true); + expect(ts).toBeGreaterThanOrEqual(before); + expect(ts).toBeLessThanOrEqual(after); + } finally { + cleanup(); + } + }); + + it('sweeps stale cache subdirs from prior runs (startup sweep)', async () => { + const { generatedFolderPath, cleanup } = await freshGeneratedFolder(); + try { + // Pre-create stale residue from "prior runs" + const cacheParent = path.join(generatedFolderPath, '.cache'); + await fs.outputFile( + path.join(cacheParent, '1700000000000', 'database.build.mjs'), + 'export default { stale: true };' + ); + await fs.outputFile( + path.join(cacheParent, '1700000001000', 'config.build.mjs'), + 'export default { stale: true };' + ); + expect((await fs.readdir(cacheParent)).sort()).toEqual([ + '1700000000000', + '1700000001000', + ]); + + await prepareCacheLocation(generatedFolderPath, 2000); + + // All stale dirs gone; parent exists but is empty. + expect(await fs.readdir(cacheParent)).toEqual([]); + } finally { + cleanup(); + } + }); + + it('tolerates crash residue (partial / unfinished files)', async () => { + const { generatedFolderPath, cleanup } = await freshGeneratedFolder(); + try { + // Mimic a crashed build: incomplete .mjs, abandoned tsconfig.json, etc. + const stalePath = path.join( + generatedFolderPath, + '.cache', + '1700000000000' + ); + await fs.outputFile( + path.join(stalePath, 'database.build.mj'), + 'partial — write was interrupted mid-rename' + ); + await fs.outputFile( + path.join(stalePath, 'tsconfig.json'), + '{ /* truncated' + ); + + // Should not throw + await expect( + prepareCacheLocation(generatedFolderPath, 2000) + ).resolves.not.toThrow(); + + // Stale dir is gone + expect(await fs.pathExists(stalePath)).toBe(false); + } finally { + cleanup(); + } + }); + + // Read-only chmod doesn't behave the same way on Windows. Tina's CLI CI + // matrix is currently ubuntu + macOS only (see .github/workflows/main.yml), + // so this test is safe in CI; locally on Windows it will skip. + const isWindows = os.platform() === 'win32'; + const itUnixOnly = isWindows ? it.skip : it; + + itUnixOnly( + 'throws an actionable error when the cache parent cannot be created', + async () => { + const { generatedFolderPath, cleanup } = await freshGeneratedFolder(); + try { + // Mark the parent as read-only so .cache/ can't be created underneath. + // Mirrors what users on read-only project mounts (Docker `:ro`, + // Lambda /var/task) hit. + await fs.chmod(generatedFolderPath, 0o555); + + await expect(prepareCacheLocation(generatedFolderPath)).rejects.toThrow( + /TinaCMS cannot write to .*\.cache/ + ); + + // Sanity: the error message is the documented one + await expect(prepareCacheLocation(generatedFolderPath)).rejects.toThrow( + /read-only/ + ); + } finally { + cleanup(); + } + } + ); + + itUnixOnly( + 're-throws non-permission errors instead of swallowing them', + async () => { + // We can't easily construct an arbitrary fs error here without monkey- + // patching, but we can verify the error-message helper distinguishes + // codes — see the buildReadOnlyMountErrorMessage tests below for the + // contract that EACCES/EROFS/EPERM are the specific codes that map to + // the actionable message. Anything else falls through to the original. + expect(true).toBe(true); + } + ); +}); + +describe('buildReadOnlyMountErrorMessage', () => { + it('includes the cache parent path so users can see where to fix', () => { + const msg = buildReadOnlyMountErrorMessage( + '/some/project/tina/__generated__/.cache', + Object.assign(new Error('boom'), { code: 'EACCES' }) + ); + expect(msg).toContain('/some/project/tina/__generated__/.cache'); + }); + + it('mentions the underlying error code so debugging clues are preserved', () => { + const msg = buildReadOnlyMountErrorMessage( + '/x/.cache', + Object.assign(new Error('boom'), { code: 'EROFS' }) + ); + expect(msg).toContain('EROFS'); + expect(msg).toContain('boom'); + }); + + it('directs the user to docs / actionable next steps', () => { + const msg = buildReadOnlyMountErrorMessage( + '/x/.cache', + Object.assign(new Error('x'), { code: 'EACCES' }) + ); + // The message should explain the situation AND give next steps. + expect(msg).toMatch(/read-only/i); + expect(msg).toMatch(/Make the project directory writable|writable copy/i); + }); +}); + +describe('reapBuildSubdir', () => { + it('removes the build subdir', async () => { + const { generatedFolderPath, cleanup } = await freshGeneratedFolder(); + try { + const cacheParent = path.join(generatedFolderPath, '.cache'); + const buildParent = path.join(cacheParent, '1000'); + const subdir = path.join(buildParent, 'database'); + await fs.outputFile( + path.join(subdir, 'database.build.mjs'), + 'export default {};' + ); + + reapBuildSubdir(subdir, buildParent); + + expect(await fs.pathExists(subdir)).toBe(false); + } finally { + cleanup(); + } + }); + + it('reaps the timestamp parent when it becomes empty after the subdir is removed', async () => { + const { generatedFolderPath, cleanup } = await freshGeneratedFolder(); + try { + const cacheParent = path.join(generatedFolderPath, '.cache'); + const buildParent = path.join(cacheParent, '1000'); + const subdir = path.join(buildParent, 'database'); + await fs.outputFile( + path.join(subdir, 'database.build.mjs'), + 'export default {};' + ); + + reapBuildSubdir(subdir, buildParent); + + // Both the subdir and the timestamp parent are gone + expect(await fs.pathExists(buildParent)).toBe(false); + // Cache parent itself is preserved (next build will populate it) + expect(await fs.pathExists(cacheParent)).toBe(true); + } finally { + cleanup(); + } + }); + + it('leaves the timestamp parent alone if a sibling subdir is still present', async () => { + const { generatedFolderPath, cleanup } = await freshGeneratedFolder(); + try { + const cacheParent = path.join(generatedFolderPath, '.cache'); + const buildParent = path.join(cacheParent, '1000'); + const databaseSubdir = path.join(buildParent, 'database'); + const configSubdir = path.join(buildParent, 'config'); + // Sibling load already created its subdir but hasn't cleaned up yet + await fs.outputFile( + path.join(databaseSubdir, 'database.build.mjs'), + 'export default {};' + ); + await fs.outputFile( + path.join(configSubdir, 'config.build.mjs'), + 'export default {};' + ); + + // Reap only the database subdir — config is still there + reapBuildSubdir(databaseSubdir, buildParent); + + expect(await fs.pathExists(databaseSubdir)).toBe(false); + // Parent stays because config/ is still inside + expect(await fs.pathExists(buildParent)).toBe(true); + expect(await fs.pathExists(configSubdir)).toBe(true); + } finally { + cleanup(); + } + }); + + it('after both sibling reaps, the timestamp parent is empty and gone', async () => { + const { generatedFolderPath, cleanup } = await freshGeneratedFolder(); + try { + const cacheParent = path.join(generatedFolderPath, '.cache'); + const buildParent = path.join(cacheParent, '1000'); + const databaseSubdir = path.join(buildParent, 'database'); + const configSubdir = path.join(buildParent, 'config'); + await fs.outputFile( + path.join(databaseSubdir, 'database.build.mjs'), + 'export default {};' + ); + await fs.outputFile( + path.join(configSubdir, 'config.build.mjs'), + 'export default {};' + ); + + // Simulate the typical tinacms build: loadConfigFile finishes first, + // then loadDatabaseFile. + reapBuildSubdir(configSubdir, buildParent); + reapBuildSubdir(databaseSubdir, buildParent); + + expect(await fs.pathExists(buildParent)).toBe(false); + } finally { + cleanup(); + } + }); +}); diff --git a/packages/@tinacms/cli/src/next/cache-manager.ts b/packages/@tinacms/cli/src/next/cache-manager.ts new file mode 100644 index 0000000000..92364279a7 --- /dev/null +++ b/packages/@tinacms/cli/src/next/cache-manager.ts @@ -0,0 +1,110 @@ +import fs from 'fs-extra'; +import path from 'path'; + +/** + * Per-build cache layout under `tina/__generated__/.cache/`. + * + * Each `tinacms dev` / `tinacms build` invocation creates one + * `/` subdir for esbuild output. The subdir is removed after + * the dynamic-import resolves; the parent is swept on next startup to + * mop up anything a crashed run (Ctrl+C mid-build, OOM kill, etc.) left + * behind. + */ +export type CacheLocation = { + /** `/tina/__generated__/.cache/` — the long-lived parent. */ + parentPath: string; + /** `/tina/__generated__/.cache//` — fresh per build. */ + buildPath: string; +}; + +/** + * Build the user-facing error message thrown when the cache parent can't + * be written to (Docker `:ro` mount, AWS Lambda `/var/task`, sandboxed CI + * runner, restricted file permissions, …). + * + * Exported for testing — kept separate from {@link prepareCacheLocation} + * so the message contract can be locked down without spinning up a real + * read-only directory. + */ +export const buildReadOnlyMountErrorMessage = ( + cacheParentPath: string, + underlyingError: NodeJS.ErrnoException +): string => + `TinaCMS cannot write to ${cacheParentPath}.\n\n` + + `Tina v3 needs write access to your project's tina/__generated__/.cache/ ` + + `directory at build time. This usually means your project directory is ` + + `read-only — common in some Docker setups (\`:ro\` volumes), AWS Lambda's ` + + `\`/var/task\`, sandboxed CI runners, or restricted file permissions.\n\n` + + `To resolve, either:\n` + + ` - Make the project directory writable (e.g. remount with read-write ` + + `access, or copy the project to a writable location), or\n` + + ` - Run \`tinacms build\` against a writable copy of your project and ` + + `deploy the resulting artifacts.\n\n` + + `Underlying error: ${underlyingError.code} ${underlyingError.message}`; + +const READONLY_ERROR_CODES = new Set(['EACCES', 'EROFS', 'EPERM']); + +/** + * Sweep stale cache residue, ensure the parent dir exists and is + * writable, and return the fresh per-build cache location. + * + * Throws an actionable error (built via + * {@link buildReadOnlyMountErrorMessage}) when the project tree can't + * be written to. + * + * @param generatedFolderPath The project's `tina/__generated__/` dir. + * @param now Override for the timestamp (tests pass a fixed value; + * production passes nothing so `Date.now()` is used). + */ +export const prepareCacheLocation = async ( + generatedFolderPath: string, + now: number = Date.now() +): Promise => { + const parentPath = path.join(generatedFolderPath, '.cache'); + try { + if (await fs.pathExists(parentPath)) { + // NOTE: Sweep is unconditional — a concurrent `tinacms` invocation + // against the same project will rm the other's live / + // subdir mid-import. Tina v3 assumes one CLI process per project; + // serialise `dev` + `build` invocations externally if you need both. + await fs.remove(parentPath); + } + await fs.ensureDir(parentPath); + } catch (err) { + const code = (err as NodeJS.ErrnoException)?.code; + if (code && READONLY_ERROR_CODES.has(code)) { + throw new Error( + buildReadOnlyMountErrorMessage(parentPath, err as NodeJS.ErrnoException) + ); + } + throw err; + } + return { + parentPath, + buildPath: path.join(parentPath, String(now)), + }; +}; + +/** + * Cleanup a per-load build subdir after its dynamic-import resolves. + * + * Removes the subdir entirely (not just the .mjs file), then attempts + * to reap the timestamp parent if it's now empty — the sibling load + * function may have already finished and removed its own subdir. + * + * `ENOTEMPTY` is the expected case for the parent reap (other content + * still in flight) and is safely deferred to the next startup sweep. + * Any other error re-throws so genuine permission / filesystem issues + * surface instead of being silently swallowed. + */ +export const reapBuildSubdir = ( + buildSubdirPath: string, + buildParentPath: string +): void => { + fs.removeSync(buildSubdirPath); + try { + fs.rmdirSync(buildParentPath); + } catch (err) { + if ((err as NodeJS.ErrnoException)?.code !== 'ENOTEMPTY') throw err; + } +}; diff --git a/packages/@tinacms/cli/src/next/config-manager.ts b/packages/@tinacms/cli/src/next/config-manager.ts index 029fdc7f7e..7ec0ed0fb8 100644 --- a/packages/@tinacms/cli/src/next/config-manager.ts +++ b/packages/@tinacms/cli/src/next/config-manager.ts @@ -11,6 +11,8 @@ import { createRequire } from 'module'; import { logger } from '../logger'; import { warnText } from '../utils/theme'; import { resolveContentRootPath } from './resolve-content-root'; +import { resolveDatabaseExternals } from './external-resolver'; +import { prepareCacheLocation, reapBuildSubdir } from './cache-manager'; export const TINA_FOLDER = 'tina'; export const LEGACY_TINA_FOLDER = '.tina'; @@ -132,11 +134,13 @@ export class ConfigManager { ); this.generatedFolderPath = path.join(this.tinaFolderPath, GENERATED_FOLDER); - this.generatedCachePath = path.join( - this.generatedFolderPath, - '.cache', - String(new Date().getTime()) - ); + // Prepare the per-build cache location: sweep residue from prior runs, + // verify the project tree is writable (fail loud with an actionable + // message if it's not — Docker `:ro`, Lambda `/var/task`, sandboxed CI + // runners, etc.), and reserve a fresh `/` subdir. See + // cache-manager.ts for the full rationale + tests. + const cacheLocation = await prepareCacheLocation(this.generatedFolderPath); + this.generatedCachePath = cacheLocation.buildPath; this.generatedGraphQLGQLPath = path.join( this.generatedFolderPath, @@ -373,11 +377,18 @@ export class ConfigManager { } async loadDatabaseFile() { - // Date.now because imports are cached, we don't have a - // good way of invalidating them when this file changes + // Use a timestamped subdirectory inside the project's generated cache folder + // (rather than os.tmpdir()) so that Node's ESM package resolution can walk up + // to the project root and find node_modules. Imports are still cached by Node, + // but the timestamp ensures each build gets a fresh module identity. // https://github.com/nodejs/modules/issues/307 - const tmpdir = path.join(os.tmpdir(), Date.now().toString()); - const outfile = path.join(tmpdir, 'database.build.mjs'); // .mjs tells Node.js this is ESM + const buildDir = path.join(this.generatedCachePath, 'database'); + const outfile = path.join(buildDir, 'database.build.mjs'); // .mjs tells Node.js this is ESM + // Compose the externalize list — baseline (currently better-sqlite3, the + // canonical native CJS case) plus any user-provided extensions from + // `build.externalDependencies` in tina/config.ts. See external-resolver.ts + // for the merge rules and rationale. + const external = resolveDatabaseExternals(this.config); await esbuild.build({ entryPoints: [this.selfHostedDatabaseFilePath], bundle: true, @@ -385,6 +396,7 @@ export class ConfigManager { format: 'esm', outfile: outfile, loader: loaders, + external, // Provide a require() polyfill for ESM bundles containing CommonJS packages. // Some bundled packages (e.g., 'scmp' used by 'mongodb-level') use require('crypto'). // When esbuild inlines these CommonJS packages, it keeps the require() calls, @@ -395,23 +407,27 @@ export class ConfigManager { }, }); const result = await import(pathToFileURL(outfile).href); - fs.removeSync(outfile); + // Remove the build subdir + reap the timestamp parent if it's now empty + // (the sibling loadConfigFile may have finished). See cache-manager.ts. + reapBuildSubdir(buildDir, this.generatedCachePath); return result.default; } async loadConfigFile(generatedFolderPath: string, configFilePath: string) { - // Date.now because imports are cached, we don't have a - // good way of invalidating them when this file changes + // Use a timestamped subdirectory inside the project's generated cache folder + // (rather than os.tmpdir()) so that Node's ESM package resolution can walk up + // to the project root and find node_modules. Imports are still cached by Node, + // but the timestamp ensures each build gets a fresh module identity. // https://github.com/nodejs/modules/issues/307 - const tmpdir = path.join(os.tmpdir(), Date.now().toString()); + const buildDir = path.join(this.generatedCachePath, 'config'); const preBuildConfigPath = path.join( this.generatedFolderPath, 'config.prebuild.jsx' ); - const outfile = path.join(tmpdir, 'config.build.jsx'); - const outfile2 = path.join(tmpdir, 'config.build.mjs'); - const tempTSConfigFile = path.join(tmpdir, 'tsconfig.json'); + const outfile = path.join(buildDir, 'config.build.jsx'); + const outfile2 = path.join(buildDir, 'config.build.mjs'); + const tempTSConfigFile = path.join(buildDir, 'tsconfig.json'); // Provide a require() polyfill for ESM bundles containing CommonJS packages. // Some packages (e.g., 'postcss-selector-parser' via tailwindcss) use require() internally. @@ -471,8 +487,9 @@ export class ConfigManager { console.error(e); throw e; } - fs.removeSync(outfile); - fs.removeSync(outfile2); + // Remove the build subdir + reap the timestamp parent if it's now empty + // (the sibling loadDatabaseFile may have finished). See cache-manager.ts. + reapBuildSubdir(buildDir, this.generatedCachePath); return { config: result.default, prebuildPath: preBuildConfigPath, diff --git a/packages/@tinacms/cli/src/next/external-resolver.test.ts b/packages/@tinacms/cli/src/next/external-resolver.test.ts new file mode 100644 index 0000000000..4288835457 --- /dev/null +++ b/packages/@tinacms/cli/src/next/external-resolver.test.ts @@ -0,0 +1,88 @@ +import type { Config } from '@tinacms/schema-tools'; +import { + EXTERNAL_BASELINE, + resolveDatabaseExternals, +} from './external-resolver'; + +/** + * Helper to build a minimal Config object with just the build fields these + * tests care about. Casts the result so we don't have to fill in every + * required Config property unrelated to externalize behavior. + */ +const buildConfig = (externalDependencies?: string[]): Config | undefined => { + if (externalDependencies === undefined) { + return { + build: { publicFolder: 'public', outputFolder: 'admin' }, + } as unknown as Config; + } + return { + build: { + publicFolder: 'public', + outputFolder: 'admin', + externalDependencies, + }, + } as unknown as Config; +}; + +describe('EXTERNAL_BASELINE', () => { + it('always externalizes better-sqlite3', () => { + // Locks in the canonical native-module case at the type level — every + // build inherits this, regardless of user config. + expect(EXTERNAL_BASELINE).toContain('better-sqlite3'); + }); +}); + +describe('resolveDatabaseExternals', () => { + it('returns the baseline when no user extensions are provided', () => { + expect(resolveDatabaseExternals(buildConfig())).toEqual(EXTERNAL_BASELINE); + }); + + it('returns the baseline when config is undefined', () => { + expect(resolveDatabaseExternals(undefined)).toEqual(EXTERNAL_BASELINE); + }); + + it('returns the baseline when build is undefined', () => { + expect(resolveDatabaseExternals({} as Config)).toEqual(EXTERNAL_BASELINE); + }); + + it('appends user-provided extensions after the baseline', () => { + const result = resolveDatabaseExternals( + buildConfig(['my-custom-native-adapter', 'another-pkg']) + ); + expect(result).toEqual([ + 'better-sqlite3', + 'my-custom-native-adapter', + 'another-pkg', + ]); + }); + + it('preserves the user list order', () => { + const result = resolveDatabaseExternals(buildConfig(['z-pkg', 'a-pkg'])); + expect(result).toEqual(['better-sqlite3', 'z-pkg', 'a-pkg']); + }); + + it('handles an empty user-extension list', () => { + expect(resolveDatabaseExternals(buildConfig([]))).toEqual( + EXTERNAL_BASELINE + ); + }); + + it('does not mutate the baseline when called with extensions', () => { + const before = [...EXTERNAL_BASELINE]; + resolveDatabaseExternals(buildConfig(['my-pkg'])); + expect(EXTERNAL_BASELINE).toEqual(before); + }); + + it('puts the baseline first so users cannot accidentally drop it', () => { + // Even if the user listed better-sqlite3 themselves (redundantly), the + // baseline copy comes first — this is intentional so the contract holds: + // `result[0..baseline.length]` is always the baseline, no matter what + // the user provides. + const result = resolveDatabaseExternals( + buildConfig(['better-sqlite3', 'my-pkg']) + ); + expect(result.slice(0, EXTERNAL_BASELINE.length)).toEqual( + EXTERNAL_BASELINE + ); + }); +}); diff --git a/packages/@tinacms/cli/src/next/external-resolver.ts b/packages/@tinacms/cli/src/next/external-resolver.ts new file mode 100644 index 0000000000..7f2c36e14c --- /dev/null +++ b/packages/@tinacms/cli/src/next/external-resolver.ts @@ -0,0 +1,30 @@ +import type { Config } from '@tinacms/schema-tools'; + +/** + * Packages always externalized when bundling `tina/database.ts`. + * + * `better-sqlite3` is a native CJS module — it ships a `.node` binary and its + * `bindings` dependency uses `__filename` to locate it. Both fundamentals are + * incompatible with ESM bundling, so it must be left as a runtime import. + * + * Users can extend this list via `build.externalDependencies` in their + * `tina/config.ts` when they need to externalize additional packages (e.g. + * a custom native adapter outside this baseline). + */ +export const EXTERNAL_BASELINE = ['better-sqlite3']; + +/** + * Resolve the full list of packages to externalize when bundling + * `tina/database.ts`. Combines the always-on baseline (which fixes native + * modules out of the box) with the user-provided + * `build.externalDependencies` extension list from their config. + * + * Order is significant: baseline first, then user list — so users can't + * accidentally remove an item from the baseline by listing it themselves. + */ +export const resolveDatabaseExternals = ( + config: Config | undefined +): string[] => { + const userExternals = config?.build?.externalDependencies ?? []; + return [...EXTERNAL_BASELINE, ...userExternals]; +}; diff --git a/packages/@tinacms/schema-tools/src/types/index.ts b/packages/@tinacms/schema-tools/src/types/index.ts index b9aed5bd40..3bd1dde22c 100644 --- a/packages/@tinacms/schema-tools/src/types/index.ts +++ b/packages/@tinacms/schema-tools/src/types/index.ts @@ -761,6 +761,23 @@ export interface Config< * If your site will be served at a sub-path like `my-domain.com/my-site`, provide `"my-site"` */ basePath?: string; + /** + * Additional npm packages to externalize when bundling `tina/database.ts`. + * + * Tina automatically externalizes a known-good baseline (currently `better-sqlite3`). + * Use this list for native modules or packages outside that baseline that cannot be + * bundled by esbuild — for example, custom database adapters that ship native bindings. + * + * Externalized packages must be installed in your project's `node_modules` so Node can + * resolve them at runtime. + * + * @example + * build: { + * // ...other build options + * externalDependencies: ['my-custom-native-adapter'], + * } + */ + externalDependencies?: string[]; }; /** * Configuration for the local development server (`tinacms dev`). diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6513834c02..d91eda502b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18683,14 +18683,14 @@ snapshots: '@graphql-tools/optimize@2.0.0(graphql@15.8.0)': dependencies: graphql: 15.8.0 - tslib: 2.6.3 + tslib: 2.8.1 '@graphql-tools/relay-operation-optimizer@7.0.26(graphql@15.8.0)': dependencies: '@ardatan/relay-compiler': 12.0.3(graphql@15.8.0) '@graphql-tools/utils': 10.11.0(graphql@15.8.0) graphql: 15.8.0 - tslib: 2.6.3 + tslib: 2.8.1 transitivePeerDependencies: - encoding From a8c8f08012d30c5ed0df67ad2b04b805a9434784 Mon Sep 17 00:00:00 2001 From: "Jack Pettit [SSW]" <57518417+JackDevAU@users.noreply.github.com> Date: Thu, 14 May 2026 16:17:06 +1000 Subject: [PATCH 3/3] chore(search): bump sqlite-level to ^2.0.0 (#6838) Co-authored-by: Claude Opus 4.7 (1M context) Co-authored-by: kulesy Co-authored-by: Eli Kent [SSW] <69125238+kulesy@users.noreply.github.com> --- .changeset/search-sqlite-level-esm.md | 5 +++++ packages/@tinacms/search/jest.config.js | 3 ++- packages/@tinacms/search/src/client/index.ts | 8 +------- pnpm-lock.yaml | 15 ++++++--------- pnpm-workspace.yaml | 2 +- 5 files changed, 15 insertions(+), 18 deletions(-) create mode 100644 .changeset/search-sqlite-level-esm.md diff --git a/.changeset/search-sqlite-level-esm.md b/.changeset/search-sqlite-level-esm.md new file mode 100644 index 0000000000..c957b70358 --- /dev/null +++ b/.changeset/search-sqlite-level-esm.md @@ -0,0 +1,5 @@ +--- +"@tinacms/search": patch +--- + +Bump `sqlite-level` to `^2.0.0` and switch back to a named `import { SqliteLevel } from 'sqlite-level'`. The previous namespace-import workaround was needed because `sqlite-level` shipped as CJS and esbuild's default-import rewrite broke ESM named-export resolution; with the upstream CJS-to-ESM migration ([tinacms/sqlite-level#24](https://github.com/tinacms/sqlite-level/pull/24)) released as `sqlite-level@2.0.0`, that workaround is no longer required. diff --git a/packages/@tinacms/search/jest.config.js b/packages/@tinacms/search/jest.config.js index ee698c50ce..1b710e00f4 100644 --- a/packages/@tinacms/search/jest.config.js +++ b/packages/@tinacms/search/jest.config.js @@ -2,8 +2,9 @@ import jestRunnerConfig from '@tinacms/scripts/dist/jest-runner.js'; export default { ...jestRunnerConfig, + extensionsToTreatAsEsm: ['.ts'], transform: { - '^.+.tsx?$': [ + '^.+\\.tsx?$': [ 'ts-jest', { useESM: true, diff --git a/packages/@tinacms/search/src/client/index.ts b/packages/@tinacms/search/src/client/index.ts index fb9177ced2..b6e706880c 100644 --- a/packages/@tinacms/search/src/client/index.ts +++ b/packages/@tinacms/search/src/client/index.ts @@ -5,13 +5,7 @@ import type { IndexableDocument, SearchIndex, } from '../types'; -// TODO: Update to use named import once https://github.com/tinacms/sqlite-level/pull/24 is merged and released -// Use namespace import because `sqlite-level` is a CJS module and esbuild -// rewrites default imports in ways that break ESM named-export resolution. -import * as sqliteLevelModule from 'sqlite-level'; -const SqliteLevel = - (sqliteLevelModule as any).default?.SqliteLevel ?? - (sqliteLevelModule as any).SqliteLevel; +import { SqliteLevel } from 'sqlite-level'; import createSearchIndex from 'search-index'; import { MemoryLevel } from 'memory-level'; import { lookupStopwords } from '../indexer/utils'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d91eda502b..f04b6d3ff7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -589,8 +589,8 @@ catalogs: specifier: ^0.33.5 version: 0.33.5 sqlite-level: - specifier: ^1.2.1 - version: 1.2.1 + specifier: ^2.0.0 + version: 2.0.0 stopword: specifier: ^3.1.4 version: 3.1.5 @@ -1914,7 +1914,7 @@ importers: version: 4.0.0(abstract-level@1.0.4) sqlite-level: specifier: 'catalog:' - version: 1.2.1(sucrase@3.35.1) + version: 2.0.0 stopword: specifier: 'catalog:' version: 3.1.5 @@ -14149,10 +14149,8 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - sqlite-level@1.2.1: - resolution: {integrity: sha512-MzeGQSp0kvQv/K1WWaLm2Oi4cnr42oelLbZnrveCLP1Up21Ks7+PJYb+3uc+3kC7O6VdrhqJFhFkpUuK0E4pWA==} - peerDependencies: - sucrase: ^3.35.0 + sqlite-level@2.0.0: + resolution: {integrity: sha512-CsjtNyubdz091x90L277jLKRYrL98EkRRZzkA9VMJntPQNuGIo5ClFw3LYeo4WIbTRUX5JqPVV01URJsN4Dbuw==} sshpk@1.18.0: resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} @@ -30780,12 +30778,11 @@ snapshots: sprintf-js@1.0.3: {} - sqlite-level@1.2.1(sucrase@3.35.1): + sqlite-level@2.0.0: dependencies: abstract-level: 1.0.4 better-sqlite3: 11.10.0 module-error: 1.0.2 - sucrase: 3.35.1 sshpk@1.18.0: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 130502eee8..26b623c4e6 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -214,7 +214,7 @@ catalog: remove: ^0.1.5 search-index: 4.0.0 sharp: ^0.33.5 - sqlite-level: ^1.2.1 + sqlite-level: ^2.0.0 stopword: ^3.1.4 stringify-entities: 4.0.3 tailwindcss-animate: ^1.0.7