@@ -11,12 +11,19 @@ import fs from 'node:fs'
1111import os from 'node:os'
1212import path from 'node:path'
1313import { spawn } from 'node:child_process'
14- import { fileURLToPath } from 'node:url'
14+ import { fileURLToPath , pathToFileURL } from 'node:url'
1515
1616const __filename = fileURLToPath ( import . meta. url )
1717const __dirname = path . dirname ( __filename )
1818const repoRoot = path . resolve ( __dirname , '..' )
19- const probePath = path . join ( __dirname , 'probe-extension-develop-resolve.cjs' )
19+ const probePathCjs = path . join ( __dirname , 'probe-extension-develop-resolve.cjs' )
20+ const probePathEsm = path . join (
21+ __dirname ,
22+ 'probe-extension-develop-resolve-esm.mjs'
23+ )
24+ // `--import` requires a URL form for absolute paths so the loader thread can
25+ // resolve the registration module the same way on POSIX and Windows.
26+ const probeEsmImportArg = pathToFileURL ( probePathEsm ) . href
2027
2128const installedExtensionBin = path . join (
2229 repoRoot ,
@@ -65,7 +72,7 @@ const result = await new Promise((resolve) => {
6572 env : {
6673 ...process . env ,
6774 NODE_OPTIONS :
68- `${ process . env . NODE_OPTIONS ?? '' } --require ${ probePath } ` . trim ( ) ,
75+ `${ process . env . NODE_OPTIONS ?? '' } --require ${ probePathCjs } --import ${ probeEsmImportArg } ` . trim ( ) ,
6976 // Strip any stale env override that could mask resolver behavior.
7077 EXTENSION_DEVELOP_ROOT : ''
7178 } ,
@@ -75,20 +82,36 @@ const result = await new Promise((resolve) => {
7582
7683 let stdout = ''
7784 let stderr = ''
85+ let killedOnMarker = false
7886 child . stdout . on ( 'data' , ( d ) => ( stdout += d . toString ( ) ) )
79- child . stderr . on ( 'data' , ( d ) => ( stderr += d . toString ( ) ) )
87+ child . stderr . on ( 'data' , ( d ) => {
88+ stderr += d . toString ( )
89+ // The ESM resolve hook lives in the loader worker thread and cannot
90+ // exit the main process directly, so kill from the parent as soon as
91+ // the marker line shows up. Avoids waiting for `extension dev` to
92+ // crash on the fake project (no package.json).
93+ if ( ! killedOnMarker && / _ _ E X T _ D E V _ R E S O L V E D _ _ : : / . test ( stderr ) ) {
94+ killedOnMarker = true
95+ child . kill ( 'SIGKILL' )
96+ }
97+ } )
8098
8199 const killTimer = setTimeout ( ( ) => {
82100 child . kill ( 'SIGKILL' )
83101 } , 60_000 )
84102
85103 child . on ( 'close' , ( code ) => {
86104 clearTimeout ( killTimer )
87- resolve ( { code : code ?? 1 , stdout, stderr} )
105+ resolve ( { code : code ?? 1 , stdout, stderr, killedOnMarker } )
88106 } )
89107 child . on ( 'error' , ( err ) => {
90108 clearTimeout ( killTimer )
91- resolve ( { code : 1 , stdout, stderr : String ( err ?. message || err ) } )
109+ resolve ( {
110+ code : 1 ,
111+ stdout,
112+ stderr : String ( err ?. message || err ) ,
113+ killedOnMarker
114+ } )
92115 } )
93116} )
94117
@@ -121,16 +144,21 @@ if (!match) {
121144 process . exit ( 1 )
122145}
123146
124- const resolved = match [ 1 ] . trim ( )
147+ const resolvedRaw = match [ 1 ] . trim ( )
148+ // CJS hook emits a filesystem path; ESM hook emits a `file://` URL. Normalize
149+ // both to a filesystem path before checking the local-install prefix.
150+ const resolvedPath = resolvedRaw . startsWith ( 'file://' )
151+ ? fileURLToPath ( resolvedRaw )
152+ : resolvedRaw
125153const expectedPrefix = installedExtensionDevelopRoot + path . sep
126154
127- if ( ! resolved . startsWith ( expectedPrefix ) ) {
155+ if ( ! resolvedPath . startsWith ( expectedPrefix ) ) {
128156 console . error (
129157 '✖ extension-develop resolved outside the local install:\n' +
130158 ` expected prefix: ${ expectedPrefix } \n` +
131- ` resolved: ${ resolved } `
159+ ` resolved: ${ resolvedPath } `
132160 )
133161 process . exit ( 1 )
134162}
135163
136- console . log ( `✔ extension-develop resolved to ${ resolved } ` )
164+ console . log ( `✔ extension-develop resolved to ${ resolvedPath } ` )
0 commit comments