11import { AzureCliCredential } from "@azure/identity" ;
22import { findWorkspacePackagesNoCheck } from "@pnpm/workspace.find-packages" ;
33import { createTypeSpecBundle } from "@typespec/bundler" ;
4- import { resolve } from "path" ;
4+ import { readFile } from "fs/promises" ;
5+ import { globby } from "globby" ;
6+ import { relative , resolve } from "path" ;
57import { join as joinUnix } from "path/posix" ;
68import pc from "picocolors" ;
79import { parse } from "semver" ;
@@ -16,6 +18,137 @@ function logSuccess(message: string) {
1618 logInfo ( pc . green ( `✔ ${ message } ` ) ) ;
1719}
1820
21+ export interface PlaygroundAssetConfig {
22+ /** Glob pattern relative to the package root (e.g. "generator/dist/pygen-*.whl"). */
23+ path : string ;
24+ /** MIME content type for the blob upload. */
25+ contentType : string ;
26+ }
27+
28+ export interface PlaygroundConfig {
29+ /** Static files to upload as binary blobs. Paths support simple glob patterns. */
30+ assets ?: PlaygroundAssetConfig [ ] ;
31+ /** Peer dependencies that should be bundled and uploaded. */
32+ bundlePeerDependencies ?: string [ ] ;
33+ }
34+
35+ export interface BundleAndUploadStandalonePackageOptions {
36+ /**
37+ * Absolute path to the package directory.
38+ */
39+ packagePath : string ;
40+ }
41+
42+ /**
43+ * Bundle and upload a standalone package that is not part of the pnpm workspace.
44+ * Uploads the bundle files and writes a `latest.json` under the package's blob directory
45+ * (e.g. `@typespec/http-client-csharp/latest.json`).
46+ *
47+ * If the package's `package.json` contains a `playgroundConfig` section, this function
48+ * will also upload static assets (resolved via glob patterns) and bundle peer dependencies.
49+ */
50+ export async function bundleAndUploadStandalonePackage ( {
51+ packagePath,
52+ } : BundleAndUploadStandalonePackageOptions ) {
53+ const pkgJsonPath = resolve ( packagePath , "package.json" ) ;
54+ const pkgJson = JSON . parse ( await readFile ( pkgJsonPath , "utf-8" ) ) ;
55+ const playgroundConfig : PlaygroundConfig | undefined = pkgJson . playgroundConfig ;
56+
57+ const bundle = await createTypeSpecBundle ( packagePath ) ;
58+ const manifest = bundle . manifest ;
59+ logInfo ( `Bundling standalone package: ${ manifest . name } @${ manifest . version } ` ) ;
60+
61+ const uploader = new TypeSpecBundledPackageUploader ( new AzureCliCredential ( ) ) ;
62+ await uploader . createIfNotExists ( ) ;
63+
64+ const result = await uploader . upload ( bundle ) ;
65+ if ( result . status === "uploaded" ) {
66+ logSuccess ( `Bundle for package ${ manifest . name } @${ manifest . version } uploaded.` ) ;
67+ } else {
68+ logInfo ( `Bundle for package ${ manifest . name } already exists for version ${ manifest . version } .` ) ;
69+ }
70+
71+ const importMap : Record < string , string > = { } ;
72+ for ( const [ key , value ] of Object . entries ( result . imports ) ) {
73+ importMap [ joinUnix ( manifest . name , key ) ] = value ;
74+ }
75+
76+ await uploadPlaygroundAssets ( uploader , packagePath , manifest , importMap , playgroundConfig ) ;
77+
78+ await uploader . updatePackageLatest ( manifest . name , {
79+ version : manifest . version ,
80+ imports : importMap ,
81+ } ) ;
82+ logSuccess ( `Updated ${ manifest . name } /latest.json for version ${ manifest . version } .` ) ;
83+ }
84+
85+ /**
86+ * Upload playground assets and bundle peer dependencies based on the provided config.
87+ */
88+ async function uploadPlaygroundAssets (
89+ uploader : TypeSpecBundledPackageUploader ,
90+ packagePath : string ,
91+ manifest : { name : string ; version : string } ,
92+ importMap : Record < string , string > ,
93+ config : PlaygroundConfig | undefined ,
94+ ) {
95+ if ( ! config ) {
96+ return ;
97+ }
98+
99+ // Upload static assets (e.g. .whl files)
100+ if ( config . assets ) {
101+ for ( const asset of config . assets ) {
102+ const matchedFiles = await globby ( asset . path , { cwd : packagePath , absolute : true } ) ;
103+ if ( matchedFiles . length === 0 ) {
104+ logInfo ( pc . yellow ( `⚠ No files matched asset pattern: ${ asset . path } ` ) ) ;
105+ continue ;
106+ }
107+ for ( const filePath of matchedFiles ) {
108+ const relativePath = relative ( packagePath , filePath ) . replace ( / \\ / g, "/" ) ;
109+ const blobPath = joinUnix ( manifest . name , manifest . version , relativePath ) ;
110+ const content = await readFile ( filePath ) ;
111+ const assetResult = await uploader . uploadBinaryAsset ( blobPath , content , asset . contentType ) ;
112+ const importKey = joinUnix ( manifest . name , relativePath ) ;
113+ importMap [ importKey ] = assetResult . url ;
114+ if ( assetResult . status === "uploaded" ) {
115+ logSuccess ( `Uploaded asset: ${ relativePath } ` ) ;
116+ } else {
117+ logInfo ( `Asset already exists: ${ relativePath } ` ) ;
118+ }
119+ }
120+ }
121+ }
122+
123+ // Bundle and upload peer dependencies
124+ if ( config . bundlePeerDependencies ) {
125+ for ( const depName of config . bundlePeerDependencies ) {
126+ const depPath = resolve ( packagePath , "node_modules" , depName ) ;
127+ try {
128+ const depBundle = await createTypeSpecBundle ( depPath ) ;
129+ const depResult = await uploader . upload ( depBundle ) ;
130+ if ( depResult . status === "uploaded" ) {
131+ logSuccess (
132+ `Bundle for peer dep ${ depBundle . manifest . name } @${ depBundle . manifest . version } uploaded.` ,
133+ ) ;
134+ } else {
135+ logInfo (
136+ `Bundle for peer dep ${ depBundle . manifest . name } already exists for version ${ depBundle . manifest . version } .` ,
137+ ) ;
138+ }
139+ for ( const [ key , value ] of Object . entries ( depResult . imports ) ) {
140+ importMap [ joinUnix ( depBundle . manifest . name , key ) ] = value ;
141+ }
142+ } catch ( e : unknown ) {
143+ throw new Error (
144+ `Failed to bundle peer dependency ${ depName } : ${ e instanceof Error ? e . message : e } ` ,
145+ { cause : e } ,
146+ ) ;
147+ }
148+ }
149+ }
150+ }
151+
19152export interface BundleAndUploadPackagesOptions {
20153 repoRoot : string ;
21154 /**
@@ -85,9 +218,12 @@ export async function bundleAndUploadPackages({
85218 }
86219 }
87220 logInfo ( `Import map for ${ indexVersion } :` , importMap ) ;
88- await uploader . updateIndex ( indexName , {
221+ const index = {
89222 version : indexVersion ,
90223 imports : importMap ,
91- } ) ;
224+ } ;
225+ await uploader . updateIndex ( indexName , index ) ;
92226 logSuccess ( `Updated index for version ${ indexVersion } .` ) ;
227+ await uploader . updateLatestIndex ( indexName , index ) ;
228+ logSuccess ( `Updated latest index for version ${ indexVersion } .` ) ;
93229}
0 commit comments