1import { mustCall } from '../common/index.mjs'; 2import { ok, deepStrictEqual, strictEqual } from 'assert'; 3import { sep } from 'path'; 4 5import { requireFixture, importFixture } from '../fixtures/pkgexports.mjs'; 6import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js'; 7 8[requireFixture, importFixture].forEach((loadFixture) => { 9 const isRequire = loadFixture === requireFixture; 10 11 const validSpecifiers = new Map([ 12 // A simple mapping of a path. 13 ['pkgexports/valid-cjs', { default: 'asdf' }], 14 // A directory mapping, pointing to the package root. 15 ['pkgexports/sub/asdf.js', { default: 'asdf' }], 16 // A mapping pointing to a file that needs special encoding (%20) in URLs. 17 ['pkgexports/space', { default: 'encoded path' }], 18 // Verifying that normal packages still work with exports turned on. 19 isRequire ? ['baz/index', { default: 'eye catcher' }] : [null], 20 // Fallbacks 21 ['pkgexports/fallbackdir/asdf.js', { default: 'asdf' }], 22 ['pkgexports/fallbackfile', { default: 'asdf' }], 23 // Conditional split for require 24 ['pkgexports/condition', isRequire ? { default: 'encoded path' } : 25 { default: 'asdf' }], 26 // String exports sugar 27 ['pkgexports-sugar', { default: 'main' }], 28 // Conditional object exports sugar 29 ['pkgexports-sugar2', isRequire ? { default: 'not-exported' } : 30 { default: 'main' }], 31 // Resolve self 32 ['pkgexports/resolve-self', isRequire ? 33 { default: 'self-cjs' } : { default: 'self-mjs' }], 34 // Resolve self sugar 35 ['pkgexports-sugar', { default: 'main' }], 36 // Path patterns 37 ['pkgexports/subpath/sub-dir1', { default: 'main' }], 38 ['pkgexports/subpath/sub-dir1.js', { default: 'main' }], 39 ['pkgexports/features/dir1', { default: 'main' }], 40 ['pkgexports/dir1/dir1/trailer', { default: 'main' }], 41 ['pkgexports/dir2/dir2/trailer', { default: 'index' }], 42 ['pkgexports/a/dir1/dir1', { default: 'main' }], 43 ['pkgexports/a/b/dir1/dir1', { default: 'main' }], 44 ]); 45 46 if (isRequire) { 47 validSpecifiers.set('pkgexports/subpath/file', { default: 'file' }); 48 validSpecifiers.set('pkgexports/subpath/dir1', { default: 'main' }); 49 validSpecifiers.set('pkgexports/subpath/dir1/', { default: 'main' }); 50 validSpecifiers.set('pkgexports/subpath/dir2', { default: 'index' }); 51 validSpecifiers.set('pkgexports/subpath/dir2/', { default: 'index' }); 52 } else { 53 // No exports or main field 54 validSpecifiers.set('no_exports', { default: 'index' }); 55 // Main field without extension 56 validSpecifiers.set('default_index', { default: 'main' }); 57 } 58 59 for (const [validSpecifier, expected] of validSpecifiers) { 60 if (validSpecifier === null) continue; 61 62 loadFixture(validSpecifier) 63 .then(mustCall((actual) => { 64 deepStrictEqual({ ...actual }, expected); 65 })); 66 } 67 68 const undefinedExports = new Map([ 69 // There's no such export - so there's nothing to do. 70 ['pkgexports/missing', './missing'], 71 // The file exists but isn't exported. The exports is a number which counts 72 // as a non-null value without any properties, just like `{}`. 73 ['pkgexports-number/hidden.js', './hidden.js'], 74 // Sugar cases still encapsulate 75 ['pkgexports-sugar/not-exported.js', './not-exported.js'], 76 ['pkgexports-sugar2/not-exported.js', './not-exported.js'], 77 // Conditional exports with no match are "not exported" errors 78 ['pkgexports/invalid1', './invalid1'], 79 ['pkgexports/invalid4', './invalid4'], 80 // Null mapping 81 ['pkgexports/null', './null'], 82 ['pkgexports/null/subpath', './null/subpath'], 83 // Empty fallback 84 ['pkgexports/nofallback1', './nofallback1'], 85 // Non pattern matches 86 ['pkgexports/trailer', './trailer'], 87 ]); 88 89 const invalidExports = new Map([ 90 // Directory mappings require a trailing / to work 91 ['pkgexports/missingtrailer/x', './missingtrailer/'], 92 // This path steps back inside the package but goes through an exports 93 // target that escapes the package, so we still catch that as invalid 94 ['pkgexports/belowdir/pkgexports/asdf.js', './belowdir/'], 95 // This target file steps below the package 96 ['pkgexports/belowfile', './belowfile'], 97 // Invalid targets 98 ['pkgexports/invalid2', './invalid2'], 99 ['pkgexports/invalid3', './invalid3'], 100 ['pkgexports/invalid5', 'invalid5'], 101 // Missing / invalid fallbacks 102 ['pkgexports/nofallback2', './nofallback2'], 103 // Reaching into nested node_modules 104 ['pkgexports/nodemodules', './nodemodules'], 105 // Self resolve invalid 106 ['pkgexports/resolve-self-invalid', './invalid2'], 107 ]); 108 109 const invalidSpecifiers = new Map([ 110 // Even though 'pkgexports/sub/asdf.js' works, alternate "path-like" 111 // variants do not to prevent confusion and accidental loopholes. 112 ['pkgexports/sub/./../asdf.js', './sub/./../asdf.js'], 113 ]); 114 115 for (const [specifier, subpath] of undefinedExports) { 116 loadFixture(specifier).catch(mustCall((err) => { 117 strictEqual(err.code, 'ERR_PACKAGE_PATH_NOT_EXPORTED'); 118 assertStartsWith(err.message, 'Package subpath '); 119 assertIncludes(err.message, subpath); 120 })); 121 } 122 123 for (const [specifier, subpath] of invalidExports) { 124 loadFixture(specifier).catch(mustCall((err) => { 125 strictEqual(err.code, 'ERR_INVALID_PACKAGE_TARGET'); 126 assertStartsWith(err.message, 'Invalid "exports"'); 127 assertIncludes(err.message, subpath); 128 if (!subpath.startsWith('./')) { 129 assertIncludes(err.message, 'targets must start with'); 130 } 131 })); 132 } 133 134 for (const [specifier, subpath] of invalidSpecifiers) { 135 loadFixture(specifier).catch(mustCall((err) => { 136 strictEqual(err.code, 'ERR_INVALID_MODULE_SPECIFIER'); 137 assertStartsWith(err.message, 'Invalid module '); 138 assertIncludes(err.message, 'is not a valid subpath'); 139 assertIncludes(err.message, subpath); 140 })); 141 } 142 143 // Conditional export, even with no match, should still be used instead 144 // of falling back to main 145 if (isRequire) { 146 loadFixture('pkgexports-main').catch(mustCall((err) => { 147 strictEqual(err.code, 'ERR_PACKAGE_PATH_NOT_EXPORTED'); 148 assertStartsWith(err.message, 'No "exports" main '); 149 })); 150 } 151 152 const notFoundExports = new Map([ 153 // Non-existing file 154 ['pkgexports/sub/not-a-file.js', `pkgexports${sep}not-a-file.js`], 155 // No extension lookups 156 ['pkgexports/no-ext', `pkgexports${sep}asdf`], 157 // Pattern specificity 158 ['pkgexports/dir2/trailer', `subpath${sep}dir2.js`], 159 ]); 160 161 if (!isRequire) { 162 const onDirectoryImport = (err) => { 163 strictEqual(err.code, 'ERR_UNSUPPORTED_DIR_IMPORT'); 164 assertStartsWith(err.message, 'Directory import'); 165 }; 166 notFoundExports.set('pkgexports/subpath/file', 'pkgexports/subpath/file'); 167 loadFixture('pkgexports/subpath/dir1').catch(mustCall(onDirectoryImport)); 168 loadFixture('pkgexports/subpath/dir2').catch(mustCall(onDirectoryImport)); 169 } 170 171 for (const [specifier, request] of notFoundExports) { 172 loadFixture(specifier).catch(mustCall((err) => { 173 strictEqual(err.code, (isRequire ? '' : 'ERR_') + 'MODULE_NOT_FOUND'); 174 assertIncludes(err.message, request); 175 assertStartsWith(err.message, 'Cannot find module'); 176 })); 177 } 178 179 // The use of %2F and %5C escapes in paths fails loading 180 loadFixture('pkgexports/sub/..%2F..%2Fbar.js').catch(mustCall((err) => { 181 strictEqual(err.code, 'ERR_INVALID_MODULE_SPECIFIER'); 182 })); 183 loadFixture('pkgexports/sub/..%5C..%5Cbar.js').catch(mustCall((err) => { 184 strictEqual(err.code, 'ERR_INVALID_MODULE_SPECIFIER'); 185 })); 186 187 // Package export with numeric index properties must throw a validation error 188 loadFixture('pkgexports-numeric').catch(mustCall((err) => { 189 strictEqual(err.code, 'ERR_INVALID_PACKAGE_CONFIG'); 190 })); 191 192 // Sugar conditional exports main mixed failure case 193 loadFixture('pkgexports-sugar-fail').catch(mustCall((err) => { 194 strictEqual(err.code, 'ERR_INVALID_PACKAGE_CONFIG'); 195 assertStartsWith(err.message, 'Invalid package'); 196 assertIncludes(err.message, '"exports" cannot contain some keys starting ' + 197 'with \'.\' and some not. The exports object must either be an object of ' + 198 'package subpath keys or an object of main entry condition name keys ' + 199 'only.'); 200 })); 201}); 202 203const { requireFromInside, importFromInside } = fromInside; 204[importFromInside, requireFromInside].forEach((loadFromInside) => { 205 const validSpecifiers = new Map([ 206 // A file not visible from outside of the package 207 ['../not-exported.js', { default: 'not-exported' }], 208 // Part of the public interface 209 ['pkgexports/valid-cjs', { default: 'asdf' }], 210 ]); 211 for (const [validSpecifier, expected] of validSpecifiers) { 212 if (validSpecifier === null) continue; 213 214 loadFromInside(validSpecifier) 215 .then(mustCall((actual) => { 216 deepStrictEqual({ ...actual }, expected); 217 })); 218 } 219}); 220 221function assertStartsWith(actual, expected) { 222 const start = actual.toString().substr(0, expected.length); 223 strictEqual(start, expected); 224} 225 226function assertIncludes(actual, expected) { 227 ok(actual.toString().indexOf(expected) !== -1, 228 `${JSON.stringify(actual)} includes ${JSON.stringify(expected)}`); 229} 230