From 0e9d54ad3b7299ca4b91150d3b1d081fb14458ea Mon Sep 17 00:00:00 2001
From: Charles Lyding <19598772+clydin@users.noreply.github.com>
Date: Tue, 9 Sep 2025 16:29:58 -0400
Subject: [PATCH 1/2] test(@angular/cli): create validation harness for example
code
This commit introduces a pre-built, minimal, standalone Angular application that will serve as a validation harness for the code snippets in the `find_examples` tool.
This harness, located in `tools/example-validation-harness`, will be used by a new CI script to programmatically compile and type-check all example code. Using a full Angular project environment ensures a high-fidelity validation that covers TypeScript, Angular-specific syntax, and HTML templates.
---
tools/example-validation-harness/angular.json | 28 ++++++++++++++++++
tools/example-validation-harness/package.json | 21 ++++++++++++++
.../example-validation-harness/src/app/app.ts | 16 ++++++++++
.../example-validation-harness/src/index.html | 13 +++++++++
tools/example-validation-harness/src/main.ts | 12 ++++++++
.../tsconfig.app.json | 9 ++++++
.../example-validation-harness/tsconfig.json | 29 +++++++++++++++++++
7 files changed, 128 insertions(+)
create mode 100644 tools/example-validation-harness/angular.json
create mode 100644 tools/example-validation-harness/package.json
create mode 100644 tools/example-validation-harness/src/app/app.ts
create mode 100644 tools/example-validation-harness/src/index.html
create mode 100644 tools/example-validation-harness/src/main.ts
create mode 100644 tools/example-validation-harness/tsconfig.app.json
create mode 100644 tools/example-validation-harness/tsconfig.json
diff --git a/tools/example-validation-harness/angular.json b/tools/example-validation-harness/angular.json
new file mode 100644
index 000000000000..35f3d1368489
--- /dev/null
+++ b/tools/example-validation-harness/angular.json
@@ -0,0 +1,28 @@
+{
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "version": 1,
+ "newProjectRoot": "projects",
+ "projects": {
+ "validation-harness": {
+ "projectType": "application",
+ "root": "",
+ "sourceRoot": "src",
+ "prefix": "app",
+ "architect": {
+ "build": {
+ "builder": "@angular/build:application",
+ "options": {
+ "outputPath": "dist/validation-harness",
+ "index": "src/index.html",
+ "browser": "src/main.ts",
+ "polyfills": ["zone.js"],
+ "tsConfig": "tsconfig.app.json",
+ "assets": [],
+ "styles": [],
+ "scripts": []
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/tools/example-validation-harness/package.json b/tools/example-validation-harness/package.json
new file mode 100644
index 000000000000..7718d33fb50b
--- /dev/null
+++ b/tools/example-validation-harness/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "example-validation-harness",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@angular/common": "^21.0.0-next",
+ "@angular/compiler": "^21.0.0-next",
+ "@angular/core": "^21.0.0-next",
+ "@angular/forms": "^21.0.0-next",
+ "@angular/platform-browser": "^21.0.0-next",
+ "rxjs": "~7.8.0",
+ "tslib": "^2.3.0",
+ "zone.js": "~0.15.0"
+ },
+ "devDependencies": {
+ "@angular/build": "^21.0.0-next",
+ "@angular/cli": "^21.0.0-next",
+ "@angular/compiler-cli": "^21.0.0-next",
+ "typescript": "~5.9.2"
+ }
+}
diff --git a/tools/example-validation-harness/src/app/app.ts b/tools/example-validation-harness/src/app/app.ts
new file mode 100644
index 000000000000..3daae97bd6b1
--- /dev/null
+++ b/tools/example-validation-harness/src/app/app.ts
@@ -0,0 +1,16 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.dev/license
+ */
+
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-root',
+ imports: [],
+ template: '',
+})
+export class App {}
diff --git a/tools/example-validation-harness/src/index.html b/tools/example-validation-harness/src/index.html
new file mode 100644
index 000000000000..82e66848efa8
--- /dev/null
+++ b/tools/example-validation-harness/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ Codestin Search App
+
+
+
+
+
+
+
+
diff --git a/tools/example-validation-harness/src/main.ts b/tools/example-validation-harness/src/main.ts
new file mode 100644
index 000000000000..ed10aa54e41e
--- /dev/null
+++ b/tools/example-validation-harness/src/main.ts
@@ -0,0 +1,12 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.dev/license
+ */
+
+import { bootstrapApplication } from '@angular/platform-browser';
+import { App } from './app/app';
+
+bootstrapApplication(App).catch((err) => console.error(err));
diff --git a/tools/example-validation-harness/tsconfig.app.json b/tools/example-validation-harness/tsconfig.app.json
new file mode 100644
index 000000000000..8426ad9558bd
--- /dev/null
+++ b/tools/example-validation-harness/tsconfig.app.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/app",
+ "types": []
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["src/**/*.spec.ts"]
+}
diff --git a/tools/example-validation-harness/tsconfig.json b/tools/example-validation-harness/tsconfig.json
new file mode 100644
index 000000000000..5aa05d2d231d
--- /dev/null
+++ b/tools/example-validation-harness/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "skipLibCheck": true,
+ "isolatedModules": true,
+ "experimentalDecorators": true,
+ "importHelpers": true,
+ "target": "ES2022",
+ "module": "preserve"
+ },
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "typeCheckHostBindings": true,
+ "strictTemplates": true
+ },
+ "files": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ }
+ ]
+}
From f1726d06f80440de2f05ddf64d2cb21895629c3b Mon Sep 17 00:00:00 2001
From: Charles Lyding <19598772+clydin@users.noreply.github.com>
Date: Wed, 10 Sep 2025 08:46:34 -0400
Subject: [PATCH 2/2] test(@angular/cli): add CI validation for MCP example
code
This commit introduces a new CI check to validate the correctness of code snippets and markdown structure within the `find_examples` markdown files.
To support this, the previous the `example_db_generator.js` script has been refactored and consolidated into a single, unified, mode-driven script: `process_examples.mjs`. This improves maintainability and ensures consistent parsing and validation logic.
The new script provides two new validation modes for CI:
- `validate-structure`: Parses all examples to validate their front matter and ensure they adhere to the required markdown heading structure.
- `validate-code`: Uses a pre-built Angular application harness to perform a full `ng build` on all example code, ensuring compilation and template correctness.
This provides a strong guarantee of the quality and reliability of the code and structure of all examples served by the `find_examples` tool.
---
.github/workflows/pr.yml | 2 +
package.json | 2 +
pnpm-lock.yaml | 12 +-
tools/BUILD.bazel | 5 +-
tools/example_db_generator.bzl | 2 +-
tools/example_db_generator.js | 207 ------------------
tools/process_examples.mjs | 389 +++++++++++++++++++++++++++++++++
7 files changed, 408 insertions(+), 211 deletions(-)
delete mode 100644 tools/example_db_generator.js
create mode 100755 tools/process_examples.mjs
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 9b580c25c0b6..48e38e8332f4 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -59,6 +59,8 @@ jobs:
uses: angular/dev-infra/github-actions/linting/licenses@5043638fd8529765b375831a4679b9013141b326
- name: Check tooling setup
run: pnpm check-tooling-setup
+ - name: Validate MCP Example Markdown
+ run: pnpm check-mcp-examples
- name: Check commit message
# Commit message validation is only done on pull requests as its too late to validate once
# it has been merged.
diff --git a/package.json b/package.json
index bfb5dfecd75f..e91ff0d41443 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
"public-api:update": "node goldens/public-api/manage.js accept",
"ts-circular-deps": "pnpm -s ng-dev ts-circular-deps --config ./scripts/circular-deps-test.conf.mjs",
"check-tooling-setup": "tsc --project .ng-dev/tsconfig.json",
+ "check-mcp-examples": "node tools/process_examples.mjs packages/angular/cli/lib/examples --mode validate-structure",
"diff-release-package": "node --no-warnings=ExperimentalWarning --loader ts-node/esm/transpile-only scripts/diff-release-package.mts"
},
"repository": {
@@ -124,6 +125,7 @@
"listr2": "9.0.3",
"lodash": "^4.17.21",
"magic-string": "0.30.19",
+ "marked": "^16.2.1",
"npm": "^11.0.0",
"prettier": "^3.0.0",
"protractor": "~7.0.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a4f65d28cb96..8fe4bf62d8e3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -253,6 +253,9 @@ importers:
magic-string:
specifier: 0.30.19
version: 0.30.19
+ marked:
+ specifier: ^16.2.1
+ version: 16.2.1
npm:
specifier: ^11.0.0
version: 11.6.0
@@ -6625,6 +6628,11 @@ packages:
resolution: {integrity: sha512-9GjpQcaUXO2xmre8JfALl8Oji8Jpo+SyY2HpqFFPHVczOld/I+JFRx9FkP/uedZzkJlI9uM5t/j6dGJv4BScQw==}
engines: {node: ^20.17.0 || >=22.9.0}
+ marked@16.2.1:
+ resolution: {integrity: sha512-r3UrXED9lMlHF97jJByry90cwrZBBvZmjG1L68oYfuPMW+uDTnuMbyJDymCWwbTE+f+3LhpNDKfpR3a3saFyjA==}
+ engines: {node: '>= 20'}
+ hasBin: true
+
marky@1.3.0:
resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==}
@@ -7543,7 +7551,7 @@ packages:
puppeteer@18.2.1:
resolution: {integrity: sha512-7+UhmYa7wxPh2oMRwA++k8UGVDxh3YdWFB52r9C3tM81T6BU7cuusUSxImz0GEYSOYUKk/YzIhkQ6+vc0gHbxQ==}
engines: {node: '>=14.1.0'}
- deprecated: < 24.10.2 is no longer supported
+ deprecated: < 22.8.2 is no longer supported
q@1.4.1:
resolution: {integrity: sha512-/CdEdaw49VZVmyIDGUQKDDT53c7qBkO6g5CefWz91Ae+l4+cRtcDYwMTXh6me4O8TMldeGHG3N2Bl84V78Ywbg==}
@@ -15859,6 +15867,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ marked@16.2.1: {}
+
marky@1.3.0: {}
math-intrinsics@1.1.0: {}
diff --git a/tools/BUILD.bazel b/tools/BUILD.bazel
index 59c817e3aa06..08e7f3970162 100644
--- a/tools/BUILD.bazel
+++ b/tools/BUILD.bazel
@@ -34,8 +34,9 @@ js_binary(
js_binary(
name = "ng_example_db",
data = [
- "example_db_generator.js",
+ "process_examples.mjs",
+ "//:node_modules/marked",
"//:node_modules/zod",
],
- entry_point = "example_db_generator.js",
+ entry_point = "process_examples.mjs",
)
diff --git a/tools/example_db_generator.bzl b/tools/example_db_generator.bzl
index 7a899e3d8de4..3fbd806a6641 100644
--- a/tools/example_db_generator.bzl
+++ b/tools/example_db_generator.bzl
@@ -8,5 +8,5 @@ def cli_example_db(name, srcs, path, out, data = []):
tool = "//tools:ng_example_db",
progress_message = "Generating code example database from %s" % path,
mnemonic = "NgExampleSqliteDb",
- args = [path, "$(rootpath %s)" % out],
+ args = ["--mode generate-db", "--output $(rootpath %s)" % out, path],
)
diff --git a/tools/example_db_generator.js b/tools/example_db_generator.js
deleted file mode 100644
index dc1f7ba8e3be..000000000000
--- a/tools/example_db_generator.js
+++ /dev/null
@@ -1,207 +0,0 @@
-/**
- * @license
- * Copyright Google LLC All Rights Reserved.
- *
- * Use of this source code is governed by an MIT-style license that can be
- * found in the LICENSE file at https://angular.dev/license
- */
-
-const { globSync, readdirSync, readFileSync, mkdirSync, existsSync, rmSync } = require('node:fs');
-const { resolve, dirname, join } = require('node:path');
-const { DatabaseSync } = require('node:sqlite');
-const { z } = require('zod');
-
-/**
- * A simple YAML front matter parser.
- *
- * This function extracts the YAML block enclosed by `---` at the beginning of a string
- * and parses it into a JavaScript object. It is not a full YAML parser and only
- * supports simple key-value pairs and string arrays.
- *
- * @param content The string content to parse.
- * @returns A record containing the parsed front matter data.
- */
-function parseFrontmatter(content) {
- const match = content.match(/^---\r?\n(.*?)\r?\n---/s);
- if (!match) {
- return {};
- }
-
- const frontmatter = match[1];
- const data = {};
- const lines = frontmatter.split(/\r?\n/);
-
- let currentKey = '';
- let isArray = false;
- const arrayValues = [];
-
- for (const line of lines) {
- const keyValueMatch = line.match(/^([^:]+):\s*(.*)/);
- if (keyValueMatch) {
- if (currentKey && isArray) {
- data[currentKey] = arrayValues.slice();
- arrayValues.length = 0;
- }
-
- const [, key, value] = keyValueMatch;
- currentKey = key.trim();
- isArray = value.trim() === '';
-
- if (!isArray) {
- const trimmedValue = value.trim();
- if (trimmedValue === 'true') {
- data[currentKey] = true;
- } else if (trimmedValue === 'false') {
- data[currentKey] = false;
- } else {
- data[currentKey] = trimmedValue;
- }
- }
- } else {
- const arrayItemMatch = line.match(/^\s*-\s*(.*)/);
- if (arrayItemMatch && currentKey && isArray) {
- arrayValues.push(arrayItemMatch[1].trim());
- }
- }
- }
-
- if (currentKey && isArray) {
- data[currentKey] = arrayValues;
- }
-
- return data;
-}
-
-function generate(inPath, outPath) {
- const dbPath = outPath;
- mkdirSync(dirname(outPath), { recursive: true });
-
- if (existsSync(dbPath)) {
- rmSync(dbPath);
- }
- const db = new DatabaseSync(dbPath);
-
- // Create a relational table to store the structured example data.
- db.exec(`
- CREATE TABLE examples (
- id INTEGER PRIMARY KEY,
- title TEXT NOT NULL,
- summary TEXT NOT NULL,
- keywords TEXT,
- required_packages TEXT,
- related_concepts TEXT,
- related_tools TEXT,
- experimental INTEGER NOT NULL DEFAULT 0,
- content TEXT NOT NULL
- );
- `);
-
- // Create an FTS5 virtual table to provide full-text search capabilities.
- db.exec(`
- CREATE VIRTUAL TABLE examples_fts USING fts5(
- title,
- summary,
- keywords,
- required_packages,
- related_concepts,
- related_tools,
- content,
- content='examples',
- content_rowid='id',
- tokenize = 'porter ascii'
- );
- `);
-
- // Create triggers to keep the FTS table synchronized with the examples table.
- db.exec(`
- CREATE TRIGGER examples_after_insert AFTER INSERT ON examples BEGIN
- INSERT INTO examples_fts(
- rowid, title, summary, keywords, required_packages, related_concepts, related_tools,
- content
- )
- VALUES (
- new.id, new.title, new.summary, new.keywords, new.required_packages,
- new.related_concepts, new.related_tools, new.content
- );
- END;
- `);
-
- const insertStatement = db.prepare(
- 'INSERT INTO examples(' +
- 'title, summary, keywords, required_packages, related_concepts, related_tools, experimental, content' +
- ') VALUES(?, ?, ?, ?, ?, ?, ?, ?);',
- );
-
- const frontmatterSchema = z.object({
- title: z.string(),
- summary: z.string(),
- keywords: z.array(z.string()).optional(),
- required_packages: z.array(z.string()).optional(),
- related_concepts: z.array(z.string()).optional(),
- related_tools: z.array(z.string()).optional(),
- experimental: z.boolean().optional(),
- });
-
- db.exec('BEGIN TRANSACTION');
- const entries = globSync
- ? globSync('**/*.md', { cwd: resolve(inPath), withFileTypes: true })
- : readdirSync(resolve(inPath), { withFileTypes: true });
- for (const entry of entries) {
- if (!entry.isFile() || !entry.name.endsWith('.md')) {
- continue;
- }
-
- const content = readFileSync(join(entry.parentPath, entry.name), 'utf-8');
- const frontmatter = parseFrontmatter(content);
-
- const validation = frontmatterSchema.safeParse(frontmatter);
- if (!validation.success) {
- console.error(`Validation failed for example file: ${entry.name}`);
- console.error('Issues:', validation.error.issues);
- throw new Error(`Invalid front matter in ${entry.name}`);
- }
-
- const {
- title,
- summary,
- keywords,
- required_packages,
- related_concepts,
- related_tools,
- experimental,
- } = validation.data;
- insertStatement.run(
- title,
- summary,
- JSON.stringify(keywords ?? []),
- JSON.stringify(required_packages ?? []),
- JSON.stringify(related_concepts ?? []),
- JSON.stringify(related_tools ?? []),
- experimental ? 1 : 0,
- content,
- );
- }
- db.exec('END TRANSACTION');
-
- db.close();
-}
-
-if (require.main === module) {
- const argv = process.argv.slice(2);
- if (argv.length !== 2) {
- console.error('Must include 2 arguments.');
- process.exit(1);
- }
-
- const [inPath, outPath] = argv;
-
- try {
- generate(inPath, outPath);
- } catch (error) {
- console.error('An error happened:');
- console.error(error);
- process.exit(127);
- }
-}
-
-exports.generate = generate;
diff --git a/tools/process_examples.mjs b/tools/process_examples.mjs
new file mode 100755
index 000000000000..143f8d95ab2f
--- /dev/null
+++ b/tools/process_examples.mjs
@@ -0,0 +1,389 @@
+#!/usr/bin/env node
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.dev/license
+ */
+
+import { marked } from 'marked';
+import { execSync } from 'node:child_process';
+import {
+ cpSync,
+ existsSync,
+ globSync,
+ mkdirSync,
+ mkdtempSync,
+ readFileSync,
+ rmSync,
+ writeFileSync,
+} from 'node:fs';
+import { tmpdir } from 'node:os';
+import { basename, dirname, join, resolve } from 'node:path';
+import { DatabaseSync } from 'node:sqlite';
+import { parseArgs } from 'node:util';
+import { z } from 'zod';
+
+const HARNESS_PATH = resolve(import.meta.dirname, 'example-validation-harness');
+const REQUIRED_HEADINGS = ['Purpose', 'When to Use', 'Key Concepts', 'Example Files'];
+
+/**
+ * A simple YAML front matter parser.
+ *
+ * This function extracts the YAML block enclosed by `---` at the beginning of a string
+ * and parses it into a JavaScript object. It is not a full YAML parser and only
+ * supports simple key-value pairs and string arrays.
+ *
+ * @param content The string content to parse.
+ * @returns A record containing the parsed front matter data.
+ */
+function parseFrontmatter(content) {
+ const match = content.match(/^---\r?\n(.*?)\r?\n---/s);
+ if (!match) {
+ return {};
+ }
+
+ const frontmatter = match[1];
+ const data = {};
+ const lines = frontmatter.split(/\r?\n/);
+
+ let currentKey = '';
+ let isArray = false;
+ const arrayValues = [];
+
+ for (const line of lines) {
+ const keyValueMatch = line.match(/^([^:]+):\s*(.*)/);
+ if (keyValueMatch) {
+ if (currentKey && isArray) {
+ data[currentKey] = arrayValues.slice();
+ arrayValues.length = 0;
+ }
+
+ const [, key, value] = keyValueMatch;
+ currentKey = key.trim();
+ isArray = value.trim() === '';
+
+ if (!isArray) {
+ const trimmedValue = value.trim();
+ if (trimmedValue === 'true') {
+ data[currentKey] = true;
+ } else if (trimmedValue === 'false') {
+ data[currentKey] = false;
+ } else {
+ data[currentKey] = trimmedValue;
+ }
+ }
+ } else {
+ const arrayItemMatch = line.match(/^\s*-\s*(.*)/);
+ if (arrayItemMatch && currentKey && isArray) {
+ arrayValues.push(arrayItemMatch[1].trim());
+ }
+ }
+ }
+
+ if (currentKey && isArray) {
+ data[currentKey] = arrayValues;
+ }
+
+ return data;
+}
+
+/**
+ * Parses a markdown example file into a structured object.
+ * This function performs a single pass over the markdown tokens to extract
+ * front matter, validate the heading structure, and associate code blocks
+ * with their preceding filenames.
+ *
+ * @param {string} filePath The absolute path to the markdown file.
+ * @returns {object} A structured representation of the example.
+ */
+function parseExampleFile(filePath) {
+ const content = readFileSync(filePath, 'utf-8');
+ const tokens = marked.lexer(content);
+
+ const frontmatter = parseFrontmatter(content);
+ if (Object.keys(frontmatter).length === 0) {
+ throw new Error(`Validation failed for ${basename(filePath)}: Missing front matter.`);
+ }
+
+ const sections = [];
+ let currentSection = null;
+ let currentFilename = null;
+
+ for (const token of tokens) {
+ if (token.type === 'heading' && token.depth === 2) {
+ currentSection = {
+ title: token.text,
+ content: '',
+ codeBlocks: [],
+ };
+ sections.push(currentSection);
+ } else if (currentSection) {
+ if (token.type === 'heading' && token.depth === 3) {
+ currentFilename = token.text.replace(/^["'`]|["'`]$/g, '');
+ } else if (token.type === 'code' && currentFilename) {
+ currentSection.codeBlocks.push({
+ filename: currentFilename,
+ lang: token.lang,
+ code: token.text,
+ });
+ currentFilename = null; // Reset after consumption
+ }
+ currentSection.content += token.raw;
+ }
+ }
+
+ // Validate the structure after parsing.
+ const headings = sections.map((s) => s.title);
+ let lastIndex = -1;
+ for (const requiredHeading of REQUIRED_HEADINGS) {
+ const currentIndex = headings.indexOf(requiredHeading);
+ if (currentIndex === -1) {
+ throw new Error(
+ `Validation failed for ${basename(filePath)}: Missing required heading '## ${requiredHeading}'.\n` +
+ `Please refer to docs/FIND_EXAMPLES_FORMAT.md for the correct structure.`,
+ );
+ }
+ if (currentIndex < lastIndex) {
+ throw new Error(
+ `Validation failed for ${basename(filePath)}: Heading '## ${requiredHeading}' is out of order.\n` +
+ `Please refer to docs/FIND_EXAMPLES_FORMAT.md for the correct structure.`,
+ );
+ }
+ lastIndex = currentIndex;
+ }
+
+ return {
+ sourcePath: filePath,
+ frontmatter,
+ sections,
+ };
+}
+
+// --- Mode Implementations ---
+
+function runValidateStructure(examplesPath) {
+ console.log('Validating markdown structure of all example files...');
+ const exampleFiles = globSync('**/*.md', { cwd: examplesPath });
+ for (const file of exampleFiles) {
+ parseExampleFile(join(examplesPath, file));
+ }
+ console.log(`Successfully validated structure of ${exampleFiles.length} files.`);
+}
+
+function runGenerateDb(examplesPath, outputPath) {
+ if (!outputPath) {
+ throw new Error('Missing required argument: --output=');
+ }
+ console.log(`Generating example database at ${outputPath}...`);
+ const dbFiles = globSync('**/*.md', { cwd: examplesPath });
+ const parsedExamplesForDb = dbFiles.map((f) => parseExampleFile(join(examplesPath, f)));
+
+ const dbPath = resolve(outputPath);
+ mkdirSync(dirname(dbPath), { recursive: true });
+ if (existsSync(dbPath)) {
+ rmSync(dbPath);
+ }
+ const db = new DatabaseSync(dbPath);
+
+ // Create tables and triggers
+ db.exec(`
+ CREATE TABLE examples (
+ id INTEGER PRIMARY KEY,
+ title TEXT NOT NULL,
+ summary TEXT NOT NULL,
+ keywords TEXT,
+ required_packages TEXT,
+ related_concepts TEXT,
+ related_tools TEXT,
+ experimental INTEGER NOT NULL DEFAULT 0,
+ content TEXT NOT NULL
+ );
+ `);
+ db.exec(`
+ CREATE VIRTUAL TABLE examples_fts USING fts5(
+ title, summary, keywords, required_packages, related_concepts, related_tools, content,
+ content='examples', content_rowid='id', tokenize = 'porter ascii'
+ );
+ `);
+ db.exec(`
+ CREATE TRIGGER examples_after_insert AFTER INSERT ON examples BEGIN
+ INSERT INTO examples_fts(
+ rowid, title, summary, keywords, required_packages, related_concepts, related_tools, content
+ ) VALUES (
+ new.id, new.title, new.summary, new.keywords, new.required_packages,
+ new.related_concepts, new.related_tools, new.content
+ );
+ END;
+ `);
+
+ const insertStatement = db.prepare(
+ 'INSERT INTO examples(' +
+ 'title, summary, keywords, required_packages, related_concepts, related_tools, experimental, content' +
+ ') VALUES(?, ?, ?, ?, ?, ?, ?, ?);',
+ );
+
+ const frontmatterSchema = z.object({
+ title: z.string(),
+ summary: z.string(),
+ keywords: z.array(z.string()).optional(),
+ required_packages: z.array(z.string()).optional(),
+ related_concepts: z.array(z.string()).optional(),
+ related_tools: z.array(z.string()).optional(),
+ experimental: z.boolean().optional(),
+ });
+
+ db.exec('BEGIN TRANSACTION');
+ for (const example of parsedExamplesForDb) {
+ const validation = frontmatterSchema.safeParse(example.frontmatter);
+ if (!validation.success) {
+ throw new Error(`Invalid front matter in ${example.sourcePath}: ${validation.error.issues}`);
+ }
+ const {
+ title,
+ summary,
+ keywords,
+ required_packages,
+ related_concepts,
+ related_tools,
+ experimental,
+ } = validation.data;
+ insertStatement.run(
+ title,
+ summary,
+ JSON.stringify(keywords ?? []),
+ JSON.stringify(required_packages ?? []),
+ JSON.stringify(related_concepts ?? []),
+ JSON.stringify(related_tools ?? []),
+ experimental ? 1 : 0,
+ readFileSync(example.sourcePath, 'utf-8'),
+ );
+ }
+ db.exec('END TRANSACTION');
+ db.close();
+ console.log(`Successfully generated database with ${parsedExamplesForDb.length} examples.`);
+}
+
+function runValidateCode(examplesPath) {
+ console.log('Setting up validation harness...');
+ const tempDir = mkdtempSync(join(tmpdir(), 'angular-cli-example-validation-'));
+ console.log(`Harness directory: ${tempDir}`);
+
+ try {
+ cpSync(HARNESS_PATH, tempDir, { recursive: true });
+ execSync('pnpm install', { cwd: tempDir, stdio: 'inherit' });
+ console.log('Harness setup complete.');
+
+ console.log('Parsing and validating example files...');
+ const codeFiles = globSync('**/*.md', { cwd: examplesPath });
+ const parsedExamplesForCode = codeFiles.map((f) => parseExampleFile(join(examplesPath, f)));
+ console.log(`Successfully parsed ${parsedExamplesForCode.length} example files.`);
+
+ console.log('Populating harness with example code...');
+ const appDir = join(tempDir, 'src', 'app');
+ const mainTsDir = join(tempDir, 'src');
+ const allBootstrapFiles = [];
+
+ for (const [index, example] of parsedExamplesForCode.entries()) {
+ const exampleDirName = `example-${index}-${basename(example.sourcePath, '.md')}`;
+ const exampleDir = join(appDir, exampleDirName);
+ mkdirSync(exampleDir);
+
+ const exampleFilesSection = example.sections.find((s) => s.title === 'Example Files');
+ if (!exampleFilesSection) {
+ continue;
+ }
+
+ const componentClassNames = [];
+ const componentImports = [];
+
+ for (const block of exampleFilesSection.codeBlocks) {
+ const match = block.code.match(/export\s+class\s+([A-Za-z0-9_]+)/);
+ if (match) {
+ const className = match[1];
+ componentClassNames.push(className);
+ const importPath = `./app/${exampleDirName}/${basename(block.filename, '.ts')}`;
+ componentImports.push(`import { ${className} } from '${importPath}';`);
+ }
+ writeFileSync(join(exampleDir, block.filename), block.code);
+ }
+
+ // Create a unique main.ts file for each example to create an isolated compilation unit.
+ if (componentClassNames.length > 0) {
+ const mainTsContent = `
+ import { bootstrapApplication } from '@angular/platform-browser';
+ ${componentImports.join('\n')}
+
+ // Bootstrap the first component found in the example.
+ bootstrapApplication(${componentClassNames[0]}).catch(err => console.error(err));
+ `;
+ const bootstrapFilename = `main.${index}.ts`;
+ writeFileSync(join(mainTsDir, bootstrapFilename), mainTsContent);
+ allBootstrapFiles.push(bootstrapFilename);
+ }
+ }
+ console.log('Harness population complete.');
+
+ // Update tsconfig to include all generated main files, effectively creating a multi-entry-point compilation.
+ const tsconfigPath = join(tempDir, 'tsconfig.app.json');
+ const tsconfig = JSON.parse(readFileSync(tsconfigPath, 'utf-8'));
+ // We remove the original main.ts and replace it with our generated files.
+ tsconfig.files = allBootstrapFiles.map((f) => `src/${f}`);
+ writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
+ console.log('Updated tsconfig.app.json with all example entry points.');
+
+ console.log('Running Angular build...');
+ execSync('npx ng build', { cwd: tempDir, stdio: 'inherit' });
+ console.log('Build successful!');
+ } finally {
+ console.log('Cleaning up temporary directory...');
+ rmSync(tempDir, { recursive: true, force: true });
+ console.log('Cleanup complete.');
+ }
+}
+
+// --- Main Entry Point ---
+function main() {
+ const { values, positionals } = parseArgs({
+ allowPositionals: true,
+ options: {
+ mode: {
+ type: 'string',
+ default: 'generate-db',
+ },
+ output: {
+ type: 'string',
+ },
+ },
+ });
+ const examplesPath = positionals[0] ?? '.';
+ const mode = values.mode;
+
+ console.log(`Running example processor in '${mode}' mode.`);
+
+ try {
+ switch (mode) {
+ case 'validate-structure':
+ runValidateStructure(examplesPath);
+ break;
+ case 'validate-code':
+ runValidateCode(examplesPath);
+ break;
+ case 'generate-db':
+ runGenerateDb(examplesPath, values.output);
+ break;
+ default:
+ throw new Error(
+ `Unknown mode: ${mode}. Available modes are: validate-structure, validate-code, generate-db.`,
+ );
+ }
+ console.log(`Successfully completed '${mode}' mode.`);
+ } catch (error) {
+ console.error(`\nERROR: Example processing failed in '${mode}' mode.`);
+ console.error(error.message);
+ process.exit(1);
+ }
+}
+
+main();