create-ngmd 0.0.1

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.
Files changed (68) hide show
  1. package/README.md +64 -0
  2. package/index.mjs +165 -0
  3. package/package.json +49 -0
  4. package/template/README.md +26 -0
  5. package/template/angular.json +54 -0
  6. package/template/index.html +47 -0
  7. package/template/link-guard.plugin.ts +190 -0
  8. package/template/package.json +76 -0
  9. package/template/page-meta.plugin.ts +132 -0
  10. package/template/public/analog.svg +1 -0
  11. package/template/public/favicon.ico +0 -0
  12. package/template/public/favicon.svg +46 -0
  13. package/template/public/logo-mark.svg +46 -0
  14. package/template/public/logo.svg +53 -0
  15. package/template/public/vite.svg +1 -0
  16. package/template/sitemap.plugin.ts +148 -0
  17. package/template/src/app/app.config.server.ts +10 -0
  18. package/template/src/app/app.config.ts +59 -0
  19. package/template/src/app/app.spec.ts +20 -0
  20. package/template/src/app/app.ts +260 -0
  21. package/template/src/app/components/breadcrumb.ts +76 -0
  22. package/template/src/app/components/code-copy.ts +90 -0
  23. package/template/src/app/components/code-group.ts +70 -0
  24. package/template/src/app/components/command-palette.ts +299 -0
  25. package/template/src/app/components/external-links.ts +55 -0
  26. package/template/src/app/components/heading-anchors.ts +95 -0
  27. package/template/src/app/components/hlm-card.ts +31 -0
  28. package/template/src/app/components/media-enhancer.ts +95 -0
  29. package/template/src/app/components/page-footer.ts +107 -0
  30. package/template/src/app/components/sidebar.ts +65 -0
  31. package/template/src/app/components/toc.ts +131 -0
  32. package/template/src/app/layout-mode.service.ts +10 -0
  33. package/template/src/app/pages/[...not-found].page.ts +47 -0
  34. package/template/src/app/pages/index.page.ts +28 -0
  35. package/template/src/app/pages/welcome.page.ts +18 -0
  36. package/template/src/app/theme.ts +54 -0
  37. package/template/src/app/title-strategy.ts +48 -0
  38. package/template/src/app/ui/alert.ts +46 -0
  39. package/template/src/app/ui/callout.ts +57 -0
  40. package/template/src/app/ui/card.ts +41 -0
  41. package/template/src/app/ui/code-block.ts +76 -0
  42. package/template/src/app/ui/hero.ts +42 -0
  43. package/template/src/app/ui/image.ts +26 -0
  44. package/template/src/app/ui/index.ts +45 -0
  45. package/template/src/app/ui/pill.ts +42 -0
  46. package/template/src/app/ui/tabs.ts +76 -0
  47. package/template/src/app/ui/video.ts +40 -0
  48. package/template/src/app/ui/workflow.ts +51 -0
  49. package/template/src/content/welcome.md +19 -0
  50. package/template/src/main.server.ts +7 -0
  51. package/template/src/main.ts +6 -0
  52. package/template/src/marked-extensions/index.ts +47 -0
  53. package/template/src/marked-extensions/ngmd-code-group.ts +126 -0
  54. package/template/src/marked-extensions/ngmd-code-highlight.ts +102 -0
  55. package/template/src/marked-extensions/ngmd-code-import.ts +118 -0
  56. package/template/src/marked-extensions/ngmd-image.ts +41 -0
  57. package/template/src/marked-extensions/ngmd-keywords.ts +75 -0
  58. package/template/src/marked-extensions/ngmd-video.ts +48 -0
  59. package/template/src/marked-extensions/shiki-shared.ts +42 -0
  60. package/template/src/ngmd.config.ts +98 -0
  61. package/template/src/server/routes/api/v1/hello.ts +3 -0
  62. package/template/src/styles.css +347 -0
  63. package/template/src/test-setup.ts +6 -0
  64. package/template/src/vite-env.d.ts +9 -0
  65. package/template/tsconfig.app.json +14 -0
  66. package/template/tsconfig.json +34 -0
  67. package/template/tsconfig.spec.json +11 -0
  68. package/template/vite.config.ts +78 -0
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # create-ngmd
2
+
3
+ Scaffold a new [NgMd](https://github.com/erkamyaman/ngmd) docs project.
4
+
5
+ ```bash
6
+ pnpm create ngmd@latest my-docs
7
+ # or
8
+ npm create ngmd@latest my-docs
9
+ # or
10
+ yarn create ngmd my-docs
11
+ # or
12
+ bun create ngmd my-docs
13
+ ```
14
+
15
+ Then:
16
+
17
+ ```bash
18
+ cd my-docs
19
+ pnpm install # or npm install / yarn / bun install
20
+ pnpm dev
21
+ ```
22
+
23
+ Open `http://localhost:5173`.
24
+
25
+ ## What you get
26
+
27
+ A working AnalogJS + Tailwind v4 + Shiki docs site with:
28
+
29
+ - File-based markdown routes — drop `.md` under `src/app/pages/`, get a route
30
+ - Authoring components: callout, alert, card, tabs, pill row, workflow, hero, code-block, video, image (all under `src/app/ui/`)
31
+ - Sticky translucent header, sidebar, breadcrumb, scroll-spy TOC, Cmd+K palette
32
+ - Per-page footer: prev/next navigation, edit-on-github, last-updated
33
+ - Heading anchor copy buttons, code-block copy buttons
34
+ - Build-time link guards (external + internal)
35
+ - Sitemap + robots.txt auto-generated
36
+ - Light / dark / auto theme with no-flash boot script
37
+ - `*Keyword` inline auto-linking
38
+ - ` ```ts file="src/foo.ts#L5-L20" ` code imports, group code tabs, line highlighting
39
+
40
+ See the [NgMd repo](https://github.com/erkamyaman/ngmd) for the feature list.
41
+
42
+ ## Next steps
43
+
44
+ 1. Edit `src/content/welcome.md` to make the first page your own.
45
+ 2. Edit `src/ngmd.config.ts` to set brand name, navigation, and accent.
46
+ 3. Drop more `.md` files in `src/app/pages/` or `src/content/`.
47
+ 4. Build with `pnpm run build`, deploy `dist/` to any static host.
48
+
49
+ ## How it works
50
+
51
+ `index.mjs` (Node 20+, zero npm deps) copies `template/` into the target directory and rewrites a few placeholders (`package.json` name, `ngmd.config.ts` brand, `index.html` title) so the new project matches the name you passed.
52
+
53
+ `template/` is generated from the parent ngmd repo by `build-template.mjs` and is git-ignored. The `prepublishOnly` script regenerates it before every publish, so the npm artifact always carries an up-to-date starter.
54
+
55
+ To preview a scaffolded project locally without publishing:
56
+
57
+ ```bash
58
+ node create-ngmd/build-template.mjs # populate create-ngmd/template/
59
+ node create-ngmd/index.mjs my-docs # scaffold ./my-docs
60
+ ```
61
+
62
+ ## Licence
63
+
64
+ MIT
package/index.mjs ADDED
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { createInterface } from 'node:readline/promises';
6
+ import { stdin, stdout } from 'node:process';
7
+
8
+ const HERE = dirname(fileURLToPath(import.meta.url));
9
+ const TEMPLATE_DIR = join(HERE, 'template');
10
+
11
+ /**
12
+ * `create-ngmd <project-name>` — scaffolds a fresh NgMd project.
13
+ *
14
+ * Behaviour:
15
+ * 1. Accept project name from argv[2] or interactive prompt.
16
+ * 2. Validate (lowercase, hyphenated, no path traversal, dir not present).
17
+ * 3. Copy `template/` into ./<project-name>/.
18
+ * 4. Replace placeholders ({{name}}) in package.json, ngmd.config.ts, index.html.
19
+ * 5. Print next-step commands tailored to the detected package manager.
20
+ *
21
+ * Zero npm dependencies. Node 20+ builtins only.
22
+ */
23
+
24
+ const c = {
25
+ reset: '\x1b[0m',
26
+ bold: '\x1b[1m',
27
+ dim: '\x1b[2m',
28
+ cyan: '\x1b[36m',
29
+ green: '\x1b[32m',
30
+ red: '\x1b[31m',
31
+ yellow: '\x1b[33m',
32
+ };
33
+
34
+ function detectPM() {
35
+ const ua = process.env.npm_config_user_agent ?? '';
36
+ if (ua.startsWith('pnpm')) return 'pnpm';
37
+ if (ua.startsWith('yarn')) return 'yarn';
38
+ if (ua.startsWith('bun')) return 'bun';
39
+ return 'npm';
40
+ }
41
+
42
+ function validName(s) {
43
+ return /^[a-z0-9][a-z0-9._-]*$/.test(s) && !s.includes('..');
44
+ }
45
+
46
+ async function prompt(rl, question, defaultValue) {
47
+ const tail = defaultValue ? ` ${c.dim}(${defaultValue})${c.reset}` : '';
48
+ const answer = (await rl.question(`${question}${tail}: `)).trim();
49
+ return answer || defaultValue || '';
50
+ }
51
+
52
+ function replacePlaceholders(target, name) {
53
+ const subs = [
54
+ {
55
+ file: 'package.json',
56
+ replacer: (s) => s.replace(/"name":\s*"ngmd"/, `"name": "${name}"`),
57
+ },
58
+ {
59
+ file: 'src/ngmd.config.ts',
60
+ replacer: (s) =>
61
+ s
62
+ .replace(/name:\s*'NgMd'/, `name: '${name}'`)
63
+ .replace(
64
+ /githubUrl:\s*'[^']*'/,
65
+ `githubUrl: 'https://github.com/your-org/${name}'`,
66
+ ),
67
+ },
68
+ {
69
+ file: 'index.html',
70
+ replacer: (s) =>
71
+ s
72
+ .replace(/<title>[^<]*<\/title>/, `<title>${name}</title>`)
73
+ .replace(/og:title"\s+content="[^"]*"/, `og:title" content="${name}"`),
74
+ },
75
+ ];
76
+
77
+ for (const { file, replacer } of subs) {
78
+ const path = join(target, file);
79
+ if (!existsSync(path)) continue;
80
+ writeFileSync(path, replacer(readFileSync(path, 'utf8')));
81
+ }
82
+ }
83
+
84
+ async function main() {
85
+ const argv = process.argv.slice(2);
86
+ const requested = argv[0];
87
+
88
+ console.log(`\n${c.bold}${c.cyan}create-ngmd${c.reset} ${c.dim}— scaffold a new NgMd project${c.reset}\n`);
89
+
90
+ if (!existsSync(TEMPLATE_DIR)) {
91
+ console.error(`${c.red}error:${c.reset} template directory missing at ${TEMPLATE_DIR}`);
92
+ console.error('Run `node create-ngmd/build-template.mjs` from the ngmd repo first.');
93
+ process.exit(1);
94
+ }
95
+
96
+ let name = requested;
97
+ if (!name) {
98
+ const rl = createInterface({ input: stdin, output: stdout });
99
+ name = await prompt(rl, 'Project name', 'my-docs');
100
+ rl.close();
101
+ }
102
+
103
+ if (!validName(name)) {
104
+ console.error(
105
+ `${c.red}error:${c.reset} "${name}" is not a valid project name.\n` +
106
+ `Use lowercase letters, digits, dots, underscores, or hyphens.`,
107
+ );
108
+ process.exit(1);
109
+ }
110
+
111
+ const target = resolve(process.cwd(), name);
112
+ if (existsSync(target)) {
113
+ const isEmpty = readdirSync(target).length === 0;
114
+ if (!isEmpty) {
115
+ console.error(`${c.red}error:${c.reset} directory "${name}" already exists and is not empty.`);
116
+ process.exit(1);
117
+ }
118
+ } else {
119
+ mkdirSync(target, { recursive: true });
120
+ }
121
+
122
+ console.log(`${c.dim}scaffolding into${c.reset} ${target}\n`);
123
+
124
+ cpSync(TEMPLATE_DIR, target, {
125
+ recursive: true,
126
+ filter: (src) => {
127
+ const base = src.replace(TEMPLATE_DIR, '').replace(/^[/\\]/, '');
128
+ // Belt-and-braces: never copy these, even if they slip into template/.
129
+ return !/^(node_modules|dist|\.angular|\.vite|pnpm-lock\.yaml|package-lock\.json|yarn\.lock|bun\.lockb)/.test(
130
+ base,
131
+ );
132
+ },
133
+ });
134
+
135
+ // npm rewrites .gitignore → .npmignore on publish; restore the dotfile.
136
+ const gitignoreFromNpm = join(target, 'gitignore');
137
+ if (existsSync(gitignoreFromNpm)) {
138
+ cpSync(gitignoreFromNpm, join(target, '.gitignore'));
139
+ const { rmSync } = await import('node:fs');
140
+ rmSync(gitignoreFromNpm);
141
+ }
142
+
143
+ replacePlaceholders(target, name);
144
+
145
+ const pm = detectPM();
146
+ const install = pm === 'yarn' ? 'yarn' : `${pm} install`;
147
+ const dev = pm === 'npm' ? 'npm run dev' : `${pm} dev`;
148
+
149
+ console.log(`${c.green}✓${c.reset} ${c.bold}done${c.reset}\n`);
150
+ console.log(`${c.bold}Next steps:${c.reset}`);
151
+ console.log(` ${c.cyan}cd${c.reset} ${name}`);
152
+ console.log(` ${c.cyan}${install}${c.reset}`);
153
+ console.log(` ${c.cyan}${dev}${c.reset}\n`);
154
+ console.log(`${c.dim}Docs:${c.reset} https://ngmd.netlify.app`);
155
+ console.log(`${c.dim}Issues:${c.reset} https://github.com/erkamyaman/ngmd/issues\n`);
156
+ }
157
+
158
+ main().catch((err) => {
159
+ console.error(`\n${c.red}aborted:${c.reset} ${err.message ?? err}\n`);
160
+ process.exit(1);
161
+ });
162
+
163
+ function statSafe(p) {
164
+ try { return statSync(p); } catch { return null; }
165
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "create-ngmd",
3
+ "version": "0.0.1",
4
+ "description": "Scaffold a NgMd Angular docs site. Run via `pnpm create ngmd@latest` / `npm create ngmd@latest` / `yarn create ngmd` / `bun create ngmd`.",
5
+ "license": "MIT",
6
+ "author": {
7
+ "name": "Kam",
8
+ "url": "https://github.com/erkamyaman"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/erkamyaman/ngmd.git",
13
+ "directory": "create-ngmd"
14
+ },
15
+ "bugs": "https://github.com/erkamyaman/ngmd/issues",
16
+ "homepage": "https://github.com/erkamyaman/ngmd#readme",
17
+ "keywords": [
18
+ "angular",
19
+ "angular-docs",
20
+ "analogjs",
21
+ "docs",
22
+ "documentation",
23
+ "starter",
24
+ "scaffolder",
25
+ "shiki",
26
+ "tailwind",
27
+ "vite",
28
+ "create"
29
+ ],
30
+ "type": "module",
31
+ "bin": {
32
+ "create-ngmd": "index.mjs"
33
+ },
34
+ "scripts": {
35
+ "build-template": "node build-template.mjs",
36
+ "prepublishOnly": "node build-template.mjs"
37
+ },
38
+ "engines": {
39
+ "node": ">=20.19.1"
40
+ },
41
+ "files": [
42
+ "index.mjs",
43
+ "README.md",
44
+ "template"
45
+ ],
46
+ "publishConfig": {
47
+ "access": "public"
48
+ }
49
+ }
@@ -0,0 +1,26 @@
1
+ # NgMd starter
2
+
3
+ A docs site scaffolded with `create-ngmd`.
4
+
5
+ ## Develop
6
+
7
+ ```bash
8
+ pnpm install # or npm / yarn / bun
9
+ pnpm dev
10
+ ```
11
+
12
+ ## Add a page
13
+
14
+ Drop a `.md` file under `src/app/pages/` (or under `src/content/` and reference it via `injectContent`). Headings become anchors automatically.
15
+
16
+ ## Build
17
+
18
+ ```bash
19
+ pnpm build
20
+ ```
21
+
22
+ Output lands in `dist/`. Sitemap and `robots.txt` emit alongside the client bundle.
23
+
24
+ ## Configure
25
+
26
+ Edit `src/ngmd.config.ts` to change brand, navigation, and public URL.
@@ -0,0 +1,54 @@
1
+ {
2
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3
+ "version": 1,
4
+ "newProjectRoot": "projects",
5
+ "projects": {
6
+ "my-app": {
7
+ "projectType": "application",
8
+ "root": ".",
9
+ "sourceRoot": "src",
10
+ "prefix": "app",
11
+ "architect": {
12
+ "build": {
13
+ "builder": "@analogjs/platform:vite",
14
+ "options": {
15
+ "configFile": "vite.config.ts",
16
+ "main": "src/main.ts",
17
+ "outputPath": "dist/client",
18
+ "tsConfig": "tsconfig.app.json"
19
+ },
20
+ "defaultConfiguration": "production",
21
+ "configurations": {
22
+ "development": {
23
+ "mode": "development"
24
+ },
25
+ "production": {
26
+ "sourcemap": false,
27
+ "mode": "production"
28
+ }
29
+ }
30
+ },
31
+ "serve": {
32
+ "builder": "@analogjs/platform:vite-dev-server",
33
+ "defaultConfiguration": "development",
34
+ "options": {
35
+ "buildTarget": "my-app:build",
36
+ "port": 5173
37
+ },
38
+ "configurations": {
39
+ "development": {
40
+ "buildTarget": "my-app:build:development",
41
+ "hmr": true
42
+ },
43
+ "production": {
44
+ "buildTarget": "my-app:build:production"
45
+ }
46
+ }
47
+ },
48
+ "test": {
49
+ "builder": "@analogjs/vitest-angular:test"
50
+ }
51
+ }
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,47 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>NgMd</title>
6
+ <meta
7
+ name="description"
8
+ content="Modern Angular docs-site starter built on AnalogJS, Spartan UI, and Tailwind. Drop a markdown file, get a route."
9
+ />
10
+ <meta property="og:title" content="NgMd" />
11
+ <meta
12
+ property="og:description"
13
+ content="Modern Angular docs-site starter built on AnalogJS, Spartan UI, and Tailwind. Drop a markdown file, get a route."
14
+ />
15
+ <meta property="og:image" content="/logo.svg" />
16
+ <base href="/" />
17
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
18
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
19
+ <link rel="alternate icon" type="image/x-icon" href="/favicon.ico" />
20
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
21
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
22
+ <link
23
+ href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@500;700&display=swap"
24
+ rel="stylesheet"
25
+ />
26
+ <link rel="stylesheet" href="/src/styles.css" />
27
+ <script>
28
+ (function () {
29
+ try {
30
+ var stored = localStorage.getItem('ngmd-theme');
31
+ var mode = stored === 'light' || stored === 'dark' ? stored : 'auto';
32
+ var resolved =
33
+ mode === 'auto'
34
+ ? matchMedia('(prefers-color-scheme: dark)').matches
35
+ ? 'dark'
36
+ : 'light'
37
+ : mode;
38
+ if (resolved === 'dark') document.documentElement.classList.add('dark');
39
+ } catch (_) {}
40
+ })();
41
+ </script>
42
+ </head>
43
+ <body>
44
+ <app-root></app-root>
45
+ <script type="module" src="/src/main.ts"></script>
46
+ </body>
47
+ </html>
@@ -0,0 +1,190 @@
1
+ import { readFileSync, readdirSync, statSync } from 'node:fs';
2
+ import { join, relative } from 'node:path';
3
+ import type { Plugin } from 'vite';
4
+
5
+ /**
6
+ * Build-time guard that errors on broken internal links inside markdown files.
7
+ *
8
+ * Validates three cases:
9
+ * - `[text](#fragment)` — fragment must be a real heading slug in the same file
10
+ * - `[text](/path)` — `/path` must be a known route
11
+ * - `[text](/path#fragment)` — both the route and the heading slug must exist
12
+ *
13
+ * Routes are discovered by scanning `src/content/*.md` (mapped via the same
14
+ * CONTENT_TO_ROUTE convention page-meta uses) and `src/app/pages/**\/*.page.ts`.
15
+ * External (`http(s)://`), mail (`mailto:`), and relative (`./foo`) links are
16
+ * skipped; the existing externalLinkGuard covers raw HTML external anchors.
17
+ *
18
+ * Heading slugs are computed with the same lowercase + dash + strip-punct
19
+ * rule the rendered TOC uses, so dev-time and runtime stay in sync.
20
+ */
21
+
22
+ const CONTENT_TO_ROUTE: Record<string, string> = {
23
+ welcome: '/welcome',
24
+ about: '/getting-started/about',
25
+ changelog: '/getting-started/changelog',
26
+ installation: '/getting-started/installation',
27
+ 'quick-start': '/getting-started/quick-start',
28
+ theming: '/concepts/theming',
29
+ components: '/concepts/components',
30
+ 'markdown-routes': '/concepts/markdown-routes',
31
+ 'stack-overview': '/stack/overview',
32
+ 'stack-technologies': '/stack/technologies',
33
+ 'stack-installation': '/stack/installation',
34
+ support: '/support',
35
+ };
36
+
37
+ function slugify(s: string): string {
38
+ return s
39
+ .toLowerCase()
40
+ .replace(/[`*_~]/g, '')
41
+ .replace(/[^\w\s-]/g, '')
42
+ .trim()
43
+ .replace(/\s+/g, '-');
44
+ }
45
+
46
+ function walkPageFiles(dir: string, root: string, out: string[] = []): string[] {
47
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
48
+ const full = join(dir, entry.name);
49
+ if (entry.isDirectory()) {
50
+ walkPageFiles(full, root, out);
51
+ } else if (entry.isFile() && entry.name.endsWith('.page.ts')) {
52
+ out.push(relative(root, full));
53
+ }
54
+ }
55
+ return out;
56
+ }
57
+
58
+ function routeFromPagePath(rel: string): string {
59
+ const trimmed = rel
60
+ .replace(/^src\/app\/pages\//, '')
61
+ .replace(/\.page\.ts$/, '');
62
+ if (trimmed === 'index') return '/';
63
+ if (trimmed.startsWith('[')) return '';
64
+ return '/' + trimmed;
65
+ }
66
+
67
+ function extractHeadings(markdown: string): Set<string> {
68
+ const slugs = new Set<string>();
69
+ const headingRe = /^#{1,6}\s+(.+?)\s*$/gm;
70
+ let m;
71
+ while ((m = headingRe.exec(markdown)) !== null) {
72
+ slugs.add(slugify(m[1]));
73
+ }
74
+ return slugs;
75
+ }
76
+
77
+ export function internalLinkGuard(): Plugin {
78
+ let root = process.cwd();
79
+ // route → headings, populated lazily on first transform() call
80
+ const headingsByRoute = new Map<string, Set<string>>();
81
+ // route → source file (relative path)
82
+ const routes = new Map<string, string>();
83
+ let primed = false;
84
+
85
+ function prime(): void {
86
+ if (primed) return;
87
+ primed = true;
88
+
89
+ // .md → route
90
+ for (const [name, route] of Object.entries(CONTENT_TO_ROUTE)) {
91
+ const rel = `src/content/${name}.md`;
92
+ const full = join(root, rel);
93
+ try {
94
+ statSync(full);
95
+ } catch {
96
+ continue;
97
+ }
98
+ routes.set(route, rel);
99
+ headingsByRoute.set(route, extractHeadings(readFileSync(full, 'utf8')));
100
+ }
101
+
102
+ // .page.ts → route (no heading scrape; just makes the route resolvable)
103
+ const pagesDir = join(root, 'src/app/pages');
104
+ try {
105
+ const pageFiles = walkPageFiles(pagesDir, root);
106
+ for (const rel of pageFiles) {
107
+ const route = routeFromPagePath(rel);
108
+ if (!route) continue;
109
+ if (!routes.has(route)) routes.set(route, rel);
110
+ }
111
+ } catch {
112
+ // src/app/pages missing — fine for non-app projects
113
+ }
114
+ }
115
+
116
+ return {
117
+ name: 'ngmd-internal-link-guard',
118
+ enforce: 'pre',
119
+ configResolved(cfg) {
120
+ root = cfg.root;
121
+ },
122
+ transform(_code, id) {
123
+ // Vite may append `?import` / `?raw` query suffixes
124
+ const cleanId = id.split('?')[0];
125
+ if (!cleanId.endsWith('.md')) return null;
126
+ prime();
127
+
128
+ const file = cleanId;
129
+ const content = readFileSync(file, 'utf8');
130
+ const ownSlugs = extractHeadings(content);
131
+ const issues: string[] = [];
132
+
133
+ const validate = (href: string, label: string) => {
134
+ if (!href) return;
135
+ // external / mail / relative — skip
136
+ if (
137
+ /^(https?:|mailto:|tel:|#)/.test(href) === false &&
138
+ !href.startsWith('/')
139
+ )
140
+ return;
141
+ if (/^(https?:|mailto:|tel:)/.test(href)) return;
142
+
143
+ const [path, fragment] = href.split('#');
144
+ if (path === '') {
145
+ // in-page fragment: must exist in this file
146
+ if (fragment && !ownSlugs.has(fragment)) {
147
+ issues.push(
148
+ ` ${label} → "#${fragment}" has no matching heading in this file`,
149
+ );
150
+ }
151
+ return;
152
+ }
153
+
154
+ // absolute route: must be a known route
155
+ if (!routes.has(path)) {
156
+ issues.push(` ${label} → "${path}" is not a known route`);
157
+ return;
158
+ }
159
+ if (fragment) {
160
+ const targetSlugs = headingsByRoute.get(path);
161
+ if (targetSlugs && !targetSlugs.has(fragment)) {
162
+ issues.push(
163
+ ` ${label} → "${path}#${fragment}" — fragment not found in target page`,
164
+ );
165
+ }
166
+ // if targetSlugs is undefined (e.g. .page.ts route), skip fragment check
167
+ }
168
+ };
169
+
170
+ const mdLinkRe = /\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
171
+ const htmlAnchorRe = /<a\s[^>]*href=["']([^"']+)["']/g;
172
+ let m: RegExpExecArray | null;
173
+ while ((m = mdLinkRe.exec(content)) !== null) {
174
+ validate(m[2], `[${m[1]}](${m[2]})`);
175
+ }
176
+ while ((m = htmlAnchorRe.exec(content)) !== null) {
177
+ validate(m[1], `<a href="${m[1]}">`);
178
+ }
179
+
180
+ if (issues.length > 0) {
181
+ this.error(
182
+ `[ngmd] Broken internal links in ${relative(root, file)}:\n${issues.join('\n')}\n` +
183
+ `Fix the link target, or update the heading slug it points to.`,
184
+ );
185
+ }
186
+
187
+ return null;
188
+ },
189
+ };
190
+ }
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "ngmd",
3
+ "version": "0.0.1",
4
+ "description": "NgMd docs site",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "angular",
8
+ "analogjs",
9
+ "spartan-ui",
10
+ "tailwind",
11
+ "docs",
12
+ "documentation",
13
+ "starter",
14
+ "shiki",
15
+ "markdown"
16
+ ],
17
+ "type": "module",
18
+ "engines": {
19
+ "node": ">=20.19.1"
20
+ },
21
+ "scripts": {
22
+ "ng": "ng",
23
+ "dev": "vite",
24
+ "start": "pnpm run dev",
25
+ "build": "vite build",
26
+ "watch": "vite build --watch",
27
+ "test": "vitest",
28
+ "preview": "node dist/analog/server/index.mjs"
29
+ },
30
+ "dependencies": {
31
+ "@analogjs/content": "^2.5.1",
32
+ "@analogjs/router": "^2.5.1",
33
+ "@angular/animations": "^21.0.0",
34
+ "@angular/common": "^21.0.0",
35
+ "@angular/compiler": "^21.0.0",
36
+ "@angular/core": "^21.0.0",
37
+ "@angular/forms": "^21.0.0",
38
+ "@angular/platform-browser": "^21.0.0",
39
+ "@angular/platform-server": "^21.0.0",
40
+ "@angular/router": "^21.0.0",
41
+ "@spartan-ng/brain": "0.0.1-alpha.694",
42
+ "@tailwindcss/typography": "^0.5.19",
43
+ "@tailwindcss/vite": "^4.1.4",
44
+ "front-matter": "^4.0.2",
45
+ "h3": "^1.13.0",
46
+ "lucide-angular": "^0.577.0",
47
+ "marked": "^15.0.7",
48
+ "marked-gfm-heading-id": "^4.1.1",
49
+ "marked-highlight": "^2.2.1",
50
+ "marked-mangle": "^1.1.10",
51
+ "marked-shiki": "^1.2.1",
52
+ "postcss": "^8.5.3",
53
+ "prismjs": "^1.29.0",
54
+ "rxjs": "~7.8.0",
55
+ "shiki": "^1.29.2",
56
+ "tailwindcss": "^4.1.4",
57
+ "tslib": "^2.3.0"
58
+ },
59
+ "devDependencies": {
60
+ "@analogjs/platform": "^2.5.1",
61
+ "@analogjs/vite-plugin-angular": "^2.5.1",
62
+ "@analogjs/vitest-angular": "^2.5.1",
63
+ "@angular/build": "^21.0.0",
64
+ "@angular/cli": "^21.0.0",
65
+ "@angular/compiler-cli": "^21.0.0",
66
+ "jsdom": "^22.0.0",
67
+ "typescript": "~5.9.0",
68
+ "vite": "^8.0.0",
69
+ "vite-tsconfig-paths": "^4.2.0",
70
+ "vitest": "^4.1.0"
71
+ },
72
+ "overrides": {
73
+ "vite": "$vite",
74
+ "vitest": "$vitest"
75
+ }
76
+ }