Make output scripts executable by default when targeting node

This is something we already did autoconf mode.
diff --git a/ChangeLog.md b/ChangeLog.md
index cd0025f..e6a8c73 100644
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -20,6 +20,9 @@
 
 3.1.33 (in development)
 -----------------------
+- When targetting `node` (i.e. when node is included in `ENVIRONMENT`) the
+  output file is now marked as executable and includes a !# line by default.
+  This can be disabled explictly via `-sEXECUTABLE_OUTPUT=0`.
 - Removed `sys/sysctl.h` compatibility header.  We don't implement the function
   it defines. (#18863)
 - Update SDL2_ttf port to 2.20.2 (#18804)
diff --git a/emcc.py b/emcc.py
index 165e0c2..21b0f9c 100755
--- a/emcc.py
+++ b/emcc.py
@@ -253,7 +253,7 @@
   def __init__(self):
     self.output_file = None
     self.post_link = False
-    self.executable = False
+    self.autoconf = False
     self.compiler_wrapper = None
     self.oformat = None
     self.requested_debug = ''
@@ -749,20 +749,23 @@
   return passes
 
 
-def make_js_executable(script):
+def make_js_executable(options, script):
   src = read_file(script)
-  cmd = config.NODE_JS
-  if settings.MEMORY64 == 1:
-    cmd += shared.node_memory64_flags()
-  elif settings.WASM_BIGINT:
-    cmd += shared.node_bigint_flags()
-  if len(cmd) > 1 or not os.path.isabs(cmd[0]):
-    # Using -S (--split-string) here means that arguments to the executable are
-    # correctly parsed.  We don't do this by default because old versions of env
-    # don't support -S.
-    cmd = '/usr/bin/env -S ' + shared.shlex_join(cmd)
+  if options.autoconf:
+    cmd = config.NODE_JS
+    if settings.MEMORY64 == 1:
+      cmd += shared.node_memory64_flags()
+    elif settings.WASM_BIGINT:
+      cmd += shared.node_bigint_flags()
+    if len(cmd) > 1 or not os.path.isabs(cmd[0]):
+      # Using -S (--split-string) here means that arguments to the executable are
+      # correctly parsed.  We don't do this by default because old versions of env
+      # don't support -S.
+      cmd = '/usr/bin/env -S ' + shared.shlex_join(cmd)
+    else:
+      cmd = shared.shlex_join(cmd)
   else:
-    cmd = shared.shlex_join(cmd)
+    cmd = '/usr/bin/env node'
   logger.debug('adding `#!` to JavaScript file: %s' % cmd)
   # add shebang
   with open(script, 'w') as f:
@@ -1760,14 +1763,14 @@
 
 @ToolchainProfiler.profile_block('linker_setup')
 def phase_linker_setup(options, state, newargs):
-  autoconf = os.environ.get('EMMAKEN_JUST_CONFIGURE') or 'conftest.c' in state.orig_args or 'conftest.cpp' in state.orig_args
-  if autoconf:
+  options.autoconf = os.environ.get('EMMAKEN_JUST_CONFIGURE') or 'conftest.c' in state.orig_args or 'conftest.cpp' in state.orig_args
+  if options.autoconf:
     # configure tests want a more shell-like style, where we emit return codes on exit()
     settings.EXIT_RUNTIME = 1
     # use node.js raw filesystem access, to behave just like a native executable
     settings.NODERAWFS = 1
     # Add `#!` line to output JS and make it executable.
-    options.executable = True
+    settings.EXECUTABLE_OUTPUT = True
 
   system_libpath = '-L' + str(cache.get_lib_dir(absolute=True))
   add_link_flag(state, sys.maxsize, system_libpath)
@@ -1775,6 +1778,9 @@
   if settings.OPT_LEVEL >= 1:
     default_setting('ASSERTIONS', 0)
 
+  if settings.ENVIRONMENT_MAY_BE_NODE and not settings.MODULARIZE:
+    default_setting('EXECUTABLE_OUTPUT', 1)
+
   if options.emrun:
     options.pre_js.append(utils.path_from_root('src/emrun_prejs.js'))
     options.post_js.append(utils.path_from_root('src/emrun_postjs.js'))
@@ -1826,7 +1832,7 @@
     dirname = os.path.dirname(target)
     if dirname and not os.path.isdir(dirname):
       exit_with_error("specified output file (%s) is in a directory that does not exist" % target)
-  elif autoconf:
+  elif options.autoconf:
     # Autoconf expects the executable output file to be called `a.out`
     target = 'a.out'
   elif settings.SIDE_MODULE:
@@ -3291,8 +3297,8 @@
   for f in generated_text_files_with_native_eols:
     tools.line_endings.convert_line_endings_in_file(f, os.linesep, options.output_eol)
 
-  if options.executable:
-    make_js_executable(js_target)
+  if settings.EXECUTABLE_OUTPUT and settings.ENVIRONMENT_MAY_BE_NODE:
+    make_js_executable(options, js_target)
 
 
 def version_string():
diff --git a/src/settings.js b/src/settings.js
index 704d987..46819fb 100644
--- a/src/settings.js
+++ b/src/settings.js
@@ -2101,6 +2101,10 @@
 // library symbol.
 var LEGACY_RUNTIME = false;
 
+// Mark JS output file as executable and include #! line at the top.
+// This defaults to true when node is included in ENVIRONMENT.
+var EXECUTABLE_OUTPUT = false;
+
 //===========================================
 // Internal, used for testing only, from here
 //===========================================
diff --git a/test/test_core.py b/test/test_core.py
index f52fe17..bf46fc1 100644
--- a/test/test_core.py
+++ b/test/test_core.py
@@ -429,9 +429,7 @@
     self.assertEqual(prefix, output[:len(prefix)])
 
   def verify_in_strict_mode(self, filename):
-    js = read_file(filename)
-    filename += '.strict.js'
-    write_file(filename, '"use strict";\n' + js)
+    self.node_args.append('--use_strict')
     self.run_js(filename)
 
   def do_core_test(self, testname, **kwargs):
diff --git a/test/test_other.py b/test/test_other.py
index bd157ba..8a6c71a 100644
--- a/test/test_other.py
+++ b/test/test_other.py
@@ -11309,6 +11309,12 @@
     output = self.run_process([os.path.abspath('a.out')], stdout=PIPE).stdout
     self.assertContained('hello, world!', output)
 
+  @no_windows('windows does not support shbang syntax')
+  def test_executable_output(self):
+    self.run_process([EMCC, test_file('hello_world.c')])
+    output = self.run_process([os.path.abspath('a.out.js')], stdout=PIPE).stdout
+    self.assertContained('hello, world!', output)
+
   def test_standalone_export_main(self):
     # Tests that explicitly exported `_main` does not fail, even though `_start` is the entry
     # point.