microsoft/typespec

Public

mirrored from https://github.com/microsoft/typespecAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/fix-8243

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

packages/bundler/src/bundler.ts

287lines · modecode

1import { compile, joinPaths, NodeHost, normalizePath, resolvePath } from "@typespec/compiler";
2import { BuildOptions, BuildResult, context, Plugin } from "esbuild";
3import { nodeModulesPolyfillPlugin } from "esbuild-plugins-node-modules-polyfill";
4import { mkdir, readFile, realpath, writeFile } from "fs/promises";
5import { basename, join, resolve } from "path";
6import { relativeTo } from "./utils.js";
7
8export interface BundleManifest {
9 name: string;
10 version: string;
11 imports: Record<string, string>;
12}
13
14export interface TypeSpecBundleDefinition {
15 path: string;
16 main: string;
17 packageJson: PackageJson;
18 exports: Record<string, string | ExportData>;
19}
20
21export interface ExportData {
22 default?: string;
23 import?: string;
24 types?: string;
25}
26
27export interface TypeSpecBundle {
28 /**
29 * Definition
30 */
31 definition: TypeSpecBundleDefinition;
32
33 /**
34 * Bundle content
35 */
36 files: TypeSpecBundleFile[];
37
38 /**
39 * Resolved manifest.
40 */
41 manifest: BundleManifest;
42}
43
44export interface TypeSpecBundleFile {
45 export?: string;
46 filename: string;
47 content: string;
48}
49
50interface PackageJson {
51 name: string;
52 version: string;
53 main: string;
54 tspMain?: string;
55 peerDependencies: string[];
56 dependencies: string[];
57 exports?: Record<string, string>;
58}
59
60export async function createTypeSpecBundle(libraryPath: string): Promise<TypeSpecBundle> {
61 const definition = await resolveTypeSpecBundleDefinition(libraryPath);
62 const context = await createEsBuildContext(definition);
63 try {
64 const result = await context.rebuild();
65 return resolveTypeSpecBundle(definition, result);
66 } finally {
67 await context.dispose();
68 }
69}
70
71export async function watchTypeSpecBundle(
72 libraryPath: string,
73 onBundle: (bundle: TypeSpecBundle) => void,
74) {
75 const definition = await resolveTypeSpecBundleDefinition(libraryPath);
76 const context = await createEsBuildContext(definition, [
77 {
78 name: "example",
79 setup(build) {
80 build.onEnd((result) => {
81 const bundle = resolveTypeSpecBundle(definition, result);
82 onBundle(bundle);
83 });
84 },
85 },
86 ]);
87 await context.watch();
88}
89
90export async function bundleTypeSpecLibrary(libraryPath: string, outputDir: string) {
91 const bundle = await createTypeSpecBundle(libraryPath);
92 await mkdir(outputDir, { recursive: true });
93 for (const file of bundle.files) {
94 await writeFile(joinPaths(outputDir, file.filename), file.content);
95 }
96 const manifest = createManifest(bundle.definition);
97 await writeFile(joinPaths(outputDir, "manifest.json"), JSON.stringify(manifest, null, 2));
98}
99
100async function resolveTypeSpecBundleDefinition(
101 libraryPath: string,
102): Promise<TypeSpecBundleDefinition> {
103 libraryPath = normalizePath(await realpath(libraryPath));
104 const pkg = await readLibraryPackageJson(libraryPath);
105
106 const exports = pkg.exports
107 ? Object.fromEntries(
108 Object.entries(pkg.exports).filter(
109 ([k, v]) => k !== "." && k !== "./testing" && k !== "./internals",
110 ),
111 )
112 : {};
113
114 return {
115 path: libraryPath,
116 main: pkg.main,
117 exports,
118 packageJson: pkg,
119 };
120}
121
122async function createEsBuildContext(definition: TypeSpecBundleDefinition, plugins: Plugin[] = []) {
123 const libraryPath = definition.path;
124 const program = await compile(NodeHost, libraryPath, {
125 noEmit: true,
126 });
127 const jsFiles = new Set([resolvePath(libraryPath, definition.packageJson.main)]);
128 for (const file of program.jsSourceFiles.keys()) {
129 if (file.startsWith(libraryPath)) {
130 jsFiles.add(file);
131 }
132 }
133 const typespecFiles: Record<string, string> = {
134 [normalizePath(join(libraryPath, "package.json"))]: JSON.stringify(definition.packageJson),
135 };
136
137 for (const [filename, sourceFile] of program.sourceFiles) {
138 typespecFiles[filename] = sourceFile.file.text;
139 }
140
141 const content = createBundleEntrypoint({
142 libraryPath,
143 mainFile: definition.main,
144 jsSourceFileNames: [...jsFiles],
145 typespecSourceFiles: typespecFiles,
146 });
147
148 const extraEntry = Object.fromEntries(
149 Object.entries(definition.exports).map(([key, value]) => {
150 return [
151 key.replace("./", ""),
152 normalizePath(resolve(libraryPath, getExportEntryPoint(value))),
153 ];
154 }),
155 );
156
157 const virtualPlugin: Plugin = {
158 name: "virtual",
159 setup(build) {
160 build.onResolve({ filter: /^virtual:/ }, (args) => {
161 return {
162 path: args.path,
163 namespace: "virtual",
164 };
165 });
166 build.onResolve({ filter: /.*/ }, (args) => {
167 if (
168 definition.packageJson.peerDependencies &&
169 Object.keys(definition.packageJson.peerDependencies).some((x) => args.path.startsWith(x))
170 ) {
171 return { path: args.path, external: true };
172 }
173 return null;
174 });
175
176 build.onLoad({ filter: /^virtual:/, namespace: "virtual" }, async (args) => {
177 return {
178 contents: content,
179 resolveDir: libraryPath,
180 };
181 });
182 },
183 };
184 return await context({
185 write: false,
186 entryPoints: {
187 index: "virtual:entry.js",
188 ...extraEntry,
189 },
190 bundle: true,
191 splitting: true,
192 outdir: "out",
193 platform: "browser",
194 format: "esm",
195 target: "es2024",
196 plugins: [virtualPlugin, nodeModulesPolyfillPlugin({}), ...plugins],
197 });
198}
199
200function resolveTypeSpecBundle(
201 definition: TypeSpecBundleDefinition,
202 result: BuildResult<BuildOptions>,
203): TypeSpecBundle {
204 return {
205 definition,
206 manifest: createManifest(definition),
207 files: result.outputFiles!.map((file) => {
208 const entry = definition.exports[basename(file.path)];
209 return {
210 filename: file.path.replaceAll("\\", "/").split("/out/")[1],
211 content: file.text,
212 export: entry ? getExportEntryPoint(entry) : undefined,
213 };
214 }),
215 };
216}
217
218function getExportEntryPoint(value: string | ExportData) {
219 const resolved = typeof value === "string" ? value : (value.import ?? value.default);
220
221 if (!resolved) {
222 throw new Error(
223 `Exports ${JSON.stringify(value, null, 2)} is missing import or default entrypoint`,
224 );
225 }
226
227 return resolved;
228}
229async function readLibraryPackageJson(path: string): Promise<PackageJson> {
230 const file = await readFile(join(path, "package.json"));
231 return JSON.parse(file.toString());
232}
233
234/**
235 * Create a virtual JS file being the entrypoint of the bundle.
236 */
237function createBundleEntrypoint({
238 libraryPath,
239 mainFile,
240 jsSourceFileNames,
241 typespecSourceFiles,
242}: {
243 mainFile: string;
244 libraryPath: string;
245 jsSourceFileNames: string[];
246 typespecSourceFiles: Record<string, string>;
247}): string {
248 const absoluteMain = normalizePath(resolve(libraryPath, mainFile));
249
250 const relativeTypeSpecFiles: Record<string, string> = {};
251 for (const [name, content] of Object.entries(typespecSourceFiles)) {
252 relativeTypeSpecFiles[relativeTo(libraryPath, name)] = content;
253 }
254 return [
255 `export * from "${absoluteMain}";`,
256 ...jsSourceFileNames.map((x, i) => `import * as f${i} from "${x}";`),
257 "",
258 `const TypeSpecJSSources = {`,
259 ...jsSourceFileNames.map((x, i) => `"${relativeTo(libraryPath, x)}": f${i},`),
260 "};",
261
262 `const TypeSpecSources = ${JSON.stringify(relativeTypeSpecFiles, null, 2)};`,
263
264 "export const _TypeSpecLibrary_ = {",
265 " jsSourceFiles: TypeSpecJSSources,",
266 " typespecSourceFiles: TypeSpecSources,",
267 "};",
268 ].join("\n");
269}
270
271function createManifest(definition: TypeSpecBundleDefinition): BundleManifest {
272 return {
273 name: definition.packageJson.name,
274 version: definition.packageJson.version,
275 imports: createImportMap(definition),
276 };
277}
278
279function createImportMap(definition: TypeSpecBundleDefinition): Record<string, string> {
280 const imports: Record<string, string> = {};
281 imports["."] = `./index.js`;
282 for (const name of Object.keys(definition.exports)) {
283 imports[name] = "./" + resolvePath(name) + ".js";
284 }
285
286 return imports;
287}
288