Repository: nodejs/node
Pull request: module: do not warn for typeless package.json when there isn't one

It was intended that warnings should only be emitted for an existing package.json without a type. This fixes a confusing warning telling users to update /package.json when there are no package.json on the lookup path at all, like this:

[MODULE_TYPELESS_PACKAGE_JSON] Warning: ... parsed as an ES module because module syntax was detected; to avoid the performance penalty of syntax detection, add "type": "module" to /package.json

Drive-by: update the warning message to be clear about reparsing and make it clear what's actionable.

<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.

If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->


Changed files:
filename: lib/internal/modules/esm/get_format.js
patch: @@ -92,8 +92,9 @@ let typelessPackageJsonFilesWarnedAbout;
 function warnTypelessPackageJsonFile(pjsonPath, url) {
   typelessPackageJsonFilesWarnedAbout ??= new SafeSet();
   if (!typelessPackageJsonFilesWarnedAbout.has(pjsonPath)) {
-    const warning = `${url} parsed as an ES module because module syntax was detected;` +
-      ` to avoid the performance penalty of syntax detection, add "type": "module" to ${pjsonPath}`;
+    const warning = `Module type of ${url} is not specified and it doesn't parse as CommonJS.\n` +
+      'Reparsing as ES module because module syntax was detected. This incurs a performance overhead.\n' +
+      `To eliminate this warning, add "type": "module" to ${pjsonPath}.`;
     process.emitWarning(warning, {
       code: 'MODULE_TYPELESS_PACKAGE_JSON',
     });
@@ -112,7 +113,7 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE
   const ext = extname(url);
 
   if (ext === '.js') {
-    const { type: packageType, pjsonPath } = getPackageScopeConfig(url);
+    const { type: packageType, pjsonPath, exists: foundPackageJson } = getPackageScopeConfig(url);
     if (packageType !== 'none') {
       return packageType;
     }
@@ -133,7 +134,7 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE
         // For ambiguous files (no type field, .js extension) we return
         // undefined from `resolve` and re-run the check in `load`.
         const format = detectModuleFormat(source, url);
-        if (format === 'module') {
+        if (format === 'module' && foundPackageJson) {
           // This module has a .js extension, a package.json with no `type` field, and ESM syntax.
           // Warn about the missing `type` field so that the user can avoid the performance penalty of detection.
           warnTypelessPackageJsonFile(pjsonPath, url);
@@ -143,7 +144,7 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE
     }
   }
   if (ext === '.ts' && getOptionValue('--experimental-strip-types')) {
-    const { type: packageType, pjsonPath } = getPackageScopeConfig(url);
+    const { type: packageType, pjsonPath, exists: foundPackageJson } = getPackageScopeConfig(url);
     if (packageType !== 'none') {
       return `${packageType}-typescript`;
     }
@@ -164,7 +165,7 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE
         const parsedSource = tsParse(source);
         const detectedFormat = detectModuleFormat(parsedSource, url);
         const format = detectedFormat ? `${detectedFormat}-typescript` : 'commonjs-typescript';
-        if (format === 'module-typescript') {
+        if (format === 'module-typescript' && foundPackageJson) {
           // This module has a .js extension, a package.json with no `type` field, and ESM syntax.
           // Warn about the missing `type` field so that the user can avoid the performance penalty of detection.
           warnTypelessPackageJsonFile(pjsonPath, url);

filename: test/es-module/test-esm-detect-ambiguous.mjs
patch: @@ -352,6 +352,18 @@ describe('Module syntax detection', { concurrency: !process.env.TEST_PARALLEL },
       });
     }
 
+    it('does not warn when there are no package.json', async () => {
+      const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
+        fixtures.path('es-modules/loose.js'),
+      ]);
+
+      strictEqual(stderr, '');
+      strictEqual(stdout, 'executed\n');
+      strictEqual(code, 0);
+      strictEqual(signal, null);
+    });
+
+
     it('warns only once for a package.json that affects multiple files', async () => {
       const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
         fixtures.path('es-modules/package-without-type/detected-as-esm.js'),