ngx-json-render 0.9.5 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,552 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mcpApp = mcpApp;
4
+ const schematics_1 = require("@angular-devkit/schematics");
5
+ const utility_1 = require("@schematics/angular/utility");
6
+ const node_path_1 = require("node:path");
7
+ const self = require('../../package.json');
8
+ /** Peers the view and the server need. */
9
+ const PEERS = [
10
+ '@json-render/core',
11
+ 'zod',
12
+ '@modelcontextprotocol/ext-apps',
13
+ '@modelcontextprotocol/sdk',
14
+ ];
15
+ /** Origins the Material view loads its fonts from. */
16
+ const MATERIAL_FONTS = [
17
+ 'https://fonts.googleapis.com',
18
+ 'https://fonts.gstatic.com',
19
+ ];
20
+ /**
21
+ * `ng generate ngx-json-render:mcp-app`: an MCP App for a catalog in one go.
22
+ * Adds an application project for the view, a `server.ts` with the
23
+ * `render-ui` tool next to it, and an `mcp` target that builds both with the
24
+ * `ngx-json-render:mcp-app` builder.
25
+ */
26
+ function mcpApp(options) {
27
+ return async (tree, context) => {
28
+ const name = options.name || 'mcp-app';
29
+ const manifest = readManifest(tree);
30
+ const workspace = await (0, utility_1.readWorkspace)(tree);
31
+ if (workspace.projects.has(name)) {
32
+ throw new schematics_1.SchematicsException(`The workspace already has a project named "${name}". Pass another --name.`);
33
+ }
34
+ const material = declares(manifest, 'ngx-json-render-material');
35
+ const source = resolveSource(tree, options, material);
36
+ for (const warning of nodeUnsafeImports(tree, source)) {
37
+ context.logger.warn(warning);
38
+ }
39
+ const rules = [
40
+ (0, schematics_1.externalSchematic)('@schematics/angular', 'application', {
41
+ name,
42
+ routing: false,
43
+ style: source.kind === 'material' ? 'scss' : 'css',
44
+ skipTests: true,
45
+ ssr: false,
46
+ inlineStyle: true,
47
+ inlineTemplate: true,
48
+ }),
49
+ writeFiles(name, source),
50
+ (0, utility_1.updateWorkspace)((workspace) => {
51
+ const project = workspace.projects.get(name);
52
+ for (const target of ['build', 'test']) {
53
+ dropZone(project.targets.get(target)?.options);
54
+ }
55
+ widenBudget(project.targets.get('build')?.configurations?.['production']);
56
+ project.targets.set('mcp', {
57
+ builder: 'ngx-json-render:mcp-app',
58
+ options: {
59
+ buildTarget: `${name}:build:production`,
60
+ outputPath: `dist/${name}`,
61
+ server: node_path_1.posix.join(project.root, 'server.ts'),
62
+ },
63
+ });
64
+ }),
65
+ ];
66
+ for (const peer of PEERS) {
67
+ if (declares(manifest, peer))
68
+ continue;
69
+ const range = self.peerDependencies?.[peer];
70
+ if (!range) {
71
+ throw new schematics_1.SchematicsException(`ngx-json-render declares no peer range for ${peer}.`);
72
+ }
73
+ rules.push((0, utility_1.addDependency)(peer, range));
74
+ context.logger.info(`Adding ${peer}@${range}`);
75
+ }
76
+ rules.push(() => {
77
+ context.logger.info([
78
+ '',
79
+ `The MCP App is in project "${name}". Build it, then run its server:`,
80
+ ` ng run ${name}:mcp`,
81
+ ` node dist/${name}/server.mjs # stdio, for Claude Desktop or Claude Code`,
82
+ ` node dist/${name}/server.mjs --http # http://localhost:3001/mcp`,
83
+ ].join('\n'));
84
+ });
85
+ return (0, schematics_1.chain)(rules);
86
+ };
87
+ }
88
+ function resolveSource(tree, options, material) {
89
+ if (!options.catalog !== !options.registry) {
90
+ throw new schematics_1.SchematicsException('Pass --catalog and --registry together: the server describes the catalog to the model, and the view renders it with the registry.');
91
+ }
92
+ if (options.catalog && options.registry) {
93
+ const catalog = reference(tree, options.catalog, 'catalog');
94
+ const registry = reference(tree, options.registry, 'registry');
95
+ return {
96
+ kind: 'own',
97
+ catalogFile: catalog.file,
98
+ catalogExport: catalog.name,
99
+ registryFile: registry.file,
100
+ registryExport: registry.name,
101
+ };
102
+ }
103
+ if (material) {
104
+ return {
105
+ kind: 'material',
106
+ catalogFile: 'ngx-json-render-material/catalog',
107
+ catalogExport: 'materialCatalog',
108
+ registryFile: 'ngx-json-render-material',
109
+ registryExport: 'materialRegistry',
110
+ };
111
+ }
112
+ return {
113
+ kind: 'starter',
114
+ catalogFile: '',
115
+ catalogExport: 'catalog',
116
+ registryFile: '',
117
+ registryExport: 'registry',
118
+ };
119
+ }
120
+ /** `<file>#<export>`, checked against the tree. */
121
+ function reference(tree, value, fallback) {
122
+ const [path, name = fallback] = value.split('#');
123
+ const file = '/' + node_path_1.posix.normalize(path).replace(/^\/+/, '');
124
+ if (!tree.exists(file)) {
125
+ throw new schematics_1.SchematicsException(`No file at ${path} (from "${value}").`);
126
+ }
127
+ const content = tree.readText(file);
128
+ const exported = new RegExp(`export\\s+(?:const|let|var|function)\\s+(?:\\{[^}]*\\b${name}\\b[^}]*\\}|${name}\\b)|export\\s*\\{[^}]*\\b${name}\\b`);
129
+ if (!exported.test(content)) {
130
+ throw new schematics_1.SchematicsException(`${path} exports no "${name}".`);
131
+ }
132
+ return { file, name };
133
+ }
134
+ /**
135
+ * The server imports the catalog in Node, where Angular components cannot
136
+ * load. Flags the imports that would pull them in.
137
+ */
138
+ function nodeUnsafeImports(tree, source) {
139
+ if (source.kind !== 'own')
140
+ return [];
141
+ const content = tree.readText(source.catalogFile);
142
+ const unsafe = [...content.matchAll(/from\s+['"]([^'"]+)['"]/g)]
143
+ .map((match) => match[1])
144
+ .filter((specifier) => specifier.startsWith('@angular/') ||
145
+ specifier === 'ngx-json-render' ||
146
+ specifier === 'ngx-json-render-material');
147
+ return unsafe.map((specifier) => `${source.catalogFile.slice(1)} imports ${specifier}, which needs Angular, and the MCP server loads the catalog in Node. ` +
148
+ 'Keep the catalog in a file that imports only ngx-json-render/schema (or ngx-json-render-material/catalog), zod and @json-render/core.');
149
+ }
150
+ /** Replaces the application's generated sources with the view and adds the server. */
151
+ function writeFiles(name, source) {
152
+ return async (tree) => {
153
+ const workspace = await (0, utility_1.readWorkspace)(tree);
154
+ const project = workspace.projects.get(name);
155
+ if (!project) {
156
+ throw new schematics_1.SchematicsException(`The project "${name}" was not created.`);
157
+ }
158
+ const root = '/' + project.root.replace(/^\/+/, '');
159
+ const src = '/' + (project.sourceRoot ?? node_path_1.posix.join(project.root, 'src'));
160
+ const appDir = node_path_1.posix.join(src, 'app');
161
+ tree.getDir(appDir).visit((file) => tree.delete(file));
162
+ // A view lives in an iframe, which shows no icon.
163
+ const favicon = node_path_1.posix.join(root, 'public', 'favicon.ico');
164
+ if (tree.exists(favicon))
165
+ tree.delete(favicon);
166
+ for (const style of ['styles.css', 'styles.scss']) {
167
+ const file = node_path_1.posix.join(src, style);
168
+ if (tree.exists(file))
169
+ tree.delete(file);
170
+ }
171
+ let catalog = source.catalogFile;
172
+ let registry = source.registryFile;
173
+ if (source.kind === 'starter') {
174
+ catalog = node_path_1.posix.join(appDir, 'catalog.ts');
175
+ registry = node_path_1.posix.join(appDir, 'registry.ts');
176
+ tree.create(catalog, STARTER_CATALOG);
177
+ tree.create(registry, STARTER_REGISTRY);
178
+ }
179
+ const material = source.kind === 'material';
180
+ const from = (file, specifier) => source.kind === 'material' ? specifier : relativeImport(file, specifier);
181
+ const appFile = node_path_1.posix.join(appDir, 'app.ts');
182
+ tree.create(appFile, viewComponent(name, source.registryExport, from(appFile, registry)));
183
+ overwrite(tree, node_path_1.posix.join(src, 'main.ts'), mainFile(material));
184
+ overwrite(tree, node_path_1.posix.join(src, 'index.html'), indexFile(name, material));
185
+ tree.create(node_path_1.posix.join(src, material ? 'styles.scss' : 'styles.css'), material ? MATERIAL_STYLES : PLAIN_STYLES);
186
+ const serverFile = node_path_1.posix.join(root, 'server.ts');
187
+ tree.create(serverFile, serverModule(name, source.catalogExport, from(serverFile, catalog), material));
188
+ };
189
+ }
190
+ function overwrite(tree, file, content) {
191
+ if (tree.exists(file))
192
+ tree.overwrite(file, content);
193
+ else
194
+ tree.create(file, content);
195
+ }
196
+ /** An import specifier for `target` (a tree path) from `file`, without `.ts`. */
197
+ function relativeImport(file, target) {
198
+ const path = node_path_1.posix
199
+ .relative(node_path_1.posix.dirname(file), target)
200
+ .replace(/\.(?:m|c)?ts$/, '');
201
+ return path.startsWith('.') ? path : `./${path}`;
202
+ }
203
+ /** The view is zoneless; `ng generate application` may still add zone.js. */
204
+ function dropZone(options) {
205
+ const polyfills = options?.['polyfills'];
206
+ if (!options || !Array.isArray(polyfills))
207
+ return;
208
+ const kept = polyfills.filter((entry) => entry !== 'zone.js' && entry !== 'zone.js/testing');
209
+ if (kept.length)
210
+ options['polyfills'] = kept;
211
+ else
212
+ delete options['polyfills'];
213
+ }
214
+ /**
215
+ * The view carries the MCP Apps SDK and zod (about 750 kB, 1.6 MB with
216
+ * Material) and loads from the host as one inlined page, so the 500 kB
217
+ * initial budget of a web app would fail the build.
218
+ */
219
+ function widenBudget(configuration) {
220
+ const budgets = configuration?.['budgets'];
221
+ if (!configuration || !Array.isArray(budgets))
222
+ return;
223
+ // A new array: the workspace writer cannot follow edits inside one.
224
+ configuration['budgets'] = budgets.map((budget) => budget['type'] === 'initial'
225
+ ? { ...budget, maximumWarning: '3MB', maximumError: '5MB' }
226
+ : budget);
227
+ }
228
+ function readManifest(tree) {
229
+ if (!tree.exists('/package.json')) {
230
+ throw new schematics_1.SchematicsException('No package.json at the workspace root.');
231
+ }
232
+ return tree.readJson('/package.json');
233
+ }
234
+ function declares(manifest, name) {
235
+ return !!(manifest.dependencies?.[name] ?? manifest.devDependencies?.[name]);
236
+ }
237
+ function viewComponent(name, registryExport, registryModule) {
238
+ const registry = registryExport === 'registry'
239
+ ? `import { registry } from '${registryModule}';`
240
+ : `import { ${registryExport} as registry } from '${registryModule}';`;
241
+ return `import { Component, effect } from '@angular/core';
242
+ import type { McpUiTheme } from '@modelcontextprotocol/ext-apps';
243
+ import { JsonRenderer } from 'ngx-json-render';
244
+ import { injectJsonRenderApp } from 'ngx-json-render/mcp';
245
+ ${registry}
246
+
247
+ /**
248
+ * The view an MCP host shows in its iframe: the spec the model passed to the
249
+ * \`render-ui\` tool, rendered with the registry. \`mcp.handlers\` handles the
250
+ * \`sendMessage\` action the server describes to the model.
251
+ */
252
+ @Component({
253
+ selector: 'app-root',
254
+ imports: [JsonRenderer],
255
+ template: \`
256
+ @if (mcp.error(); as error) {
257
+ <p class="status error">Could not connect to the host: {{ error.message }}</p>
258
+ } @else if (mcp.spec(); as spec) {
259
+ <json-render
260
+ [spec]="spec"
261
+ [loading]="mcp.loading()"
262
+ [registry]="registry"
263
+ [handlers]="mcp.handlers"
264
+ />
265
+ @if (mcp.lastMessage(); as outcome) {
266
+ @if (outcome.ok) {
267
+ <p class="status" role="status">Message passed to the chat.</p>
268
+ } @else {
269
+ <p class="status error" role="alert">
270
+ Could not send the message: {{ outcome.error.message }}
271
+ </p>
272
+ }
273
+ }
274
+ } @else {
275
+ <p class="status">Waiting for the model's spec…</p>
276
+ }
277
+ \`,
278
+ styles: \`
279
+ :host {
280
+ display: block;
281
+ padding: 16px;
282
+ }
283
+ json-render + .status {
284
+ margin-top: 12px;
285
+ }
286
+ .status {
287
+ margin: 0;
288
+ opacity: 0.7;
289
+ }
290
+ .error {
291
+ color: #b3261e;
292
+ opacity: 1;
293
+ }
294
+ \`,
295
+ })
296
+ export class App {
297
+ readonly mcp = injectJsonRenderApp({ name: '${name}', version: '0.0.0' });
298
+ readonly registry = registry;
299
+
300
+ constructor() {
301
+ // Follow the host's light or dark theme.
302
+ const applyTheme = (theme: McpUiTheme | undefined) => {
303
+ if (theme) document.documentElement.style.colorScheme = theme;
304
+ };
305
+ this.mcp.app.addEventListener('hostcontextchanged', (context) =>
306
+ applyTheme(context.theme),
307
+ );
308
+ effect(() => {
309
+ if (this.mcp.connected()) applyTheme(this.mcp.app.getHostContext()?.theme);
310
+ });
311
+ }
312
+ }
313
+ `;
314
+ }
315
+ function mainFile(material) {
316
+ if (!material) {
317
+ return `import { provideZonelessChangeDetection } from '@angular/core';
318
+ import { bootstrapApplication } from '@angular/platform-browser';
319
+ import { App } from './app/app';
320
+
321
+ bootstrapApplication(App, {
322
+ providers: [provideZonelessChangeDetection()],
323
+ }).catch((err) => console.error(err));
324
+ `;
325
+ }
326
+ return `import {
327
+ inject,
328
+ provideEnvironmentInitializer,
329
+ provideZonelessChangeDetection,
330
+ } from '@angular/core';
331
+ import { MatIconRegistry } from '@angular/material/icon';
332
+ import { bootstrapApplication } from '@angular/platform-browser';
333
+ import { App } from './app/app';
334
+
335
+ bootstrapApplication(App, {
336
+ providers: [
337
+ provideZonelessChangeDetection(),
338
+ // The Material catalog names icons as Material Symbols ligatures.
339
+ provideEnvironmentInitializer(() =>
340
+ inject(MatIconRegistry).setDefaultFontSetClass('material-symbols-outlined'),
341
+ ),
342
+ ],
343
+ }).catch((err) => console.error(err));
344
+ `;
345
+ }
346
+ function indexFile(name, material) {
347
+ const fonts = material
348
+ ? `
349
+ <link
350
+ href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined&family=Roboto:wght@400;500&display=swap"
351
+ rel="stylesheet"
352
+ />`
353
+ : '';
354
+ return `<!doctype html>
355
+ <html lang="en">
356
+ <head>
357
+ <meta charset="utf-8" />
358
+ <title>${name}</title>
359
+ <meta name="viewport" content="width=device-width, initial-scale=1" />${fonts}
360
+ </head>
361
+ <body>
362
+ <app-root></app-root>
363
+ </body>
364
+ </html>
365
+ `;
366
+ }
367
+ const PLAIN_STYLES = `html {
368
+ color-scheme: light dark;
369
+ }
370
+
371
+ body {
372
+ margin: 0;
373
+ background: transparent;
374
+ font: 14px/1.5 system-ui, sans-serif;
375
+ }
376
+ `;
377
+ const MATERIAL_STYLES = `@use '@angular/material' as mat;
378
+
379
+ // Both palettes in one pass; the view sets \`color-scheme\` from the host's
380
+ // theme, which picks one.
381
+ html {
382
+ color-scheme: light dark;
383
+
384
+ @include mat.theme(
385
+ (
386
+ color: mat.$azure-palette,
387
+ typography: Roboto,
388
+ density: 0,
389
+ )
390
+ );
391
+ }
392
+
393
+ body {
394
+ margin: 0;
395
+ background: transparent;
396
+ color: var(--mat-sys-on-surface);
397
+ font-family: Roboto, system-ui, sans-serif;
398
+ }
399
+ `;
400
+ function serverModule(name, catalogExport, catalogModule, material) {
401
+ const catalog = catalogExport === 'catalog'
402
+ ? `import { catalog } from '${catalogModule}';`
403
+ : `import { ${catalogExport} as catalog } from '${catalogModule}';`;
404
+ const csp = material
405
+ ? `
406
+ // The Material view loads its fonts from Google Fonts.
407
+ csp: {
408
+ resourceDomains: [${MATERIAL_FONTS.map((origin) => `'${origin}'`).join(', ')}],
409
+ },`
410
+ : `
411
+ // Origins the view may load from or connect to; the host allows none
412
+ // without them: csp: { resourceDomains: [...], connectDomains: [...] }.`;
413
+ return `// The MCP server for the view: one \`render-ui\` tool for the catalog, and
414
+ // the view as its \`ui://\` resource. \`ng run ${name}:mcp\` bundles it into
415
+ // dist/${name}/server.mjs, next to view.html.
416
+ //
417
+ // node dist/${name}/server.mjs stdio, for Claude Desktop or Claude Code
418
+ // node dist/${name}/server.mjs --http http://localhost:3001/mcp (PORT to change)
419
+ import { readFileSync } from 'node:fs';
420
+ import { createServer } from 'node:http';
421
+ import { Readable } from 'node:stream';
422
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
423
+ import {
424
+ type RenderUiServerOptions,
425
+ createRenderUiServer,
426
+ handleRenderUiRequest,
427
+ } from 'ngx-json-render/mcp/server';
428
+ ${catalog}
429
+
430
+ const options: RenderUiServerOptions = {
431
+ catalog,
432
+ html: readFileSync(new URL('./view.html', import.meta.url), 'utf8'),
433
+ name: '${name}',
434
+ version: '0.0.0',
435
+ // Lets a button in the view post a message to the chat as the user.
436
+ sendMessage: true,${csp}
437
+ };
438
+
439
+ if (process.argv.includes('--http')) {
440
+ const port = Number(process.env['PORT'] ?? 3001);
441
+ createServer(async (req, res) => {
442
+ const hasBody = req.method !== 'GET' && req.method !== 'HEAD';
443
+ const request = new Request(\`http://localhost\${req.url}\`, {
444
+ method: req.method,
445
+ headers: req.headers as Record<string, string>,
446
+ body: hasBody ? (Readable.toWeb(req) as ReadableStream) : undefined,
447
+ duplex: 'half',
448
+ } as RequestInit);
449
+ const response = await handleRenderUiRequest(options, request);
450
+ res.writeHead(response.status, Object.fromEntries(response.headers));
451
+ res.end(Buffer.from(await response.arrayBuffer()));
452
+ }).listen(port, () => console.error(\`MCP on http://localhost:\${port}/mcp\`));
453
+ } else {
454
+ await createRenderUiServer(options).connect(new StdioServerTransport());
455
+ }
456
+ `;
457
+ }
458
+ const STARTER_CATALOG = `// The components the model may use in the view. The MCP server imports this
459
+ // file in Node, so it imports no Angular: components live in registry.ts.
460
+ import { schema } from 'ngx-json-render/schema';
461
+ import { z } from 'zod';
462
+
463
+ export const catalog = schema.createCatalog({
464
+ components: {
465
+ Card: {
466
+ props: z.object({ title: z.string().optional() }),
467
+ slots: ['default'],
468
+ description: 'A card that groups its children under an optional title.',
469
+ },
470
+ Text: {
471
+ props: z.object({ content: z.string() }),
472
+ slots: [],
473
+ description: 'A paragraph of text.',
474
+ },
475
+ Button: {
476
+ props: z.object({ label: z.string() }),
477
+ slots: [],
478
+ description: "A button that emits 'press'.",
479
+ },
480
+ },
481
+ actions: {},
482
+ });
483
+ `;
484
+ const STARTER_REGISTRY = `import { Component } from '@angular/core';
485
+ import {
486
+ type InferComponentProps,
487
+ JrChildren,
488
+ defineRegistry,
489
+ injectRenderContext,
490
+ } from 'ngx-json-render';
491
+ import { catalog } from './catalog';
492
+
493
+ @Component({
494
+ selector: 'app-card',
495
+ imports: [JrChildren],
496
+ template: \`
497
+ <section class="card">
498
+ @if (ctx.props().title; as title) {
499
+ <h3>{{ title }}</h3>
500
+ }
501
+ <jr-children />
502
+ </section>
503
+ \`,
504
+ styles: \`
505
+ .card {
506
+ display: grid;
507
+ gap: 8px;
508
+ padding: 16px;
509
+ border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
510
+ border-radius: 12px;
511
+ }
512
+ h3 {
513
+ margin: 0;
514
+ }
515
+ \`,
516
+ })
517
+ export class CardComponent {
518
+ readonly ctx =
519
+ injectRenderContext<InferComponentProps<typeof catalog, 'Card'>>();
520
+ }
521
+
522
+ @Component({
523
+ selector: 'app-text',
524
+ template: \`<p>{{ ctx.props().content }}</p>\`,
525
+ styles: \`
526
+ p {
527
+ margin: 0;
528
+ }
529
+ \`,
530
+ })
531
+ export class TextComponent {
532
+ readonly ctx =
533
+ injectRenderContext<InferComponentProps<typeof catalog, 'Text'>>();
534
+ }
535
+
536
+ @Component({
537
+ selector: 'app-button',
538
+ template: \`<button (click)="ctx.emit('press')">{{ ctx.props().label }}</button>\`,
539
+ })
540
+ export class ButtonComponent {
541
+ readonly ctx =
542
+ injectRenderContext<InferComponentProps<typeof catalog, 'Button'>>();
543
+ }
544
+
545
+ export const { registry } = defineRegistry(catalog, {
546
+ components: {
547
+ Card: CardComponent,
548
+ Text: TextComponent,
549
+ Button: ButtonComponent,
550
+ },
551
+ });
552
+ `;
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema",
3
+ "$id": "ngx-json-render-mcp-app",
4
+ "title": "ngx-json-render MCP App schematic",
5
+ "description": "Generates an MCP App for a catalog: an Angular view project, its MCP server, and an mcp target that builds both.",
6
+ "type": "object",
7
+ "properties": {
8
+ "name": {
9
+ "type": "string",
10
+ "description": "The name of the view's application project; the build lands in dist/<name>.",
11
+ "default": "mcp-app",
12
+ "$default": {
13
+ "$source": "argv",
14
+ "index": 0
15
+ }
16
+ },
17
+ "catalog": {
18
+ "type": "string",
19
+ "description": "The catalog the model may use, as <file>#<export> relative to the workspace root, such as \"src/app/catalog.ts#catalog\". The server imports it in Node, so the file must not import Angular. Leave it out for the Angular Material catalog when ngx-json-render-material is installed, or a starter catalog otherwise."
20
+ },
21
+ "registry": {
22
+ "type": "string",
23
+ "description": "The registry that renders the catalog, as <file>#<export>, such as \"src/app/registry.ts#registry\". Required with --catalog."
24
+ }
25
+ },
26
+ "required": []
27
+ }
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildMcpApp = buildMcpApp;
4
+ const architect_1 = require("@angular-devkit/architect");
5
+ const node_fs_1 = require("node:fs");
6
+ const node_module_1 = require("node:module");
7
+ const node_path_1 = require("node:path");
8
+ const inline_cjs_1 = require("./inline.cjs");
9
+ /**
10
+ * `ngx-json-render:mcp-app`: runs the view's application build, folds its
11
+ * browser output into one `view.html` (an MCP Apps host loads the view as a
12
+ * single document), and bundles the server entry, if there is one, into
13
+ * `server.mjs` next to it.
14
+ */
15
+ async function buildMcpApp(options, context) {
16
+ const root = context.workspaceRoot;
17
+ const target = (0, architect_1.targetFromTargetString)(options.buildTarget, context.target?.project);
18
+ const run = await context.scheduleTarget(target);
19
+ const result = await run.result;
20
+ await run.stop();
21
+ if (!result.success)
22
+ return result;
23
+ const esbuild = loadEsbuild(root);
24
+ const browserDir = await browserOutput(context, target);
25
+ const view = await (0, inline_cjs_1.inlineView)(browserDir, async (entry) => {
26
+ const { outputFiles } = await esbuild.build({
27
+ entryPoints: [entry],
28
+ bundle: true,
29
+ format: 'esm',
30
+ minify: true,
31
+ write: false,
32
+ logLevel: 'warning',
33
+ });
34
+ return outputFiles[0].text;
35
+ });
36
+ const out = (0, node_path_1.resolve)(root, options.outputPath);
37
+ (0, node_fs_1.mkdirSync)(out, { recursive: true });
38
+ const viewPath = (0, node_path_1.join)(out, 'view.html');
39
+ (0, node_fs_1.writeFileSync)(viewPath, view.html);
40
+ context.logger.info(`Built ${(0, node_path_1.relative)(root, viewPath)} (${Math.round(view.html.length / 1024)} kB)`);
41
+ if (view.leftOut.length > 0) {
42
+ context.logger.warn(`Not in view.html, so the host cannot load them: ${view.leftOut.join(', ')}. ` +
43
+ 'Reference assets from a stylesheet (they are inlined) or from an https: URL the view CSP allows.');
44
+ }
45
+ if (options.server) {
46
+ const serverPath = (0, node_path_1.join)(out, 'server.mjs');
47
+ await esbuild.build({
48
+ entryPoints: [(0, node_path_1.resolve)(root, options.server)],
49
+ outfile: serverPath,
50
+ bundle: true,
51
+ platform: 'node',
52
+ format: 'esm',
53
+ target: 'node20',
54
+ tsconfig: options.tsConfig ? (0, node_path_1.resolve)(root, options.tsConfig) : undefined,
55
+ packages: options.externalPackages ? 'external' : undefined,
56
+ // The view as text, for a server that imports it rather than reading
57
+ // the file next to it.
58
+ loader: { '.html': 'text' },
59
+ // Bundled CommonJS dependencies may call require(); give the ES module
60
+ // one.
61
+ banner: options.externalPackages
62
+ ? undefined
63
+ : {
64
+ js: "import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);",
65
+ },
66
+ logLevel: 'warning',
67
+ });
68
+ context.logger.info(`Built ${(0, node_path_1.relative)(root, serverPath)}`);
69
+ }
70
+ return { success: true, outputPath: out };
71
+ }
72
+ /** Where the application build wrote its browser files. */
73
+ async function browserOutput(context, target) {
74
+ const options = await context.getTargetOptions(target);
75
+ const outputPath = options['outputPath'];
76
+ const base = typeof outputPath === 'string'
77
+ ? outputPath
78
+ : (outputPath?.base ?? (0, node_path_1.join)('dist', target.project));
79
+ const browser = typeof outputPath === 'object' && outputPath.browser !== undefined
80
+ ? outputPath.browser
81
+ : 'browser';
82
+ return (0, node_path_1.resolve)(context.workspaceRoot, base, browser);
83
+ }
84
+ /**
85
+ * esbuild, from `@angular/build` (which every application build already
86
+ * installs at a version it was tested with), or else from the workspace.
87
+ */
88
+ function loadEsbuild(root) {
89
+ const workspace = (0, node_module_1.createRequire)((0, node_path_1.join)(root, 'package.json'));
90
+ try {
91
+ const build = (0, node_path_1.dirname)(workspace.resolve('@angular/build/package.json'));
92
+ return (0, node_module_1.createRequire)((0, node_path_1.join)(build, 'package.json'))('esbuild');
93
+ }
94
+ catch {
95
+ return workspace('esbuild');
96
+ }
97
+ }
98
+ exports.default = (0, architect_1.createBuilder)(buildMcpApp);