• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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