summaryrefslogtreecommitdiff
path: root/test/es-module/test-esm-exports.mjs
blob: a361bafaa8380468cbd5dc5b56778ba79000c3bb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
// Flags: --experimental-modules
import { mustCall } from '../common/index.mjs';
import { ok, deepStrictEqual, strictEqual } from 'assert';

import { requireFixture, importFixture } from '../fixtures/pkgexports.mjs';
import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';

[requireFixture, importFixture].forEach((loadFixture) => {
  const isRequire = loadFixture === requireFixture;

  const validSpecifiers = new Map([
    // A simple mapping of a path.
    ['pkgexports/valid-cjs', { default: 'asdf' }],
    // A directory mapping, pointing to the package root.
    ['pkgexports/sub/asdf.js', { default: 'asdf' }],
    // A mapping pointing to a file that needs special encoding (%20) in URLs.
    ['pkgexports/space', { default: 'encoded path' }],
    // Verifying that normal packages still work with exports turned on.
    isRequire ? ['baz/index', { default: 'eye catcher' }] : [null],
    // Fallbacks
    ['pkgexports/fallbackdir/asdf.js', { default: 'asdf' }],
    ['pkgexports/fallbackfile', { default: 'asdf' }],
    // Dot main
    ['pkgexports', { default: 'asdf' }],
    // Conditional split for require
    ['pkgexports/condition', isRequire ? { default: 'encoded path' } :
      { default: 'asdf' }],
    // String exports sugar
    ['pkgexports-sugar', { default: 'main' }],
    // Conditional object exports sugar
    ['pkgexports-sugar2', isRequire ? { default: 'not-exported' } :
      { default: 'main' }]
  ]);

  for (const [validSpecifier, expected] of validSpecifiers) {
    if (validSpecifier === null) continue;

    loadFixture(validSpecifier)
      .then(mustCall((actual) => {
        deepStrictEqual({ ...actual }, expected);
      }));
  }

  const undefinedExports = new Map([
    // There's no such export - so there's nothing to do.
    ['pkgexports/missing', './missing'],
    // The file exists but isn't exported. The exports is a number which counts
    // as a non-null value without any properties, just like `{}`.
    ['pkgexports-number/hidden.js', './hidden.js'],
    // Sugar cases still encapsulate
    ['pkgexports-sugar/not-exported.js', './not-exported.js'],
    ['pkgexports-sugar2/not-exported.js', './not-exported.js']
  ]);

  const invalidExports = new Map([
    // Even though 'pkgexports/sub/asdf.js' works, alternate "path-like"
    // variants do not to prevent confusion and accidental loopholes.
    ['pkgexports/sub/./../asdf.js', './sub/./../asdf.js'],
    // This path steps back inside the package but goes through an exports
    // target that escapes the package, so we still catch that as invalid
    ['pkgexports/belowdir/pkgexports/asdf.js', './belowdir/pkgexports/asdf.js'],
    // This target file steps below the package
    ['pkgexports/belowfile', './belowfile'],
    // Directory mappings require a trailing / to work
    ['pkgexports/missingtrailer/x', './missingtrailer/x'],
    // Invalid target handling
    ['pkgexports/null', './null'],
    ['pkgexports/invalid1', './invalid1'],
    ['pkgexports/invalid2', './invalid2'],
    ['pkgexports/invalid3', './invalid3'],
    ['pkgexports/invalid4', './invalid4'],
    // Missing / invalid fallbacks
    ['pkgexports/nofallback1', './nofallback1'],
    ['pkgexports/nofallback2', './nofallback2'],
    // Reaching into nested node_modules
    ['pkgexports/nodemodules', './nodemodules'],
  ]);

  for (const [specifier, subpath] of undefinedExports) {
    loadFixture(specifier).catch(mustCall((err) => {
      strictEqual(err.code, (isRequire ? '' : 'ERR_') + 'MODULE_NOT_FOUND');
      assertStartsWith(err.message, 'Package exports');
      assertIncludes(err.message, `do not define a '${subpath}' subpath`);
    }));
  }

  for (const [specifier, subpath] of invalidExports) {
    loadFixture(specifier).catch(mustCall((err) => {
      strictEqual(err.code, (isRequire ? '' : 'ERR_') + 'MODULE_NOT_FOUND');
      assertStartsWith(err.message, (isRequire ? 'Package exports' :
        'Cannot resolve'));
      assertIncludes(err.message, isRequire ?
        `do not define a valid '${subpath}' target` :
        `matched for '${subpath}'`);
    }));
  }

  // Covering out bases - not a file is still not a file after dir mapping.
  loadFixture('pkgexports/sub/not-a-file.js').catch(mustCall((err) => {
    strictEqual(err.code, (isRequire ? '' : 'ERR_') + 'MODULE_NOT_FOUND');
    // ESM returns a full file path
    assertStartsWith(err.message, isRequire ?
      'Cannot find module \'pkgexports/sub/not-a-file.js\'' :
      'Cannot find module');
  }));

  // The use of %2F escapes in paths fails loading
  loadFixture('pkgexports/sub/..%2F..%2Fbar.js').catch(mustCall((err) => {
    strictEqual(err.code, isRequire ? 'ERR_INVALID_FILE_URL_PATH' :
      'ERR_MODULE_NOT_FOUND');
  }));

  // Sugar conditional exports main mixed failure case
  loadFixture('pkgexports-sugar-fail').catch(mustCall((err) => {
    strictEqual(err.code, 'ERR_INVALID_PACKAGE_CONFIG');
    assertStartsWith(err.message, (isRequire ? 'Invalid package' :
      'Cannot resolve'));
    assertIncludes(err.message, '"exports" cannot contain some keys starting ' +
    'with \'.\' and some not. The exports object must either be an object of ' +
    'package subpath keys or an object of main entry condition name keys ' +
    'only.');
  }));
});

const { requireFromInside, importFromInside } = fromInside;
[importFromInside, requireFromInside].forEach((loadFromInside) => {
  const validSpecifiers = new Map([
    // A file not visible from outside of the package
    ['../not-exported.js', { default: 'not-exported' }],
    // Part of the public interface
    ['@pkgexports/name/valid-cjs', { default: 'asdf' }],
  ]);
  for (const [validSpecifier, expected] of validSpecifiers) {
    if (validSpecifier === null) continue;

    loadFromInside(validSpecifier)
      .then(mustCall((actual) => {
        deepStrictEqual({ ...actual }, expected);
      }));
  }
});

function assertStartsWith(actual, expected) {
  const start = actual.toString().substr(0, expected.length);
  strictEqual(start, expected);
}

function assertIncludes(actual, expected) {
  ok(actual.toString().indexOf(expected) !== -1,
     `${JSON.stringify(actual)} includes ${JSON.stringify(expected)}`);
}