Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import Module from 'node:module'

import {describe, expect, test} from 'vitest'

import {setupImportErrorHandler} from '../importErrorHandler'

interface ModuleConstructor {
_load(request: string, parent: Module | undefined, isMain: boolean): any
}

describe('setupImportErrorHandler', () => {
test('should handle themer.sanity.build URL imports', () => {
const handler = setupImportErrorHandler()

const ModuleConstructor = Module as unknown as ModuleConstructor

// Try to load a themer.sanity.build URL (which would normally fail)
const result = ModuleConstructor._load(
'https://themer.sanity.build/api/hues?default=0078ff',
undefined,
false,
)

expect(result).toBeDefined()
expect(result.__esModule).toBe(true)
expect(result.default).toBeDefined()
expect(result.someProperty.deepProperty).toBeDefined()

handler.cleanup()
})

test('should re-throw errors for non-themer URLs', () => {
const handler = setupImportErrorHandler()

const ModuleConstructor = Module as unknown as ModuleConstructor

expect(() => {
ModuleConstructor._load(
'https://example.com/this-module-definitely-does-not-exist-xyz',
undefined,
false,
)
}).toThrow()

handler.cleanup()
})

test('should restore original Module._load after cleanup', () => {
const ModuleConstructor = Module as unknown as ModuleConstructor
const originalLoad = ModuleConstructor._load

const handler = setupImportErrorHandler()

expect(ModuleConstructor._load).not.toBe(originalLoad)
handler.cleanup()
expect(ModuleConstructor._load).toBe(originalLoad)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {Worker} from 'node:worker_threads'

import {describe, expect, test} from 'vitest'

import {type ThemerImportWorkerData} from './themerImportWorker'

function getImportWorker(importName: string) {
const workerData: ThemerImportWorkerData = {
workDir: process.cwd(),
importName,
}

const filepath = new URL('./themerImportWorker.ts', import.meta.url).pathname

const worker = new Worker(
`
const { register } = require('esbuild-register/dist/node')
const { unregister } = register({
target: 'node18',
format: 'cjs',
extensions: ['.ts'],
})
require(${JSON.stringify(filepath)})
`,
{eval: true, workerData},
)

return new Promise<{success: boolean; error?: string}>((resolve, reject) => {
worker.on('message', resolve)
worker.on('error', reject)
worker.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`Worker stopped with exit code ${code}`))
}
})
})
}

describe('mockBrowserEnvironment', () => {
test('should handle themer.sanity.build imports in worker thread', async () => {
await expect(getImportWorker('https://themer.sanity.build/api/hues')).resolves.toEqual({
success: true,
})
})

test('should still error on non-themer.sanity.build imports in worker thread', async () => {
await expect(getImportWorker('https://foobar.official/package')).resolves.toEqual({
success: false,
error: expect.stringContaining("Cannot find module 'https://foobar.official/package'"),
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {isMainThread, parentPort, workerData as _workerData} from 'node:worker_threads'

import {mockBrowserEnvironment} from '../mockBrowserEnvironment'

export type ThemerImportWorkerData = {
workDir: string
importName: string
}

const {workDir, importName} = _workerData satisfies ThemerImportWorkerData

async function main() {
if (isMainThread || !parentPort) {
throw new Error('This module must be run as a worker thread')
}

const cleanup = mockBrowserEnvironment(workDir)

try {
// eslint-disable-next-line import/no-dynamic-require
require(importName)

// If we get here, the import was handled successfully
parentPort?.postMessage({success: true})
} catch (error) {
// If we catch an error, the import error handler didn't work
parentPort?.postMessage({
success: false,
error: error instanceof Error ? error.message : String(error),
})
} finally {
cleanup()
}
}

void main().then(() => process.exit())
65 changes: 65 additions & 0 deletions packages/sanity/src/_internal/cli/util/importErrorHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import Module from 'node:module'

export interface ImportErrorHandlerResult {
cleanup: () => void
}

// Module._load is an internal Node.js API not exposed in types
interface ModuleConstructor {
_load(request: string, parent: Module | undefined, isMain: boolean): any
}

/**
* Return safe empty module with Proxy for deep property access. This ensures any property
* access or function call returns a safe value
*/
function getProxyHandler() {
const handler: ProxyHandler<object> = {
get: (_target, prop) => {
if (prop === '__esModule') return true
if (prop === 'default') return new Proxy({}, handler)
return new Proxy({}, handler)
},
apply: () => new Proxy({}, handler),
}
return new Proxy({}, handler)
}

/**
* Sets up a Module._load wrapper to silently ignore imports from https://themer.sanity.build
* This allows users to use themer URL imports in their config without breaking CLI commands.
*
* @returns Handler result with cleanup function
* @internal
*/
export function setupImportErrorHandler(): ImportErrorHandlerResult {
// Store original Module._load
const ModuleConstructor = Module as unknown as ModuleConstructor
const originalLoad = ModuleConstructor._load

// Override Module._load to catch and handle themer.sanity.build imports
ModuleConstructor._load = function (
request: string,
parent: Module | undefined,
isMain: boolean,
) {
try {
return originalLoad.call(this, request, parent, isMain)
} catch (error) {
// Check if this is a themer.sanity.build URL import
if (request.startsWith('https://themer.sanity.build/api/')) {
// Return a safe proxy object that can be used in place of the theme
return getProxyHandler()
}

// Re-throw all other errors
throw error
}
}

return {
cleanup: () => {
ModuleConstructor._load = originalLoad
},
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {addHook} from 'pirates'
import resolveFrom from 'resolve-from'

import {getStudioEnvironmentVariables} from '../server/getStudioEnvironmentVariables'
import {setupImportErrorHandler} from './importErrorHandler'

const require = createRequire(import.meta.url)

Expand All @@ -24,6 +25,9 @@ export function mockBrowserEnvironment(basePath: string): () => void {
}
}

// Set up import error handler before esbuild-register to silently ignore themer.sanity.build URLs
const importErrorHandler = setupImportErrorHandler()

const btoa = global.btoa
const domCleanup = jsdomGlobal(jsdomDefaultHtml, {url: 'http://localhost:3333/'})

Expand Down Expand Up @@ -60,6 +64,7 @@ export function mockBrowserEnvironment(basePath: string): () => void {
globalCleanup()
windowCleanup()
domCleanup()
importErrorHandler.cleanup()
}
}

Expand Down
Loading