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 .changeset/dedupe-acorn-in-mdx-bundle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@tinacms/scripts": patch
"@tinacms/mdx": patch
---

Ship one copy of the `acorn` parser in `@tinacms/mdx` instead of three, cutting `dist/index.browser.js` from 1,976,421 to 1,578,764 bytes (440,787 to 356,471 gzipped) and `dist/index.js` from 2,013,419 to 1,615,828 bytes (452,063 to 367,630 gzipped). The catalog pinned `acorn` to 8.8.2 while `micromark-extension-mdxjs` pulled 8.16.0, so two 8.x copies were bundled side by side; separately, `acorn-jsx` reaches `acorn` through `require`, which acorn's export map answers with its CJS build while every other importer gets the ESM build, bundling the parser a second time. Aligning the catalog to `^8.16.0` and aliasing `acorn` to its ESM entry in the `@tinacms/mdx` esbuild config collapses all three into one. Parser and serializer output is unchanged — `parseMDX`/`serializeMDX` round-trips over the package's 64 markdown fixtures produce byte-identical results from the old and new bundles.
57 changes: 57 additions & 0 deletions .github/workflows/playwright-prebuilt-admin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Playwright — prebuilt admin (production build)

on:
pull_request:
branches:
- main
paths:
- 'packages/@tinacms/cli/**'
- 'packages/@tinacms/app/**'
- 'packages/tinacms/**'
- 'packages/@tinacms/schema-tools/**'
- 'packages/@tinacms/mdx/**'
- 'packages/@tinacms/graphql/**'
- 'playwright/prebuilt-admin/**'
- '.github/workflows/playwright-prebuilt-admin.yml'

permissions:
contents: read

jobs:
e2e:
name: prebuilt-admin-e2e
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup Node.js environment
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: '.nvmrc'
- name: Install pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
package_json_file: package.json
run_install: false
- name: Install dependencies
run: pnpm install
- name: Build workspace packages
run: pnpm build
- name: Install Playwright browsers
working-directory: playwright/prebuilt-admin
run: npx playwright install --with-deps chromium
- name: Run prebuilt-admin Playwright tests
working-directory: playwright/prebuilt-admin
run: npx playwright test
env:
CI: true
- name: Upload traces and test results
if: ${{ !cancelled() }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: prebuilt-admin-playwright-report
path: |
playwright/prebuilt-admin/test-results/
playwright/prebuilt-admin/playwright-test-results.json
retention-days: 7
if-no-files-found: ignore
2 changes: 1 addition & 1 deletion examples/astro/kitchen-sink/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"@tailwindcss/typography": "catalog:",
"@tailwindcss/vite": "^4.2.1",
"@tinacms/datalayer": "workspace:*",
"astro": "^7.0.6",
"astro": "^7.1.1",
"clsx": "^2.1.1",
"graphql": "^15.10.1",
"react": "^18.3.1",
Expand Down
2 changes: 1 addition & 1 deletion examples/astro/visual-editing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"@tinacms/astro": "workspace:*",
"@tinacms/datalayer": "workspace:*",
"@tinacms/mdx": "workspace:*",
"astro": "^7.0.6",
"astro": "^7.1.1",
"clsx": "^2.1.1",
"graphql": "^15.10.1",
"react-icons": "^5.6.0",
Expand Down
2 changes: 1 addition & 1 deletion examples/next/kitchen-sink/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"clsx": "^2.1.1",
"graphql": "^15.10.1",
"mongodb-level": "^0.0.4",
"next": "15.5.12",
"next": "15.5.21",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^4.12.0",
Expand Down
2 changes: 1 addition & 1 deletion examples/next/tina-self-hosted-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"@tinacms/datalayer": "workspace:*",
"date-fns": "^2.30.0",
"mongodb-level": "^0.0.3",
"next": "14.2.35",
"next": "15.5.21",
"next-auth": "^4.24.15",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down
2 changes: 1 addition & 1 deletion examples/react/kitchen-sink/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^4.12.0",
"react-router-dom": "^6.28.0",
"react-router-dom": "^7.0.0",
"tailwind-merge": "^2.6.0",
"tinacms": "workspace:*"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/@tinacms/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@
"devDependencies": {
"@tinacms/scripts": "workspace:*",
"@types/node": "^22.13.1",
"astro": "^7.0.6",
"astro": "^7.1.1",
"typescript": "^5.7.3",
"vite": "^8.0.0",
"vitest": "^3.0.0"
Expand Down
2 changes: 1 addition & 1 deletion packages/@tinacms/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"@tinacms/scripts": "workspace:*",
"@types/jest": "^29.5.14",
"jest": "catalog:",
"next": "14.2.35",
"next": "15.5.21",
"ts-jest": "catalog:",
"typescript": "^5.7.3"
},
Expand Down
17 changes: 17 additions & 0 deletions packages/@tinacms/scripts/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as fs from 'fs';
import { exec } from 'node:child_process';
import { createRequire } from 'node:module';
import path from 'node:path';
import chalk from 'chalk';
import chokidar from 'chokidar';
Expand Down Expand Up @@ -424,6 +425,20 @@ export class BuildTina {
});
} else if (['@tinacms/mdx'].includes(packageJSON.name)) {
const peerDeps = packageJSON.peerDependencies;

// acorn-jsx reaches acorn with `require`, which acorn's export map
// answers with the CJS build, while every other importer gets the ESM
// build. Without this the whole parser is bundled twice.
const acornManifest = createRequire(
path.join(process.cwd(), 'package.json')
).resolve('acorn/package.json');
const alias = {
acorn: path.join(
path.dirname(acornManifest),
JSON.parse(fs.readFileSync(acornManifest, 'utf8')).module
),
};

await esbuild({
entryPoints: [path.join(process.cwd(), entry)],
bundle: true,
Expand All @@ -432,6 +447,7 @@ export class BuildTina {
format: 'esm',
outfile: path.join(process.cwd(), 'dist', 'index.js'),
external: Object.keys({ ...peerDeps }),
alias,
});

// The ES version is targeting the browser. This is used by the rich-text's raw mode
Expand All @@ -446,6 +462,7 @@ export class BuildTina {
// and includes "development" export maps which actually throw errors during
// development, which we don't want to expose our users to.
external: Object.keys({ ...peerDeps }),
alias,
});
} else {
await esbuild({
Expand Down
2 changes: 1 addition & 1 deletion packages/next-tinacms-azure/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"@types/crypto-js": "^3.1.47",
"@types/js-cookie": "^2.2.7",
"@types/node": "^22.13.1",
"next": "14.2.35",
"next": "15.5.21",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tinacms": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion packages/next-tinacms-cloudinary/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"@types/crypto-js": "^3.1.47",
"@types/js-cookie": "^2.2.7",
"@types/node": "^22.13.1",
"next": "14.2.35",
"next": "15.5.21",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tinacms": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion packages/next-tinacms-dos/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"@types/crypto-js": "^3.1.47",
"@types/js-cookie": "^2.2.7",
"@types/node": "^22.13.1",
"next": "14.2.35",
"next": "15.5.21",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tinacms": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion packages/next-tinacms-s3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"@types/js-cookie": "^2.2.7",
"@types/node": "^22.13.1",
"@types/react": "^18.3.18",
"next": "14.2.35",
"next": "15.5.21",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tinacms": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion packages/tinacms/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@
"identity-obj-proxy": "catalog:",
"jest-file-snapshot": "^0.7.0",
"lowlight": "catalog:",
"next": "14.2.35",
"next": "15.5.21",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-is": "^18.3.1",
Expand Down
17 changes: 17 additions & 0 deletions playwright/prebuilt-admin/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# dependencies
/node_modules

# generated tina client + admin build output
/tina/__generated__
/public/admin

# playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright-test-results.json

# misc
.DS_Store
*.log
83 changes: 83 additions & 0 deletions playwright/prebuilt-admin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# prebuilt-admin

The repo's first Playwright suite that runs against a **production `tinacms build`
output** instead of `tinacms dev`. It is driven by a deliberately hostile fixture
config so that a browser-only, production-only regression in the admin bundle
fails here instead of slipping through CI.

Green on `main` today is the baseline. Issue #7243 will rerun this identical
suite with the prebuilt flag on; the specs are written to be **mode-agnostic**.

## How the webServer works

`pnpm serve:prod` runs:

```
tinacms build --local --skip-cloud-checks --noTelemetry -c "node serve-admin.mjs"
```

1. `tinacms build --local` builds the **production** admin SPA into
`public/admin/` (minified vite build — the same output a real deploy ships)
**and** keeps a local GraphQL + media server alive on `:4001`.
2. The `-c` sub-command starts `serve-admin.mjs`, a tiny static server that
serves the built SPA under `/my-site/admin/` on `:3456`.

The browser loads the production bundle from `:3456`; the admin queries the API
cross-origin on `:4001` (localhost origins are CORS-allowed by the dev server).
Playwright waits for `http://localhost:3456/my-site/admin/` before running.

### Why `contentApiUrlOverride` is set

`tinacms build` bakes `codegen.productionUrl` (the TinaCloud content URL) into
the SPA **regardless of `--local`** — `--local` only affects the generated
client SDK and runs a local server for build-time indexing. Without an override
the admin would point at `https://content.tinajs.io/...` (with this fixture's
empty clientId/branch that URL is even malformed and the admin crashes on boot).
Setting `contentApiUrlOverride: 'http://localhost:4001/graphql'` redirects the
baked SPA at the local server. This diverges from the issue brief's assumption
that "in local mode the baked apiUrl points at the local GraphQL server"; it
does not, and the override is the fix.

## The hostile fixture (seven ingredients)

Each guards a class of production-only bundling bug. `e2e/setup.spec.ts` greps
the built chunks to prove they actually reached the bundle (guards against an
accidentally-tame fixture).

| Ingredient | Where |
|---|---|
| Custom field component colocated with the schema | `tina/fields/fixture-field.tsx` |
| Component importing `next/image` (CJS `require('react')`) | `tina/fields/fixture-field.tsx` |
| Arbitrary Tailwind class `aspect-w-9` + themed `bg-blue-500` | `tina/fields/fixture-field.tsx` |
| Real `media.loadCustomStore` via dynamic `import()` | `tina/media/fixture-media-store.ts` |
| `UsernamePasswordAuthJSProvider` from `tinacms-authjs/dist/tinacms` | `tina/config.tsx` |
| `cmsCallback` registering a screen plugin | `tina/config.tsx` |
| `build.basePath: 'my-site'` | `tina/config.tsx` |

The auth provider is selected at runtime: `window.__TINA_FIXTURE_AUTHJS__`
(set by `auth.spec` before boot) activates `UsernamePasswordAuthJSProvider` so
its custom login screen renders; every other spec boots the local provider and
clicks through the "Enter Edit Mode" dialog. Both providers are referenced so
the bundler keeps `tinacms-authjs` (and its CJS `next-auth/react`) in the build
either way.

## Specs

- `boot.spec` — collection list renders; single React reconciler; zero console errors on boot
- `custom-field.spec` — custom field renders and a save round-trips to disk (its own document, `roundtrip.mdx`, isolated from the read-only specs)
- `tailwind.spec` — `bg-blue-500` computes to Tina's `rgb(0, 132, 255)`; `aspect-w-9` compiled; also re-asserts the single-React/zero-console-error health check, since this is where `next/image` actually renders
- `basepath.spec` — admin boots at `/my-site/admin/` **and** the bare `/my-site/admin`
- `media.spec` — media manager lists via the custom store
- `auth.spec` — the custom AuthJS login screen appears
- `screen.spec` — navigates through the admin nav (not a deep link) to the `cmsCallback` screen plugin, guarding a second react-router-dom instance
- `setup.spec` — meta-assertion: the CJS module, custom store and screen plugin are present in the built chunks

## Running locally

```
pnpm install # from repo root
pnpm build # build the workspace packages the fixture consumes
cd playwright/prebuilt-admin
npx playwright install chromium
npx playwright test
```
6 changes: 6 additions & 0 deletions playwright/prebuilt-admin/content/post/hello.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: Hello Prebuilt
marker: initial-marker
---

Hello from the prebuilt-admin fixture.
8 changes: 8 additions & 0 deletions playwright/prebuilt-admin/content/post/roundtrip.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: Roundtrip Fixture
marker: initial-marker
---

Dedicated document for custom-field.spec's save round-trip. Kept separate
from hello.mdx (read by boot.spec and tailwind.spec) so the mutating spec
never races a read-only spec over the same file.
27 changes: 27 additions & 0 deletions playwright/prebuilt-admin/e2e/auth.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { expect, test } from '@playwright/test';
import { ADMIN_ROOT } from './utils/admin-helpers';

// The `UsernamePasswordAuthJSProvider` ingredient drags the ESM entry that
// requires CJS `next-auth/react` into the bundle. When that provider is the
// active one, the admin must render its custom username/password login screen
// instead of the local edit-mode flow — proving next-auth/react loaded and ran
// in the browser. The fixture selects this provider at runtime when
// `window.__TINA_FIXTURE_AUTHJS__` is set before boot.
test('custom AuthJS login screen appears', async ({ page }) => {
await page.addInitScript(() => {
(
window as unknown as { __TINA_FIXTURE_AUTHJS__: boolean }
).__TINA_FIXTURE_AUTHJS__ = true;
});

await page.goto(ADMIN_ROOT, { waitUntil: 'domcontentloaded' });

// The username/password modal from UsernamePasswordAuthJSProvider.
await expect(page.getByPlaceholder('Username')).toBeVisible({
timeout: 30000,
});
await expect(page.getByPlaceholder('Password')).toBeVisible();
await expect(
page.getByRole('button', { name: 'Login', exact: true })
).toBeVisible();
});
28 changes: 28 additions & 0 deletions playwright/prebuilt-admin/e2e/basepath.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { expect, test } from '@playwright/test';
import { waitForAdminShell } from './utils/admin-helpers';

// `build.basePath: 'my-site'` must serve a bootable admin at BOTH the
// trailing-slash and bare forms. The bare form is the spike's trap: without
// a trailing slash, `document.baseURI` drops the last segment and every
// relative asset 404s. Assets are absolute in the production build, so the
// bare form must still boot — this locks that in.
for (const path of ['/my-site/admin/', '/my-site/admin']) {
test(`admin boots at "${path}"`, async ({ page }) => {
const consoleErrors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') consoleErrors.push(msg.text());
});

await page.goto(path, { waitUntil: 'domcontentloaded' });
await waitForAdminShell(page);

// The shell mounted into #root (assets resolved).
const rootChildren = await page.evaluate(
() => document.getElementById('root')?.childElementCount ?? 0
);
expect(rootChildren).toBeGreaterThan(0);

// The build's inline asset-load guard never fired.
expect(consoleErrors.join('\n')).not.toContain('Failed to load assets');
});
}
Loading
Loading