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