Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): add aliases to mitigate context issues #7172

Merged
merged 3 commits into from
Jul 19, 2024
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
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', () => {
ricokahler marked this conversation as resolved.
Show resolved Hide resolved
// 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
Loading