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