simple-site-generator 0.1.2 → 0.2.0

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 (57) hide show
  1. package/dist/cli.js +165 -2
  2. package/dist/commands/add-page.js +50 -0
  3. package/dist/commands/build.js +20 -0
  4. package/dist/commands/check.js +53 -0
  5. package/dist/commands/init.js +229 -0
  6. package/dist/commands/verify.js +150 -0
  7. package/dist/pages/page-schema.js +161 -0
  8. package/dist/pages/render-page.js +46 -0
  9. package/dist/paths.js +27 -0
  10. package/dist/types/cli.d.ts +1 -0
  11. package/dist/types/commands/add-page.d.ts +26 -0
  12. package/dist/types/commands/build.d.ts +18 -0
  13. package/dist/types/commands/check.d.ts +20 -0
  14. package/dist/types/commands/init.d.ts +40 -0
  15. package/dist/types/commands/verify.d.ts +47 -0
  16. package/dist/types/pages/page-schema.d.ts +81 -0
  17. package/dist/types/pages/render-page.d.ts +26 -0
  18. package/dist/types/paths.d.ts +16 -0
  19. package/dist/types/verify/budgets.d.ts +114 -0
  20. package/dist/types/verify/class-coverage.d.ts +65 -0
  21. package/dist/types/verify/ignore.d.ts +8 -0
  22. package/dist/types/verify/lighthouse.d.ts +27 -0
  23. package/dist/types/verify/seo-surface.d.ts +23 -0
  24. package/dist/types/verify/serve.d.ts +6 -0
  25. package/dist/verify/budgets.js +180 -0
  26. package/dist/verify/class-coverage.js +114 -0
  27. package/dist/verify/ignore.js +11 -0
  28. package/dist/verify/lighthouse.js +98 -0
  29. package/dist/verify/seo-surface.js +97 -0
  30. package/dist/verify/serve.js +123 -0
  31. package/package.json +16 -10
  32. package/templates/app/.github/workflows/deploy-rsync.yaml.hbs +58 -0
  33. package/templates/app/.oxfmtrc.json +32 -0
  34. package/templates/app/.oxlintrc.json +10 -0
  35. package/templates/app/.vscode/extensions.json +3 -0
  36. package/templates/app/build/Caddyfile.hbs +24 -0
  37. package/templates/app/build/Caddyfile.preview +34 -0
  38. package/templates/app/index.html +35 -0
  39. package/templates/app/logo.svg +143 -0
  40. package/templates/app/pnpm-workspace.yaml +10 -0
  41. package/templates/app/public/favicon-128.png +0 -0
  42. package/templates/app/public/favicon-48.png +0 -0
  43. package/templates/app/public/favicon-96.png +0 -0
  44. package/templates/app/public/favicon.ico +0 -0
  45. package/templates/app/public/images/landing/landing.png +0 -0
  46. package/templates/app/public/robots.txt +5 -0
  47. package/templates/app/src/App.vue +22 -0
  48. package/templates/app/src/pages/Contact.md +25 -0
  49. package/templates/app/src/pages/Error404Page.vue +31 -0
  50. package/templates/app/src/pages/GettingStarted/GettingStarted.md +52 -0
  51. package/templates/app/src/pages/Index.md.hbs +46 -0
  52. package/templates/app/src/router/index.ts +21 -0
  53. package/templates/app/src/router/routes.ts +218 -0
  54. package/templates/app/src/setup.ts +8 -0
  55. package/templates/app/src/shims-virtual.d.ts +67 -0
  56. package/templates/app/src/shims-vue.d.ts +6 -0
  57. package/templates/app/vitrify.config.ts.hbs +249 -0
@@ -0,0 +1,161 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * The page frontmatter contract.
4
+ *
5
+ * This is the guard that stops the class of bug the old template suffered from:
6
+ * a page whose frontmatter is subtly wrong builds and deploys, and the damage is
7
+ * only visible on the live site. `ssg check` runs this before a build, so a typo
8
+ * fails in the terminal instead of in production.
9
+ */
10
+ /** Tags the templates understand. An unknown tag means a menu entry silently vanishes. */
11
+ export const PAGE_TAGS = ['menu', 'footer', 'contact'];
12
+ export const pageFrontmatterSchema = z.object({
13
+ /** Route segment: `undefined`-free, because the router builds `/${id}` from it. */
14
+ id: z.string().min(1, 'id must not be empty'),
15
+ /** Used verbatim as the `<h1>`; the `<title>` unless `headTitle` overrides it. */
16
+ title: z.string().min(1, 'title is required'),
17
+ /**
18
+ * The page's `<title>`, used verbatim.
19
+ *
20
+ * Without it the document title names the site as well — `<site> | <title>` —
21
+ * which is right for every page except the front page, whose head title is the
22
+ * site name and whose `<h1>` is a sentence.
23
+ */
24
+ headTitle: z.string().min(1, 'headTitle must not be empty').optional(),
25
+ /** The meta description. Required: a page without one is invisible to search. */
26
+ description: z.string().min(1, 'description is required'),
27
+ /** Navigation label; falls back to `title` in the templates. */
28
+ label: z.string().min(1).optional(),
29
+ /** Sort weight; the home page is always first. */
30
+ order: z.number().optional(),
31
+ tags: z.array(z.enum(['menu', 'footer', 'contact'])).optional(),
32
+ /** Keep the page out of the index and the sitemap. */
33
+ noindex: z.boolean().optional(),
34
+ /** Render client-side only (for pages that cannot be prerendered). */
35
+ noSsr: z.boolean().optional(),
36
+ /** Template component to render, e.g. `Landing`. */
37
+ page: z.string().min(1).optional(),
38
+ image: z.string().min(1).optional(),
39
+ breadcrumbs: z.array(z.string().min(1)).optional(),
40
+ faq: z
41
+ .array(z.object({
42
+ question: z.string().min(1),
43
+ answer: z.string().min(1)
44
+ }))
45
+ .optional(),
46
+ cta: z
47
+ .object({
48
+ message: z.string().min(1),
49
+ route: z.string().min(1).optional(),
50
+ /**
51
+ * The quieter action beside the primary one, rendered as an outline.
52
+ *
53
+ * Validated here as well as typed, because a malformed second action is
54
+ * invisible: the template only checks that `message` is present, so a
55
+ * missing `message` would drop the button without a word.
56
+ */
57
+ secondary: z
58
+ .object({
59
+ message: z.string().min(1),
60
+ route: z.string().min(1).optional()
61
+ })
62
+ .optional()
63
+ })
64
+ .optional(),
65
+ /**
66
+ * The hero's animated process graphic (`shared/HeroDemo.vue`).
67
+ *
68
+ * Two stops minimum: the graphic draws a route between them, and one stop is a
69
+ * dot with a label, not a process. Rejected here rather than rendered badly,
70
+ * because the templates degrade silently — a malformed `demo` would just show
71
+ * nothing.
72
+ */
73
+ demo: z
74
+ .object({
75
+ steps: z.array(z.string().min(1)).min(2),
76
+ label: z.string().min(1).optional()
77
+ })
78
+ .optional(),
79
+ /**
80
+ * The principles band (`shared/PrincipleGrid.vue`).
81
+ *
82
+ * At least one item, and every item needs both halves: the component renders a
83
+ * heading and a body per card, so a title with no text is an empty box in the
84
+ * layout. Rejected here rather than rendered badly, because a malformed
85
+ * `principles` shows nothing at all.
86
+ */
87
+ principles: z
88
+ .object({
89
+ heading: z.string().min(1).optional(),
90
+ leading: z.string().min(1).optional(),
91
+ items: z
92
+ .array(z.object({
93
+ title: z.string().min(1),
94
+ text: z.string().min(1)
95
+ }))
96
+ .min(1),
97
+ note: z.string().min(1).optional()
98
+ })
99
+ .optional(),
100
+ /** Ids of pages nested under this one in the navigation. */
101
+ subpages: z.array(z.string().min(1)).optional(),
102
+ lists: z.array(z.unknown()).optional(),
103
+ cards: z.array(z.unknown()).optional(),
104
+ images: z.array(z.unknown()).optional(),
105
+ videos: z.array(z.unknown()).optional(),
106
+ extra: z.record(z.string(), z.unknown()).optional()
107
+ });
108
+ /** Validate one page's frontmatter, returning every problem rather than the first. */
109
+ export function validatePage(file, data) {
110
+ const result = pageFrontmatterSchema.safeParse(data);
111
+ if (result.success)
112
+ return [];
113
+ return result.error.issues.map((issue) => ({
114
+ file,
115
+ message: issue.path.length
116
+ ? `${issue.path.join('.')}: ${issue.message}`
117
+ : issue.message
118
+ }));
119
+ }
120
+ /**
121
+ * Validate the collection as a whole.
122
+ *
123
+ * Uniqueness and referential integrity are only checkable with every page in
124
+ * hand: two files claiming the same `id` produce two identical routes (one
125
+ * silently unreachable), and a `subpages` entry pointing at a missing page
126
+ * produces a dead navigation link.
127
+ */
128
+ export function validateCollection(pages) {
129
+ const issues = [];
130
+ const seen = new Map();
131
+ for (const page of pages) {
132
+ const id = page.data?.id;
133
+ if (typeof id !== 'string' || !id)
134
+ continue;
135
+ const existing = seen.get(id);
136
+ if (existing) {
137
+ issues.push({
138
+ file: page.file,
139
+ message: `duplicate id "${id}" — already used by ${existing}`
140
+ });
141
+ }
142
+ else {
143
+ seen.set(id, page.file);
144
+ }
145
+ }
146
+ for (const page of pages) {
147
+ const data = page.data;
148
+ const subpages = data?.subpages;
149
+ if (!Array.isArray(subpages))
150
+ continue;
151
+ for (const childId of subpages) {
152
+ if (typeof childId !== 'string' || seen.has(childId))
153
+ continue;
154
+ issues.push({
155
+ file: page.file,
156
+ message: `subpages references "${childId}", which no page declares as its id`
157
+ });
158
+ }
159
+ }
160
+ return issues;
161
+ }
@@ -0,0 +1,46 @@
1
+ import matter from 'gray-matter';
2
+ /**
3
+ * Build the markdown source for a new page.
4
+ *
5
+ * Frontmatter is serialised by `gray-matter`, so keys it does not understand are
6
+ * preserved and the output round-trips through the same parser `ssg check` uses.
7
+ * The body is a placeholder that already respects the heading contract — the
8
+ * page template owns the `<h1>`, so content starts at `##`.
9
+ */
10
+ export function renderPage(input) {
11
+ const data = {
12
+ id: input.id,
13
+ title: input.title,
14
+ description: input.description
15
+ };
16
+ if (input.label)
17
+ data.label = input.label;
18
+ if (input.order !== undefined)
19
+ data.order = input.order;
20
+ if (input.tags?.length)
21
+ data.tags = input.tags;
22
+ if (input.page)
23
+ data.page = input.page;
24
+ const body = [
25
+ '## Overview',
26
+ '',
27
+ `${input.title} — replace this with the page content.`,
28
+ '',
29
+ '<!--',
30
+ ' Writing notes:',
31
+ ' - The page title above is already the <h1>. Start body headings at "##".',
32
+ " - Answer the page's main question in the first paragraph.",
33
+ ' - See CONTENT-BLUEPRINT.md for the service x location matrix and',
34
+ ' internal-linking rules.',
35
+ '-->',
36
+ ''
37
+ ].join('\n');
38
+ return matter.stringify(body, data);
39
+ }
40
+ /**
41
+ * Derive a page id from a file path, so `src/pages/GettingStarted/Install.md`
42
+ * becomes `GettingStarted/Install`.
43
+ */
44
+ export function idFromPath(relativePath) {
45
+ return relativePath.replace(/\\/g, '/').replace(/\.md$/, '');
46
+ }
package/dist/paths.js ADDED
@@ -0,0 +1,27 @@
1
+ import { join, resolve } from 'node:path';
2
+ /**
3
+ * Where things live inside a generated project.
4
+ *
5
+ * Every command resolves paths through here so `ssg check`, `ssg build` and
6
+ * `ssg verify` cannot disagree about which directory they are reading.
7
+ */
8
+ /** The generated project root. */
9
+ export function appDir(cwd = process.cwd()) {
10
+ return resolve(cwd);
11
+ }
12
+ /** `src/pages` — the single source of truth for the site's pages. */
13
+ export function pagesDir(app = appDir()) {
14
+ return join(app, 'src', 'pages');
15
+ }
16
+ /** `public` — copied verbatim into the build output. */
17
+ export function publicDir(app = appDir()) {
18
+ return join(app, 'public');
19
+ }
20
+ /** `dist/static` — the SSG output that gets deployed. */
21
+ export function distDir(app = appDir()) {
22
+ return join(app, 'dist', 'static');
23
+ }
24
+ /** Where the generated app's `vitrify.config.ts` lives. */
25
+ export function configPath(app = appDir()) {
26
+ return join(app, 'vitrify.config.ts');
27
+ }
@@ -1 +1,2 @@
1
1
  #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,26 @@
1
+ import { PAGE_TAGS } from '../pages/page-schema.js';
2
+ import type { PageTag } from '../pages/page-schema.js';
3
+ export interface AddPageOptions {
4
+ /** Route segment: `services`, or `services/leaks` for a nested page. */
5
+ id: string;
6
+ title?: string;
7
+ description?: string;
8
+ label?: string;
9
+ order?: number;
10
+ tags?: PageTag[];
11
+ page?: string;
12
+ app?: string;
13
+ }
14
+ export interface AddPageResult {
15
+ file: string;
16
+ id: string;
17
+ }
18
+ /**
19
+ * `ssg add-page` — create a page with valid frontmatter.
20
+ *
21
+ * Generates the file through the same `renderPage` the schema validates, so a
22
+ * page created by the CLI cannot fail `ssg check`. It refuses to overwrite: an
23
+ * agent re-running a command must never silently destroy content.
24
+ */
25
+ export declare function addPage(options: AddPageOptions): Promise<AddPageResult>;
26
+ export { PAGE_TAGS };
@@ -0,0 +1,18 @@
1
+ export interface BuildOptions {
2
+ app?: string;
3
+ /** Extra arguments forwarded to `vitrify build`. */
4
+ passthrough?: string[];
5
+ }
6
+ export interface BuildResult {
7
+ code: number;
8
+ outputDir: string;
9
+ }
10
+ /**
11
+ * `ssg build` — delegate to `vitrify build -m ssg`.
12
+ *
13
+ * Deliberately a thin wrapper: the build belongs to vitrify, and reimplementing
14
+ * it here would fork the build pipeline from the one the dev server uses. The
15
+ * value this command adds is the fixed SSG mode, the resolved output path, and a
16
+ * single place for the agent-facing instructions to point at.
17
+ */
18
+ export declare function build(options?: BuildOptions): Promise<BuildResult>;
@@ -0,0 +1,20 @@
1
+ import { type PageIssue } from '../pages/page-schema.js';
2
+ export interface CheckOptions {
3
+ app?: string;
4
+ }
5
+ export interface CheckResult {
6
+ ok: boolean;
7
+ pages: number;
8
+ issues: PageIssue[];
9
+ }
10
+ /**
11
+ * `ssg check` — validate every page against the frontmatter contract.
12
+ *
13
+ * This is the guard that stops the defect class the old template suffered from:
14
+ * frontmatter that is subtly wrong still builds, and the damage only shows on the
15
+ * live site. Running it before a build turns that into a terminal error.
16
+ *
17
+ * An empty pages directory is a warning, not a failure — a brand new project has
18
+ * no pages yet, and failing there would be hostile.
19
+ */
20
+ export declare function check(options?: CheckOptions): Promise<CheckResult>;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `ssg init` — scaffold a new site from the app template.
3
+ *
4
+ * Replaces the old `create-simple-site-generator` package. Prompts only when the
5
+ * process is attached to a TTY **and** `--yes` was not passed, so an agent can
6
+ * run it non-interactively and get the same project every time.
7
+ */
8
+ export declare const TEMPLATES: readonly ["docs", "landing", "business", "plain"];
9
+ export type TemplateName = (typeof TEMPLATES)[number];
10
+ export interface InitOptions {
11
+ /** Directory to create the project in. */
12
+ directory?: string;
13
+ template?: TemplateName;
14
+ /** npm package name. */
15
+ name?: string;
16
+ /** Displayed site name, used as the product name and default `<title>`. */
17
+ productName?: string;
18
+ /** Absolute site origin, e.g. `https://example.com`. */
19
+ url?: string;
20
+ description?: string;
21
+ author?: string;
22
+ /** Skip every prompt and use defaults. */
23
+ yes?: boolean;
24
+ /** Force the interactive prompts even when stdin is not a TTY. */
25
+ interactive?: boolean;
26
+ }
27
+ export interface InitResult {
28
+ directory: string;
29
+ template: TemplateName;
30
+ }
31
+ /**
32
+ * Resolve options, prompting only when it is safe and wanted.
33
+ *
34
+ * A non-interactive run must never hang waiting on stdin, which is why the
35
+ * `isTTY` check comes before any prompt.
36
+ */
37
+ export declare function resolveInitOptions(options: InitOptions): Promise<Required<Omit<InitOptions, 'yes' | 'interactive'>>>;
38
+ export declare function init(options?: InitOptions): Promise<InitResult>;
39
+ /** Confirm overwriting a non-empty directory (interactive runs only). */
40
+ export declare function confirmOverwrite(directory: string): Promise<boolean>;
@@ -0,0 +1,47 @@
1
+ import { type Budgets } from '../verify/budgets.js';
2
+ export interface VerifyOptions {
3
+ app?: string;
4
+ /** Absolute site origin, so canonical URLs can be checked against it. */
5
+ siteUrl?: string;
6
+ budgets?: Budgets;
7
+ /**
8
+ * Skip the Lighthouse half. Used where no Chromium is available — the
9
+ * deterministic SEO/byte checks still run and still gate.
10
+ */
11
+ skipLighthouse?: boolean;
12
+ /**
13
+ * Glob patterns, relative to the output root, of HTML files to leave out of
14
+ * the SEO surface check. A third-party drop-in that happens to end in `.html`
15
+ * (a Google site-verification file, for instance) is not a page and cannot
16
+ * carry a title, canonical or JSON-LD.
17
+ */
18
+ ignore?: string[];
19
+ /** Chromium binary for Lighthouse. */
20
+ chromePath?: string;
21
+ }
22
+ export interface VerifyResult {
23
+ ok: boolean;
24
+ /** Hard failures — a non-zero exit. */
25
+ issues: string[];
26
+ /** Advisory: reported, but does not fail the run. */
27
+ warnings: string[];
28
+ report: {
29
+ pages: number;
30
+ budgets: {
31
+ js: number;
32
+ css: number;
33
+ };
34
+ scores?: Record<string, number>;
35
+ };
36
+ }
37
+ /**
38
+ * `ssg verify` — prove the built site is correct and fast.
39
+ *
40
+ * Two tiers by design: the SEO surface and the byte budgets are deterministic
41
+ * and **fail** the run; Lighthouse performance only **warns**, because it varies
42
+ * with the measuring machine and a gate that fails randomly gets switched off.
43
+ *
44
+ * It does not build. `verify` measures what is on disk, so it can be run against
45
+ * a deployed artifact as well as a local build.
46
+ */
47
+ export declare function verify(options?: VerifyOptions): Promise<VerifyResult>;
@@ -0,0 +1,81 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * The page frontmatter contract.
4
+ *
5
+ * This is the guard that stops the class of bug the old template suffered from:
6
+ * a page whose frontmatter is subtly wrong builds and deploys, and the damage is
7
+ * only visible on the live site. `ssg check` runs this before a build, so a typo
8
+ * fails in the terminal instead of in production.
9
+ */
10
+ /** Tags the templates understand. An unknown tag means a menu entry silently vanishes. */
11
+ export declare const PAGE_TAGS: readonly ["menu", "footer", "contact"];
12
+ export type PageTag = (typeof PAGE_TAGS)[number];
13
+ export declare const pageFrontmatterSchema: z.ZodObject<{
14
+ id: z.ZodString;
15
+ title: z.ZodString;
16
+ headTitle: z.ZodOptional<z.ZodString>;
17
+ description: z.ZodString;
18
+ label: z.ZodOptional<z.ZodString>;
19
+ order: z.ZodOptional<z.ZodNumber>;
20
+ tags: z.ZodOptional<z.ZodArray<z.ZodEnum<{
21
+ menu: "menu";
22
+ footer: "footer";
23
+ contact: "contact";
24
+ }>>>;
25
+ noindex: z.ZodOptional<z.ZodBoolean>;
26
+ noSsr: z.ZodOptional<z.ZodBoolean>;
27
+ page: z.ZodOptional<z.ZodString>;
28
+ image: z.ZodOptional<z.ZodString>;
29
+ breadcrumbs: z.ZodOptional<z.ZodArray<z.ZodString>>;
30
+ faq: z.ZodOptional<z.ZodArray<z.ZodObject<{
31
+ question: z.ZodString;
32
+ answer: z.ZodString;
33
+ }, z.core.$strip>>>;
34
+ cta: z.ZodOptional<z.ZodObject<{
35
+ message: z.ZodString;
36
+ route: z.ZodOptional<z.ZodString>;
37
+ secondary: z.ZodOptional<z.ZodObject<{
38
+ message: z.ZodString;
39
+ route: z.ZodOptional<z.ZodString>;
40
+ }, z.core.$strip>>;
41
+ }, z.core.$strip>>;
42
+ demo: z.ZodOptional<z.ZodObject<{
43
+ steps: z.ZodArray<z.ZodString>;
44
+ label: z.ZodOptional<z.ZodString>;
45
+ }, z.core.$strip>>;
46
+ principles: z.ZodOptional<z.ZodObject<{
47
+ heading: z.ZodOptional<z.ZodString>;
48
+ leading: z.ZodOptional<z.ZodString>;
49
+ items: z.ZodArray<z.ZodObject<{
50
+ title: z.ZodString;
51
+ text: z.ZodString;
52
+ }, z.core.$strip>>;
53
+ note: z.ZodOptional<z.ZodString>;
54
+ }, z.core.$strip>>;
55
+ subpages: z.ZodOptional<z.ZodArray<z.ZodString>>;
56
+ lists: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
57
+ cards: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
58
+ images: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
59
+ videos: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
60
+ extra: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
61
+ }, z.core.$strip>;
62
+ export type PageFrontmatter = z.infer<typeof pageFrontmatterSchema>;
63
+ export interface PageIssue {
64
+ /** Project-relative path of the offending page. */
65
+ file: string;
66
+ message: string;
67
+ }
68
+ /** Validate one page's frontmatter, returning every problem rather than the first. */
69
+ export declare function validatePage(file: string, data: unknown): PageIssue[];
70
+ /**
71
+ * Validate the collection as a whole.
72
+ *
73
+ * Uniqueness and referential integrity are only checkable with every page in
74
+ * hand: two files claiming the same `id` produce two identical routes (one
75
+ * silently unreachable), and a `subpages` entry pointing at a missing page
76
+ * produces a dead navigation link.
77
+ */
78
+ export declare function validateCollection(pages: {
79
+ file: string;
80
+ data: unknown;
81
+ }[]): PageIssue[];
@@ -0,0 +1,26 @@
1
+ import type { PageTag } from './page-schema.js';
2
+ export interface NewPageInput {
3
+ /** Route segment, e.g. `services` or `services/leaks`. */
4
+ id: string;
5
+ title: string;
6
+ description: string;
7
+ label?: string;
8
+ order?: number;
9
+ tags?: PageTag[];
10
+ /** Template component to render, e.g. `Landing`. */
11
+ page?: string;
12
+ }
13
+ /**
14
+ * Build the markdown source for a new page.
15
+ *
16
+ * Frontmatter is serialised by `gray-matter`, so keys it does not understand are
17
+ * preserved and the output round-trips through the same parser `ssg check` uses.
18
+ * The body is a placeholder that already respects the heading contract — the
19
+ * page template owns the `<h1>`, so content starts at `##`.
20
+ */
21
+ export declare function renderPage(input: NewPageInput): string;
22
+ /**
23
+ * Derive a page id from a file path, so `src/pages/GettingStarted/Install.md`
24
+ * becomes `GettingStarted/Install`.
25
+ */
26
+ export declare function idFromPath(relativePath: string): string;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Where things live inside a generated project.
3
+ *
4
+ * Every command resolves paths through here so `ssg check`, `ssg build` and
5
+ * `ssg verify` cannot disagree about which directory they are reading.
6
+ */
7
+ /** The generated project root. */
8
+ export declare function appDir(cwd?: string): string;
9
+ /** `src/pages` — the single source of truth for the site's pages. */
10
+ export declare function pagesDir(app?: string): string;
11
+ /** `public` — copied verbatim into the build output. */
12
+ export declare function publicDir(app?: string): string;
13
+ /** `dist/static` — the SSG output that gets deployed. */
14
+ export declare function distDir(app?: string): string;
15
+ /** Where the generated app's `vitrify.config.ts` lives. */
16
+ export declare function configPath(app?: string): string;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Transfer-size budgets.
3
+ *
4
+ * The tiered policy from the plan: byte budgets are **hard** failures because
5
+ * they are deterministic and directly measurable, while Lighthouse performance
6
+ * is a **warn** because it varies with machine and network simulation. A gate
7
+ * that fails randomly gets disabled, so only the deterministic half blocks.
8
+ */
9
+ export interface ResourceEntry {
10
+ /**
11
+ * Lighthouse's `resource-summary` resource type: `script`, `stylesheet`,
12
+ * `image`, `font`, `document`, `media`, `other`, or the aggregate `total`.
13
+ *
14
+ * Aggregating by type rather than by URL matters: the per-request audit
15
+ * (`network-requests`) only reports requests Lighthouse actually observed in
16
+ * its trace, so a warm HTTP cache makes it under-report — and an under-reported
17
+ * budget is a gate that passes when it should fail.
18
+ */
19
+ resourceType: string;
20
+ /** Bytes over the wire, as reported by Lighthouse's resource summary. */
21
+ transferSize: number;
22
+ }
23
+ export interface Budgets {
24
+ /** Hard ceiling for transferred JavaScript, in bytes. */
25
+ jsBytes: number;
26
+ /** Hard ceiling for transferred CSS, in bytes. */
27
+ cssBytes: number;
28
+ }
29
+ export declare const DEFAULT_BUDGETS: Budgets;
30
+ export interface BudgetResult {
31
+ ok: boolean;
32
+ /** Total transferred bytes per resource type. */
33
+ totals: {
34
+ js: number;
35
+ css: number;
36
+ };
37
+ issues: string[];
38
+ }
39
+ export declare function formatBytes(bytes: number): string;
40
+ /**
41
+ * Evaluate transferred bytes against the budgets.
42
+ *
43
+ * `transferSize` (compressed, over the wire) is used rather than decoded size,
44
+ * because that is what the visitor actually pays for.
45
+ */
46
+ export declare function evaluateBudgets(resources: ResourceEntry[], budgets?: Budgets): BudgetResult;
47
+ /** Scores below these are a warning, not a failure (see the tiering note above). */
48
+ /**
49
+ * Warn when a measured total is within this fraction of its ceiling.
50
+ *
51
+ * A budget a site is always near is a cliff: everything passes until the day a
52
+ * dependency bump pushes it over, and then the failure looks sudden. A warning at
53
+ * 10% remaining turns "no headroom" into something visible on every run.
54
+ *
55
+ * Deliberately not a failure. Being inside the budget is not a defect, and the
56
+ * deterministic tier is reserved for things that are.
57
+ */
58
+ export declare const HEADROOM_WARNING_FRACTION = 0.1;
59
+ export declare function budgetHeadroomWarnings(totals: {
60
+ js: number;
61
+ css: number;
62
+ }, budgets?: Budgets, fraction?: number): string[];
63
+ export declare const DEFAULT_SCORE_TARGETS: {
64
+ /** Must equal `required` exactly — anything lower is a defect, not a trade-off. */
65
+ readonly required: {
66
+ readonly accessibility: 100;
67
+ readonly 'best-practices': 100;
68
+ readonly seo: 100;
69
+ };
70
+ /** A warning threshold: performance varies with the measuring machine. */
71
+ readonly warnBelow: {
72
+ readonly performance: 90;
73
+ };
74
+ };
75
+ export interface ScoreResult {
76
+ ok: boolean;
77
+ warnings: string[];
78
+ issues: string[];
79
+ }
80
+ export declare function evaluateScores(scores: Record<string, number>, targets?: {
81
+ required: Record<string, number>;
82
+ warnBelow: Record<string, number>;
83
+ }): ScoreResult;
84
+ /**
85
+ * Measure the transferred JavaScript and CSS of a built site **from disk**.
86
+ *
87
+ * This is deliberately not read from Lighthouse. Its `resource-summary` audit
88
+ * reports `transferSize: 0` when the trace captures no network activity
89
+ * (reproduced in a sandboxed Chromium), and a budget that measures nothing
90
+ * silently reports "OK" — a gate that cannot fail is worse than no gate.
91
+ * Reading the built files is deterministic, needs no browser, and lets the byte
92
+ * half of `ssg verify` run with `--no-lighthouse`.
93
+ *
94
+ * Sizes are gzipped, because that is what actually crosses the network.
95
+ */
96
+ export declare function measureBuildAssets(root: string): Promise<{
97
+ js: number;
98
+ css: number;
99
+ files: {
100
+ file: string;
101
+ bytes: number;
102
+ type: 'js' | 'css';
103
+ }[];
104
+ }>;
105
+ /**
106
+ * Evaluate measured byte totals against the budgets.
107
+ *
108
+ * An all-zero measurement is **no measurement**, not a pass: a built site always
109
+ * ships some JavaScript, so zero means the measurement failed.
110
+ */
111
+ export declare function evaluateTotals(totals: {
112
+ js: number;
113
+ css: number;
114
+ }, budgets?: Budgets): BudgetResult;