• 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/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