Skip to content

Commit

Permalink
feat(core): add aliases to mitigate context issues
Browse files Browse the repository at this point in the history
  • Loading branch information
ricokahler committed Jul 16, 2024
1 parent 9dbb9f6 commit 8c40b74
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 25 deletions.
1 change: 1 addition & 0 deletions packages/sanity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@
"read-pkg-up": "^7.0.1",
"refractor": "^3.6.0",
"resolve-from": "^5.0.0",
"resolve.exports": "^2.0.2",
"rimraf": "^3.0.2",
"rxjs": "^7.8.0",
"rxjs-exhaustmap-with-trailing": "^2.1.1",
Expand Down
92 changes: 92 additions & 0 deletions packages/sanity/src/_internal/cli/server/__tests__/aliases.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import path from 'node:path'

import {describe, expect, it, jest} from '@jest/globals'
import resolve from 'resolve.exports'

import {browserCompatibleSanityPackageSpecifiers, getAliases} from '../aliases'

const sanityPkgPath = path.resolve(__dirname, '../../../../../package.json')
// eslint-disable-next-line import/no-dynamic-require
const pkg = require(sanityPkgPath)

describe('browserCompatibleSanityPackageSpecifiers', () => {
it('should have all specifiers listed in the package.json', () => {
const currentSpecifiers = Object.keys(pkg.exports)
.map((subpath) => path.join('sanity', subpath))
.sort()

// NOTE: this test is designed to fail if there are any changes to the
// package exports in the sanity package.json so you can stop and consider if that
// new subpath should also go into `browserCompatibleSanityPackageSpecifiers`.
// If there are changes, you may need to update this variable. New subpaths
// should go into `browserCompatibleSanityPackageSpecifiers` if that subpath
// is meant to be imported in the browser (e.g. a new subpath that is only meant
// for the CLI doesn't need to go into `browserCompatibleSanityPackageSpecifiers`).
expect(currentSpecifiers).toEqual([
'sanity',
'sanity/_internal',
'sanity/_singletons',
'sanity/cli',
'sanity/desk',
'sanity/migrate',
'sanity/package.json',
'sanity/presentation',
'sanity/router',
'sanity/structure',
])

expect(browserCompatibleSanityPackageSpecifiers).toHaveLength(7)

for (const specifier of browserCompatibleSanityPackageSpecifiers) {
expect(currentSpecifiers).toContain(specifier)
}
})
})

describe('getAliases', () => {
it('returns the correct aliases for normal builds', () => {
const aliases = getAliases({
sanityPkgPath,
conditions: ['import', 'browser'],
browser: true,
})

const expectedAliases = browserCompatibleSanityPackageSpecifiers.reduce<Record<string, string>>(
(acc, specifier) => {
const dest = resolve.exports(pkg, specifier, {
browser: true,
conditions: ['import', 'browser'],
})?.[0]
if (dest) {
acc[specifier] = path.resolve(path.dirname(sanityPkgPath), dest)
}
return acc
},
{},
)

expect(aliases).toMatchObject(expectedAliases)
})

it('returns the correct aliases for the monorepo', () => {
const monorepoPath = path.resolve(__dirname, '../../../../../monorepo')
const devAliases = {
'sanity/_singletons': 'packages/sanity/src/_singletons.ts',
'sanity/desk': 'packages/sanity/src/desk.ts',
'sanity/presentation': 'packages/sanity/src/presentation.ts',
}
jest.doMock(path.resolve(monorepoPath, 'dev/aliases.cjs'), () => devAliases, {virtual: true})

const aliases = getAliases({
monorepo: {path: monorepoPath},
})

const expectedAliases = Object.fromEntries(
Object.entries(devAliases).map(([key, modulePath]) => {
return [key, path.resolve(monorepoPath, modulePath)]
}),
)

expect(aliases).toMatchObject(expectedAliases)
})
})
105 changes: 88 additions & 17 deletions packages/sanity/src/_internal/cli/server/aliases.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,103 @@
import path from 'node:path'

import resolve from 'resolve.exports'
import {type AliasOptions} from 'vite'

import {type SanityMonorepo} from './sanityMonorepo'

/**
* Returns an object of aliases for vite to use
* @internal
*/
export interface GetAliasesOptions {
/** An optional monorepo configuration object. */
monorepo?: SanityMonorepo
/** The path to the sanity package.json file. */
sanityPkgPath?: string
/** The list of conditions to resolve package exports. */
conditions?: string[]
}

/**
* The following are the specifiers that are expected/allowed to be used within
* a built Sanity studio in the browser. These are used in combination with
* `resolve.exports` to determine the final entry point locations for each allowed specifier.
*
* There is also a corresponding test for this file that expects these to be
* included in the `sanity` package.json. That test is meant to keep this list
* in sync in the event we add another package subpath.
*
* @internal
*/
export const browserCompatibleSanityPackageSpecifiers = [
'sanity',
'sanity/_singletons',
'sanity/desk',
'sanity/presentation',
'sanity/router',
'sanity/structure',
'sanity/package.json',
]

/**
* Returns an object of aliases for Vite to use.
*
* This function is used within our build tooling to prevent multiple context errors
* due to multiple instances of our library. It resolves the appropriate paths for
* modules based on whether the current project is inside the Sanity monorepo or not.
*
* If the project is within the monorepo, it uses the source files directly for a better
* development experience. Otherwise, it uses the `sanityPkgPath` and `conditions` to locate
* the entry points for each subpath the Sanity module exports.
*
* @internal
*/
export function getAliases(opts: {monorepo?: SanityMonorepo}): Record<string, string> {
const {monorepo} = opts
export function getAliases({monorepo, sanityPkgPath, conditions}: GetAliasesOptions): AliasOptions {
// If the current Studio is located within the Sanity monorepo
if (monorepo?.path) {
// Load monorepo aliases. This ensures that the Vite server uses the source files
// instead of the compiled output, allowing for a better development experience.
const aliasesPath = path.resolve(monorepo.path, 'dev/aliases.cjs')

if (!monorepo?.path) {
return {}
// Import the development aliases configuration
// eslint-disable-next-line import/no-dynamic-require
const devAliases: Record<string, string> = require(aliasesPath)

// Resolve each alias path relative to the monorepo path
const monorepoAliases = Object.fromEntries(
Object.entries(devAliases).map(([key, modulePath]) => {
return [key, path.resolve(monorepo.path, modulePath)]
}),
)

// Return the aliases configuration for monorepo
return monorepoAliases
}

// Load monorepo aliases (if the current Studio is located within the sanity monorepo)
// This is done in order for the Vite server to use the source files instead of
// the compiled output, allowing for a better dev experience.
const aliasesPath = path.resolve(monorepo.path, 'dev/aliases.cjs')
// If not in the monorepo, use the `sanityPkgPath` and `conditions`
// to locate the entry points for each subpath the Sanity module exports
if (sanityPkgPath && conditions) {
// Load the package.json of the Sanity package
// eslint-disable-next-line import/no-dynamic-require
const pkg = require(sanityPkgPath)
const dirname = path.dirname(sanityPkgPath)

// eslint-disable-next-line import/no-dynamic-require
const devAliases: Record<string, string> = require(aliasesPath)
// Resolve the entry points for each allowed specifier
const unifiedSanityAliases = browserCompatibleSanityPackageSpecifiers.reduce<
Record<string, string>
>((acc, next) => {
// Resolve the export path for the specifier using resolve.exports
const dest = resolve.exports(pkg, next, {browser: true, conditions})?.[0]
if (!dest) return acc

const monorepoAliases = Object.fromEntries(
Object.entries(devAliases).map(([key, modulePath]) => {
return [key, path.resolve(monorepo.path, modulePath)]
}),
)
// Map the specifier to its resolved path
acc[next] = path.resolve(dirname, dest)
return acc
}, {})

// Return the aliases configuration for external projects
return unifiedSanityAliases
}

return monorepoAliases
// Return an empty aliases configuration if no conditions are met
return {}
}
11 changes: 10 additions & 1 deletion packages/sanity/src/_internal/cli/server/getViteConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ export async function getViteConfig(options: ViteOptions): Promise<InlineConfig>
const defaultFaviconsPath = path.join(path.dirname(sanityPkgPath), 'static', 'favicons')
const staticPath = `${basePath}static`

const conditions = [
'import',
'browser',
// the `es2015` condition is primarily rxjs
// https://github.com/ReactiveX/rxjs/blob/4a2d0d29a7b17607e74afcb6fb8037fe58ef9021/package.json#L22
'es2015',
]

const viteConfig: InlineConfig = {
// Define a custom cache directory so that sanity's vite cache
// does not conflict with any potential local vite projects
Expand Down Expand Up @@ -113,7 +121,8 @@ export async function getViteConfig(options: ViteOptions): Promise<InlineConfig>
envPrefix: 'SANITY_STUDIO_',
logLevel: mode === 'production' ? 'silent' : 'info',
resolve: {
alias: getAliases({monorepo}),
alias: getAliases({monorepo, conditions, sanityPkgPath}),
conditions,
},
define: {
// eslint-disable-next-line no-process-env
Expand Down
16 changes: 9 additions & 7 deletions pnpm-lock.yaml

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

0 comments on commit 8c40b74

Please sign in to comment.