mirror of
https://github.com/CopilotKit/CopilotKit.git
synced 2026-09-14 16:26:20 +08:00
71d9731d45
Moves the published packages from 0.0.57 to the current AG-UI release across
@ag-ui/client, core, encoder and proto — 27 declarations in 18 packages.
0.0.59 is the first release carrying the subagent protocol surface
(SUBAGENT_STARTED/FINISHED/ERROR, subagentRunId) along with the null-omission
cleanup, so this is the dependency CopilotKit's subagent work needs.
Scope is packages/** plus the release script noted below. The examples and
showcases sit on a spread of older pins (0.0.40 through 0.0.58) and are left
alone.
One behavioural change comes with the bump. channels-core ships
sanitizeAgentEventStream because @ag-ui/client used to reject a TOOL_CALL_START
carrying parentMessageId: null — the shape @ag-ui/langgraph emits for an
interrupt-triggering tool call. 0.0.59 accepts that null and treats it as
absent, so the two tests asserting the run dies WITHOUT the sanitizer no longer
hold. They now assert the run survives, and the one at agent level still checks
the tool call actually arrives so it cannot pass vacuously. The sanitizer is
untouched and its coercion tests are unchanged; it is simply no longer the
thing keeping such a run alive.
The bump also broke the packed Angular consumer matrix. That job generates a
smoke app from scripts/release/lib/angular-package.ts, whose manifest restated
"@ag-ui/client": "0.0.57" as a literal while packages/angular moved to 0.0.59.
pnpm then installed both copies and the app failed to compile:
TS2322: Type 'SmokeAgent' is not assignable to type 'AbstractAgent'.
Types have separate declarations of a private property '_debug'.
The smoke app imports AbstractAgent directly, so it has to resolve the identical
copy the library ships against. Read that version off the packed manifest --
which verify-angular-package.ts already parses for the Angular support contract
-- instead of restating it, so no future AG-UI bump can desynchronise it.
699 lines
20 KiB
TypeScript
699 lines
20 KiB
TypeScript
import { major as semverMajor, satisfies } from "semver";
|
|
|
|
export interface AngularSupportEntry {
|
|
angular: string;
|
|
cdk: string;
|
|
cli: string;
|
|
major: number;
|
|
typescript: string;
|
|
}
|
|
|
|
export interface AngularSupportContract {
|
|
compilerMajor: number;
|
|
rxjs: string;
|
|
testedRxjs: string;
|
|
supportedMajors: AngularSupportEntry[];
|
|
}
|
|
|
|
interface AngularConsumerManifestOptions {
|
|
agUiClient: string;
|
|
angularTarball: string;
|
|
packageManager: string;
|
|
siblingTarballs: ReadonlyMap<string, string>;
|
|
support: AngularSupportEntry;
|
|
testedRxjs: string;
|
|
}
|
|
|
|
export interface DependencyNode {
|
|
name?: string;
|
|
version?: string;
|
|
dependencies?: Record<string, DependencyNode>;
|
|
devDependencies?: Record<string, DependencyNode>;
|
|
optionalDependencies?: Record<string, DependencyNode>;
|
|
}
|
|
|
|
const AG_UI_CLIENT = "@ag-ui/client";
|
|
|
|
const FRAMEWORK_PEERS = [
|
|
"@angular/cdk",
|
|
"@angular/common",
|
|
"@angular/core",
|
|
] as const;
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function requireRecord(value: unknown, path: string): Record<string, unknown> {
|
|
if (!isRecord(value)) throw new Error(`${path} must be an object`);
|
|
return value;
|
|
}
|
|
|
|
function requireString(value: unknown, path: string): string {
|
|
if (typeof value !== "string" || !value.length) {
|
|
throw new Error(`${path} must be a non-empty string`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function requireInteger(value: unknown, path: string): number {
|
|
if (!Number.isInteger(value)) throw new Error(`${path} must be an integer`);
|
|
return value as number;
|
|
}
|
|
|
|
function readSupportEntry(value: unknown, index: number): AngularSupportEntry {
|
|
const path = `copilotkit.angularSupport.supportedMajors[${index}]`;
|
|
const entry = requireRecord(value, path);
|
|
return {
|
|
angular: requireString(entry.angular, `${path}.angular`),
|
|
cdk: requireString(entry.cdk, `${path}.cdk`),
|
|
cli: requireString(entry.cli, `${path}.cli`),
|
|
major: requireInteger(entry.major, `${path}.major`),
|
|
typescript: requireString(entry.typescript, `${path}.typescript`),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Reads the package's single machine-readable Angular compatibility contract.
|
|
*/
|
|
export function readAngularSupportContract(
|
|
manifest: unknown,
|
|
): AngularSupportContract {
|
|
const root = requireRecord(manifest, "package manifest");
|
|
const copilotkit = requireRecord(root.copilotkit, "copilotkit");
|
|
const support = requireRecord(
|
|
copilotkit.angularSupport,
|
|
"copilotkit.angularSupport",
|
|
);
|
|
if (!Array.isArray(support.supportedMajors)) {
|
|
throw new Error(
|
|
"copilotkit.angularSupport.supportedMajors must be an array",
|
|
);
|
|
}
|
|
|
|
return {
|
|
compilerMajor: requireInteger(
|
|
support.compilerMajor,
|
|
"copilotkit.angularSupport.compilerMajor",
|
|
),
|
|
rxjs: requireString(support.rxjs, "copilotkit.angularSupport.rxjs"),
|
|
testedRxjs: requireString(
|
|
support.testedRxjs,
|
|
"copilotkit.angularSupport.testedRxjs",
|
|
),
|
|
supportedMajors: support.supportedMajors.map(readSupportEntry),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Reads the AG-UI client version the package itself ships against. The smoke
|
|
* consumer imports `AbstractAgent` directly, so it must resolve the identical
|
|
* copy; a second one makes the two `AbstractAgent` declarations structurally
|
|
* incompatible over their private fields.
|
|
*/
|
|
export function readAgUiClientDependency(manifest: unknown): string {
|
|
const root = requireRecord(manifest, "package manifest");
|
|
const dependencies = requireRecord(root.dependencies, "dependencies");
|
|
return requireString(
|
|
dependencies[AG_UI_CLIENT],
|
|
`dependencies["${AG_UI_CLIENT}"]`,
|
|
);
|
|
}
|
|
|
|
function peerRange(support: AngularSupportContract): string {
|
|
return support.supportedMajors
|
|
.map((entry) => `^${entry.major}.0.0`)
|
|
.join(" || ");
|
|
}
|
|
|
|
function versionMajor(version: string): number | undefined {
|
|
try {
|
|
return semverMajor(version);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates that published peers, build dependencies, and the CI matrix all
|
|
* express the same Angular compatibility promise.
|
|
*/
|
|
export function validateAngularPackageManifest(manifest: unknown): string[] {
|
|
let support: AngularSupportContract;
|
|
try {
|
|
support = readAngularSupportContract(manifest);
|
|
} catch (error) {
|
|
return [error instanceof Error ? error.message : String(error)];
|
|
}
|
|
|
|
const root = requireRecord(manifest, "package manifest");
|
|
const peers = isRecord(root.peerDependencies) ? root.peerDependencies : {};
|
|
const dev = isRecord(root.devDependencies) ? root.devDependencies : {};
|
|
const problems: string[] = [];
|
|
if (root.sideEffects !== false) {
|
|
problems.push(
|
|
"sideEffects must be false so packed consumers can tree-shake the Angular FESM bundle",
|
|
);
|
|
}
|
|
if (!support.supportedMajors.length) {
|
|
problems.push("supportedMajors must contain at least one entry");
|
|
return problems;
|
|
}
|
|
|
|
for (let index = 0; index < support.supportedMajors.length; index += 1) {
|
|
const entry = support.supportedMajors[index];
|
|
const previous = support.supportedMajors[index - 1];
|
|
if (previous && entry.major <= previous.major) {
|
|
problems.push("supportedMajors must be unique and strictly increasing");
|
|
}
|
|
if (versionMajor(entry.angular) !== entry.major) {
|
|
problems.push(
|
|
`Angular version ${entry.angular} does not match major ${entry.major}`,
|
|
);
|
|
}
|
|
if (versionMajor(entry.cdk) !== entry.major) {
|
|
problems.push(
|
|
`CDK version ${entry.cdk} does not match major ${entry.major}`,
|
|
);
|
|
}
|
|
if (versionMajor(entry.cli) !== entry.major) {
|
|
problems.push(
|
|
`Angular CLI version ${entry.cli} does not match major ${entry.major}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
const floor = support.supportedMajors[0];
|
|
if (support.compilerMajor !== floor.major) {
|
|
problems.push("compilerMajor must equal the lowest supported major");
|
|
}
|
|
|
|
const expectedPeerRange = peerRange(support);
|
|
for (const name of FRAMEWORK_PEERS) {
|
|
if (peers[name] !== expectedPeerRange) {
|
|
problems.push(
|
|
`${name} peer range must be ${expectedPeerRange}; found ${String(peers[name] ?? "missing")}`,
|
|
);
|
|
}
|
|
}
|
|
if (peers.rxjs !== support.rxjs) {
|
|
problems.push(
|
|
`rxjs peer range must be ${support.rxjs}; found ${String(peers.rxjs ?? "missing")}`,
|
|
);
|
|
}
|
|
if (!satisfies(support.testedRxjs, support.rxjs)) {
|
|
problems.push(
|
|
`testedRxjs ${support.testedRxjs} must satisfy the rxjs peer range ${support.rxjs}`,
|
|
);
|
|
}
|
|
|
|
for (const name of [
|
|
"@angular/common",
|
|
"@angular/compiler-cli",
|
|
"@angular/core",
|
|
]) {
|
|
if (dev[name] !== floor.angular) {
|
|
problems.push(
|
|
`${name} dev dependency must use compiler floor ${floor.angular}; found ${String(dev[name] ?? "missing")}`,
|
|
);
|
|
}
|
|
}
|
|
if (dev.typescript !== floor.typescript) {
|
|
problems.push(
|
|
`typescript dev dependency must use compiler floor ${floor.typescript}; found ${String(dev.typescript ?? "missing")}`,
|
|
);
|
|
}
|
|
|
|
return problems;
|
|
}
|
|
|
|
/**
|
|
* Creates a consumer that installs the packed library against one exact
|
|
* framework toolchain. Overrides are restricted to local CopilotKit siblings;
|
|
* Angular itself always resolves normally so peer incompatibilities fail CI.
|
|
*/
|
|
export function createAngularConsumerManifest({
|
|
agUiClient,
|
|
angularTarball,
|
|
packageManager,
|
|
siblingTarballs,
|
|
support,
|
|
testedRxjs,
|
|
}: AngularConsumerManifestOptions): Record<string, unknown> {
|
|
const manifest: Record<string, unknown> = {
|
|
name: `copilotkit-angular-${support.major}-consumer`,
|
|
version: "0.0.0",
|
|
private: true,
|
|
scripts: {
|
|
build: "ng build --configuration=production",
|
|
"serve:ssr": "node dist/smoke/server/server.mjs",
|
|
},
|
|
dependencies: {
|
|
[AG_UI_CLIENT]: agUiClient,
|
|
"@angular/cdk": support.cdk,
|
|
"@angular/common": support.angular,
|
|
"@angular/core": support.angular,
|
|
"@angular/platform-browser": support.angular,
|
|
"@angular/platform-server": support.angular,
|
|
"@angular/router": support.angular,
|
|
"@angular/ssr": support.cli,
|
|
"@copilotkit/angular": `file:${angularTarball}`,
|
|
express: "^5.1.0",
|
|
rxjs: testedRxjs,
|
|
tslib: "^2.8.1",
|
|
zod: "^3.25.75",
|
|
},
|
|
devDependencies: {
|
|
"@angular/build": support.cli,
|
|
"@angular/cli": support.cli,
|
|
"@angular/compiler": support.angular,
|
|
"@angular/compiler-cli": support.angular,
|
|
"@types/express": "^5.0.1",
|
|
"@types/node": "^22.5.1",
|
|
typescript: support.typescript,
|
|
},
|
|
packageManager,
|
|
};
|
|
|
|
manifest.pnpm = {
|
|
overrides: {
|
|
// Angular CLI 22's prompt adapter declares this exact peer while the
|
|
// CLI's own range otherwise resolves a newer incompatible patch.
|
|
listr2: "10.2.1",
|
|
...Object.fromEntries(
|
|
[...siblingTarballs].map(([name, tarball]) => [
|
|
name,
|
|
`file:${tarball}`,
|
|
]),
|
|
),
|
|
},
|
|
};
|
|
|
|
return manifest;
|
|
}
|
|
|
|
/**
|
|
* Creates a strict, production-built, zoneless Angular SSR application that
|
|
* exercises the package's chat, popup, tool-rendering, and cleanup contracts.
|
|
*/
|
|
export function createAngularConsumerSources(): ReadonlyMap<string, string> {
|
|
return new Map([
|
|
[
|
|
"angular.json",
|
|
`${JSON.stringify(
|
|
{
|
|
version: 1,
|
|
projects: {
|
|
smoke: {
|
|
projectType: "application",
|
|
root: "",
|
|
sourceRoot: "src",
|
|
architect: {
|
|
build: {
|
|
builder: "@angular/build:application",
|
|
options: {
|
|
browser: "src/main.ts",
|
|
index: "src/index.html",
|
|
outputPath: "dist/smoke",
|
|
server: "src/main.server.ts",
|
|
ssr: { entry: "src/server.ts" },
|
|
outputMode: "server",
|
|
security: { allowedHosts: [] },
|
|
styles: ["src/styles.css"],
|
|
tsConfig: "tsconfig.app.json",
|
|
},
|
|
configurations: {
|
|
production: {
|
|
optimization: true,
|
|
outputHashing: "all",
|
|
sourceMap: false,
|
|
},
|
|
},
|
|
defaultConfiguration: "production",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
)}\n`,
|
|
],
|
|
[
|
|
"tsconfig.json",
|
|
`${JSON.stringify(
|
|
{
|
|
compilerOptions: {
|
|
strict: true,
|
|
noImplicitOverride: true,
|
|
noImplicitReturns: true,
|
|
noFallthroughCasesInSwitch: true,
|
|
noPropertyAccessFromIndexSignature: true,
|
|
target: "ES2022",
|
|
module: "preserve",
|
|
moduleResolution: "bundler",
|
|
experimentalDecorators: true,
|
|
importHelpers: true,
|
|
isolatedModules: true,
|
|
skipLibCheck: true,
|
|
},
|
|
angularCompilerOptions: {
|
|
strictInjectionParameters: true,
|
|
strictTemplates: true,
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
)}\n`,
|
|
],
|
|
[
|
|
"tsconfig.app.json",
|
|
`${JSON.stringify(
|
|
{
|
|
extends: "./tsconfig.json",
|
|
compilerOptions: {
|
|
outDir: "./out-tsc/app",
|
|
types: ["node"],
|
|
},
|
|
angularCompilerOptions: {
|
|
strictInjectionParameters: true,
|
|
strictTemplates: true,
|
|
},
|
|
include: ["src/**/*.ts"],
|
|
},
|
|
null,
|
|
2,
|
|
)}\n`,
|
|
],
|
|
[
|
|
"src/index.html",
|
|
'<!doctype html><html lang="en"><head><meta charset="utf-8"><title>CopilotKit Angular packed smoke</title><base href="/"></head><body><copilot-smoke></copilot-smoke></body></html>\n',
|
|
],
|
|
[
|
|
"src/main.ts",
|
|
`import { bootstrapApplication } from "@angular/platform-browser";
|
|
import { App } from "./app";
|
|
import { appConfig } from "./app.config";
|
|
|
|
bootstrapApplication(App, appConfig).catch((error: unknown) => {
|
|
console.error(error);
|
|
});
|
|
`,
|
|
],
|
|
[
|
|
"src/main.server.ts",
|
|
`import { type BootstrapContext, bootstrapApplication } from "@angular/platform-browser";
|
|
import { App } from "./app";
|
|
import { serverConfig } from "./app.config.server";
|
|
|
|
const bootstrap = (context: BootstrapContext) =>
|
|
bootstrapApplication(App, serverConfig, context);
|
|
|
|
export default bootstrap;
|
|
`,
|
|
],
|
|
[
|
|
"src/app.config.ts",
|
|
`import {
|
|
type ApplicationConfig,
|
|
provideZonelessChangeDetection,
|
|
} from "@angular/core";
|
|
import { provideClientHydration, withEventReplay } from "@angular/platform-browser";
|
|
import { provideRouter } from "@angular/router";
|
|
import { AbstractAgent, type BaseEvent, type RunAgentInput } from "@ag-ui/client";
|
|
import { provideCopilotKit } from "@copilotkit/angular";
|
|
import { provideMCPApps } from "@copilotkit/angular/mcp-apps";
|
|
import { EMPTY, type Observable } from "rxjs";
|
|
import { z } from "zod";
|
|
import { SmokeToolRenderer } from "./app";
|
|
|
|
class SmokeAgent extends AbstractAgent {
|
|
constructor() {
|
|
super();
|
|
this.agentId = "default";
|
|
}
|
|
|
|
override run(_input: RunAgentInput): Observable<BaseEvent> {
|
|
return EMPTY;
|
|
}
|
|
}
|
|
|
|
export const appConfig: ApplicationConfig = {
|
|
providers: [
|
|
provideZonelessChangeDetection(),
|
|
provideClientHydration(withEventReplay()),
|
|
provideRouter([]),
|
|
provideMCPApps(),
|
|
provideCopilotKit({
|
|
licenseKey: "ck_pub_00000000000000000000000000000000",
|
|
agents: { default: new SmokeAgent() },
|
|
renderToolCalls: [
|
|
{
|
|
name: "packed_smoke_tool",
|
|
args: z.object({ label: z.string() }),
|
|
component: SmokeToolRenderer,
|
|
},
|
|
],
|
|
}),
|
|
],
|
|
};
|
|
`,
|
|
],
|
|
[
|
|
"src/app.config.server.ts",
|
|
`import { type ApplicationConfig, mergeApplicationConfig } from "@angular/core";
|
|
import { provideServerRendering, withRoutes } from "@angular/ssr";
|
|
import { appConfig } from "./app.config";
|
|
import { serverRoutes } from "./app.routes.server";
|
|
|
|
const ssrConfig: ApplicationConfig = {
|
|
providers: [provideServerRendering(withRoutes(serverRoutes))],
|
|
};
|
|
|
|
export const serverConfig = mergeApplicationConfig(appConfig, ssrConfig);
|
|
`,
|
|
],
|
|
[
|
|
"src/app.routes.server.ts",
|
|
`import { RenderMode, type ServerRoute } from "@angular/ssr";
|
|
|
|
export const serverRoutes: ServerRoute[] = [
|
|
{ path: "**", renderMode: RenderMode.Server },
|
|
];
|
|
`,
|
|
],
|
|
[
|
|
"src/server.ts",
|
|
`import {
|
|
AngularNodeAppEngine,
|
|
createNodeRequestHandler,
|
|
isMainModule,
|
|
writeResponseToNodeResponse,
|
|
} from "@angular/ssr/node";
|
|
import express from "express";
|
|
import { join } from "node:path";
|
|
|
|
const browserDistFolder = join(import.meta.dirname, "../browser");
|
|
const app = express();
|
|
const angularApp = new AngularNodeAppEngine();
|
|
|
|
app.use(
|
|
express.static(browserDistFolder, {
|
|
maxAge: "1y",
|
|
index: false,
|
|
redirect: false,
|
|
}),
|
|
);
|
|
|
|
app.use((request, response, next) => {
|
|
angularApp
|
|
.handle(request)
|
|
.then((result) =>
|
|
result ? writeResponseToNodeResponse(result, response) : next(),
|
|
)
|
|
.catch(next);
|
|
});
|
|
|
|
if (isMainModule(import.meta.url) || process.env["pm_id"]) {
|
|
const port = process.env["PORT"] ?? "4000";
|
|
app.listen(port, (error) => {
|
|
if (error) throw error;
|
|
console.log("CopilotKit Angular packed smoke listening on http://127.0.0.1:" + port);
|
|
});
|
|
}
|
|
|
|
export const requestHandler = createNodeRequestHandler(app);
|
|
`,
|
|
],
|
|
[
|
|
"src/app.ts",
|
|
`import {
|
|
ChangeDetectionStrategy,
|
|
Component,
|
|
ElementRef,
|
|
afterNextRender,
|
|
inject,
|
|
input,
|
|
signal,
|
|
} from "@angular/core";
|
|
import type { AssistantMessage, Message } from "@ag-ui/client";
|
|
import {
|
|
type AngularToolCall,
|
|
CopilotKit,
|
|
CopilotPopup,
|
|
RenderToolCalls,
|
|
type ToolRenderer,
|
|
registerFrontendTool,
|
|
} from "@copilotkit/angular";
|
|
import { z } from "zod";
|
|
|
|
interface SmokeToolArgs extends Record<string, unknown> {
|
|
label: string;
|
|
}
|
|
|
|
@Component({
|
|
selector: "packed-smoke-tool-renderer",
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
template: \`
|
|
<output data-testid="tool-renderer">
|
|
{{ toolCall().args.label }}:{{ toolCall().status }}
|
|
</output>
|
|
\`,
|
|
})
|
|
export class SmokeToolRenderer implements ToolRenderer<SmokeToolArgs> {
|
|
readonly toolCall = input.required<AngularToolCall<SmokeToolArgs>>();
|
|
}
|
|
|
|
@Component({
|
|
selector: "packed-lifecycle-probe",
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
template: '<span data-testid="lifecycle-probe">registered</span>',
|
|
})
|
|
class LifecycleProbe {
|
|
constructor() {
|
|
registerFrontendTool({
|
|
name: "packed_lifecycle_tool",
|
|
description: "Verifies destroy-scoped frontend tool cleanup",
|
|
parameters: z.object({ value: z.string() }),
|
|
handler: async ({ value }) => ({ value }),
|
|
});
|
|
}
|
|
}
|
|
|
|
@Component({
|
|
selector: "copilot-smoke",
|
|
imports: [CopilotPopup, LifecycleProbe, RenderToolCalls],
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
template: \`
|
|
<main data-testid="ssr-smoke">
|
|
<h1>CopilotKit Angular packed consumer</h1>
|
|
<p data-testid="lifecycle-count">
|
|
{{ copilotKit.clientToolCallRenderConfigs().length }}
|
|
</p>
|
|
<button type="button" data-testid="destroy-probe" (click)="showProbe.set(false)">
|
|
Destroy lifecycle probe
|
|
</button>
|
|
@if (showProbe()) {
|
|
<packed-lifecycle-probe />
|
|
}
|
|
<copilot-render-tool-calls
|
|
[message]="assistantMessage"
|
|
[messages]="messages"
|
|
/>
|
|
<copilot-popup [(open)]="popupOpen" title="Packed consumer chat" />
|
|
</main>
|
|
\`,
|
|
})
|
|
export class App {
|
|
readonly copilotKit = inject(CopilotKit);
|
|
readonly #host = inject(ElementRef<HTMLElement>);
|
|
readonly popupOpen = signal(false);
|
|
readonly showProbe = signal(true);
|
|
readonly assistantMessage: AssistantMessage = {
|
|
id: "assistant-smoke",
|
|
role: "assistant",
|
|
content: "",
|
|
toolCalls: [
|
|
{
|
|
id: "tool-call-smoke",
|
|
type: "function",
|
|
function: {
|
|
name: "packed_smoke_tool",
|
|
arguments: JSON.stringify({ label: "packed" }),
|
|
},
|
|
},
|
|
],
|
|
};
|
|
readonly messages: Message[] = [
|
|
this.assistantMessage,
|
|
{
|
|
id: "tool-result-smoke",
|
|
role: "tool",
|
|
toolCallId: "tool-call-smoke",
|
|
content: "complete",
|
|
},
|
|
];
|
|
|
|
constructor() {
|
|
afterNextRender(() => {
|
|
this.#host.nativeElement.setAttribute("data-hydrated", "true");
|
|
});
|
|
}
|
|
}
|
|
`,
|
|
],
|
|
[
|
|
"src/styles.css",
|
|
`html { font-family: sans-serif; }
|
|
body { margin: 0; }
|
|
`,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/** Validates that the fixture response contains content rendered on the server. */
|
|
export function validateAngularSsrHtml(html: string): string[] {
|
|
const problems: string[] = [];
|
|
if (!html.includes('data-testid="ssr-smoke"')) {
|
|
problems.push('SSR response is missing data-testid="ssr-smoke"');
|
|
}
|
|
if (!html.includes('data-testid="tool-renderer"')) {
|
|
problems.push('SSR response is missing data-testid="tool-renderer"');
|
|
}
|
|
if (!html.includes("packed:complete")) {
|
|
problems.push("SSR response is missing the completed packed tool result");
|
|
}
|
|
return problems;
|
|
}
|
|
|
|
/** Returns forbidden packages resolved anywhere in a consumer dependency tree. */
|
|
export function findPackageResolutions(
|
|
trees: readonly DependencyNode[],
|
|
forbidden: ReadonlySet<string>,
|
|
): string[] {
|
|
const found = new Set<string>();
|
|
const queue = [...trees];
|
|
|
|
while (queue.length) {
|
|
const node = queue.shift();
|
|
if (!node) break;
|
|
if (node.name && forbidden.has(node.name)) {
|
|
found.add(`${node.name}@${node.version ?? "unknown"}`);
|
|
}
|
|
for (const dependencies of [
|
|
node.dependencies,
|
|
node.devDependencies,
|
|
node.optionalDependencies,
|
|
]) {
|
|
for (const [name, child] of Object.entries(dependencies ?? {})) {
|
|
if (forbidden.has(name)) {
|
|
found.add(`${name}@${child.version ?? "unknown"}`);
|
|
}
|
|
queue.push(child);
|
|
}
|
|
}
|
|
}
|
|
|
|
return [...found].sort();
|
|
}
|