@sanity/pkg-utils 6.1.0 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/pkg-utils",
3
- "version": "6.1.0",
3
+ "version": "6.2.0",
4
4
  "description": "Simple utilities for modern npm packages.",
5
5
  "keywords": [
6
6
  "sanity-io",
@@ -34,7 +34,6 @@
34
34
  },
35
35
  "main": "./dist/index.cjs",
36
36
  "module": "./dist/index.js",
37
- "source": "./src/node/index.ts",
38
37
  "types": "./dist/index.d.ts",
39
38
  "bin": {
40
39
  "pkg": "./bin/pkg-utils.cjs",
@@ -44,7 +43,8 @@
44
43
  "bin",
45
44
  "dist",
46
45
  "!dist/stats.html",
47
- "src"
46
+ "src",
47
+ "!src/**/*.test.ts"
48
48
  ],
49
49
  "commitlint": {
50
50
  "extends": [
@@ -135,7 +135,7 @@
135
135
  "typescript": "5.4.4",
136
136
  "vitest": "^1.4.0",
137
137
  "vitest-github-actions-reporter": "^0.11.1",
138
- "@sanity/pkg-utils": "6.1.0"
138
+ "@sanity/pkg-utils": "6.2.0"
139
139
  },
140
140
  "peerDependencies": {
141
141
  "typescript": "5.4.x"
@@ -159,9 +159,9 @@
159
159
  "lint": "eslint . --ext .cjs,.js,.ts,.tsx",
160
160
  "playground:build": "pnpm --filter './playground/**' build",
161
161
  "playground:clean": "pnpm --filter './playground/**' --silent clean",
162
- "test": "run-s playground:clean test:clean vitest:run",
163
- "test:clean": "rimraf test/env/__tmp__",
164
- "vitest:run": "vitest run",
162
+ "pretest": "run-s playground:clean",
163
+ "test": "vitest",
164
+ "test:update-snapshots": "pnpm test run -- --update",
165
165
  "watch": "node -r esbuild-register scripts/watch"
166
166
  }
167
167
  }
@@ -1,6 +1,7 @@
1
- import {legacyEnding} from '../../tasks/dts/getTargetPaths'
1
+ import {defaultEnding, fileEnding, legacyEnding} from '../../tasks/dts/getTargetPaths'
2
2
  import type {PkgExport} from '../config'
3
3
  import {isRecord} from '../isRecord'
4
+ import {pkgExtMap} from './pkgExt'
4
5
  import type {PackageJSON} from './types'
5
6
  import {validateExports} from './validateExports'
6
7
 
@@ -11,8 +12,109 @@ export function parseExports(options: {
11
12
  legacyExports: boolean
12
13
  }): (PkgExport & {_path: string})[] {
13
14
  const {pkg, strict, legacyExports} = options
15
+ const type = pkg.type || 'commonjs'
16
+ const errors: string[] = []
17
+
18
+ if (pkg.source) {
19
+ if (
20
+ strict &&
21
+ pkg.exports?.['.'] &&
22
+ typeof pkg.exports['.'] === 'object' &&
23
+ 'source' in pkg.exports['.'] &&
24
+ pkg.exports['.'].source === pkg.source
25
+ ) {
26
+ errors.push(
27
+ 'package.json: the "source" property can be removed, as it is equal to exports["."].source.',
28
+ )
29
+ } else if (!pkg.exports && pkg.main) {
30
+ const extMap = pkgExtMap[type]
31
+ const importExport = pkg.main.replace(fileEnding, extMap.esm)
32
+ const requireExport = pkg.main.replace(fileEnding, extMap.commonjs)
33
+ const defaultExport = pkg.main.replace(fileEnding, defaultEnding)
34
+
35
+ const maybeBrowserCondition = []
36
+
37
+ if (pkg.browser) {
38
+ const browserConditions = []
39
+
40
+ if (pkg.module && pkg.browser?.[pkg.module]) {
41
+ browserConditions.push(
42
+ ` "import": ${JSON.stringify(pkg.browser[pkg.module].replace(fileEnding, extMap.esm))}`,
43
+ )
44
+ } else if (pkg.browser?.[pkg.main]) {
45
+ browserConditions.push(
46
+ ` "import": ${JSON.stringify(pkg.browser?.[pkg.main].replace(fileEnding, extMap.esm))}`,
47
+ )
48
+ } else if (legacyExports) {
49
+ const browserImport = pkg.main.replace(fileEnding, `.browser${extMap.esm}`)
50
+
51
+ browserConditions.push(` "import": ${JSON.stringify(browserImport)}`)
52
+ }
53
+
54
+ if (pkg.browser?.[pkg.main]) {
55
+ browserConditions.push(
56
+ ` "require": ${JSON.stringify(pkg.browser[pkg.main].replace(fileEnding, extMap.commonjs))}`,
57
+ )
58
+ } else if (legacyExports) {
59
+ const browserRequire = pkg.main.replace(fileEnding, `.browser${extMap.commonjs}`)
60
+
61
+ browserConditions.push(` "require": ${JSON.stringify(browserRequire)}`)
62
+ }
63
+
64
+ if (browserConditions.length) {
65
+ maybeBrowserCondition.push(
66
+ ` "browser": {`,
67
+ ` "source": ${JSON.stringify(pkg.browser?.[pkg.source] || pkg.source)},`,
68
+ ...browserConditions,
69
+ ` }`,
70
+ )
71
+ }
72
+ }
14
73
 
15
- const rootExport: PkgExport & {_path: string} = {
74
+ errors.push(
75
+ ...([
76
+ 'package.json: `exports` are missing, it should be:',
77
+ `"exports": {`,
78
+ ` ".": {`,
79
+ ` "source": ${JSON.stringify(pkg.source)},`,
80
+ // If browser conditions are detected then add them to the suggestion
81
+ ...(maybeBrowserCondition.length > 0 ? maybeBrowserCondition : []),
82
+ // If legacy exports are enabled we suggest the full list of exports, if not we can use the terse version
83
+ (legacyExports || type === 'commonjs') &&
84
+ ` "import": ${JSON.stringify(importExport)},`,
85
+ (legacyExports || type === 'module') &&
86
+ ` "require": ${JSON.stringify(requireExport)},`,
87
+ ` "default": ${JSON.stringify(defaultExport)}`,
88
+ ` },`,
89
+ ` "./package.json": "./package.json"`,
90
+ `}`,
91
+ ].filter(Boolean) as string[]),
92
+ )
93
+ }
94
+ }
95
+
96
+ if (errors.length) {
97
+ throw new Error('\n- ' + errors.join('\n- '))
98
+ }
99
+
100
+ if (!pkg.exports) {
101
+ throw new Error(
102
+ '\n- ' +
103
+ [
104
+ 'package.json: `exports` are missing, please set a minimal configuration, for example:',
105
+ `"exports": {`,
106
+ ` ".": {`,
107
+ ` "source": "./src/index.js",`,
108
+ ` "default": "./dist/index.js"`,
109
+ ` },`,
110
+ ` "./package.json": "./package.json"`,
111
+ `}`,
112
+ ].join('\n- '),
113
+ )
114
+ }
115
+
116
+ /*
117
+ const rootExports: PkgExport & {_path: string} = {
16
118
  _exported: true,
17
119
  _path: '.',
18
120
  source: pkg.source || '',
@@ -37,10 +139,9 @@ export function parseExports(options: {
37
139
  ? pkg.exports['.'].default || ''
38
140
  : pkg.module || pkg.main || '',
39
141
  }
142
+ // */
40
143
 
41
- const extraExports: (PkgExport & {_path: string})[] = []
42
-
43
- const errors: string[] = []
144
+ const _exports: (PkgExport & {_path: string})[] = []
44
145
 
45
146
  // @TODO validate typesVersions when legacyExports is true
46
147
 
@@ -48,88 +149,96 @@ export function parseExports(options: {
48
149
  errors.push('package.json: `typings` should be `types`')
49
150
  }
50
151
 
51
- if (pkg.exports) {
52
- if (strict && !pkg.types && pkg.source?.endsWith('.ts')) {
53
- errors.push(
54
- 'package.json: `types` must be declared for the npm listing to show as a TypeScript module.',
55
- )
56
- }
152
+ if (strict && !pkg.types && pkg.source?.endsWith('.ts')) {
153
+ errors.push(
154
+ 'package.json: `types` must be declared for the npm listing to show as a TypeScript module.',
155
+ )
156
+ }
57
157
 
58
- if (strict && !pkg.exports['./package.json']) {
59
- errors.push('package.json: `exports["./package.json"] must be declared.')
60
- }
158
+ if (strict && !pkg.exports['./package.json']) {
159
+ errors.push('package.json: `exports["./package.json"] must be declared.')
160
+ }
61
161
 
62
- for (const [exportPath, exportEntry] of Object.entries(pkg.exports)) {
63
- if (exportPath.endsWith('.json')) {
64
- if (exportPath === './package.json') {
65
- if (exportEntry !== './package.json') {
66
- errors.push('package.json: `exports["./package.json"] must be "./package.json".')
67
- }
162
+ for (const [exportPath, exportEntry] of Object.entries(pkg.exports)) {
163
+ if (exportPath.endsWith('.json')) {
164
+ if (exportPath === './package.json') {
165
+ if (exportEntry !== './package.json') {
166
+ errors.push('package.json: `exports["./package.json"] must be "./package.json".')
68
167
  }
69
- } else if (isRecord(exportEntry) && 'svelte' in exportEntry) {
70
- // @TODO should we report a warning or a debug message here about a detected svelte export that is ignored?
71
- } else if (isRecord(exportEntry)) {
72
- if (exportPath === '.') {
73
- if (
74
- exportEntry.require &&
75
- rootExport.require &&
76
- exportEntry.require !== rootExport.require
77
- ) {
168
+ }
169
+ } else if (isRecord(exportEntry) && 'svelte' in exportEntry) {
170
+ // @TODO should we report a warning or a debug message here about a detected svelte export that is ignored?
171
+ } else if (isRecord(exportEntry)) {
172
+ const exp = {
173
+ _exported: true,
174
+ _path: exportPath,
175
+ ...exportEntry,
176
+ } satisfies PkgExport & {_path: string}
177
+
178
+ // Infer the `default` condition based on the `type` and other conditions
179
+ if (!exp.default) {
180
+ const fallback = type === 'module' ? exp.import : exp.require
181
+
182
+ if (fallback) {
183
+ exp.default = fallback
184
+ }
185
+
186
+ if (legacyExports) {
187
+ if (fallback) {
78
188
  errors.push(
79
- 'package.json: mismatch between "main" and "exports.require". These must be equal.',
189
+ `package.json - \`exports["${exp._path}"].default\` should be set to "${fallback}" when "legacyExports" is true`,
80
190
  )
81
- }
82
-
83
- if (legacyExports) {
84
- const indexLegacyExport = (
85
- exportEntry.browser?.import ||
86
- exportEntry.import ||
87
- exportEntry.browser?.require ||
88
- exportEntry.require ||
89
- ''
90
- ).replace(/(\.esm)?\.[mc]?js$/, legacyEnding)
91
-
92
- if (indexLegacyExport !== rootExport.import) {
93
- errors.push(
94
- `package.json: "module" should be "${indexLegacyExport}" when "legacyExports" is true`,
95
- )
96
- }
97
191
  } else {
98
- if (
99
- exportEntry.import &&
100
- rootExport.import &&
101
- exportEntry.import !== rootExport.import
102
- ) {
103
- errors.push(
104
- 'package.json: mismatch between "module" and "exports.import" These must be equal.',
105
- )
106
- }
192
+ errors.push(
193
+ `package.json - \`exports["${exp._path}"].default\` should be specified when "legacyExports" is true`,
194
+ )
107
195
  }
196
+ }
197
+ }
108
198
 
109
- if (exportEntry.source && rootExport.source && exportEntry.source !== rootExport.source) {
199
+ // Infer the `require` condition based on the `type` and other conditions
200
+ if (!exp.require && type === 'commonjs' && exp.default) {
201
+ exp.require = exp.default
202
+ }
203
+
204
+ // Infer the `import` condition based on the `type` and other conditions
205
+ if (!exp.import && type === 'module' && exp.default) {
206
+ exp.import = exp.default
207
+ }
208
+
209
+ if (exportPath === '.') {
210
+ if (exportEntry.require && pkg.main && exportEntry.require !== pkg.main) {
211
+ errors.push(
212
+ 'package.json: mismatch between "main" and "exports.require". These must be equal.',
213
+ )
214
+ }
215
+
216
+ if (legacyExports) {
217
+ const indexLegacyExport = (exportEntry.import || exportEntry.require || '').replace(
218
+ /(\.esm)?\.[mc]?js$/,
219
+ legacyEnding,
220
+ )
221
+
222
+ if (indexLegacyExport !== pkg.module) {
110
223
  errors.push(
111
- 'package.json: mismatch between "source" and "exports.source". These must be equal.',
224
+ `package.json: "module" should be "${indexLegacyExport}" when "legacyExports" is true`,
112
225
  )
113
226
  }
114
-
115
- Object.assign(rootExport, exportEntry)
116
227
  } else {
117
- const extraExport: PkgExport & {_path: string} = {
118
- _exported: true,
119
- _path: exportPath,
120
- ...exportEntry,
228
+ if (exportEntry.import && pkg.module && exportEntry.import !== pkg.module) {
229
+ errors.push(
230
+ 'package.json: mismatch between "module" and "exports.import" These must be equal.',
231
+ )
121
232
  }
122
-
123
- extraExports.push(extraExport)
124
233
  }
125
- } else {
126
- errors.push('package.json: exports must be an object')
127
234
  }
235
+
236
+ _exports.push(exp)
237
+ } else {
238
+ errors.push('package.json: exports must be an object')
128
239
  }
129
240
  }
130
241
 
131
- const _exports = [rootExport, ...extraExports]
132
-
133
242
  errors.push(...validateExports(_exports, {pkg}))
134
243
 
135
244
  if (errors.length) {
@@ -1,4 +1,4 @@
1
- import {legacyEnding} from '../../tasks/dts/getTargetPaths'
1
+ import {cjsEnding, defaultEnding, legacyEnding, mjsEnding} from '../../tasks/dts/getTargetPaths'
2
2
 
3
3
  /** @internal */
4
4
  export interface PkgExtMap {
@@ -8,18 +8,18 @@ export interface PkgExtMap {
8
8
  }
9
9
 
10
10
  /** @internal */
11
- export const pkgExtMap: PkgExtMap = {
11
+ export const pkgExtMap = {
12
12
  // pkg.type: "commonjs"
13
13
  commonjs: {
14
- commonjs: '.js',
15
- esm: '.mjs',
14
+ commonjs: defaultEnding,
15
+ esm: mjsEnding,
16
16
  },
17
17
 
18
18
  // pkg.type: "module"
19
19
  module: {
20
- commonjs: '.cjs',
21
- esm: '.js',
20
+ commonjs: cjsEnding,
21
+ esm: defaultEnding,
22
22
  },
23
23
  // package.config.legacyExports: true
24
24
  legacy: legacyEnding,
25
- }
25
+ } satisfies PkgExtMap
@@ -51,8 +51,31 @@ const pkgSchema = z.object({
51
51
  ),
52
52
  browserslist: z.optional(z.union([z.string(), z.array(z.string())])),
53
53
  sideEffects: z.optional(z.union([z.boolean(), z.array(z.string())])),
54
+ // @TODO type this properly
55
+ typesVersions: z.optional(z.any()),
54
56
  })
55
57
 
58
+ // Create a map over known keys to catch casing mistakes
59
+ const typoMap = new Map<string, string>()
60
+
61
+ for (const key of pkgSchema.keyof()._def.values) {
62
+ typoMap.set(key.toUpperCase(), key)
63
+ }
64
+
56
65
  export function validatePkg(input: unknown): PackageJSON {
57
- return pkgSchema.parse(input)
66
+ const pkg = pkgSchema.parse(input)
67
+
68
+ const invalidKey = Object.keys(input as PackageJSON).find((key) => {
69
+ const needle = key.toUpperCase()
70
+ return typoMap.has(needle) ? typoMap.get(needle) !== key : false
71
+ })
72
+
73
+ if (invalidKey) {
74
+ throw new TypeError(
75
+ `
76
+ - package.json: "${invalidKey}" is not a valid key. Did you mean "${typoMap.get(invalidKey.toUpperCase())}"?`,
77
+ )
78
+ }
79
+
80
+ return pkg
58
81
  }
@@ -3,11 +3,17 @@ import type {PackageJSON, PkgBundle, PkgExport} from '../../core'
3
3
  /** @internal */
4
4
  export const fileEnding = /\.[mc]?js$/
5
5
  /** @internal */
6
- export const dtsEnding = '.d.ts'
6
+ export const dtsEnding = '.d.ts' as const
7
7
  /** @internal */
8
- export const legacyEnding = '.esm.js'
9
- const mtsEnding = '.d.mts'
10
- const ctsEnding = '.d.cts'
8
+ export const defaultEnding = '.js' as const
9
+ /** @internal */
10
+ export const legacyEnding = `.esm${defaultEnding}` as const
11
+ /** @internal */
12
+ export const mjsEnding = '.mjs' as const
13
+ /** @internal */
14
+ export const cjsEnding = '.cjs' as const
15
+ const mtsEnding = '.d.mts' as const
16
+ const ctsEnding = '.d.cts' as const
11
17
 
12
18
  /** @internal */
13
19
  export function getTargetPaths(
@@ -1,10 +0,0 @@
1
- import {expect, test} from 'vitest'
2
-
3
- import {findCommonDirPath} from './findCommonPath'
4
-
5
- test('should find common parent directory of paths', () => {
6
- expect(findCommonDirPath(['test/a', 'test/b/a'])).toBe('test')
7
- expect(findCommonDirPath(['/test/a', '/test/b/a'])).toBe('/test')
8
- expect(findCommonDirPath(['/test/a/b', '/test/a/b/c'])).toBe('/test/a')
9
- expect(findCommonDirPath(['test/a/b', 'test/a/b', 'test/b/c', 'test/b/a'])).toBe('test')
10
- })