@@ -5,6 +5,8 @@ import { resource } from "lively.resources";
55import { parseQuery } from "lively.resources" ;
66import { arr , obj } from "lively.lang" ;
77const Generator = System . get ( '@jspm_generator' ) . default ;
8+ const semver = System . _nodeRequire ( 'semver' ) ;
9+ let localDependerIndex ;
810
911// Deps that cannot be resolved via jspm.io CDN:
1012// - native binary packages (platform-specific compiled addons)
@@ -33,6 +35,89 @@ function extractFailingPackage (errMsg) {
3335 return null ;
3436}
3537
38+ function extractImportingPackageScope ( errMsg ) {
39+ const importerMatch = errMsg . match ( / i m p o r t e d f r o m ( h t t p s : \/ \/ g a \. j s p m \. i o \/ n p m : (?: @ [ ^ / ] + \/ ) ? [ ^ @ \s / ] + @ [ ^ / ] + \/ ) / ) ;
40+ return importerMatch ? importerMatch [ 1 ] : null ;
41+ }
42+
43+ function buildPackageUrl ( name , version ) {
44+ return `https://ga.jspm.io/npm:${ name } @${ version } /` ;
45+ }
46+
47+ function toCachedScopeUrl ( scopeUrl ) {
48+ return scopeUrl . replace ( / ^ h t t p s : \/ \/ / , 'esm://' ) ;
49+ }
50+
51+ function toGeneratorScopeUrl ( scopeUrl ) {
52+ return scopeUrl . replace ( / ^ e s m : \/ \/ / , 'https://' ) ;
53+ }
54+
55+ function indexPackageDependers ( packageJson , index ) {
56+ if ( ! packageJson ?. name || ! packageJson ?. version ) return ;
57+ const deps = {
58+ ...( packageJson . dependencies || { } ) ,
59+ ...( packageJson . peerDependencies || { } ) ,
60+ ...( packageJson . optionalDependencies || { } )
61+ } ;
62+ for ( const [ depName , depRange ] of Object . entries ( deps ) ) {
63+ if ( typeof depRange !== 'string' ) continue ;
64+ ( index [ depName ] ||= [ ] ) . push ( {
65+ scopeUrl : buildPackageUrl ( packageJson . name , packageJson . version ) ,
66+ range : depRange
67+ } ) ;
68+ }
69+ }
70+
71+ function getLocalDependerIndex ( ) {
72+ if ( localDependerIndex ) return localDependerIndex ;
73+
74+ const index = { } ;
75+ const packageRoot = join ( process . cwd ( ) , 'lively.next-node_modules' ) ;
76+ if ( ! fs . existsSync ( packageRoot ) ) {
77+ localDependerIndex = index ;
78+ return index ;
79+ }
80+
81+ const stack = [ packageRoot ] ;
82+ while ( stack . length ) {
83+ const dir = stack . pop ( ) ;
84+ let entries ;
85+ try {
86+ entries = fs . readdirSync ( dir , { withFileTypes : true } ) ;
87+ } catch {
88+ continue ;
89+ }
90+ for ( const entry of entries ) {
91+ const fullPath = join ( dir , entry . name ) ;
92+ if ( entry . isDirectory ( ) ) {
93+ stack . push ( fullPath ) ;
94+ continue ;
95+ }
96+ if ( ! entry . isFile ( ) || entry . name !== 'package.json' ) continue ;
97+ try {
98+ indexPackageDependers ( JSON . parse ( fs . readFileSync ( fullPath , 'utf8' ) ) , index ) ;
99+ } catch { }
100+ }
101+ }
102+
103+ localDependerIndex = index ;
104+ return index ;
105+ }
106+
107+ function findScopedPinTargets ( pkgName , goodVersion , importerScope = null ) {
108+ const scopedPins = new Set ( ) ;
109+ if ( importerScope ) scopedPins . add ( importerScope ) ;
110+ const dependers = getLocalDependerIndex ( ) [ pkgName ] || [ ] ;
111+ for ( const { scopeUrl, range } of dependers ) {
112+ try {
113+ if ( semver . satisfies ( goodVersion , range , { includePrerelease : true } ) ) {
114+ scopedPins . add ( scopeUrl ) ;
115+ }
116+ } catch { }
117+ }
118+ return [ ...scopedPins ] ;
119+ }
120+
36121/**
37122 * Given a package name and failing version, find a nearby version that
38123 * actually exists on the jspm.io CDN by walking backwards from the failing
@@ -66,16 +151,26 @@ async function findAvailableCDNVersion (name, failingVersion) {
66151 return null ;
67152}
68153
69- function createGenerator ( inputMap , resolutions ) {
70- return new Generator ( {
154+ function createGenerator ( inputMap , resolutions , scopedResolutions = { } ) {
155+ const generator = new Generator ( {
71156 env : [ "browser" ] ,
72157 defaultProvider : 'jspm.io' ,
73158 inputMap,
74159 ...( Object . keys ( resolutions ) . length ? { resolutions } : { } )
75160 } ) ;
161+ const installs = generator . traceMap . installer . installs ;
162+ installs . secondary ||= { } ;
163+ installs . flattened ||= { } ;
164+ for ( const [ scopeUrl , pins ] of Object . entries ( scopedResolutions ) ) {
165+ const scope = installs . secondary [ toGeneratorScopeUrl ( scopeUrl ) ] ||= { } ;
166+ for ( const [ pkgName , version ] of Object . entries ( pins || { } ) ) {
167+ scope [ pkgName ] = { installUrl : buildPackageUrl ( pkgName , version ) } ;
168+ }
169+ }
170+ return generator ;
76171}
77172
78- async function installDeps ( generator , deps , failed , resolutions , inputMap ) {
173+ async function installDeps ( generator , deps , failed , resolutions , inputMap , scopedResolutions ) {
79174 const depNames = deps . map ( ( [ name ] ) => name ) ;
80175 let needsRestart = false ;
81176
@@ -90,12 +185,29 @@ async function installDeps (generator, deps, failed, resolutions, inputMap) {
90185 } catch ( firstErr ) {
91186 const errMsg = firstErr . message || String ( firstErr ) ;
92187 const failingPkg = extractFailingPackage ( errMsg ) ;
93- if ( failingPkg && ! resolutions [ failingPkg . name ] ) {
188+ const importingPkgScope = extractImportingPackageScope ( errMsg ) ;
189+ const hasGlobalPin = ! ! resolutions [ failingPkg ?. name ] ;
190+ const hasPinnedImporterScope = ! ! (
191+ failingPkg &&
192+ importingPkgScope &&
193+ scopedResolutions [ importingPkgScope ] ?. [ failingPkg . name ]
194+ ) ;
195+ if ( failingPkg && ! hasGlobalPin && ! hasPinnedImporterScope ) {
94196 console . warn ( `\x1b[33m [!] Import map: ${ depSpec } failed — transitive dep ${ failingPkg . name } @${ failingPkg . version } not on CDN, searching for available version...\x1b[0m` ) ;
95197 const goodVersion = await findAvailableCDNVersion ( failingPkg . name , failingPkg . version ) ;
96198 if ( goodVersion ) {
97- console . log ( `\x1b[32m [✓] Found ${ failingPkg . name } @${ goodVersion } on CDN, pinning via resolutions\x1b[0m` ) ;
98- resolutions [ failingPkg . name ] = goodVersion ;
199+ const scopedPins = findScopedPinTargets ( failingPkg . name , goodVersion , importingPkgScope )
200+ . map ( toCachedScopeUrl )
201+ . filter ( scopeUrl => scopedResolutions [ scopeUrl ] ?. [ failingPkg . name ] !== goodVersion ) ;
202+ if ( scopedPins . length ) {
203+ for ( const scopeUrl of scopedPins ) {
204+ ( scopedResolutions [ scopeUrl ] ||= { } ) [ failingPkg . name ] = goodVersion ;
205+ }
206+ console . log ( `\x1b[32m [✓] Found ${ failingPkg . name } @${ goodVersion } on CDN, pinning via scoped locks for ${ scopedPins . length } requester(s)\x1b[0m` ) ;
207+ } else {
208+ console . log ( `\x1b[32m [✓] Found ${ failingPkg . name } @${ goodVersion } on CDN, pinning via global resolutions\x1b[0m` ) ;
209+ resolutions [ failingPkg . name ] = goodVersion ;
210+ }
99211 needsRestart = true ;
100212 continue ; // don't mark as failed — will be retried in second pass
101213 } else {
@@ -119,8 +231,9 @@ async function installDeps (generator, deps, failed, resolutions, inputMap) {
119231 // If we discovered new resolution pins, recreate the generator and
120232 // redo the entire install so all deps benefit from the pins.
121233 if ( needsRestart ) {
122- console . log ( `\x1b[36m [↻] Restarting import map resolution with ${ Object . keys ( resolutions ) . length } pinned resolution(s)...\x1b[0m` ) ;
123- generator = createGenerator ( inputMap , resolutions ) ;
234+ const scopedPinCount = Object . values ( scopedResolutions ) . reduce ( ( sum , pins ) => sum + Object . keys ( pins || { } ) . length , 0 ) ;
235+ console . log ( `\x1b[36m [↻] Restarting import map resolution with ${ Object . keys ( resolutions ) . length } global and ${ scopedPinCount } scoped pinned resolution(s)...\x1b[0m` ) ;
236+ generator = createGenerator ( inputMap , resolutions , scopedResolutions ) ;
124237 for ( const key of Object . keys ( failed ) ) delete failed [ key ] ;
125238 for ( let dep of deps ) {
126239 if ( dep [ 0 ] == 'tar-fs' || isUnresolvableOnCDN ( dep ) || ! ! generator . map . imports [ dep [ 0 ] ] ) continue ;
@@ -154,18 +267,21 @@ export async function generateImportMap (packageName) {
154267 inputMap = JSON . parse ( ( await cachedImportMap . read ( ) ) . replace ( / e s m : \/ \/ / g, 'https://' ) ) ; // replace esm to make generator install again
155268 }
156269 const resolutions = { } ;
157- let generator = createGenerator ( inputMap , resolutions ) ;
270+ const scopedResolutions = Object . assign ( { } , inputMap ?. _scopedResolutions || { } ) ;
271+ let generator = createGenerator ( inputMap , resolutions , scopedResolutions ) ;
158272 const failed = inputMap ?. _failed || { } ;
159273 generator = await installDeps (
160274 generator ,
161275 Object . entries ( pkg . config . dependencies || { } ) . filter ( ( [ dep ] ) => ! dep . match ( / l i v e l y ( \. | - ) / ) ) ,
162276 failed ,
163277 resolutions ,
164- inputMap
278+ inputMap ,
279+ scopedResolutions
165280 ) ;
166281 const importMap = JSON . parse ( JSON . stringify ( generator . getMap ( ) ) . replace ( / h t t p s : \/ \/ / g, 'esm://' ) )
167282 if ( ! obj . isEmpty ( failed ) ) importMap . _failed = failed ;
168283 if ( ! obj . isEmpty ( resolutions ) ) importMap . _resolutions = resolutions ;
284+ if ( ! obj . isEmpty ( scopedResolutions ) ) importMap . _scopedResolutions = scopedResolutions ;
169285 if ( ! obj . isEmpty ( importMap ) ) await cachedImportMap . writeJson ( importMap ) ;
170286 else if ( inputMap ) { await cachedImportMap . remove ( ) }
171287 return importMap ;
0 commit comments