Skip to content
Draft
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
2 changes: 1 addition & 1 deletion packages/@sanity/parse-package-json/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface PkgExport {
require?: string
}
types?: string
source: string
source?: string
development?: string
monorepo?: string
import?: string
Expand Down
1 change: 1 addition & 0 deletions packages/@sanity/pkg-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"lightningcss": "^1.31.1",
"mkdirp": "^3.0.1",
"outdent": "^0.8.0",
"package-manager-detector": "^1.6.0",
"prettier": "^3.8.1",
"pretty-bytes": "^7.1.0",
"prompts": "^2.4.2",
Expand Down
6 changes: 4 additions & 2 deletions packages/@sanity/pkg-utils/src/node/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {BuildFailure, Message} from 'esbuild'
import {createConsoleSpy} from './consoleSpy.ts'
import {loadConfig} from './core/config/loadConfig.ts'
import type {BuildContext} from './core/contexts/index.ts'
import {getSourcePath} from './core/exportUtils.ts'
import {loadPkgWithReporting} from './core/pkg/loadPkgWithReporting.ts'
import {fileExists} from './fileExists.ts'
import {createLogger, type Logger} from './logger.ts'
Expand Down Expand Up @@ -40,8 +41,9 @@ export async function check(options: {

// Check if there are missing files
for (const [, exp] of Object.entries(ctx.exports || {})) {
if (exp.source && !fileExists(path.resolve(cwd, exp.source))) {
missingFiles.push(exp.source)
const sourcePath = getSourcePath(exp)
if (sourcePath && !fileExists(path.resolve(cwd, sourcePath))) {
missingFiles.push(sourcePath)
}

if (exp.require && !fileExists(path.resolve(cwd, exp.require))) {
Expand Down
16 changes: 16 additions & 0 deletions packages/@sanity/pkg-utils/src/node/core/exportUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type {PkgExport} from './config/types.ts'

/**
* Gets the source path from an export entry.
* Returns the value of `source` if it exists, otherwise returns `monorepo`.
*/
export function getSourcePath(exp: PkgExport): string | undefined {
return exp.source || exp.monorepo
}

/**
* Checks if an export has a source or monorepo path
*/
export function hasSourcePath(exp: PkgExport): boolean {
return Boolean(exp.source || exp.monorepo)
}
105 changes: 105 additions & 0 deletions packages/@sanity/pkg-utils/src/node/core/pkg/detectPackageManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import {readFile} from 'node:fs/promises'
import {resolve} from 'node:path'
import {detect} from 'package-manager-detector/detect'

/**
* Result of package manager detection
*/
export interface PackageManagerInfo {
name: string
version?: string
}

/**
* Detects the package manager from the packageManager field in package.json
* Walks up the directory tree to find a package.json with the packageManager field
*/
async function detectPackageManagerFromField(cwd: string): Promise<PackageManagerInfo | null> {
let currentDir = cwd
const root = resolve('/')

while (currentDir !== root) {
try {
const pkgPath = resolve(currentDir, 'package.json')
const pkgContent = await readFile(pkgPath, 'utf-8')
const pkg = JSON.parse(pkgContent)

if (pkg.packageManager && typeof pkg.packageManager === 'string') {
// Parse packageManager field (format: "pnpm@9.0.0" or "npm@10.0.0")
const match = pkg.packageManager.match(/^([^@]+)@?(.*)$/)
if (match && match[1]) {
return {
name: match[1],
version: match[2] || undefined,
}
}
}
} catch {
// Package.json doesn't exist or can't be read, continue to parent
}

const parentDir = resolve(currentDir, '..')
if (parentDir === currentDir) break
currentDir = parentDir
}

return null
}

/**
* Detects the package manager used in the project
* Uses package-manager-detector to check for lock files and packageManager field
*/
export async function detectPackageManager(cwd: string): Promise<PackageManagerInfo | null> {
try {
// First try using package-manager-detector which checks lock files and packageManager field
const result = await detect({
cwd,
strategies: ['packageManager-field', 'lockfile'],
})

if (result) {
return {
name: result.agent,
version: result.version,
}
}

// If package-manager-detector doesn't find anything,
// walk up the tree looking for packageManager field
const fromField = await detectPackageManagerFromField(cwd)
if (fromField) {
return fromField
}

return null
} catch {
return null
}
}

/**
* Checks if the package manager is pnpm
*/
export async function isPnpm(cwd: string): Promise<boolean> {
const pm = await detectPackageManager(cwd)
return pm?.name === 'pnpm'
}

/**
* Validates that pnpm is being used as the package manager
* Returns an error message if pnpm is not being used, null otherwise
*/
export async function validatePnpmPackageManager(cwd: string): Promise<string | null> {
const pm = await detectPackageManager(cwd)

if (!pm) {
return 'Cannot determine package manager. The `monorepo` export condition requires `packageManager` field to be set to `pnpm` in package.json.'
}

if (pm.name !== 'pnpm') {
return `The \`monorepo\` export condition requires pnpm but found ${pm.name}. Set \`"packageManager": "pnpm@<version>"\` in package.json.`
}

return null
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {StrictOptions} from '../../strict.ts'
import {defaultEnding, fileEnding} from '../../tasks/dts/getTargetPaths.ts'
import type {PkgExport} from '../config/types.ts'
import {isRecord} from '../isRecord.ts'
import {validatePnpmPackageManager} from './detectPackageManager.ts'
import {pkgExtMap} from './pkgExt.ts'
import {validateExports} from './validateExports.ts'

Expand All @@ -15,13 +16,13 @@ function isTruthy<T>(value: T | false | null | undefined | 0 | ''): value is T {
}

/** @alpha */
export function parseAndValidateExports(options: {
export async function parseAndValidateExports(options: {
cwd: string
pkg: PackageJSON
strict: boolean
strictOptions: StrictOptions
logger: Logger
}): (PkgExport & {_path: string})[] {
}): Promise<(PkgExport & {_path: string})[]> {
const {cwd, pkg, strict, strictOptions, logger} = options
const type = pkg.type || 'commonjs'
const errors: string[] = []
Expand Down Expand Up @@ -139,8 +140,8 @@ export function parseAndValidateExports(options: {
strictOptions.alwaysPackageJsonTypes !== 'off' &&
!pkg.types &&
typeof pkg.exports?.['.'] === 'object' &&
'source' in pkg.exports['.'] &&
pkg.exports['.'].source?.endsWith('.ts')
(('source' in pkg.exports['.'] && pkg.exports['.'].source?.endsWith('.ts')) ||
('monorepo' in pkg.exports['.'] && pkg.exports['.'].monorepo?.endsWith('.ts')))
) {
report(
strictOptions.alwaysPackageJsonTypes,
Expand Down Expand Up @@ -218,6 +219,28 @@ export function parseAndValidateExports(options: {
}
}

// Validate monorepo condition requirements
const hasMonorepoCondition = Object.entries(pkg.exports).some(([, exp]) => {
if (typeof exp === 'string') return false
if (typeof exp === 'object' && 'svelte' in exp) return false
return Boolean(exp.monorepo)
})

if (hasMonorepoCondition) {
// Check that publishConfig.exports is defined
if (!pkg.publishConfig?.exports) {
errors.push(
'package.json: When using the `monorepo` export condition, `publishConfig.exports` must be defined to ensure the package is published without the monorepo condition.',
)
}

// Validate that pnpm is being used as the package manager
const pnpmError = await validatePnpmPackageManager(cwd)
if (pnpmError) {
errors.push(`package.json: ${pnpmError}`)
}
}

errors.push(...validateExports(_exports, {pkg}))

if (errors.length) {
Expand All @@ -228,5 +251,9 @@ export function parseAndValidateExports(options: {
}

function isPkgExport(value: unknown): value is PkgExport {
return isRecord(value) && 'source' in value && typeof value['source'] === 'string'
return (
isRecord(value) &&
(('source' in value && typeof value['source'] === 'string') ||
('monorepo' in value && typeof value['monorepo'] === 'string'))
)
}
5 changes: 4 additions & 1 deletion packages/@sanity/pkg-utils/src/node/printPackageTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import chalk from 'chalk'
import treeify from 'treeify'
import type {PkgExport} from './core/config/types.ts'
import type {BuildContext} from './core/contexts/buildContext.ts'
import {getSourcePath} from './core/exportUtils.ts'
import {fileExists} from './fileExists.ts'
import {getFilesize} from './getFilesize.ts'

Expand Down Expand Up @@ -47,8 +48,10 @@ export function printPackageTree(ctx: BuildContext): void {
Object.entries(exports)
.filter(([, entry]) => entry._exported)
.map(([exportPath, entry]) => {
const sourcePath = getSourcePath(entry)
const exp: Omit<PkgExport, '_exported'> = {
source: fileInfo(entry.source),
source: sourcePath ? fileInfo(sourcePath) : undefined,
monorepo: entry.monorepo ? fileInfo(entry.monorepo) : undefined,
browser: undefined,
require: undefined,
node: undefined,
Expand Down
16 changes: 9 additions & 7 deletions packages/@sanity/pkg-utils/src/node/resolveBuildContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,15 @@ export async function resolveBuildContext(options: {
'node': nodeTarget,
}

const parsedExports = parseAndValidateExports({
cwd,
pkg,
strict,
strictOptions,
logger,
}).reduce<PkgExports>(
const parsedExports = (
await parseAndValidateExports({
cwd,
pkg,
strict,
strictOptions,
logger,
})
).reduce<PkgExports>(
(acc, {_path: exportPath, ...exportEntry}) => Object.assign(acc, {[exportPath]: exportEntry}),
{},
)
Expand Down
16 changes: 10 additions & 6 deletions packages/@sanity/pkg-utils/src/node/resolveBuildTasks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import path from 'node:path'
import type {PkgExport, PkgFormat, PkgRuntime} from './core/config/types.ts'
import type {BuildContext} from './core/contexts/buildContext.ts'
import {getSourcePath} from './core/exportUtils.ts'
import {getTargetPaths} from './tasks/dts/getTargetPaths.ts'
import type {DtsTask} from './tasks/dts/types.ts'
import type {BuildTask, RolldownDtsTask, RollupTask, RollupTaskEntry} from './tasks/types.ts'
Expand Down Expand Up @@ -63,12 +64,13 @@ export function resolveBuildTasks(ctx: BuildContext): BuildTask[] {
// Parse `dts` tasks
for (const exp of exports) {
const importId = path.join(pkg.name, exp._path)
const sourcePath = getSourcePath(exp)

if (exp.source?.endsWith('.ts')) {
if (sourcePath?.endsWith('.ts')) {
dtsTask.entries.push({
importId,
exportPath: exp._path,
sourcePath: exp.source,
sourcePath,
targetPaths: getTargetPaths(pkg.type, exp),
})
}
Expand Down Expand Up @@ -119,25 +121,27 @@ export function resolveBuildTasks(ctx: BuildContext): BuildTask[] {
// Parse rollup:commonjs:* tasks
for (const exp of exports) {
const output = exp.require
const sourcePath = getSourcePath(exp)

if (!output) continue

addRollupTaskEntry('commonjs', ctx.runtime, {
path: exp._path,
source: exp.source,
source: sourcePath,
output,
})
}

// Parse rollup:esm:* tasks
for (const exp of exports) {
const output = exp.import
const sourcePath = getSourcePath(exp)

if (!output) continue

addRollupTaskEntry('esm', ctx.runtime, {
path: exp._path,
source: exp.source,
source: sourcePath,
output,
})
}
Expand All @@ -150,7 +154,7 @@ export function resolveBuildTasks(ctx: BuildContext): BuildTask[] {

addRollupTaskEntry('commonjs', 'browser', {
path: exp._path,
source: exp.browser?.source || exp.source,
source: exp.browser?.source || getSourcePath(exp),
output,
})
}
Expand All @@ -163,7 +167,7 @@ export function resolveBuildTasks(ctx: BuildContext): BuildTask[] {

addRollupTaskEntry('esm', 'browser', {
path: exp._path,
source: exp.browser?.source || exp.source,
source: exp.browser?.source || getSourcePath(exp),
output,
})
}
Expand Down
Loading