Skip to content

Commit

Permalink
feat(core): add aliases to mitigate context issues (#7172)
Browse files Browse the repository at this point in the history
* feat(core): add aliases to mitigate context issues

* fix(core): use `find` `replacement` syntax for aliases

* docs: add TODO comment about improving test
  • Loading branch information
ricokahler authored and cngonzalez committed Aug 20, 2024
1 parent 4283866 commit 3243e7b
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 54 deletions.
1 change: 1 addition & 0 deletions packages/sanity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,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
114 changes: 114 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,114 @@
import path from 'node:path'

import {describe, expect, it, jest} from '@jest/globals'
import {escapeRegExp} from 'lodash'
import resolve from 'resolve.exports'
import {type Alias} from 'vite'

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/_createContext',
'sanity/_internal',
'sanity/_singletons',
'sanity/cli',
'sanity/desk',
'sanity/migrate',
'sanity/package.json',
'sanity/presentation',
'sanity/router',
'sanity/structure',
])

expect(browserCompatibleSanityPackageSpecifiers).toHaveLength(8)

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

describe('getAliases', () => {
// TODO: this test would be better if it called `vite.build` with fixtures
// but vite does not seem to be compatible in our jest environment.
// Error from trying to import vite:
//
// > Invariant violation: "new TextEncoder().encode("") instanceof Uint8Array" is incorrectly false
// >
// > This indicates that your JavaScript environment is broken. You cannot use
// > esbuild in this environment because esbuild relies on this invariant. This
// > is not a problem with esbuild. You need to fix your environment instead.
it('returns the correct aliases for normal builds', () => {
const aliases = getAliases({
sanityPkgPath,
conditions: ['import', 'browser'],
})

// Prepare expected aliases
const dirname = path.dirname(sanityPkgPath)
const expectedAliases = browserCompatibleSanityPackageSpecifiers.reduce<Alias[]>(
(acc, next) => {
const dest = resolve.exports(pkg, next, {
browser: true,
conditions: ['import', 'browser'],
})?.[0]
if (dest) {
acc.push({
find: new RegExp(`^${escapeRegExp(next)}$`),
replacement: path.resolve(dirname, dest),
})
}
return acc
},
[],
)

expect(aliases).toEqual(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)
})

it('returns an empty object if no conditions are met', () => {
const aliases = getAliases({})

expect(aliases).toEqual({})
})
})
111 changes: 94 additions & 17 deletions packages/sanity/src/_internal/cli/server/aliases.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,109 @@
import path from 'node:path'

import {escapeRegExp} from 'lodash'
import resolve from 'resolve.exports'
import {type Alias, 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/_createContext',
'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<Alias[]>(
(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.push({
find: new RegExp(`^${escapeRegExp(next)}$`),
replacement: 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
Loading

0 comments on commit 3243e7b

Please sign in to comment.