@webstir-io/webstir-frontend 0.1.40

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 (167) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +158 -0
  3. package/dist/assets/assetManifest.d.ts +16 -0
  4. package/dist/assets/assetManifest.js +31 -0
  5. package/dist/assets/imageOptimizer.d.ts +6 -0
  6. package/dist/assets/imageOptimizer.js +93 -0
  7. package/dist/assets/precompression.d.ts +1 -0
  8. package/dist/assets/precompression.js +21 -0
  9. package/dist/builders/contentBuilder.d.ts +2 -0
  10. package/dist/builders/contentBuilder.js +1052 -0
  11. package/dist/builders/cssBuilder.d.ts +2 -0
  12. package/dist/builders/cssBuilder.js +439 -0
  13. package/dist/builders/htmlBuilder.d.ts +2 -0
  14. package/dist/builders/htmlBuilder.js +430 -0
  15. package/dist/builders/index.d.ts +2 -0
  16. package/dist/builders/index.js +14 -0
  17. package/dist/builders/jsBuilder.d.ts +2 -0
  18. package/dist/builders/jsBuilder.js +300 -0
  19. package/dist/builders/staticAssetsBuilder.d.ts +2 -0
  20. package/dist/builders/staticAssetsBuilder.js +158 -0
  21. package/dist/builders/types.d.ts +12 -0
  22. package/dist/builders/types.js +1 -0
  23. package/dist/cli.d.ts +2 -0
  24. package/dist/cli.js +105 -0
  25. package/dist/config/manifest.d.ts +7 -0
  26. package/dist/config/manifest.js +17 -0
  27. package/dist/config/paths.d.ts +3 -0
  28. package/dist/config/paths.js +11 -0
  29. package/dist/config/schema.d.ts +413 -0
  30. package/dist/config/schema.js +44 -0
  31. package/dist/config/setup.d.ts +2 -0
  32. package/dist/config/setup.js +12 -0
  33. package/dist/config/workspace.d.ts +2 -0
  34. package/dist/config/workspace.js +131 -0
  35. package/dist/config/workspaceManifest.d.ts +23 -0
  36. package/dist/config/workspaceManifest.js +1 -0
  37. package/dist/core/constants.d.ts +70 -0
  38. package/dist/core/constants.js +70 -0
  39. package/dist/core/diagnostics.d.ts +15 -0
  40. package/dist/core/diagnostics.js +21 -0
  41. package/dist/core/index.d.ts +3 -0
  42. package/dist/core/index.js +3 -0
  43. package/dist/core/pages.d.ts +6 -0
  44. package/dist/core/pages.js +23 -0
  45. package/dist/hooks.d.ts +19 -0
  46. package/dist/hooks.js +115 -0
  47. package/dist/html/criticalCss.d.ts +4 -0
  48. package/dist/html/criticalCss.js +192 -0
  49. package/dist/html/htmlSecurity.d.ts +5 -0
  50. package/dist/html/htmlSecurity.js +73 -0
  51. package/dist/html/lazyLoad.d.ts +6 -0
  52. package/dist/html/lazyLoad.js +21 -0
  53. package/dist/html/pageScaffold.d.ts +10 -0
  54. package/dist/html/pageScaffold.js +51 -0
  55. package/dist/html/resourceHints.d.ts +7 -0
  56. package/dist/html/resourceHints.js +64 -0
  57. package/dist/index.d.ts +5 -0
  58. package/dist/index.js +5 -0
  59. package/dist/modes/ssg/index.d.ts +4 -0
  60. package/dist/modes/ssg/index.js +4 -0
  61. package/dist/modes/ssg/metadata.d.ts +5 -0
  62. package/dist/modes/ssg/metadata.js +50 -0
  63. package/dist/modes/ssg/routing.d.ts +2 -0
  64. package/dist/modes/ssg/routing.js +186 -0
  65. package/dist/modes/ssg/seo.d.ts +4 -0
  66. package/dist/modes/ssg/seo.js +208 -0
  67. package/dist/modes/ssg/validation.d.ts +3 -0
  68. package/dist/modes/ssg/validation.js +27 -0
  69. package/dist/modes/ssg/views.d.ts +2 -0
  70. package/dist/modes/ssg/views.js +236 -0
  71. package/dist/operations.d.ts +5 -0
  72. package/dist/operations.js +102 -0
  73. package/dist/pipeline.d.ts +7 -0
  74. package/dist/pipeline.js +71 -0
  75. package/dist/provider.d.ts +2 -0
  76. package/dist/provider.js +176 -0
  77. package/dist/types.d.ts +61 -0
  78. package/dist/types.js +1 -0
  79. package/dist/utils/changedFile.d.ts +8 -0
  80. package/dist/utils/changedFile.js +26 -0
  81. package/dist/utils/fs.d.ts +11 -0
  82. package/dist/utils/fs.js +39 -0
  83. package/dist/utils/hash.d.ts +1 -0
  84. package/dist/utils/hash.js +5 -0
  85. package/dist/utils/pagePaths.d.ts +5 -0
  86. package/dist/utils/pagePaths.js +36 -0
  87. package/dist/utils/pathMatch.d.ts +3 -0
  88. package/dist/utils/pathMatch.js +29 -0
  89. package/dist/watch/frontendFiles.d.ts +3 -0
  90. package/dist/watch/frontendFiles.js +25 -0
  91. package/dist/watch/hotUpdateTracker.d.ts +51 -0
  92. package/dist/watch/hotUpdateTracker.js +205 -0
  93. package/dist/watch/pipelineHelpers.d.ts +26 -0
  94. package/dist/watch/pipelineHelpers.js +177 -0
  95. package/dist/watch/types.d.ts +27 -0
  96. package/dist/watch/types.js +1 -0
  97. package/dist/watch/watchCoordinator.d.ts +36 -0
  98. package/dist/watch/watchCoordinator.js +551 -0
  99. package/dist/watch/watchDaemon.d.ts +17 -0
  100. package/dist/watch/watchDaemon.js +127 -0
  101. package/dist/watch/watchReporter.d.ts +21 -0
  102. package/dist/watch/watchReporter.js +64 -0
  103. package/package.json +92 -0
  104. package/scripts/publish.sh +101 -0
  105. package/scripts/smoke.mjs +35 -0
  106. package/scripts/update-contract.sh +121 -0
  107. package/src/assets/assetManifest.ts +51 -0
  108. package/src/assets/imageOptimizer.ts +112 -0
  109. package/src/assets/precompression.ts +25 -0
  110. package/src/builders/contentBuilder.ts +1400 -0
  111. package/src/builders/cssBuilder.ts +552 -0
  112. package/src/builders/htmlBuilder.ts +540 -0
  113. package/src/builders/index.ts +16 -0
  114. package/src/builders/jsBuilder.ts +358 -0
  115. package/src/builders/staticAssetsBuilder.ts +174 -0
  116. package/src/builders/types.ts +15 -0
  117. package/src/cli.ts +108 -0
  118. package/src/config/manifest.ts +24 -0
  119. package/src/config/paths.ts +14 -0
  120. package/src/config/schema.ts +49 -0
  121. package/src/config/setup.ts +14 -0
  122. package/src/config/workspace.ts +150 -0
  123. package/src/config/workspaceManifest.ts +27 -0
  124. package/src/core/constants.ts +73 -0
  125. package/src/core/diagnostics.ts +40 -0
  126. package/src/core/index.ts +3 -0
  127. package/src/core/pages.ts +31 -0
  128. package/src/hooks.ts +175 -0
  129. package/src/html/criticalCss.ts +214 -0
  130. package/src/html/htmlSecurity.ts +86 -0
  131. package/src/html/lazyLoad.ts +30 -0
  132. package/src/html/pageScaffold.ts +70 -0
  133. package/src/html/resourceHints.ts +91 -0
  134. package/src/index.ts +5 -0
  135. package/src/modes/ssg/index.ts +4 -0
  136. package/src/modes/ssg/metadata.ts +63 -0
  137. package/src/modes/ssg/routing.ts +230 -0
  138. package/src/modes/ssg/seo.ts +261 -0
  139. package/src/modes/ssg/validation.ts +37 -0
  140. package/src/modes/ssg/views.ts +309 -0
  141. package/src/operations.ts +138 -0
  142. package/src/pipeline.ts +88 -0
  143. package/src/provider.ts +249 -0
  144. package/src/types.ts +67 -0
  145. package/src/utils/changedFile.ts +39 -0
  146. package/src/utils/fs.ts +48 -0
  147. package/src/utils/hash.ts +6 -0
  148. package/src/utils/pagePaths.ts +43 -0
  149. package/src/utils/pathMatch.ts +36 -0
  150. package/src/watch/frontendFiles.ts +32 -0
  151. package/src/watch/hotUpdateTracker.ts +285 -0
  152. package/src/watch/pipelineHelpers.ts +242 -0
  153. package/src/watch/types.ts +23 -0
  154. package/src/watch/watchCoordinator.ts +666 -0
  155. package/src/watch/watchDaemon.ts +144 -0
  156. package/src/watch/watchReporter.ts +98 -0
  157. package/tests/add-page-defaults.test.js +64 -0
  158. package/tests/content-pages.test.js +81 -0
  159. package/tests/css-app-imports.test.js +64 -0
  160. package/tests/css-page-imports.test.js +100 -0
  161. package/tests/diagnostics.test.js +48 -0
  162. package/tests/features.test.js +63 -0
  163. package/tests/hooks.test.js +71 -0
  164. package/tests/provider.integration.test.js +137 -0
  165. package/tests/ssg-defaults.test.js +201 -0
  166. package/tests/ssg-guardrails.test.js +69 -0
  167. package/tsconfig.json +27 -0
@@ -0,0 +1,70 @@
1
+ export const FOLDERS = {
2
+ src: 'src',
3
+ build: 'build',
4
+ dist: 'dist',
5
+ webstir: '.webstir',
6
+ tests: 'tests',
7
+ frontend: 'frontend',
8
+ backend: 'backend',
9
+ shared: 'shared',
10
+ types: 'types',
11
+ app: 'app',
12
+ pages: 'pages',
13
+ styles: 'styles',
14
+ scripts: 'scripts',
15
+ images: 'images',
16
+ fonts: 'fonts',
17
+ media: 'media',
18
+ chunks: 'chunks',
19
+ home: 'home',
20
+ nodeModules: 'node_modules',
21
+ seed: 'seed',
22
+ demo: 'demo',
23
+ temp: 'temp'
24
+ };
25
+ export const FILES = {
26
+ packageJson: 'package.json',
27
+ packageLockJson: 'package-lock.json',
28
+ tsBuildInfo: '.tsbuildinfo',
29
+ baseTsConfigJson: 'base.tsconfig.json',
30
+ manifestJson: 'manifest.json',
31
+ test: '.test',
32
+ index: 'index',
33
+ indexHtml: 'index.html',
34
+ refreshJs: 'refresh.js',
35
+ hmrJs: 'hmr.js',
36
+ robotsTxt: 'robots.txt'
37
+ };
38
+ export const EXTENSIONS = {
39
+ html: '.html',
40
+ css: '.css',
41
+ br: '.br',
42
+ gz: '.gz',
43
+ dts: '.d.ts',
44
+ ts: '.ts',
45
+ js: '.js',
46
+ map: '.map',
47
+ png: '.png',
48
+ jpg: '.jpg',
49
+ jpeg: '.jpeg',
50
+ gif: '.gif',
51
+ svg: '.svg',
52
+ webp: '.webp',
53
+ avif: '.avif',
54
+ ico: '.ico',
55
+ woff: '.woff',
56
+ woff2: '.woff2',
57
+ ttf: '.ttf',
58
+ otf: '.otf',
59
+ eot: '.eot',
60
+ mp3: '.mp3',
61
+ m4a: '.m4a',
62
+ wav: '.wav',
63
+ ogg: '.ogg',
64
+ mp4: '.mp4',
65
+ webm: '.webm',
66
+ mov: '.mov'
67
+ };
68
+ export const FILE_NAMES = {
69
+ htmlAppTemplate: 'app.html'
70
+ };
@@ -0,0 +1,15 @@
1
+ export type DiagnosticSeverity = 'info' | 'warning' | 'error';
2
+ export interface DiagnosticEvent {
3
+ readonly code: string;
4
+ readonly kind: string;
5
+ readonly stage: string;
6
+ readonly severity: DiagnosticSeverity;
7
+ readonly message: string;
8
+ readonly data?: Record<string, unknown>;
9
+ readonly suggestion?: string;
10
+ }
11
+ export interface DiagnosticPayload extends DiagnosticEvent {
12
+ readonly type: 'diagnostic';
13
+ }
14
+ export declare const STRUCTURED_DIAGNOSTIC_PREFIX = "WEBSTIR_DIAGNOSTIC ";
15
+ export declare function emitDiagnostic(event: DiagnosticEvent): void;
@@ -0,0 +1,21 @@
1
+ export const STRUCTURED_DIAGNOSTIC_PREFIX = 'WEBSTIR_DIAGNOSTIC ';
2
+ export function emitDiagnostic(event) {
3
+ const payload = {
4
+ type: 'diagnostic',
5
+ ...event
6
+ };
7
+ const logMessage = `[webstir-frontend][${event.code}] ${event.message}`;
8
+ switch (event.severity) {
9
+ case 'error':
10
+ console.error(logMessage);
11
+ break;
12
+ case 'warning':
13
+ console.warn(logMessage);
14
+ break;
15
+ default:
16
+ console.info(logMessage);
17
+ break;
18
+ }
19
+ const serialized = JSON.stringify(payload);
20
+ console.log(`${STRUCTURED_DIAGNOSTIC_PREFIX}${serialized}`);
21
+ }
@@ -0,0 +1,3 @@
1
+ export * from './constants.js';
2
+ export * from './diagnostics.js';
3
+ export * from './pages.js';
@@ -0,0 +1,3 @@
1
+ export * from './constants.js';
2
+ export * from './diagnostics.js';
3
+ export * from './pages.js';
@@ -0,0 +1,6 @@
1
+ export interface PageInfo {
2
+ readonly name: string;
3
+ readonly directory: string;
4
+ }
5
+ export declare function getPages(root: string): Promise<PageInfo[]>;
6
+ export declare function getPageDirectories(root: string): Promise<PageInfo[]>;
@@ -0,0 +1,23 @@
1
+ import path from 'node:path';
2
+ import { glob } from 'glob';
3
+ import { pathExists } from '../utils/fs.js';
4
+ export async function getPages(root) {
5
+ const directories = await getPageDirectories(root);
6
+ return directories.map((entry) => ({
7
+ name: entry.name,
8
+ directory: entry.directory
9
+ }));
10
+ }
11
+ export async function getPageDirectories(root) {
12
+ if (!(await pathExists(root))) {
13
+ return [];
14
+ }
15
+ const entries = await glob('*/', { cwd: root, absolute: false, withFileTypes: false });
16
+ return entries.map((entry) => {
17
+ const name = entry.endsWith('/') ? entry.slice(0, -1) : entry;
18
+ return {
19
+ name,
20
+ directory: path.join(root, name)
21
+ };
22
+ });
23
+ }
@@ -0,0 +1,19 @@
1
+ import type { FrontendConfig } from './types.js';
2
+ import type { PipelineMode } from './pipeline.js';
3
+ export interface HookContext {
4
+ readonly config: FrontendConfig;
5
+ readonly mode: PipelineMode;
6
+ readonly workspaceRoot: string;
7
+ readonly builderName?: string;
8
+ readonly changedFile?: string;
9
+ }
10
+ export type HookHandler = (context: HookContext) => unknown | Promise<unknown>;
11
+ export interface ResolvedHooks {
12
+ readonly pipelineBefore: HookHandler[];
13
+ readonly pipelineAfter: HookHandler[];
14
+ readonly builderBefore: Map<string, HookHandler[]>;
15
+ readonly builderAfter: Map<string, HookHandler[]>;
16
+ }
17
+ export declare function loadHooks(workspaceRoot: string, cacheBust: boolean): Promise<ResolvedHooks>;
18
+ export declare function createHookContext(config: FrontendConfig, mode: PipelineMode, changedFile: string | undefined, builderName?: string): HookContext;
19
+ export declare function executeHooks(label: string, handlers: HookHandler[], context: HookContext): Promise<void>;
package/dist/hooks.js ADDED
@@ -0,0 +1,115 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ const CONFIG_CANDIDATES = ['webstir.config.mjs', 'webstir.config.js', 'webstir.config.cjs'];
5
+ const EMPTY_HOOKS = {
6
+ pipelineBefore: [],
7
+ pipelineAfter: [],
8
+ builderBefore: new Map(),
9
+ builderAfter: new Map()
10
+ };
11
+ export async function loadHooks(workspaceRoot, cacheBust) {
12
+ const configPath = findConfigPath(workspaceRoot);
13
+ if (!configPath) {
14
+ return EMPTY_HOOKS;
15
+ }
16
+ let moduleConfig;
17
+ try {
18
+ const fileUrl = pathToFileURL(configPath).href;
19
+ const url = cacheBust ? `${fileUrl}?update=${Date.now()}` : fileUrl;
20
+ moduleConfig = await import(url);
21
+ }
22
+ catch (error) {
23
+ const message = error instanceof Error ? error.message : String(error);
24
+ throw new Error(`Failed to load hooks from ${configPath}: ${message}`);
25
+ }
26
+ const exported = moduleConfig?.default ?? moduleConfig;
27
+ if (!exported || typeof exported !== 'object') {
28
+ return EMPTY_HOOKS;
29
+ }
30
+ const rawHooks = exported.hooks ?? exported;
31
+ if (!rawHooks || typeof rawHooks !== 'object') {
32
+ return EMPTY_HOOKS;
33
+ }
34
+ return normalizeHooks(rawHooks, configPath);
35
+ }
36
+ export function createHookContext(config, mode, changedFile, builderName) {
37
+ return {
38
+ config,
39
+ mode,
40
+ workspaceRoot: config.paths.workspace,
41
+ builderName,
42
+ changedFile
43
+ };
44
+ }
45
+ export async function executeHooks(label, handlers, context) {
46
+ for (const handler of handlers) {
47
+ try {
48
+ await handler(context);
49
+ }
50
+ catch (error) {
51
+ throw wrapHookError(label, error);
52
+ }
53
+ }
54
+ }
55
+ function wrapHookError(label, error) {
56
+ if (error instanceof Error) {
57
+ error.message = `[hook:${label}] ${error.message}`;
58
+ return error;
59
+ }
60
+ return new Error(`[hook:${label}] ${String(error)}`);
61
+ }
62
+ function normalizeHooks(raw, configPath) {
63
+ const pipeline = raw.pipeline ?? {};
64
+ const pipelineBefore = normalizeHandlerSet(pipeline.beforeAll, `${configPath} pipeline.beforeAll`);
65
+ const pipelineAfter = normalizeHandlerSet(pipeline.afterAll, `${configPath} pipeline.afterAll`);
66
+ const builderBefore = new Map();
67
+ const builderAfter = new Map();
68
+ const builders = raw.builders ?? {};
69
+ for (const [name, value] of Object.entries(builders)) {
70
+ if (typeof value === 'function' || Array.isArray(value)) {
71
+ builderBefore.set(name, normalizeHandlerSet(value, `${configPath} builders.${name}`));
72
+ continue;
73
+ }
74
+ if (!value || typeof value !== 'object') {
75
+ continue;
76
+ }
77
+ const before = normalizeHandlerSet(value.before, `${configPath} builders.${name}.before`);
78
+ const after = normalizeHandlerSet(value.after, `${configPath} builders.${name}.after`);
79
+ if (before.length > 0) {
80
+ builderBefore.set(name, before);
81
+ }
82
+ if (after.length > 0) {
83
+ builderAfter.set(name, after);
84
+ }
85
+ }
86
+ return {
87
+ pipelineBefore,
88
+ pipelineAfter,
89
+ builderBefore,
90
+ builderAfter
91
+ };
92
+ }
93
+ function normalizeHandlerSet(value, label) {
94
+ if (value === undefined) {
95
+ return [];
96
+ }
97
+ const handlers = Array.isArray(value) ? value : [value];
98
+ const normalized = [];
99
+ for (const handler of handlers) {
100
+ if (typeof handler !== 'function') {
101
+ throw new Error(`Invalid hook handler in ${label}; expected function`);
102
+ }
103
+ normalized.push(handler);
104
+ }
105
+ return normalized;
106
+ }
107
+ function findConfigPath(workspaceRoot) {
108
+ for (const candidate of CONFIG_CANDIDATES) {
109
+ const fullPath = path.join(workspaceRoot, candidate);
110
+ if (fs.existsSync(fullPath)) {
111
+ return fullPath;
112
+ }
113
+ }
114
+ return undefined;
115
+ }
@@ -0,0 +1,4 @@
1
+ import type { CheerioAPI } from 'cheerio';
2
+ export declare function inlineCriticalCss(document: CheerioAPI, pageName: string, pagesRoot: string, pagesUrlPrefix: string, cssFile?: string): Promise<void>;
3
+ export declare function ensureAppShellCriticalCss(document: CheerioAPI, appCssHref: string): void;
4
+ export declare function ensureDocsShellCriticalCss(document: CheerioAPI): void;
@@ -0,0 +1,192 @@
1
+ import path from 'node:path';
2
+ import * as cssoModule from 'csso';
3
+ import { EXTENSIONS } from '../core/constants.js';
4
+ import { pathExists, readFile, stat } from '../utils/fs.js';
5
+ import { resolvePageAssetUrl } from '../utils/pagePaths.js';
6
+ const INLINE_THRESHOLD_BYTES = 6 * 1024;
7
+ const csso = (cssoModule.default ?? cssoModule);
8
+ function minifyCriticalCss(css) {
9
+ return csso.minify(css).css;
10
+ }
11
+ const APP_SHELL_CRITICAL_CSS = minifyCriticalCss(`
12
+ @layer tokens {
13
+ :root {
14
+ --ws-header-control-size: 2.6rem;
15
+ --ws-header-block-padding: 0.75rem;
16
+ --ws-header-sticky-offset: calc(var(--ws-header-control-size) + (var(--ws-header-block-padding) * 2) + 1px);
17
+ --ws-font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
18
+ Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial, sans-serif;
19
+ }
20
+ }
21
+
22
+ @layer reset {
23
+ *,
24
+ *::before,
25
+ *::after {
26
+ box-sizing: border-box;
27
+ }
28
+ }
29
+
30
+ @layer base {
31
+ html,
32
+ body {
33
+ height: 100%;
34
+ }
35
+
36
+ body {
37
+ margin: 0;
38
+ font-family: var(--ws-font-sans);
39
+ font-size: 16px;
40
+ line-height: 1.6;
41
+ padding-top: var(--ws-header-sticky-offset, 0px);
42
+ }
43
+
44
+ h1,
45
+ h2,
46
+ h3,
47
+ h4,
48
+ h5,
49
+ h6 {
50
+ line-height: 1.25;
51
+ margin: 0 0 0.5rem 0;
52
+ }
53
+
54
+ h1 {
55
+ font-size: clamp(2rem, 4vw, 2.75rem);
56
+ letter-spacing: -0.02em;
57
+ }
58
+
59
+ h2 {
60
+ font-size: clamp(1.5rem, 2.5vw, 2rem);
61
+ letter-spacing: -0.01em;
62
+ }
63
+
64
+ h3 {
65
+ font-size: 1.35rem;
66
+ }
67
+
68
+ p {
69
+ margin: 0 0 1rem 0;
70
+ }
71
+ }
72
+
73
+ @layer components {
74
+ .app-header {
75
+ position: fixed;
76
+ top: 0;
77
+ left: 0;
78
+ right: 0;
79
+ height: calc(var(--ws-header-sticky-offset) - 1px);
80
+ }
81
+ }
82
+ `.trim());
83
+ const DOCS_SHELL_CRITICAL_CSS = minifyCriticalCss(`
84
+ @layer overrides {
85
+ .docs-layout {
86
+ --ws-docs-sidebar-width: clamp(14rem, 20vw, 18rem);
87
+ --ws-docs-layout-padding: 48px 0 96px;
88
+ --ws-container: 100%;
89
+ padding: var(--ws-docs-layout-padding);
90
+ padding-top: 0;
91
+ }
92
+
93
+ .docs-layout__inner {
94
+ display: grid;
95
+ grid-template-columns: var(--ws-docs-sidebar-width, 16rem) minmax(0, 1fr);
96
+ gap: var(--ws-space-6, 1.5rem);
97
+ align-items: start;
98
+ padding-inline: 0;
99
+ margin-inline: 0;
100
+ min-height: calc(100vh - var(--ws-header-sticky-offset, 0px));
101
+ }
102
+
103
+ .docs-main {
104
+ width: 100%;
105
+ max-width: var(--ws-article, 72ch);
106
+ margin-inline: 0;
107
+ min-width: 0;
108
+ grid-column: 2;
109
+ padding-top: var(--ws-space-5, 1.25rem);
110
+ padding-right: var(--ws-space-6, 1.5rem);
111
+ }
112
+
113
+ @media (max-width: 40rem) {
114
+ .docs-layout__inner {
115
+ grid-template-columns: minmax(0, 1fr);
116
+ padding-left: max(var(--ws-container-pad, 1rem), env(safe-area-inset-left));
117
+ padding-right: max(var(--ws-container-pad, 1rem), env(safe-area-inset-right));
118
+ }
119
+
120
+ .docs-main {
121
+ max-width: none;
122
+ justify-self: stretch;
123
+ grid-column: auto;
124
+ padding-top: var(--ws-space-4, 1rem);
125
+ padding-right: 0;
126
+ }
127
+ }
128
+ }
129
+ `.trim());
130
+ export async function inlineCriticalCss(document, pageName, pagesRoot, pagesUrlPrefix, cssFile) {
131
+ if (!cssFile) {
132
+ return;
133
+ }
134
+ const cssPath = path.join(pagesRoot, pageName, cssFile);
135
+ if (!(await pathExists(cssPath))) {
136
+ return;
137
+ }
138
+ const info = await stat(cssPath).catch(() => null);
139
+ if (!info || !info.isFile() || info.size > INLINE_THRESHOLD_BYTES) {
140
+ return;
141
+ }
142
+ const cssContent = minifyCriticalCss(await readFile(cssPath));
143
+ const head = document('head').first();
144
+ if (head.length === 0) {
145
+ return;
146
+ }
147
+ const href = resolvePageAssetUrl(pagesUrlPrefix, pageName, cssFile);
148
+ document(`link[href="${href}"]`).remove();
149
+ if (cssFile.endsWith(EXTENSIONS.css)) {
150
+ document(`link[rel="preload"][href="${href}"]`).remove();
151
+ }
152
+ head.append(`\n<style data-critical>\n${cssContent}\n</style>\n`);
153
+ }
154
+ export function ensureAppShellCriticalCss(document, appCssHref) {
155
+ const head = document('head').first();
156
+ if (head.length === 0) {
157
+ return;
158
+ }
159
+ const existing = head.find('style[data-critical="app"]').first();
160
+ if (existing.length > 0) {
161
+ return;
162
+ }
163
+ const stylesheet = document(`link[rel="stylesheet"][href="${appCssHref}"]`).first();
164
+ const styleTag = `<style data-critical="app">\n${APP_SHELL_CRITICAL_CSS}\n</style>`;
165
+ if (stylesheet.length > 0) {
166
+ stylesheet.before(styleTag);
167
+ }
168
+ else {
169
+ head.append(styleTag);
170
+ }
171
+ }
172
+ export function ensureDocsShellCriticalCss(document) {
173
+ const head = document('head').first();
174
+ if (head.length === 0) {
175
+ return;
176
+ }
177
+ const existing = head.find('style[data-critical="docs"]').first();
178
+ if (existing.length > 0) {
179
+ return;
180
+ }
181
+ const docsStylesheet = document('link[rel="stylesheet"]').filter((_, element) => {
182
+ const href = document(element).attr('href');
183
+ return typeof href === 'string' && href.includes('/docs/');
184
+ }).first();
185
+ const styleTag = `<style data-critical="docs">\n${DOCS_SHELL_CRITICAL_CSS}\n</style>`;
186
+ if (docsStylesheet.length > 0) {
187
+ docsStylesheet.before(styleTag);
188
+ }
189
+ else {
190
+ head.append(styleTag);
191
+ }
192
+ }
@@ -0,0 +1,5 @@
1
+ import type { CheerioAPI } from 'cheerio';
2
+ export interface SubresourceIntegrityResult {
3
+ readonly failures: string[];
4
+ }
5
+ export declare function addSubresourceIntegrity(document: CheerioAPI): Promise<SubresourceIntegrityResult>;
@@ -0,0 +1,73 @@
1
+ import { createHash } from 'node:crypto';
2
+ const HTTP_TIMEOUT_MS = 5000;
3
+ export async function addSubresourceIntegrity(document) {
4
+ const failures = [];
5
+ await Promise.all([
6
+ processScripts(document, failures),
7
+ processStylesheets(document, failures)
8
+ ]);
9
+ return { failures };
10
+ }
11
+ async function processScripts(document, failures) {
12
+ const scripts = document('script[src]').toArray();
13
+ await Promise.all(scripts.map(async (element) => {
14
+ const script = document(element);
15
+ const src = script.attr('src');
16
+ if (!src || !isExternal(src) || script.attr('integrity')) {
17
+ return;
18
+ }
19
+ const sri = await fetchIntegrity(src);
20
+ if (!sri) {
21
+ failures.push(src);
22
+ return;
23
+ }
24
+ script.attr('integrity', sri);
25
+ if (!script.attr('crossorigin')) {
26
+ script.attr('crossorigin', 'anonymous');
27
+ }
28
+ }));
29
+ }
30
+ async function processStylesheets(document, failures) {
31
+ const links = document('link[rel="stylesheet"][href]').toArray();
32
+ await Promise.all(links.map(async (element) => {
33
+ const link = document(element);
34
+ const href = link.attr('href');
35
+ if (!href || !isExternal(href) || link.attr('integrity')) {
36
+ return;
37
+ }
38
+ const sri = await fetchIntegrity(href);
39
+ if (!sri) {
40
+ failures.push(href);
41
+ return;
42
+ }
43
+ link.attr('integrity', sri);
44
+ if (!link.attr('crossorigin')) {
45
+ link.attr('crossorigin', 'anonymous');
46
+ }
47
+ }));
48
+ }
49
+ function isExternal(url) {
50
+ return url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//');
51
+ }
52
+ async function fetchIntegrity(url) {
53
+ try {
54
+ const normalizedUrl = url.startsWith('//') ? `https:${url}` : url;
55
+ const controller = new AbortController();
56
+ const timeout = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);
57
+ try {
58
+ const response = await fetch(normalizedUrl, { signal: controller.signal });
59
+ if (!response.ok) {
60
+ return null;
61
+ }
62
+ const arrayBuffer = await response.arrayBuffer();
63
+ const hash = createHash('sha384').update(Buffer.from(arrayBuffer)).digest('base64');
64
+ return `sha384-${hash}`;
65
+ }
66
+ finally {
67
+ clearTimeout(timeout);
68
+ }
69
+ }
70
+ catch {
71
+ return null;
72
+ }
73
+ }
@@ -0,0 +1,6 @@
1
+ import type { CheerioAPI } from 'cheerio';
2
+ interface LazyOptions {
3
+ readonly skip: number;
4
+ }
5
+ export declare function applyLazyLoading(document: CheerioAPI, options?: LazyOptions): void;
6
+ export {};
@@ -0,0 +1,21 @@
1
+ const DEFAULT_OPTIONS = {
2
+ skip: 1
3
+ };
4
+ export function applyLazyLoading(document, options = DEFAULT_OPTIONS) {
5
+ const { skip } = options;
6
+ let index = 0;
7
+ document('img').each((_i, element) => {
8
+ const img = document(element);
9
+ if (img.attr('loading')) {
10
+ return;
11
+ }
12
+ index += 1;
13
+ if (index <= skip) {
14
+ return;
15
+ }
16
+ img.attr('loading', 'lazy');
17
+ if (!img.attr('fetchpriority')) {
18
+ img.attr('fetchpriority', 'low');
19
+ }
20
+ });
21
+ }
@@ -0,0 +1,10 @@
1
+ export interface PageScaffoldOptions {
2
+ readonly workspaceRoot: string;
3
+ readonly pageName: string;
4
+ readonly mode?: 'standard' | 'ssg';
5
+ readonly paths: {
6
+ readonly pages: string;
7
+ readonly app: string;
8
+ };
9
+ }
10
+ export declare function createPageScaffold(options: PageScaffoldOptions): Promise<void>;
@@ -0,0 +1,51 @@
1
+ import path from 'node:path';
2
+ import { FILES, EXTENSIONS } from '../core/constants.js';
3
+ import { ensureDir, pathExists, writeFile } from '../utils/fs.js';
4
+ export async function createPageScaffold(options) {
5
+ const pageDir = path.join(options.paths.pages, options.pageName);
6
+ if (await pathExists(pageDir)) {
7
+ throw new Error(`Page '${options.pageName}' already exists.`);
8
+ }
9
+ await ensureDir(pageDir);
10
+ const mode = options.mode ?? 'standard';
11
+ const writes = [
12
+ writeFile(path.join(pageDir, `${FILES.index}${EXTENSIONS.html}`), buildHtmlTemplate(options.pageName, mode)),
13
+ writeFile(path.join(pageDir, `${FILES.index}${EXTENSIONS.css}`), buildCssTemplate(options.pageName))
14
+ ];
15
+ if (mode === 'standard') {
16
+ writes.push(writeFile(path.join(pageDir, `${FILES.index}${EXTENSIONS.ts}`), buildScriptTemplate()));
17
+ }
18
+ await Promise.all(writes);
19
+ }
20
+ function buildHtmlTemplate(pageName, mode) {
21
+ const script = mode === 'standard'
22
+ ? ` <script type="module" src="${FILES.index}${EXTENSIONS.js}" async></script>`
23
+ : ` <!-- Add ${FILES.index}${EXTENSIONS.ts} to enable JS on this page. -->`;
24
+ return `<head>
25
+ <meta charset="utf-8">
26
+ <title>${pageName}</title>
27
+ <link rel="stylesheet" href="${FILES.index}${EXTENSIONS.css}">
28
+ </head>
29
+ <body>
30
+ <main>
31
+ <h1>${pageName}</h1>
32
+ <p>Content for the ${pageName} page.</p>
33
+ </main>
34
+ ${script}
35
+ </body>
36
+ `;
37
+ }
38
+ function buildCssTemplate(pageName) {
39
+ return `/* ${pageName} Page Styles */
40
+ @import "@app/app.css";
41
+
42
+ /* Add your page-specific styles here */
43
+ `;
44
+ }
45
+ function buildScriptTemplate() {
46
+ return `// Page entry point
47
+ import '../../app/app';
48
+
49
+ // Add page-specific logic here
50
+ `;
51
+ }
@@ -0,0 +1,7 @@
1
+ import type { CheerioAPI } from 'cheerio';
2
+ export interface ResourceHintResult {
3
+ readonly added: number;
4
+ readonly candidates: string[];
5
+ readonly missingHead: boolean;
6
+ }
7
+ export declare function injectResourceHints(document: CheerioAPI, currentPage: string, pagesUrlPrefix: string, useRootIndex: boolean): ResourceHintResult;