@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/dist/_chunks-cjs/consoleSpy.cjs +119 -53
- package/dist/_chunks-cjs/consoleSpy.cjs.map +1 -1
- package/dist/_chunks-cjs/logger.cjs +16 -3
- package/dist/_chunks-cjs/logger.cjs.map +1 -1
- package/dist/cli.cjs +1 -1
- package/dist/index.d.cts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +135 -56
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/node/core/pkg/parseExports.ts +193 -72
- package/src/node/core/pkg/pkgExt.ts +7 -7
- package/src/node/core/pkg/validatePkg.ts +24 -1
- package/src/node/tasks/dts/getTargetPaths.ts +10 -4
- package/src/node/core/findCommonPath.test.ts +0 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/pkg-utils",
|
|
3
|
-
"version": "6.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": [
|
|
@@ -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.
|
|
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
|
|
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
|
-
"
|
|
163
|
-
"test
|
|
164
|
-
"
|
|
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
|
-
|
|
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:
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
158
|
+
if (strict && !pkg.exports['./package.json']) {
|
|
159
|
+
errors.push('package.json: `exports["./package.json"] must be declared.')
|
|
160
|
+
}
|
|
49
161
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
11
|
+
export const pkgExtMap = {
|
|
12
12
|
// pkg.type: "commonjs"
|
|
13
13
|
commonjs: {
|
|
14
|
-
commonjs:
|
|
15
|
-
esm:
|
|
14
|
+
commonjs: defaultEnding,
|
|
15
|
+
esm: mjsEnding,
|
|
16
16
|
},
|
|
17
17
|
|
|
18
18
|
// pkg.type: "module"
|
|
19
19
|
module: {
|
|
20
|
-
commonjs:
|
|
21
|
-
esm:
|
|
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
|
-
|
|
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
|
|
9
|
-
|
|
10
|
-
const
|
|
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
|
-
})
|