diff --git a/.changeset/authjs-cjs-interop.md b/.changeset/authjs-cjs-interop.md new file mode 100644 index 0000000000..29151ca3e7 --- /dev/null +++ b/.changeset/authjs-cjs-interop.md @@ -0,0 +1,5 @@ +--- +"tinacms-authjs": patch +--- + +Fix the package outside webpack. Unwraps the CommonJS default export of `next-auth`, and stops the node entry importing the browser `tinacms` package for a single string constant. Netlify Functions, Vercel Functions and plain Node ESM previously failed to load with `(0 , import_credentials.default) is not a function`. Dropping that import also shrinks bundled backends: the Netlify Functions sample from the docs goes from 21.0 MB to 5.4 MB. diff --git a/packages/tinacms-authjs/src/constants.ts b/packages/tinacms-authjs/src/constants.ts new file mode 100644 index 0000000000..7b1dbb85e9 --- /dev/null +++ b/packages/tinacms-authjs/src/constants.ts @@ -0,0 +1 @@ +export const TINA_CREDENTIALS_PROVIDER_NAME = 'TinaCredentials'; diff --git a/packages/tinacms-authjs/src/index.ts b/packages/tinacms-authjs/src/index.ts index a4521c449f..7663b5faec 100644 --- a/packages/tinacms-authjs/src/index.ts +++ b/packages/tinacms-authjs/src/index.ts @@ -1,8 +1,17 @@ -import NextAuth, { AuthOptions } from 'next-auth'; -import CredentialsProvider from 'next-auth/providers/credentials'; -import { getServerSession } from 'next-auth/next'; import type { BackendAuthProvider } from '@tinacms/datalayer'; -import { TINA_CREDENTIALS_PROVIDER_NAME } from './tinacms'; +import NextAuthImport, { AuthOptions } from 'next-auth'; +import { getServerSession } from 'next-auth/next'; +import CredentialsImport from 'next-auth/providers/credentials'; +// Not from './tinacms': that entry pulls the browser `tinacms` package into the +// node build, and Node's ESM loader cannot resolve its transitive imports. +import { TINA_CREDENTIALS_PROVIDER_NAME } from './constants'; + +// next-auth v4 is CommonJS: webpack unwraps its `__esModule` default but Node's +// ESM loader and esbuild do not, breaking every non-Next.js backend. See #7434. +const interopDefault = (mod: T & { default?: T }): T => mod.default ?? mod; + +const NextAuth = interopDefault(NextAuthImport); +const CredentialsProvider = interopDefault(CredentialsImport); const authenticate = async ( databaseClient: any, diff --git a/packages/tinacms-authjs/src/tinacms.ts b/packages/tinacms-authjs/src/tinacms.ts index 7fa402e079..60567663d9 100644 --- a/packages/tinacms-authjs/src/tinacms.ts +++ b/packages/tinacms-authjs/src/tinacms.ts @@ -8,8 +8,9 @@ import { } from 'next-auth/react'; import { AbstractAuthProvider } from 'tinacms'; import type { FC } from 'react'; +import { TINA_CREDENTIALS_PROVIDER_NAME } from './constants'; -export const TINA_CREDENTIALS_PROVIDER_NAME = 'TinaCredentials'; +export { TINA_CREDENTIALS_PROVIDER_NAME }; export class DefaultAuthJSProvider extends AbstractAuthProvider { readonly callbackUrl: string; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e8a9386af9..4edd8bbd7b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -529,8 +529,8 @@ catalogs: specifier: 2.2.6 version: 2.2.6 postcss: - specifier: ^8.5.16 - version: 8.5.16 + specifier: ^8.5.23 + version: 8.5.26 postcss-import: specifier: ^14.1.0 version: 14.1.0 @@ -919,7 +919,7 @@ importers: version: link:../../../packages/@tinacms/scripts autoprefixer: specifier: 'catalog:' - version: 10.4.23(postcss@8.5.16) + version: 10.4.23(postcss@8.5.26) cross-env: specifier: ^7.0.3 version: 7.0.3 @@ -928,10 +928,10 @@ importers: version: 0.158.0 postcss: specifier: 'catalog:' - version: 8.5.16 + version: 8.5.26 postcss-cli: specifier: ^11.0.0 - version: 11.0.1(jiti@2.6.1)(postcss@8.5.16) + version: 11.0.1(jiti@2.6.1)(postcss@8.5.26) tailwindcss: specifier: ^4.0.0 version: 4.2.1 @@ -998,13 +998,13 @@ importers: version: 25.1.0 autoprefixer: specifier: 'catalog:' - version: 10.4.23(postcss@8.5.16) + version: 10.4.23(postcss@8.5.26) cross-env: specifier: ^7.0.3 version: 7.0.3 postcss: specifier: 'catalog:' - version: 8.5.16 + version: 8.5.26 tailwindcss: specifier: ^4.0.0 version: 4.1.18 @@ -1083,7 +1083,7 @@ importers: version: 5.62.0(eslint@8.57.1)(typescript@5.9.3) autoprefixer: specifier: 'catalog:' - version: 10.4.23(postcss@8.5.16) + version: 10.4.23(postcss@8.5.26) cross-env: specifier: ^7.0.3 version: 7.0.3 @@ -1092,13 +1092,13 @@ importers: version: 8.57.1 postcss: specifier: 'catalog:' - version: 8.5.16 + version: 8.5.26 postcss-import: specifier: 'catalog:' - version: 14.1.0(postcss@8.5.16) + version: 14.1.0(postcss@8.5.26) postcss-nesting: specifier: 'catalog:' - version: 10.2.0(postcss@8.5.16) + version: 10.2.0(postcss@8.5.26) tailwindcss: specifier: ^3.4.17 version: 3.4.19(yaml@2.8.2) @@ -1171,13 +1171,13 @@ importers: version: 4.7.0(vite@6.4.1(@types/node@25.1.0)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)) autoprefixer: specifier: 'catalog:' - version: 10.4.23(postcss@8.5.16) + version: 10.4.23(postcss@8.5.26) cross-env: specifier: ^7.0.3 version: 7.0.3 postcss: specifier: 'catalog:' - version: 8.5.16 + version: 8.5.26 tailwindcss: specifier: ^4.0.0 version: 4.2.1 @@ -13258,6 +13258,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + nanoid@5.1.6: resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==} engines: {node: ^18 || >=20} @@ -13867,6 +13872,10 @@ packages: resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -23277,7 +23286,7 @@ snapshots: '@alloc/quick-lru': 5.2.0 '@tailwindcss/node': 4.2.1 '@tailwindcss/oxide': 4.2.1 - postcss: 8.5.6 + postcss: 8.5.16 tailwindcss: 4.2.1 '@tailwindcss/typography@0.5.20(tailwindcss@3.4.19(yaml@2.8.2))': @@ -25371,13 +25380,13 @@ snapshots: auto-bind@5.0.1: {} - autoprefixer@10.4.23(postcss@8.5.16): + autoprefixer@10.4.23(postcss@8.5.26): dependencies: browserslist: 4.28.1 caniuse-lite: 1.0.30001760 fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.5.16 + postcss: 8.5.26 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -31043,6 +31052,8 @@ snapshots: nanoid@3.3.15: {} + nanoid@3.3.18: {} + nanoid@5.1.6: {} napi-build-utils@2.0.0: {} @@ -31647,15 +31658,15 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-cli@11.0.1(jiti@2.6.1)(postcss@8.5.16): + postcss-cli@11.0.1(jiti@2.6.1)(postcss@8.5.26): dependencies: chokidar: 3.6.0 dependency-graph: 1.0.0 fs-extra: 11.3.2 picocolors: 1.1.1 - postcss: 8.5.16 - postcss-load-config: 5.1.0(jiti@2.6.1)(postcss@8.5.16) - postcss-reporter: 7.1.0(postcss@8.5.16) + postcss: 8.5.26 + postcss-load-config: 5.1.0(jiti@2.6.1)(postcss@8.5.26) + postcss-reporter: 7.1.0(postcss@8.5.26) pretty-hrtime: 1.0.3 read-cache: 1.0.0 slash: 5.1.0 @@ -31665,39 +31676,39 @@ snapshots: - jiti - tsx - postcss-import@14.1.0(postcss@8.5.16): + postcss-import@14.1.0(postcss@8.5.26): dependencies: - postcss: 8.5.16 + postcss: 8.5.26 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.11 - postcss-import@15.1.0(postcss@8.5.6): + postcss-import@15.1.0(postcss@8.5.16): dependencies: - postcss: 8.5.6 + postcss: 8.5.16 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.11 - postcss-js@4.1.0(postcss@8.5.6): + postcss-js@4.1.0(postcss@8.5.16): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.6 + postcss: 8.5.16 - postcss-load-config@5.1.0(jiti@2.6.1)(postcss@8.5.16): + postcss-load-config@5.1.0(jiti@2.6.1)(postcss@8.5.26): dependencies: lilconfig: 3.1.3 yaml: 2.8.2 optionalDependencies: jiti: 2.6.1 - postcss: 8.5.16 + postcss: 8.5.26 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.8.2): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.16)(yaml@2.8.2): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 1.21.7 - postcss: 8.5.6 + postcss: 8.5.16 yaml: 2.8.2 postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(yaml@2.8.2): @@ -31708,21 +31719,21 @@ snapshots: postcss: 8.5.6 yaml: 2.8.2 - postcss-nested@6.2.0(postcss@8.5.6): + postcss-nested@6.2.0(postcss@8.5.16): dependencies: - postcss: 8.5.6 + postcss: 8.5.16 postcss-selector-parser: 6.1.2 - postcss-nesting@10.2.0(postcss@8.5.16): + postcss-nesting@10.2.0(postcss@8.5.26): dependencies: '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.2) - postcss: 8.5.16 + postcss: 8.5.26 postcss-selector-parser: 6.1.2 - postcss-reporter@7.1.0(postcss@8.5.16): + postcss-reporter@7.1.0(postcss@8.5.26): dependencies: picocolors: 1.1.1 - postcss: 8.5.16 + postcss: 8.5.26 thenby: 1.3.4 postcss-selector-parser@6.0.10: @@ -31739,7 +31750,7 @@ snapshots: postcss@8.4.31: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.15 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -31749,6 +31760,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -33466,11 +33483,11 @@ snapshots: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.6 - postcss-import: 15.1.0(postcss@8.5.6) - postcss-js: 4.1.0(postcss@8.5.6) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.8.2) - postcss-nested: 6.2.0(postcss@8.5.6) + postcss: 8.5.16 + postcss-import: 15.1.0(postcss@8.5.16) + postcss-js: 4.1.0(postcss@8.5.16) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.16)(yaml@2.8.2) + postcss-nested: 6.2.0(postcss@8.5.16) postcss-selector-parser: 6.1.2 resolve: 1.22.11 sucrase: 3.35.1 @@ -34523,7 +34540,7 @@ snapshots: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - postcss: 8.5.6 + postcss: 8.5.16 rollup: 4.53.4 tinyglobby: 0.2.15 optionalDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1a08d7eaf8..97f05f00ae 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -217,7 +217,7 @@ catalog: parse-entities: 4.0.1 picomatch: ^4.0.2 picomatch-browser: 2.2.6 - postcss: ^8.5.16 + postcss: ^8.5.23 postcss-import: ^14.1.0 postcss-nesting: ^10.2.0 prettier: ^2.8.8 diff --git a/tests/authjs-node-entry.test.ts b/tests/authjs-node-entry.test.ts new file mode 100644 index 0000000000..30d9a0e89e --- /dev/null +++ b/tests/authjs-node-entry.test.ts @@ -0,0 +1,97 @@ +import { execFileSync } from 'node:child_process'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const ROOT = path.resolve(__dirname, '..'); +const PKG = path.join(ROOT, 'packages/tinacms-authjs'); +const NODE_ENTRY = path.join(PKG, 'dist/index.js'); + +// Runs against the build, and shells out to a real node process, because Vite +// resolves what Node's ESM loader will not and would mask both regressions (#7434). +describe('tinacms-authjs node entry', () => { + it('imports only next-auth', () => { + const built = fs.readFileSync(NODE_ENTRY, 'utf-8'); + const specifiers = [ + ...built.matchAll(/^import\s[^'"]*['"]([^'"]+)['"]/gm), + ].map((m) => m[1]); + + expect(specifiers.length).toBeGreaterThan(0); + expect( + specifiers.filter((s) => s !== 'next-auth' && !s.startsWith('next-auth/')) + ).toEqual([]); + }); + + it('builds a credentials provider under plain node ESM', () => { + const script = ` + const { TinaCredentialsProvider } = await import(${JSON.stringify( + NODE_ENTRY + )}) + process.stdout.write(TinaCredentialsProvider({ databaseClient: {} }).type) + `; + + const type = execFileSync( + process.execPath, + ['--input-type=module', '-e', script], + { + encoding: 'utf-8', + } + ); + + expect(type).toBe('credentials'); + }); + + // The NextAuth binding is unwrapped separately from the credentials one and is + // only reached inside the auth route, so it needs its own call to guard it. + it('serves an auth route under plain node ESM', () => { + const script = ` + const { TinaAuthJSOptions, AuthJsBackendAuthProvider } = await import(${JSON.stringify( + NODE_ENTRY + )}) + const databaseClient = { authenticate: async () => ({ data: {} }) } + const authOptions = TinaAuthJSOptions({ databaseClient, secret: 'test-secret' }) + const { extraRoutes } = AuthJsBackendAuthProvider({ authOptions }) + + const req = { + url: '/api/tina/auth/session', + method: 'GET', + headers: { host: 'localhost' }, + query: {}, + cookies: {}, + body: {}, + } + let body + const res = { + statusCode: 200, + setHeader: () => res, + getHeader: () => undefined, + appendHeader: () => res, + status: (code) => { res.statusCode = code; return res }, + json: (value) => { body = value; return res }, + send: (value) => { body = value; return res }, + end: (value) => { body = body ?? value; return res }, + redirect: () => res, + } + + await extraRoutes.auth.handler(req, res, { basePath: '/api/tina/' }) + process.stdout.write(JSON.stringify({ status: res.statusCode, body })) + `; + + const out = execFileSync( + process.execPath, + ['--input-type=module', '-e', script], + { + encoding: 'utf-8', + env: { + ...process.env, + NEXTAUTH_URL: 'http://localhost/api/tina/auth', + NEXTAUTH_SECRET: 'test-secret', + }, + } + ); + + expect(JSON.parse(out)).toEqual({ status: 200, body: {} }); + }); +});