namespace pxt { const CONFIG_FIELDS_ORDER = [ "name" , "version" , "description" , "license" , "dependencies" , "files" , "testFiles" , "testDependencies" , "fileDependencies" , "public" , "targetVersions" , "supportedTargets" , "preferredEditor" , "languageRestriction" , "additionalFilePath" , "additionalFilePaths" ] ; export class Package { static stringifyConfig ( config ) { const configMap = config as any ; const newCfg = { } ; for ( const f of CONFIG_FIELDS_ORDER ) { if ( configMap . hasOwnProperty ( f ) ) newCfg [ f ] = configMap [ f ] ; } for ( const f of Object . keys ( configMap ) ) { if ( ! newCfg . hasOwnProperty ( f ) ) newCfg [ f ] = configMap [ f ] ; } return JSON . stringify ( newCfg , null , 4 ) + "\n" ; } static parseAndValidConfig ( configStr ) { const json = Util . jsonTryParse ( configStr ) as pxt . PackageConfig ; return json && json . name !== undefined && typeof json . name === "string" && json . files && Array . isArray ( json . files ) && json . files . every ( f => typeof f === "string" ) && json . dependencies && Object . keys ( json . dependencies ) . every ( k => typeof json . dependencies [ k ] === "string" ) && json ; } static getConfigAsync ( pkgTargetVersion , id , fullVers ) { return Promise . resolve ( ) . then ( ( ) => { if ( pxt . github . isGithubId ( fullVers ) ) { const repoInfo = pxt . github . parseRepoId ( fullVers ) ; return pxt . packagesConfigAsync ( ) . then ( config => pxt . github . repoAsync ( repoInfo . fullName , config ) ) . then ( gitRepo => gitRepo ? pxt . github . pkgConfigAsync ( repoInfo . fullName , repoInfo . tag ) : null ) ; } else { const updatedRef = pxt . patching . upgradePackageReference ( pkgTargetVersion , id , fullVers ) ; const bundledPkg = pxt . appTarget . bundledpkgs [ updatedRef ] ; return JSON . parse ( bundledPkg [ CONFIG_NAME ] ) as pxt . PackageConfig ; } } ) ; } static corePackages ( ) { const pkgs = pxt . appTarget . bundledpkgs ; return Object . keys ( pkgs ) . map ( id => JSON . parse ( pkgs [ id ] [ pxt . CONFIG_NAME ] ) as pxt . PackageConfig ) . filter ( cfg => ! ! cfg ) ; } public addedBy : Package [ ] ; public config : PackageConfig ; public level = - 1 ; public isLoaded = false ; private resolvedVersion : string ; public ignoreTests = false ; public cppOnly = false ; public installedVersion : string ; constructor ( public id , public _verspec , public parent , addedBy ) { if ( addedBy ) { this . level = addedBy . level + 1 ; } this . addedBy = [ addedBy ] ; } disablesVariant ( v ) { return this . config && this . config . disablesVariants && this . config . disablesVariants . indexOf ( v ) >= 0 ; } invalid ( ) { return /^invalid:/ . test ( this . version ( ) ) ; } version ( ) { return this . resolvedVersion || this . _verspec ; } verProtocol ( ) { let spl = this . version ( ) . split ( ':' ) ; if ( spl . length > 1 ) return spl [ 0 ] ; else return "" ; } verArgument ( ) { let p = this . verProtocol ( ) ; if ( p ) return this . version ( ) . slice ( p . length + 1 ) ; return this . version ( ) ; } targetVersion ( ) { return ( this . parent && this . parent != < Package > this ) ? this . parent . targetVersion ( ) : this . config . targetVersions ? this . config . targetVersions . target : undefined ; } commonDownloadAsync ( ) { const proto = this . verProtocol ( ) ; if ( proto == "pub" ) { return Cloud . downloadScriptFilesAsync ( this . verArgument ( ) ) ; } else if ( proto == "github" ) { return pxt . packagesConfigAsync ( ) . then ( config => pxt . github . downloadPackageAsync ( this . verArgument ( ) , config ) ) . then ( resp => resp . files ) ; } else if ( proto == "embed" ) { const resp = pxt . getEmbeddedScript ( this . verArgument ( ) ) ; return Promise . resolve ( resp ) ; } else if ( proto == "pkg" ) { const src = this . parent || this ; const pkgFilesSrc = src . readFile ( this . verArgument ( ) ) ; const pkgFilesJson = ts . pxtc . Util . jsonTryParse ( pkgFilesSrc ) as Map < string > ; if ( ! pkgFilesJson ) pxt . log ( `unable to find ${ this . verArgument ( ) }` ) ; return Promise . resolve ( pkgFilesJson ) ; } else return Promise . resolve ( null as Map < string > ) ; } host ( ) { return this . parent . _host ; } readFile ( fn ) { return this . host ( ) . readFile ( this , fn ) ; } readGitJson ( ) { const gitJsonText = this . readFile ( pxt . github . GIT_JSON ) ; return pxt . Util . jsonTryParse ( gitJsonText ) as pxt . github . GitJson ; } resolveDep ( id ) { if ( this . parent . deps . hasOwnProperty ( id ) ) return this . parent . deps [ id ] ; return null ; } saveConfig ( ) { const cfg = U . clone ( this . config ) ; delete cfg . additionalFilePaths ; const text = pxt . Package . stringifyConfig ( cfg ) ; this . host ( ) . writeFile ( this , pxt . CONFIG_NAME , text ) ; } setPreferredEditor ( editor ) { if ( this . config . preferredEditor != editor ) { this . config . preferredEditor = editor ; this . saveConfig ( ) ; } } getPreferredEditor ( ) { let editor = this . config . preferredEditor ; if ( ! editor ) { const hasMainBlocks = this . getFiles ( ) . indexOf ( pxt . MAIN_BLOCKS ) >= 0 ; if ( ! hasMainBlocks ) return pxt . JAVASCRIPT_PROJECT_NAME ; const mainBlocks = this . readFile ( pxt . MAIN_BLOCKS ) ; const mainTs = this . readFile ( pxt . MAIN_TS ) ; if ( ! mainBlocks && mainTs ) return pxt . JAVASCRIPT_PROJECT_NAME ; return pxt . BLOCKS_PROJECT_NAME ; } return editor ; } parseJRes ( allres = { } ) { for ( const f of this . getFiles ( ) ) { if ( U . endsWith ( f , ".jres" ) ) { inflateJRes ( JSON . parse ( this . readFile ( f ) ) , allres ) ; } } return allres ; } private resolveVersionAsync ( ) { let v = this . _verspec ; if ( getEmbeddedScript ( this . id ) ) { this . resolvedVersion = v = "embed:" + this . id ; } else if ( ! v || v == "*" ) { this . configureAsInvalidPackage ( lf ( "version not specified for {0}" , this . id ) ) ; v = this . _verspec ; } else if ( this . verProtocol ( ) == "github" ) { const ghid = pxt . github . parseRepoId ( this . _verspec ) ; if ( ghid && ! ghid . tag && this . parent ) { pxt . debug ( `dep: unbound github extensions, trying to resolve tag` ) ; const others = pxt . semver . sortLatestTags ( Util . values ( this . parent ?. deps || { } ) . map ( dep => pxt . github . parseRepoId ( dep . version ( ) ) ) . filter ( v => v ?. slug === ghid . slug ) . map ( v => v . tag ) ) ; const best = others [ 0 ] ; if ( best ) { ghid . tag = best ; this . resolvedVersion = v = pxt . github . stringifyRepo ( ghid ) ; pxt . debug ( `dep: github patched ${ this . _verspec } to ${ v }` ) ; } } } return Promise . resolve ( v ) ; } private downloadAsync ( ) { return this . resolveVersionAsync ( ) . then ( verNo => { if ( this . invalid ( ) ) { pxt . debug ( `skip download of invalid package ${ this . id }` ) ; return undefined ; } if ( ! /^embed:/ . test ( verNo ) && this . installedVersion == verNo ) return undefined ; pxt . debug ( 'downloading ' + verNo ) ; return this . host ( ) . downloadPackageAsync ( this ) . then ( ( ) => { this . loadConfig ( ) ; pxt . debug ( `installed ${ this . id } /${ verNo }` ) ; } ) ; } ) ; } loadConfig ( ) { if ( this . level != 0 && this . invalid ( ) ) return ; const confStr = this . readFile ( pxt . CONFIG_NAME ) ; if ( ! confStr ) U . userError ( `extension ${ this . id } is missing ${ pxt . CONFIG_NAME }` ) ; this . parseConfig ( confStr ) ; if ( this . level != 0 ) this . installedVersion = this . version ( ) ; this . saveConfig ( ) ; } protected validateConfig ( ) { if ( ! this . config . dependencies ) U . userError ( "Missing dependencies in config of: " + this . id ) ; if ( ! Array . isArray ( this . config . files ) ) U . userError ( "Missing files in config of: " + this . id ) ; if ( typeof this . config . name != "string" || ! this . config . name ) this . config . name = lf ( "Untitled" ) ; if ( this . config . targetVersions && this . config . targetVersions . target && this . config . targetVersions . targetId === pxt . appTarget . id && appTarget . versions && semver . majorCmp ( this . config . targetVersions . target , appTarget . versions . target ) > 0 ) U . userError ( lf ( "{0} requires target version {1} (you are running {2})" , this . config . name , this . config . targetVersions . target , appTarget . versions . target ) ) ; } isPackageInUse ( pkgId , ts = this . readFile ( pxt . MAIN_TS ) ) { let regex = null ; const upgrades = pxt . patching . computePatches ( this . targetVersion ( ) , "missingPackage" ) ; if ( upgrades ) { upgrades . forEach ( ( rule ) => { Object . keys ( rule . map ) . forEach ( ( match ) => { if ( rule . map [ match ] === pkgId ) { regex = new RegExp ( match , "g" ) ; } } ) ; } ) ; } if ( ! regex ) { regex = new RegExp ( pkgId + "\\." , "g" ) ; } return regex . test ( ts ) ; } private upgradePackagesAsync ( ) { if ( ! this . config ) this . loadConfig ( ) ; return pxt . packagesConfigAsync ( ) . then ( packagesConfig => { let numfixes = 0 ; let fixes = { } ; U . iterMap ( this . config . dependencies , ( pkg , ver ) => { if ( pxt . github . isGithubId ( ver ) ) { const upgraded = pxt . github . upgradedPackageReference ( packagesConfig , ver ) ; if ( upgraded && upgraded != ver ) { pxt . log ( `upgrading dep ${ pkg }: ${ ver } -> ${ upgraded }` ) ; fixes [ ver ] = upgraded ; this . config . dependencies [ pkg ] = upgraded ; numfixes ++ ; } } } ) ; if ( numfixes ) this . saveConfig ( ) ; return numfixes && fixes ; } ) ; } private getMissingPackages ( config , ts ) { const upgrades = pxt . patching . computePatches ( this . targetVersion ( ) , "missingPackage" ) ; const missing = { } ; if ( ts && upgrades ) upgrades . forEach ( rule => { Object . keys ( rule . map ) . forEach ( match => { const regex = new RegExp ( match , 'g' ) ; const pkg = rule . map [ match ] ; ts . replace ( regex , ( m ) => { if ( ! config . dependencies [ pkg ] ) { missing [ pkg ] = "*" ; } return "" ; } ) ; } ) ; } ) ; return missing ; } /**
         * For the given package config or ID, looks through all the currently installed packages to find conflicts in
         * Yotta settings and version spec
         */ findConflictsAsync ( pkgOrId , version ) { let conflicts = [ ] ; let pkgCfg ; return Promise . resolve ( ) . then ( ( ) => { if ( typeof pkgOrId === "string" ) { return Package . getConfigAsync ( this . targetVersion ( ) , pkgOrId , version ) ; } else { return Promise . resolve ( pkgOrId as PackageConfig ) ; } } ) . then ( ( cfg ) => { pkgCfg = cfg ; if ( pkgCfg ) { const yottaCfg = pkgCfg . yotta ? U . jsonFlatten ( pkgCfg . yotta . config ) : null ; this . parent . sortedDeps ( ) . forEach ( ( depPkg ) => { if ( pkgCfg . core && depPkg . config . core && pkgCfg . name != depPkg . config . name ) { const conflict = new cpp . PkgConflictError ( lf ( "conflict between core extensions {0} and {1}" , pkgCfg . name , depPkg . id ) ) ; conflict . pkg0 = depPkg ; conflicts . push ( conflict ) ; return ; } let foundYottaConflict = false ; if ( yottaCfg ) { const depConfig = depPkg . config || JSON . parse ( depPkg . readFile ( CONFIG_NAME ) ) as PackageConfig ; const hasYottaSettings = ! ! depConfig && ! ! depConfig . yotta && ! ! depPkg . config . yotta . config ; if ( hasYottaSettings ) { const depYottaCfg = U . jsonFlatten ( depConfig . yotta . config ) ; for ( const settingName of Object . keys ( yottaCfg ) ) { const depSetting = depYottaCfg [ settingName ] ; const isJustDefaults = pkgCfg . yotta . configIsJustDefaults || depConfig . yotta . configIsJustDefaults ; if ( depYottaCfg . hasOwnProperty ( settingName ) && depSetting !== yottaCfg [ settingName ] && ! isJustDefaults && ( ! depPkg . parent . config . yotta || ! depPkg . parent . config . yotta . ignoreConflicts ) ) { const conflict = new cpp . PkgConflictError ( lf ( "conflict on yotta setting {0} between extensions {1} and {2}" , settingName , pkgCfg . name , depPkg . id ) ) ; conflict . pkg0 = depPkg ; conflict . settingName = settingName ; conflicts . push ( conflict ) ; foundYottaConflict = true ; } } } } if ( ! foundYottaConflict && pkgCfg . name === depPkg . id && depPkg . _verspec !== version && ! /^file:/ . test ( depPkg . _verspec ) && ! /^file:/ . test ( version ) ) { const ghCurrent = /^github:/ . test ( depPkg . _verspec ) && pxt . github . parseRepoId ( depPkg . _verspec ) ; const ghNew = /^github:/ . test ( version ) && pxt . github . parseRepoId ( version ) ; if ( ! ghCurrent || ! ghNew || ghCurrent . fullName !== ghNew . fullName || ( ghNew . tag && pxt . semver . strcmp ( ghCurrent . tag , ghNew . tag ) < 0 ) ) { const conflict = new cpp . PkgConflictError ( lf ( "version mismatch for extension {0} (installed: {1}, installing: {2})" , depPkg . id , depPkg . _verspec , version ) ) ; conflict . pkg0 = depPkg ; conflict . isVersionConflict = true ; conflicts . push ( conflict ) ; } } } ) ; } return Object . keys ( pkgCfg . dependencies ) . reduce ( ( soFar , pkgDep ) => { return soFar . then ( ( ) => this . findConflictsAsync ( pkgDep , pkgCfg . dependencies [ pkgDep ] ) ) . then ( ( childConflicts ) => conflicts . push . apply ( conflicts , childConflicts ) ) ; } , Promise . resolve ( ) ) ; } ) . then ( ( ) => { const allAncestors = ( p ) : Package [ ] => { const ancestors = [ ] ; p . addedBy . forEach ( ( a ) => { if ( a . id !== this . id ) { ancestors . push . apply ( allAncestors ( a ) ) ; ancestors . push ( a ) ; } } ) ; return ancestors ; } ; const additionalConflicts = [ ] ; conflicts . forEach ( ( c ) => { additionalConflicts . push . apply ( additionalConflicts , allAncestors ( c . pkg0 ) . map ( ( anc ) => { const confl = new cpp . PkgConflictError ( c . isVersionConflict ? lf ( "a dependency of {0} has a version mismatch with extension {1} (installed: {1}, installing: {2})" , anc . id , pkgCfg . name , c . pkg0 . _verspec , version ) : lf ( "conflict on yotta setting {0} between extensions {1} and {2}" , c . settingName , pkgCfg . name , c . pkg0 . id ) ) ; confl . pkg0 = anc ; return confl ; } ) ) ; } ) ; conflicts . push . apply ( conflicts , additionalConflicts ) ; conflicts = conflicts . filter ( ( c , index ) => { for ( let i = 0 ; i < index ; ++ i ) { if ( c . pkg0 . id === conflicts [ i ] . pkg0 . id ) { return false ; } } return true ; } ) ; return conflicts ; } ) ; } public configureAsInvalidPackage ( reason ) { pxt . log ( `invalid package ${ this . id }: ${ reason }` ) ; this . _verspec = "invalid:" + this . id ; this . config = < PackageConfig > { name : this . id , description : reason , dependencies : { } , files : [ ] } ; } private parseConfig ( cfgSrc , targetVersion ? ) { try { const cfg = < PackageConfig > JSON . parse ( cfgSrc ) ; this . config = cfg ; } catch ( e ) { this . configureAsInvalidPackage ( lf ( "Syntax error in pxt.json" ) ) ; pxt . tickEvent ( "package.invalidConfigEncountered" ) ; } const currentConfig = JSON . stringify ( this . config ) ; for ( const dep in this . config . dependencies ) { const value = pxt . patching . upgradePackageReference ( this . targetVersion ( ) , dep , this . config . dependencies [ dep ] ) ; if ( value != dep ) { delete this . config . dependencies [ dep ] ; if ( value ) { this . config . dependencies [ value ] = "*" ; } } } if ( targetVersion ) { this . config . targetVersions = { target : targetVersion } ; } if ( JSON . stringify ( this . config ) != currentConfig ) { this . saveConfig ( ) ; } this . validateConfig ( ) ; } private patchCorePackage ( ) { Util . assert ( appTarget . simulator && appTarget . simulator . dynamicBoardDefinition ) ; Util . assert ( this . level == 0 ) ; const corePackages = Object . keys ( this . config . dependencies ) . filter ( dep => ! ! dep && ( dep == pxt . BLOCKS_PROJECT_NAME || dep == pxt . JAVASCRIPT_PROJECT_NAME || ( < pxt . PackageConfig > JSON . parse ( ( pxt . appTarget . bundledpkgs [ dep ] || { } ) [ pxt . CONFIG_NAME ] || "{}" ) . core ) ) ) ; if ( corePackages . length == 0 ) { const allCorePkgs = pxt . Package . corePackages ( ) ; if ( allCorePkgs . length ) this . config . dependencies [ allCorePkgs [ 0 ] . name ] ; } else if ( corePackages . length > 1 ) { corePackages . pop ( ) ; corePackages . forEach ( dep => { pxt . log ( `removing core package ${ dep }` ) ; delete this . config . dependencies [ dep ] ; } ) ; } } resolvedDependencies ( ) { return Object . keys ( this . dependencies ( ) ) . map ( n => this . resolveDep ( n ) ) ; } dependencies ( includeCpp = false ) { if ( ! this . config ) return { } ; const dependencies = Util . clone ( this . config . dependencies || { } ) ; if ( this . level == 0 && this . config . testDependencies ) { Util . iterMap ( this . config . testDependencies , ( k , v ) => { if ( v != "*" || pxt . appTarget . bundledpkgs [ k ] ) dependencies [ k ] = v ; } ) ; } if ( includeCpp && this . config . cppDependencies ) { Util . jsonMergeFrom ( dependencies , this . config . cppDependencies ) ; } return dependencies ; } loadAsync ( isInstall = false , targetVersion ? ) { if ( this . isLoaded ) return Promise . resolve ( ) ; let initPromise = Promise . resolve ( ) ; if ( this . level == 0 && ! pxt . appTarget . multiVariants ) pxt . setAppTargetVariant ( null ) ; this . isLoaded = true ; const str = this . readFile ( pxt . CONFIG_NAME ) ; if ( str == null ) { if ( ! isInstall ) U . userError ( "Package not installed: " + this . id + ", did you forget to run `pxt install`?" ) ; } else { initPromise = initPromise . then ( ( ) => this . parseConfig ( str ) ) ; } if ( this . level == 0 && ! isInstall ) { initPromise = initPromise . then ( ( ) => this . upgradePackagesAsync ( ) . then ( ( ) => { } ) ) ; } if ( isInstall ) initPromise = initPromise . then ( ( ) => this . downloadAsync ( ) ) ; if ( this . level == 0 && isInstall ) { initPromise = initPromise . then ( ( ) => this . upgradePackagesAsync ( ) ) . then ( fixes => { if ( fixes ) { Object . keys ( fixes ) . forEach ( key => pxt . tickEvent ( "package.doubleload" , { "extension" : key } ) ) ; pxt . log ( `upgraded, downloading again` ) ; pxt . debug ( fixes ) ; return this . downloadAsync ( ) ; } else return Promise . resolve ( ) ; } ) ; } if ( appTarget . simulator && appTarget . simulator . dynamicBoardDefinition ) { if ( this . level == 0 ) initPromise = initPromise . then ( ( ) => this . patchCorePackage ( ) ) ; initPromise = initPromise . then ( ( ) => { if ( this . config . compileServiceVariant ) pxt . setAppTargetVariant ( this . config . compileServiceVariant ) ; if ( this . config . files . indexOf ( "board.json" ) < 0 ) return ; const def = appTarget . simulator . boardDefinition = JSON . parse ( this . readFile ( "board.json" ) ) as pxsim . BoardDefinition ; def . id = this . config . name ; appTarget . appTheme . boardName = def . boardName || lf ( "board" ) ; appTarget . appTheme . driveDisplayName = def . driveDisplayName || lf ( "DRIVE" ) ; let expandPkg = ( v ) => { let m = /^pkg:\/\/(.*)/ . exec ( v ) ; if ( m ) { let fn = m [ 1 ] ; let content = this . readFile ( fn ) ; return U . toDataUri ( content , U . getMime ( fn ) ) ; } else { return v ; } } ; let bd = appTarget . simulator . boardDefinition ; if ( typeof bd . visual == "object" ) { let vis = bd . visual as pxsim . BoardImageDefinition ; vis . image = expandPkg ( vis . image ) ; vis . outlineImage = expandPkg ( vis . outlineImage ) ; } } ) ; } const handleVerMismatch = ( mod , ver ) => { pxt . debug ( `version spec mismatch: ${ mod . _verspec } != ${ ver }` ) ; if ( /^github:/ . test ( mod . _verspec ) && /^github:/ . test ( ver ) ) { const modid = pxt . github . parseRepoId ( mod . _verspec ) ; const verid = pxt . github . parseRepoId ( ver ) ; if ( modid ?. slug && modid ?. slug === verid ?. slug ) { const modtag = modid ?. tag || mod . config ?. version ; const c = pxt . semver . strcmp ( modtag , verid . tag ) ; if ( c == 0 ) { pxt . debug ( `resolved version are ${ modtag }` ) ; return ; } else if ( c < 0 ) { pxt . debug ( `auto-upgraded ${ ver } to ${ modtag }` ) ; return ; } } } if ( /^file:/ . test ( mod . _verspec ) || /^file:/ . test ( ver ) ) { pxt . debug ( `ignore file: mismatch issues` ) ; return ; } mod . configureAsInvalidPackage ( lf ( "version mismatch" ) ) ; } ; const loadDepsRecursive = async ( deps , from , isCpp = false ) => { if ( ! deps ) deps = from . dependencies ( isCpp ) ; pxt . debug ( `deps: ${ from . id }->${ Object . keys ( deps ) . join ( ", " ) }` ) ; deps = pxt . github . resolveMonoRepoVersions ( deps ) ; pxt . debug ( `deps: resolved ${ from . id }->${ Object . keys ( deps ) . join ( ", " ) }` ) ; for ( let id of Object . keys ( deps ) ) { let ver = deps [ id ] || "*" ; pxt . debug ( `dep: load ${ from . id }.${ id }${ isCpp ? "++" : "" }: ${ ver }` ) ; if ( id == "hw" && pxt . hwVariant ) id = "hw---" + pxt . hwVariant ; let mod = from . resolveDep ( id ) ; if ( mod ) { if ( ! mod . invalid ( ) && mod . _verspec !== ver ) handleVerMismatch ( mod , ver ) ; if ( mod . invalid ( ) ) { mod . level = Math . min ( mod . level , from . level + 1 ) ; mod . addedBy . push ( from ) ; continue ; } if ( ! isCpp ) { mod . level = Math . min ( mod . level , from . level + 1 ) ; mod . addedBy . push ( from ) ; } } else { let mod = new Package ( id , ver , from . parent , from ) ; if ( isCpp ) mod . cppOnly = true ; from . parent . deps [ id ] = mod ; from . parent . deps [ id . replace ( /---.*/ , "" ) ] = mod ; await mod . loadAsync ( isInstall ) ; } } } ; return initPromise . then ( ( ) => loadDepsRecursive ( null , this ) ) . then ( ( ) => { if ( this . config . palette && appTarget . runtime ) { appTarget . runtime . palette = U . clone ( this . config . palette ) ; if ( this . config . paletteNames ) appTarget . runtime . paletteNames = this . config . paletteNames ; } if ( this . config . screenSize && appTarget . runtime ) appTarget . runtime . screenSize = U . clone ( this . config . screenSize ) ; if ( this . level === 0 ) { const mainTs = this . readFile ( pxt . MAIN_TS ) ; if ( ! mainTs ) return Promise . resolve ( null ) ; const missingPackages = this . getMissingPackages ( this . config , mainTs ) ; let didAddPackages = false ; return Object . keys ( missingPackages ) . reduce ( ( addPackagesPromise , missing ) => { return addPackagesPromise . then ( ( ) => this . findConflictsAsync ( missing , missingPackages [ missing ] ) ) . then ( ( conflicts ) => { if ( conflicts . length ) { const conflictNames = conflicts . map ( ( c ) => c . pkg0 . id ) . join ( ", " ) ; const settingNames = conflicts . map ( ( c ) => c . settingName ) . filter ( ( s ) => ! ! s ) . join ( ", " ) ; pxt . log ( `skipping missing package ${ missing } because it conflicts with the following packages: ${ conflictNames } (conflicting settings: ${ settingNames })` ) ; return Promise . resolve ( null ) ; } else { pxt . log ( `adding missing package ${ missing }` ) ; didAddPackages = true ; this . config . dependencies [ missing ] = "*" ; const addDependency = { } ; addDependency [ missing ] = missingPackages [ missing ] ; return loadDepsRecursive ( addDependency , this ) ; } } ) ; } , Promise . resolve ( null ) ) . then ( ( ) => { if ( didAddPackages ) { this . saveConfig ( ) ; this . validateConfig ( ) ; } return Promise . resolve ( null ) ; } ) ; } return Promise . resolve ( null ) ; } ) . then < any > ( ( ) => { if ( this . level != 0 ) return Promise . resolve ( ) ; return Promise . all ( U . values ( this . parent . deps ) . map ( pkg => loadDepsRecursive ( null , pkg , true ) ) ) ; } ) . then ( ( ) => { pxt . debug ( `  installed ${ this . id }` ) ; } ) ; } static depWarnings : Map < boolean > = { } ; getFiles ( ) { let res ; if ( this . level == 0 && ! this . ignoreTests ) res = this . config . files . concat ( this . config . testFiles || [ ] ) ; else res = this . config . files . slice ( 0 ) ; const fd = this . config . fileDependencies ; if ( this . config . fileDependencies ) res = res . filter ( fn => { const evalCond = ( cond ) : boolean => { cond = cond . trim ( ) ; if ( cond [ 0 ] == '!' ) return ! evalCond ( cond . slice ( 1 ) ) ; if ( /^[\w-]+$/ . test ( cond ) ) { const dep = this . parent . resolveDep ( cond ) ; if ( dep && ! dep . cppOnly ) return true ; return false ; } const m = /^target:(\w+)$/ . exec ( cond ) ; if ( m ) return m [ 1 ] == pxt . appTarget . id || m [ 1 ] == pxt . appTarget . platformid ; if ( ! Package . depWarnings [ cond ] ) { Package . depWarnings [ cond ] = true ; pxt . log ( `invalid dependency expression: ${ cond } in ${ this . id }/${ fn }` ) ; } return false ; } ; const cond = U . lookup ( fd , fn ) ; if ( ! cond || ! cond . trim ( ) ) return true ; return cond . split ( '||' ) . some ( c => c . split ( '&&' ) . every ( evalCond ) ) ; } ) ; return res ; } addSnapshot ( files , exts = [ "" ] ) { for ( let fn of this . getFiles ( ) ) { if ( exts . some ( e => U . endsWith ( fn , e ) ) ) { files [ this . id + "/" + fn ] = this . readFile ( fn ) ; } } files [ this . id + "/" + pxt . CONFIG_NAME ] = this . readFile ( pxt . CONFIG_NAME ) ; } /**
         * Returns localized strings qName -> translation
         */ packageLocalizationStringsAsync ( lang ) { const targetId = pxt . appTarget . id ; const filenames = [ this . config . name + "-jsdoc" , this . config . name ] ; const r = { } ; const theme = pxt . appTarget . appTheme || { } ; if ( this . config . skipLocalization ) return Promise . resolve ( r ) ; if ( pxt . Util . liveLocalizationEnabled ( ) && this . id != "this" && pxt . appTarget . bundledpkgs [ this . id ] ) { pxt . debug ( `loading live translations for ${ this . id }` ) ; return Promise . all ( filenames . map ( fn => pxt . Util . downloadLiveTranslationsAsync ( lang , `${ targetId }/${ fn }-strings.json` , theme . crowdinBranch ) . then ( tr => { if ( tr && Object . keys ( tr ) . length ) { Util . jsonMergeFrom ( r , tr ) ; } else { pxt . tickEvent ( "translations.livetranslationsfailed" , { "filename" : fn } ) ; Util . jsonMergeFrom ( r , this . bundledStringsForFile ( lang , fn ) ) ; } } ) . catch ( e => { pxt . tickEvent ( "translations.livetranslationsfailed" , { "filename" : fn } ) ; pxt . log ( `error while downloading ${ targetId }/${ fn }-strings.json` ) ; Util . jsonMergeFrom ( r , this . bundledStringsForFile ( lang , fn ) ) ; } ) ) ) . then ( ( ) => r ) ; } else { filenames . map ( name => { return this . bundledStringsForFile ( lang , name ) ; } ) . filter ( d => ! ! d ) . forEach ( d => Util . jsonMergeFrom ( r , d ) ) ; return Promise . resolve ( r ) ; } } bundledStringsForFile ( lang , filename ) { let r = { } ; let [ initialLang , baseLang , initialLangLowerCase ] = pxt . Util . normalizeLanguageCode ( lang ) ; const files = this . config . files ; let fn = `_locales/${ initialLang }/${ filename }-strings.json` ; if ( files . indexOf ( fn ) > - 1 ) { r = JSON . parse ( this . readFile ( fn ) ) as Map < string > ; } else if ( initialLangLowerCase ) { fn = `_locales/${ initialLangLowerCase }/${ filename }-strings.json` ; if ( files . indexOf ( fn ) > - 1 ) r = JSON . parse ( this . readFile ( fn ) ) as Map < string > ; else if ( baseLang ) { fn = `_locales/${ baseLang }/${ filename }-strings.json` ; if ( files . indexOf ( fn ) > - 1 ) { r = JSON . parse ( this . readFile ( fn ) ) as Map < string > ; } } } return r ; } } export class MainPackage extends Package { public deps : Map < Package > = { } ; private _jres : Map < JRes > ; constructor ( public _host ) { super ( "this" , "file:." , null , null ) ; this . parent = this ; this . addedBy = [ this ] ; this . level = 0 ; this . deps [ this . id ] = this ; } installAllAsync ( targetVersion ? ) { return this . loadAsync ( true , targetVersion ) ; } sortedDeps ( includeCpp = false ) { let visited = { } ; let ids = [ ] ; const weight = ( p ) => p . config ? Object . keys ( p . config . cppDependencies || { } ) . length : 0 ; const rec = ( p ) => { if ( ! p || U . lookup ( visited , p . id ) ) return ; visited [ p . id ] = true ; const depNames = Object . keys ( p . dependencies ( includeCpp ) ) ; const deps = depNames . map ( id => this . resolveDep ( id ) ) ; deps . sort ( ( a , b ) => weight ( b ) - weight ( a ) || U . strcmp ( a . id , b . id ) ) ; deps . forEach ( rec ) ; ids . push ( p . id ) ; } ; rec ( this ) ; return ids . map ( id => this . resolveDep ( id ) ) ; } localizationStringsAsync ( lang ) { const loc = { } ; return Promise . all ( Util . values ( this . deps ) . map ( dep => dep . packageLocalizationStringsAsync ( lang ) . then ( depLoc => { if ( depLoc ) Object . keys ( depLoc ) . forEach ( k => { if ( ! loc [ k ] ) loc [ k ] = depLoc [ k ] ; } ) ; } ) ) ) . then ( ( ) => { const strings = U . getLocalizedStrings ( ) ; Object . keys ( loc ) . forEach ( ( l ) => { if ( U . startsWith ( l , "{id:subcategory}" ) || U . startsWith ( l , "{id:group}" ) ) { if ( ! strings [ l ] ) { strings [ l ] = loc [ l ] ; } } } ) ; U . setLocalizedStrings ( strings ) ; return Promise . resolve ( loc ) ; } ) ; } getTargetOptions ( ) { let res = U . clone ( appTarget . compile ) ; U . assert ( ! ! res ) ; if ( ! res . utf8 ) { this . sortedDeps ( true ) . forEach ( p => { if ( p . config && p . config . utf8 ) { pxt . debug ( "forcing utf8 mode: pkg=" + p . id ) ; res . utf8 = true ; } } ) ; } return res ; } getJRes ( ) { if ( ! this . _jres ) { this . _jres = { } ; for ( const pkg of this . sortedDeps ( ) ) { pkg . parseJRes ( this . _jres ) ; } const palBuf = ( appTarget . runtime && appTarget . runtime . palette ? appTarget . runtime . palette : [ "#000000" , "#ffffff" ] ) . map ( s => ( "000000" + parseInt ( s . replace ( /#/ , "" ) , 16 ) . toString ( 16 ) ) . slice ( - 6 ) ) . join ( "" ) ; this . _jres [ "__palette" ] = { id : "__palette" , data : palBuf , dataEncoding : "hex" , mimeType : "application/x-palette" } ; } return this . _jres ; } updateJRes ( ) { if ( this . _jres ) { this . parseJRes ( this . _jres ) ; } } private _resolvedBannedCategories : string [ ] ; resolveBannedCategories ( ) { if ( this . _resolvedBannedCategories !== undefined ) return this . _resolvedBannedCategories ; let bannedCategories = [ ] ; if ( pxt . appTarget && pxt . appTarget . runtime && pxt . appTarget . runtime . bannedCategories && pxt . appTarget . runtime . bannedCategories . length ) { bannedCategories = pxt . appTarget . runtime . bannedCategories . slice ( ) ; Object . keys ( this . deps ) . map ( dep => this . deps [ dep ] ) . filter ( dep => ! ! dep ) . map ( pk => pxt . Util . jsonTryParse ( pk . readFile ( pxt . CONFIG_NAME ) ) as pxt . PackageConfig ) . filter ( config => config && config . requiredCategories ) . forEach ( config => config . requiredCategories . forEach ( rc => { const i = bannedCategories . indexOf ( rc ) ; if ( i > - 1 ) bannedCategories . splice ( i , 1 ) ; } ) ) ; this . _resolvedBannedCategories = bannedCategories ; } this . _resolvedBannedCategories = bannedCategories ; if ( ! this . _resolvedBannedCategories . length ) this . _resolvedBannedCategories = null ; return this . _resolvedBannedCategories ; } async getCompileOptionsAsync ( target = this . getTargetOptions ( ) ) { let opts = { sourceFiles : [ ] , fileSystem : { } , target : target , name : this . config ? this . config . name : "" } ; const generateFile = ( fn , cont ) => { if ( this . config . files . indexOf ( fn ) < 0 ) U . userError ( lf ( "please add '{0}' to \"files\" in {1}" , fn , pxt . CONFIG_NAME ) ) ; cont = "// Auto-generated. Do not edit.\n" + cont + "\n// Auto-generated. Do not edit. Really.\n" ; if ( this . host ( ) . readFile ( this , fn , true ) !== cont ) { pxt . debug ( `updating ${ fn } (size=${ cont . length })...` ) ; this . host ( ) . writeFile ( this , fn , cont , true ) ; } } ; let shimsGenerated = false ; const fillExtInfoAsync = async ( variant ) => { const res = { extinfo : null , target : null } ; const prevVariant = pxt . appTargetVariant ; if ( variant ) pxt . setAppTargetVariant ( variant , { temporary : true } ) ; try { let einfo = cpp . getExtensionInfo ( this ) ; if ( ! shimsGenerated && ( einfo . shimsDTS || einfo . enumsDTS ) ) { shimsGenerated = true ; if ( einfo . shimsDTS ) generateFile ( "shims.d.ts" , einfo . shimsDTS ) ; if ( einfo . enumsDTS ) generateFile ( "enums.d.ts" , einfo . enumsDTS ) ; } const inf = target . isNative ? await this . host ( ) . getHexInfoAsync ( einfo ) : null ; einfo = U . flatClone ( einfo ) ; if ( ! target . keepCppFiles ) { delete einfo . compileData ; delete einfo . generatedFiles ; delete einfo . extensionFiles ; } einfo . hexinfo = inf ; res . extinfo = einfo ; res . target = pxt . appTarget . compile ; } finally { if ( variant ) pxt . setAppTargetVariant ( prevVariant , { temporary : true } ) ; } return res ; } ; await this . loadAsync ( ) ; opts . bannedCategories = this . resolveBannedCategories ( ) ; opts . target . preferredEditor = this . getPreferredEditor ( ) ; pxt . debug ( `building: ${ this . sortedDeps ( ) . map ( p => p . config . name ) . join ( ", " ) }` ) ; let variants ; if ( pxt . appTarget . multiVariants ) { if ( pxt . appTargetVariant ) { variants = [ pxt . appTargetVariant ] ; } else if ( pxt . appTarget . alwaysMultiVariant || pxt . appTarget . compile . switches . multiVariant ) { variants = pxt . appTarget . multiVariants ; } else { variants = [ pxt . appTarget . multiVariants [ 0 ] ] ; } } else { variants = [ null ] ; } let ext = null ; for ( let v of variants ) { if ( ext ) pxt . debug ( `building for ${ v }` ) ; const etarget = await fillExtInfoAsync ( v ) ; const einfo = etarget . extinfo ; einfo . appVariant = v ; einfo . outputPrefix = variants . length == 1 || ! v ? "" : v + "-" ; if ( ext ) { opts . otherMultiVariants . push ( etarget ) ; } else { ext = einfo ; opts . otherMultiVariants = [ ] ; } } opts . extinfo = ext ; const noFileEmbed = appTarget . compile . shortPointers || appTarget . compile . nativeType == "vm" || this . config . binaryonly || ! opts . target . isNative ; if ( ! noFileEmbed ) { const files = await this . filesToBePublishedAsync ( true ) ; const headerString = JSON . stringify ( { name : this . config . name , comment : this . config . description , status : "unpublished" , scriptId : this . installedVersion , cloudId : pxt . CLOUD_ID + appTarget . id , editor : this . getPreferredEditor ( ) , targetVersions : pxt . appTarget . versions } ) ; const programText = JSON . stringify ( files ) ; const buf = await lzmaCompressAsync ( headerString + programText ) ; if ( buf ) { opts . embedMeta = JSON . stringify ( { compression : "LZMA" , headerSize : headerString . length , textSize : programText . length , name : this . config . name , eURL : pxt . appTarget . appTheme . embedUrl , eVER : pxt . appTarget . versions ? pxt . appTarget . versions . target : "" , pxtTarget : appTarget . id , } ) ; opts . embedBlob = ts . pxtc . encodeBase64 ( U . uint8ArrayToString ( buf ) ) ; } } for ( const pkg of this . sortedDeps ( ) ) { for ( const f of pkg . getFiles ( ) ) { if ( /\.(ts|asm|py)$/ . test ( f ) ) { let sn = f ; if ( pkg . level > 0 ) sn = "pxt_modules/" + pkg . id + "/" + f ; opts . sourceFiles . push ( sn ) ; opts . fileSystem [ sn ] = pkg . readFile ( f ) ; } } } opts . jres = this . getJRes ( ) ; const functionOpts = pxt . appTarget . runtime && pxt . appTarget . runtime . functionsOptions ; opts . allowedArgumentTypes = functionOpts && functionOpts . extraFunctionEditorTypes && functionOpts . extraFunctionEditorTypes . map ( info => info . typeName ) . concat ( "number" , "boolean" , "string" ) ; return opts ; } private prepareConfigToBePublished ( ) { const cfg = U . clone ( this . config ) ; delete ( < any > cfg ) . installedVersion ; delete cfg . additionalFilePath ; delete cfg . additionalFilePaths ; if ( ! cfg . targetVersions ) cfg . targetVersions = Util . clone ( pxt . appTarget . versions ) ; U . iterMap ( cfg . dependencies , ( k , v ) => { if ( ! v || /^(file|workspace):/ . test ( v ) ) { v = "*" ; try { const d = this . resolveDep ( k ) ; const gitjson = d . readGitJson ( ) ; if ( gitjson && gitjson . repo ) { let parsed = pxt . github . parseRepoId ( gitjson . repo ) ; parsed . tag = gitjson . commit . tag || gitjson . commit . sha ; v = pxt . github . stringifyRepo ( parsed ) ; } } catch ( e ) { } cfg . dependencies [ k ] = v ; } } ) ; return cfg ; } filesToBePublishedAsync ( allowPrivate = false ) { const files = { } ; return this . loadAsync ( ) . then ( ( ) => { if ( ! allowPrivate && ! this . config . public ) U . userError ( 'Only packages with "public":true can be published' ) ; const cfg = this . prepareConfigToBePublished ( ) ; files [ pxt . CONFIG_NAME ] = pxt . Package . stringifyConfig ( cfg ) ; for ( let f of this . getFiles ( ) ) { if ( f == pxt . CONFIG_NAME ) continue ; let str = this . readFile ( f ) ; if ( str == null ) U . userError ( "referenced file missing: " + f ) ; files [ f ] = str ; } return U . sortObjectFields ( files ) ; } ) ; } saveToJsonAsync ( ) { return this . filesToBePublishedAsync ( true ) . then ( files => { const project = { meta : { cloudId : pxt . CLOUD_ID + pxt . appTarget . id , targetVersions : pxt . appTarget . versions , editor : this . getPreferredEditor ( ) , name : this . config . name } , source : JSON . stringify ( files , null , 2 ) } ; return project ; } ) ; } compressToFileAsync ( ) { return this . saveToJsonAsync ( ) . then ( project => pxt . lzmaCompressAsync ( JSON . stringify ( project , null , 2 ) ) ) ; } computePartDefinitions ( parts ) { if ( ! parts || ! parts . length ) return { } ; let res = { } ; this . sortedDeps ( ) . forEach ( d => { let pjson = d . readFile ( "pxtparts.json" ) ; if ( pjson ) { try { let p = JSON . parse ( pjson ) as pxt . Map < pxsim . PartDefinition > ; Object . keys ( p ) . forEach ( k => { if ( parts . indexOf ( k ) >= 0 ) { let part = res [ k ] = p [ k ] ; if ( typeof part . visual . image === "string" && /\.svg$/i . test ( part . visual . image ) ) { let f = d . readFile ( part . visual . image ) ; if ( ! f ) pxt . reportError ( "parts" , "invalid part definition" , { "error" : `missing visual ${ part . visual . image }` } ) ; if ( ! /^data:image\/svg\+xml/ . test ( f ) ) f = `data:image/svg+xml,` + encodeURIComponent ( f ) ; part . visual . image = f ; } } } ) ; } catch ( e ) { pxt . reportError ( "parts" , "invalid pxtparts.json file" ) ; } } } ) ; return res ; } } export function inflateJRes ( js , allres = { } ) { let base = js [ "*" ] || { } as any ; for ( let k of Object . keys ( js ) ) { if ( k == "*" ) continue ; let v = js [ k ] ; if ( typeof v == "string" ) { v = { data : v } as any ; } let ns = v . namespace || base . namespace || "" ; if ( ns ) ns += "." ; let id = v . id || ns + k ; let icon = v . icon ; let mimeType = v . mimeType || base . mimeType ; let dataEncoding = v . dataEncoding || base . dataEncoding || "base64" ; if ( ! icon && dataEncoding == "base64" && ( mimeType == "image/png" || mimeType == "image/jpeg" ) ) { icon = "data:" + mimeType + ";base64," + v . data ; } allres [ id ] = { id , data : v . data , dataEncoding , icon , namespace : ns , mimeType , tilemapTile : v . tilemapTile , displayName : v . displayName , tileset : v . tileset } ; } return allres ; } export function allPkgFiles ( cfg ) { return [ pxt . CONFIG_NAME ] . concat ( cfg . files || [ ] ) . concat ( cfg . testFiles || [ ] ) ; } export function isPkgBeta ( cfg ) { return cfg && /\bbeta\b/ . test ( cfg . description ) ; } }