1import { spawnPromisified } from '../common/index.mjs'; 2import * as fixtures from '../common/fixtures.mjs'; 3import assert from 'node:assert'; 4import { execPath } from 'node:process'; 5import { describe, it } from 'node:test'; 6 7describe('Loader hooks throwing errors', { concurrency: true }, () => { 8 it('throws on nonexistent modules', async () => { 9 const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ 10 '--no-warnings', 11 '--experimental-loader', 12 fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'), 13 '--input-type=module', 14 '--eval', 15 'import "nonexistent/file.mjs"', 16 ]); 17 18 assert.match(stderr, /ERR_MODULE_NOT_FOUND/); 19 assert.strictEqual(stdout, ''); 20 assert.strictEqual(code, 1); 21 assert.strictEqual(signal, null); 22 }); 23 24 it('throws on unknown extensions', async () => { 25 const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ 26 '--no-warnings', 27 '--experimental-loader', 28 fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'), 29 '--input-type=module', 30 '--eval', 31 `import ${JSON.stringify(fixtures.fileURL('/es-modules/file.unknown'))}`, 32 ]); 33 34 assert.match(stderr, /ERR_UNKNOWN_FILE_EXTENSION/); 35 assert.strictEqual(stdout, ''); 36 assert.strictEqual(code, 1); 37 assert.strictEqual(signal, null); 38 }); 39 40 it('throws on invalid return values', async () => { 41 const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ 42 '--no-warnings', 43 '--experimental-loader', 44 fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'), 45 '--input-type=module', 46 '--eval', 47 'import "esmHook/badReturnVal.mjs"', 48 ]); 49 50 assert.match(stderr, /ERR_INVALID_RETURN_VALUE/); 51 assert.strictEqual(stdout, ''); 52 assert.strictEqual(code, 1); 53 assert.strictEqual(signal, null); 54 }); 55 56 it('throws on boolean false', async () => { 57 const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ 58 '--no-warnings', 59 '--experimental-loader', 60 fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'), 61 '--input-type=module', 62 '--eval', 63 'import "esmHook/format.false"', 64 ]); 65 66 assert.match(stderr, /ERR_INVALID_RETURN_PROPERTY_VALUE/); 67 assert.strictEqual(stdout, ''); 68 assert.strictEqual(code, 1); 69 assert.strictEqual(signal, null); 70 }); 71 it('throws on boolean true', async () => { 72 const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ 73 '--no-warnings', 74 '--experimental-loader', 75 fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'), 76 '--input-type=module', 77 '--eval', 78 'import "esmHook/format.true"', 79 ]); 80 81 assert.match(stderr, /ERR_INVALID_RETURN_PROPERTY_VALUE/); 82 assert.strictEqual(stdout, ''); 83 assert.strictEqual(code, 1); 84 assert.strictEqual(signal, null); 85 }); 86 87 it('throws on invalid returned object', async () => { 88 const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ 89 '--no-warnings', 90 '--experimental-loader', 91 fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'), 92 '--input-type=module', 93 '--eval', 94 'import "esmHook/badReturnFormatVal.mjs"', 95 ]); 96 97 assert.match(stderr, /ERR_INVALID_RETURN_PROPERTY_VALUE/); 98 assert.strictEqual(stdout, ''); 99 assert.strictEqual(code, 1); 100 assert.strictEqual(signal, null); 101 }); 102 103 it('throws on unsupported format', async () => { 104 const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ 105 '--no-warnings', 106 '--experimental-loader', 107 fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'), 108 '--input-type=module', 109 '--eval', 110 'import "esmHook/unsupportedReturnFormatVal.mjs"', 111 ]); 112 113 assert.match(stderr, /ERR_UNKNOWN_MODULE_FORMAT/); 114 assert.strictEqual(stdout, ''); 115 assert.strictEqual(code, 1); 116 assert.strictEqual(signal, null); 117 }); 118 119 it('throws on invalid format property type', async () => { 120 const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ 121 '--no-warnings', 122 '--experimental-loader', 123 fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'), 124 '--input-type=module', 125 '--eval', 126 'import "esmHook/badReturnSourceVal.mjs"', 127 ]); 128 129 assert.match(stderr, /ERR_INVALID_RETURN_PROPERTY_VALUE/); 130 assert.strictEqual(stdout, ''); 131 assert.strictEqual(code, 1); 132 assert.strictEqual(signal, null); 133 }); 134 135 it('rejects dynamic imports for all of the error cases checked above', async () => { 136 const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ 137 '--no-warnings', 138 '--experimental-loader', 139 fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'), 140 '--input-type=module', 141 '--eval', 142 `import assert from 'node:assert'; 143 await Promise.allSettled([ 144 import('nonexistent/file.mjs'), 145 import(${JSON.stringify(fixtures.fileURL('/es-modules/file.unknown'))}), 146 import('esmHook/badReturnVal.mjs'), 147 import('esmHook/format.false'), 148 import('esmHook/format.true'), 149 import('esmHook/badReturnFormatVal.mjs'), 150 import('esmHook/unsupportedReturnFormatVal.mjs'), 151 import('esmHook/badReturnSourceVal.mjs'), 152 ]).then((results) => { 153 assert.strictEqual(results.every((result) => result.status === 'rejected'), true); 154 })`, 155 ]); 156 157 assert.strictEqual(stderr, ''); 158 assert.strictEqual(stdout, ''); 159 assert.strictEqual(code, 0); 160 assert.strictEqual(signal, null); 161 }); 162}); 163 164describe('Loader hooks parsing modules', { concurrency: true }, () => { 165 it('can parse .js files as ESM', async () => { 166 const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ 167 '--no-warnings', 168 '--experimental-loader', 169 fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'), 170 '--input-type=module', 171 '--eval', 172 `import assert from 'node:assert'; 173 await import(${JSON.stringify(fixtures.fileURL('/es-module-loaders/js-as-esm.js'))}) 174 .then((parsedModule) => { 175 assert.strictEqual(typeof parsedModule, 'object'); 176 assert.strictEqual(parsedModule.namedExport, 'named-export'); 177 })`, 178 ]); 179 180 assert.strictEqual(stderr, ''); 181 assert.strictEqual(stdout, ''); 182 assert.strictEqual(code, 0); 183 assert.strictEqual(signal, null); 184 }); 185 186 it('can define .ext files as ESM', async () => { 187 const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ 188 '--no-warnings', 189 '--experimental-loader', 190 fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'), 191 '--input-type=module', 192 '--eval', 193 `import assert from 'node:assert'; 194 await import(${JSON.stringify(fixtures.fileURL('/es-modules/file.ext'))}) 195 .then((parsedModule) => { 196 assert.strictEqual(typeof parsedModule, 'object'); 197 const { default: defaultExport } = parsedModule; 198 assert.strictEqual(typeof defaultExport, 'function'); 199 assert.strictEqual(defaultExport.name, 'iAmReal'); 200 assert.strictEqual(defaultExport(), true); 201 })`, 202 ]); 203 204 assert.strictEqual(stderr, ''); 205 assert.strictEqual(stdout, ''); 206 assert.strictEqual(code, 0); 207 assert.strictEqual(signal, null); 208 }); 209 210 it('can predetermine the format in the custom loader resolve hook', async () => { 211 const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ 212 '--no-warnings', 213 '--experimental-loader', 214 fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'), 215 '--input-type=module', 216 '--eval', 217 `import assert from 'node:assert'; 218 await import('esmHook/preknownFormat.pre') 219 .then((parsedModule) => { 220 assert.strictEqual(typeof parsedModule, 'object'); 221 assert.strictEqual(parsedModule.default, 'hello world'); 222 })`, 223 ]); 224 225 assert.strictEqual(stderr, ''); 226 assert.strictEqual(stdout, ''); 227 assert.strictEqual(code, 0); 228 assert.strictEqual(signal, null); 229 }); 230 231 it('can provide source for a nonexistent file', async () => { 232 const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ 233 '--no-warnings', 234 '--experimental-loader', 235 fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'), 236 '--input-type=module', 237 '--eval', 238 `import assert from 'node:assert'; 239 await import('esmHook/virtual.mjs') 240 .then((parsedModule) => { 241 assert.strictEqual(typeof parsedModule, 'object'); 242 assert.strictEqual(typeof parsedModule.default, 'undefined'); 243 assert.strictEqual(parsedModule.message, 'WOOHOO!'); 244 })`, 245 ]); 246 247 assert.strictEqual(stderr, ''); 248 assert.strictEqual(stdout, ''); 249 assert.strictEqual(code, 0); 250 assert.strictEqual(signal, null); 251 }); 252 253 it('ensures that loaders have a separate context from userland', async () => { 254 const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ 255 '--no-warnings', 256 '--experimental-loader', 257 fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'), 258 '--input-type=module', 259 '--eval', 260 `import assert from 'node:assert'; 261 await import(${JSON.stringify(fixtures.fileURL('/es-modules/stateful.mjs'))}) 262 .then(({ default: count }) => { 263 assert.strictEqual(count(), 1); 264 });`, 265 ]); 266 267 assert.strictEqual(stderr, ''); 268 assert.strictEqual(stdout, ''); 269 assert.strictEqual(code, 0); 270 assert.strictEqual(signal, null); 271 }); 272 273 it('ensures that user loaders are not bound to the internal loader', async () => { 274 const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ 275 '--no-warnings', 276 '--experimental-loader', 277 fixtures.fileURL('/es-module-loaders/loader-this-value-inside-hook-functions.mjs'), 278 '--input-type=module', 279 '--eval', 280 ';', // Actual test is inside the loader module. 281 ]); 282 283 assert.strictEqual(stderr, ''); 284 assert.strictEqual(stdout, ''); 285 assert.strictEqual(code, 0); 286 assert.strictEqual(signal, null); 287 }); 288}); 289