@sanity/pkg-utils 6.0.2 → 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.0.2",
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": [
@@ -132,10 +132,10 @@
132
132
  "rollup-plugin-visualizer": "^5.12.0",
133
133
  "semver": "^7.6.0",
134
134
  "ts-node": "^10.9.2",
135
- "typescript": "5.4.2",
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.0.2"
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 || '',
@@ -21,14 +123,25 @@ export function parseExports(options: {
21
123
  require: pkg.main && pkg.browser[pkg.main],
22
124
  import: pkg.module && pkg.browser[pkg.module],
23
125
  },
24
- require: pkg.main,
25
- import: pkg.module,
26
- default: pkg.module || pkg.main || '',
126
+ require: legacyExports
127
+ ? pkg.main
128
+ : typeof pkg.exports?.['.'] === 'object' && 'require' in pkg.exports['.']
129
+ ? pkg.exports['.'].require
130
+ : '',
131
+ import: legacyExports
132
+ ? pkg.module
133
+ : typeof pkg.exports?.['.'] === 'object' && 'import' in pkg.exports['.']
134
+ ? pkg.exports['.'].import
135
+ : '',
136
+ default: legacyExports
137
+ ? pkg.module || pkg.main || ''
138
+ : typeof pkg.exports?.['.'] === 'object' && 'default' in pkg.exports['.']
139
+ ? pkg.exports['.'].default || ''
140
+ : pkg.module || pkg.main || '',
27
141
  }
142
+ // */
28
143
 
29
- const extraExports: (PkgExport & {_path: string})[] = []
30
-
31
- const errors: string[] = []
144
+ const _exports: (PkgExport & {_path: string})[] = []
32
145
 
33
146
  // @TODO validate typesVersions when legacyExports is true
34
147
 
@@ -36,88 +149,96 @@ export function parseExports(options: {
36
149
  errors.push('package.json: `typings` should be `types`')
37
150
  }
38
151
 
39
- if (pkg.exports) {
40
- if (strict && !pkg.types && pkg.source?.endsWith('.ts')) {
41
- errors.push(
42
- 'package.json: `types` must be declared for the npm listing to show as a TypeScript module.',
43
- )
44
- }
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
+ }
45
157
 
46
- if (strict && !pkg.exports['./package.json']) {
47
- errors.push('package.json: `exports["./package.json"] must be declared.')
48
- }
158
+ if (strict && !pkg.exports['./package.json']) {
159
+ errors.push('package.json: `exports["./package.json"] must be declared.')
160
+ }
49
161
 
50
- for (const [exportPath, exportEntry] of Object.entries(pkg.exports)) {
51
- if (exportPath.endsWith('.json')) {
52
- if (exportPath === './package.json') {
53
- if (exportEntry !== './package.json') {
54
- errors.push('package.json: `exports["./package.json"] must be "./package.json".')
55
- }
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".')
56
167
  }
57
- } else if (isRecord(exportEntry) && 'svelte' in exportEntry) {
58
- // @TODO should we report a warning or a debug message here about a detected svelte export that is ignored?
59
- } else if (isRecord(exportEntry)) {
60
- if (exportPath === '.') {
61
- if (
62
- exportEntry.require &&
63
- rootExport.require &&
64
- exportEntry.require !== rootExport.require
65
- ) {
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) {
66
188
  errors.push(
67
- '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`,
68
190
  )
69
- }
70
-
71
- if (legacyExports) {
72
- const indexLegacyExport = (
73
- exportEntry.browser?.import ||
74
- exportEntry.import ||
75
- exportEntry.browser?.require ||
76
- exportEntry.require ||
77
- ''
78
- ).replace(/(\.esm)?\.[mc]?js$/, legacyEnding)
79
-
80
- if (indexLegacyExport !== rootExport.import) {
81
- errors.push(
82
- `package.json: "module" should be "${indexLegacyExport}" when "legacyExports" is true`,
83
- )
84
- }
85
191
  } else {
86
- if (
87
- exportEntry.import &&
88
- rootExport.import &&
89
- exportEntry.import !== rootExport.import
90
- ) {
91
- errors.push(
92
- 'package.json: mismatch between "module" and "exports.import" These must be equal.',
93
- )
94
- }
192
+ errors.push(
193
+ `package.json - \`exports["${exp._path}"].default\` should be specified when "legacyExports" is true`,
194
+ )
95
195
  }
196
+ }
197
+ }
96
198
 
97
- 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) {
98
223
  errors.push(
99
- 'package.json: mismatch between "source" and "exports.source". These must be equal.',
224
+ `package.json: "module" should be "${indexLegacyExport}" when "legacyExports" is true`,
100
225
  )
101
226
  }
102
-
103
- Object.assign(rootExport, exportEntry)
104
227
  } else {
105
- const extraExport: PkgExport & {_path: string} = {
106
- _exported: true,
107
- _path: exportPath,
108
- ...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
+ )
109
232
  }
110
-
111
- extraExports.push(extraExport)
112
233
  }
113
- } else {
114
- errors.push('package.json: exports must be an object')
115
234
  }
235
+
236
+ _exports.push(exp)
237
+ } else {
238
+ errors.push('package.json: exports must be an object')
116
239
  }
117
240
  }
118
241
 
119
- const _exports = [rootExport, ...extraExports]
120
-
121
242
  errors.push(...validateExports(_exports, {pkg}))
122
243
 
123
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
- })