design-clone 2.1.0 → 3.0.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 (177) hide show
  1. package/README.md +13 -34
  2. package/SKILL.md +69 -45
  3. package/bin/cli.js +22 -4
  4. package/bin/commands/clone-site.js +31 -171
  5. package/bin/commands/help.js +19 -6
  6. package/bin/commands/init.js +9 -86
  7. package/bin/commands/uninstall.js +105 -0
  8. package/bin/commands/update.js +70 -0
  9. package/bin/commands/verify.js +7 -14
  10. package/bin/utils/paths.js +28 -0
  11. package/bin/utils/validate.js +2 -22
  12. package/bin/utils/version.js +23 -0
  13. package/docs/code-standards.md +789 -0
  14. package/docs/codebase-summary.md +533 -286
  15. package/docs/index.md +74 -0
  16. package/docs/project-overview-pdr.md +797 -0
  17. package/docs/system-architecture.md +718 -0
  18. package/package.json +14 -17
  19. package/src/ai/prompts/design-tokens/basic.md +80 -0
  20. package/src/ai/prompts/design-tokens/section-with-css.md +41 -0
  21. package/src/ai/prompts/design-tokens/section.md +48 -0
  22. package/src/ai/prompts/design-tokens/with-css.md +87 -0
  23. package/src/ai/prompts/structure-analysis/basic.md +55 -0
  24. package/src/ai/prompts/structure-analysis/with-context.md +59 -0
  25. package/src/ai/prompts/structure-analysis/with-dimensions.md +63 -0
  26. package/src/ai/prompts/structure-analysis/with-hierarchy.md +73 -0
  27. package/src/ai/prompts/ux-audit/aggregation.md +42 -0
  28. package/src/ai/prompts/ux-audit/desktop.md +92 -0
  29. package/src/ai/prompts/ux-audit/mobile.md +93 -0
  30. package/src/ai/prompts/ux-audit/tablet.md +92 -0
  31. package/src/core/animation/animation-extractor-ast.js +183 -0
  32. package/src/core/animation/animation-extractor-output.js +152 -0
  33. package/src/core/animation/animation-extractor.js +178 -0
  34. package/src/core/animation/state-capture-detection.js +200 -0
  35. package/src/core/animation/state-capture.js +193 -0
  36. package/src/core/capture/browser-context-pool.js +96 -0
  37. package/src/core/capture/multi-page-screenshot-page.js +110 -0
  38. package/src/core/capture/multi-page-screenshot.js +208 -0
  39. package/src/core/capture/screenshot-extraction.js +186 -0
  40. package/src/core/capture/screenshot-helpers.js +175 -0
  41. package/src/core/capture/screenshot-orchestrator.js +174 -0
  42. package/src/core/capture/screenshot-viewport.js +93 -0
  43. package/src/core/capture/screenshot.js +192 -0
  44. package/src/core/content/content-counter-dom.js +191 -0
  45. package/src/core/content/content-counter.js +76 -0
  46. package/src/core/css/breakpoint-detector.js +66 -0
  47. package/src/core/css/chromium-defaults.json +23 -0
  48. package/src/core/css/computed-style-extractor.js +102 -0
  49. package/src/core/css/css-chunker.js +103 -0
  50. package/src/core/css/filter-css-dead-code.js +120 -0
  51. package/src/core/css/filter-css-html-analyzer.js +110 -0
  52. package/src/core/css/filter-css-selector-matcher.js +172 -0
  53. package/src/core/css/filter-css.js +206 -0
  54. package/src/core/css/merge-css-atrule-processor.js +158 -0
  55. package/src/core/css/merge-css-file-io.js +68 -0
  56. package/src/core/css/merge-css.js +148 -0
  57. package/src/core/detection/framework-detector-routing.js +68 -0
  58. package/src/core/detection/framework-detector-signals.js +65 -0
  59. package/src/core/detection/framework-detector.js +198 -0
  60. package/src/core/dimension/dimension-extractor-card-detector.js +82 -0
  61. package/src/core/dimension/dimension-extractor.js +317 -0
  62. package/src/core/dimension/dimension-output-ai-summary.js +111 -0
  63. package/src/core/dimension/dimension-output.js +173 -0
  64. package/src/core/dimension/dom-tree-analyzer-tree-builders.js +95 -0
  65. package/src/core/dimension/dom-tree-analyzer.js +191 -0
  66. package/src/core/discovery/app-state-snapshot-capture.js +195 -0
  67. package/src/core/discovery/app-state-snapshot-utils.js +178 -0
  68. package/src/core/discovery/app-state-snapshot.js +131 -0
  69. package/src/core/discovery/discover-pages-routes.js +84 -0
  70. package/src/core/discovery/discover-pages-utils.js +177 -0
  71. package/src/core/discovery/discover-pages.js +191 -0
  72. package/src/core/html/html-extractor-inline-styler.js +70 -0
  73. package/src/core/html/html-extractor.js +147 -0
  74. package/src/core/html/semantic-enhancer-mappings.js +200 -0
  75. package/src/core/html/semantic-enhancer-page.js +148 -0
  76. package/src/core/html/semantic-enhancer.js +135 -0
  77. package/src/core/links/rewrite-links-css-rewriter.js +53 -0
  78. package/src/core/links/rewrite-links.js +173 -0
  79. package/src/core/media/asset-validator.js +118 -0
  80. package/src/core/media/extract-assets-downloader.js +187 -0
  81. package/src/core/media/extract-assets-page-scraper.js +115 -0
  82. package/src/core/media/extract-assets.js +159 -0
  83. package/src/core/media/video-capture-convert.js +200 -0
  84. package/src/core/media/video-capture.js +201 -0
  85. package/src/core/{lazy-loader.js → page-prep/lazy-loader.js} +37 -39
  86. package/src/core/section/section-cropper-helpers.js +43 -0
  87. package/src/core/{section-cropper.js → section/section-cropper.js} +11 -88
  88. package/src/core/section/section-detector-strategies.js +139 -0
  89. package/src/core/section/section-detector-utils.js +100 -0
  90. package/src/core/section/section-detector.js +88 -0
  91. package/src/core/tests/test-section-cropper.js +2 -2
  92. package/src/core/tests/test-section-detector.js +2 -2
  93. package/src/post-process/enhance-assets.js +29 -4
  94. package/src/post-process/fetch-images-unsplash-client.js +123 -0
  95. package/src/post-process/fetch-images.js +60 -263
  96. package/src/post-process/inject-gosnap.js +88 -0
  97. package/src/post-process/inject-icons-svg-replacer.js +76 -0
  98. package/src/post-process/inject-icons.js +47 -200
  99. package/src/route-discoverers/base-discoverer-utils.js +137 -0
  100. package/src/route-discoverers/base-discoverer.js +29 -118
  101. package/src/route-discoverers/index.js +1 -1
  102. package/src/shared/config.js +38 -0
  103. package/src/shared/error-codes.js +31 -0
  104. package/src/shared/viewports.js +46 -0
  105. package/src/utils/browser.js +0 -7
  106. package/src/utils/helpers.js +4 -0
  107. package/src/utils/log.js +12 -0
  108. package/src/utils/playwright-loader.js +76 -0
  109. package/src/utils/playwright.js +3 -69
  110. package/src/utils/progress.js +32 -0
  111. package/src/verification/generate-audit-report-css-fixes.js +52 -0
  112. package/src/verification/generate-audit-report-sections.js +158 -0
  113. package/src/verification/generate-audit-report.js +5 -281
  114. package/src/verification/quality-scorer.js +92 -0
  115. package/src/verification/verify-footer-checks.js +103 -0
  116. package/src/verification/verify-footer-helpers.js +178 -0
  117. package/src/verification/verify-footer.js +23 -381
  118. package/src/verification/verify-header-checks.js +104 -0
  119. package/src/verification/verify-header-helpers.js +156 -0
  120. package/src/verification/verify-header.js +23 -365
  121. package/src/verification/verify-layout-report.js +101 -0
  122. package/src/verification/verify-layout.js +13 -259
  123. package/src/verification/verify-menu-checks.js +104 -0
  124. package/src/verification/verify-menu-helpers.js +112 -0
  125. package/src/verification/verify-menu.js +17 -285
  126. package/src/verification/verify-slider-checks.js +115 -0
  127. package/src/verification/verify-slider-constants.js +65 -0
  128. package/src/verification/verify-slider-helpers.js +164 -0
  129. package/src/verification/verify-slider.js +23 -414
  130. package/.env.example +0 -14
  131. package/docs/basic-clone.md +0 -63
  132. package/docs/cli-reference.md +0 -316
  133. package/docs/design-clone-architecture.md +0 -492
  134. package/docs/pixel-perfect.md +0 -117
  135. package/docs/project-roadmap.md +0 -382
  136. package/docs/troubleshooting.md +0 -170
  137. package/requirements.txt +0 -5
  138. package/src/ai/__pycache__/analyze-structure.cpython-313.pyc +0 -0
  139. package/src/ai/__pycache__/extract-design-tokens.cpython-313.pyc +0 -0
  140. package/src/ai/analyze-structure.py +0 -375
  141. package/src/ai/extract-design-tokens.py +0 -782
  142. package/src/ai/prompts/__init__.py +0 -2
  143. package/src/ai/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
  144. package/src/ai/prompts/__pycache__/design_tokens.cpython-313.pyc +0 -0
  145. package/src/ai/prompts/__pycache__/structure_analysis.cpython-313.pyc +0 -0
  146. package/src/ai/prompts/__pycache__/ux_audit.cpython-313.pyc +0 -0
  147. package/src/ai/prompts/design_tokens.py +0 -316
  148. package/src/ai/prompts/structure_analysis.py +0 -592
  149. package/src/ai/prompts/ux_audit.py +0 -198
  150. package/src/ai/ux-audit.js +0 -596
  151. package/src/core/animation-extractor.js +0 -526
  152. package/src/core/app-state-snapshot.js +0 -511
  153. package/src/core/content-counter.js +0 -342
  154. package/src/core/design-tokens.js +0 -103
  155. package/src/core/dimension-extractor.js +0 -438
  156. package/src/core/dimension-output.js +0 -305
  157. package/src/core/discover-pages.js +0 -542
  158. package/src/core/dom-tree-analyzer.js +0 -298
  159. package/src/core/extract-assets.js +0 -468
  160. package/src/core/filter-css.js +0 -499
  161. package/src/core/framework-detector.js +0 -538
  162. package/src/core/html-extractor.js +0 -212
  163. package/src/core/merge-css.js +0 -407
  164. package/src/core/multi-page-screenshot.js +0 -380
  165. package/src/core/rewrite-links.js +0 -226
  166. package/src/core/screenshot.js +0 -701
  167. package/src/core/section-detector.js +0 -386
  168. package/src/core/semantic-enhancer.js +0 -492
  169. package/src/core/state-capture.js +0 -598
  170. package/src/core/video-capture.js +0 -546
  171. package/src/utils/__init__.py +0 -16
  172. package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  173. package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
  174. package/src/utils/env.py +0 -134
  175. /package/src/core/{css-extractor.js → css/css-extractor.js} +0 -0
  176. /package/src/core/{cookie-handler.js → page-prep/cookie-handler.js} +0 -0
  177. /package/src/core/{page-readiness.js → page-prep/page-readiness.js} +0 -0
@@ -1,380 +0,0 @@
1
- /**
2
- * Multi-page Screenshot Capture
3
- *
4
- * Capture screenshots + extract HTML/CSS for multiple pages
5
- * using a shared browser session for efficiency.
6
- *
7
- * Usage:
8
- * import { captureMultiplePages } from './multi-page-screenshot.js';
9
- * const result = await captureMultiplePages(pages, { outputDir: './output' });
10
- */
11
-
12
- import path from 'path';
13
- import fs from 'fs/promises';
14
-
15
- import { getBrowser, getPage, disconnectBrowser } from '../utils/browser.js';
16
- import { captureViewport, VIEWPORTS, DEFAULT_SCROLL_DELAY } from './screenshot.js';
17
- import { waitForDomStable, waitForPageReady } from './page-readiness.js';
18
- import { dismissCookieBanner } from './cookie-handler.js';
19
- import { extractAndEnhanceHtml, JS_FRAMEWORK_PATTERNS, MAX_HTML_SIZE } from './html-extractor.js';
20
- import { extractAllCss, MAX_CSS_SIZE } from './css-extractor.js';
21
- import { filterCssFile } from './filter-css.js';
22
-
23
- // Default options
24
- const DEFAULT_OPTIONS = {
25
- viewports: ['desktop', 'tablet', 'mobile'],
26
- fullPage: true,
27
- extractHtml: true,
28
- extractCss: true,
29
- filterUnused: true,
30
- maxSize: 5, // MB for screenshots
31
- scrollDelay: DEFAULT_SCROLL_DELAY,
32
- timeout: 60000,
33
- onProgress: null // (current, total, pageInfo) => {}
34
- };
35
-
36
- /**
37
- * Convert page path to safe filename
38
- * @param {string} pagePath - URL path (e.g., '/about', '/services/consulting')
39
- * @returns {string} Safe filename (e.g., 'about', 'services-consulting')
40
- */
41
- export function pathToFilename(pagePath) {
42
- if (!pagePath || pagePath === '/') return 'index';
43
- return pagePath
44
- .replace(/^\//, '') // Remove leading slash
45
- .replace(/\/$/, '') // Remove trailing slash
46
- .replace(/\//g, '-') // Replace slashes with dashes
47
- .replace(/[^a-z0-9-]/gi, '-') // Replace special chars
48
- .replace(/-+/g, '-') // Collapse multiple dashes
49
- .toLowerCase();
50
- }
51
-
52
- /**
53
- * Create output directory structure
54
- * @param {string} outputDir - Base output directory
55
- * @param {string[]} viewports - Viewport names
56
- */
57
- async function createOutputStructure(outputDir, viewports) {
58
- const dirs = [
59
- outputDir,
60
- path.join(outputDir, 'html'),
61
- path.join(outputDir, 'css'),
62
- ...viewports.map(vp => path.join(outputDir, 'analysis', vp))
63
- ];
64
-
65
- for (const dir of dirs) {
66
- await fs.mkdir(dir, { recursive: true });
67
- }
68
- }
69
-
70
- /**
71
- * Capture a single page (all viewports + HTML/CSS extraction)
72
- * @param {Page} page - Playwright page instance
73
- * @param {Object} pageInfo - Page info { path, name, url }
74
- * @param {string} outputDir - Output directory
75
- * @param {Object} options - Capture options
76
- * @returns {Promise<Object>} Capture result for this page
77
- */
78
- async function captureSinglePage(page, pageInfo, outputDir, options) {
79
- const filename = pathToFilename(pageInfo.path);
80
- const result = {
81
- path: pageInfo.path,
82
- name: pageInfo.name,
83
- url: pageInfo.url,
84
- filename,
85
- screenshots: {},
86
- html: null,
87
- css: null,
88
- warnings: []
89
- };
90
-
91
- try {
92
- // Navigate to page
93
- await page.goto(pageInfo.url, {
94
- waitUntil: 'networkidle',
95
- timeout: options.timeout
96
- });
97
-
98
- // Wait for page ready
99
- await waitForPageReady(page);
100
-
101
- // Dismiss cookie banner (may already be dismissed)
102
- await dismissCookieBanner(page).catch(() => {});
103
-
104
- // Extra stabilization
105
- await waitForDomStable(page, 300, 3000);
106
-
107
- // Extract HTML with semantic enhancement
108
- if (options.extractHtml) {
109
- try {
110
- const enhanceSemantic = options.enhanceSemantic !== false;
111
- const htmlResult = await extractAndEnhanceHtml(page, { enhanceSemantic });
112
- const htmlSize = Buffer.byteLength(htmlResult.html, 'utf-8');
113
-
114
- if (htmlSize > MAX_HTML_SIZE) {
115
- result.warnings.push(`HTML size exceeds limit: ${(htmlSize / 1024 / 1024).toFixed(1)}MB`);
116
- } else {
117
- const htmlPath = path.join(outputDir, 'html', `${filename}.html`);
118
- await fs.writeFile(htmlPath, htmlResult.html, 'utf-8');
119
- result.html = {
120
- path: htmlPath,
121
- size: htmlSize,
122
- elementCount: htmlResult.elementCount,
123
- semanticEnhanced: enhanceSemantic,
124
- semanticStats: htmlResult.semanticStats || null
125
- };
126
- if (htmlResult.warnings.length > 0) {
127
- result.warnings.push(...htmlResult.warnings);
128
- }
129
- }
130
- } catch (err) {
131
- result.warnings.push(`HTML extraction failed: ${err.message}`);
132
- result.html = { error: err.message, failed: true };
133
- }
134
- }
135
-
136
- // Extract CSS
137
- if (options.extractCss) {
138
- try {
139
- const cssData = await extractAllCss(page, pageInfo.url);
140
- const rawCss = cssData.cssBlocks
141
- .map(b => `/* Source: ${b.source} */\n${b.css}`)
142
- .join('\n\n');
143
- const cssSize = Buffer.byteLength(rawCss, 'utf-8');
144
-
145
- if (cssSize > MAX_CSS_SIZE) {
146
- result.warnings.push(`CSS size exceeds limit: ${(cssSize / 1024 / 1024).toFixed(1)}MB`);
147
- } else {
148
- const cssPath = path.join(outputDir, 'css', `${filename}-raw.css`);
149
- await fs.writeFile(cssPath, rawCss, 'utf-8');
150
- result.css = {
151
- path: cssPath,
152
- size: cssSize,
153
- ruleCount: cssData.totalRules,
154
- corsBlocked: cssData.corsBlocked.length
155
- };
156
- if (cssData.warnings.length > 0) {
157
- result.warnings.push(...cssData.warnings);
158
- }
159
- }
160
- } catch (err) {
161
- result.warnings.push(`CSS extraction failed: ${err.message}`);
162
- result.css = { error: err.message, failed: true };
163
- }
164
- }
165
-
166
- // Filter CSS if both HTML and CSS extracted successfully
167
- if (options.filterUnused && result.html?.path && result.css?.path &&
168
- !result.html.failed && !result.css.failed) {
169
- try {
170
- const filteredPath = path.join(outputDir, 'css', `${filename}.css`);
171
- const filterResult = await filterCssFile(
172
- result.html.path,
173
- result.css.path,
174
- filteredPath,
175
- false,
176
- outputDir
177
- );
178
- result.cssFiltered = {
179
- path: filteredPath,
180
- size: filterResult.output.size,
181
- reduction: filterResult.stats.reduction
182
- };
183
- } catch (err) {
184
- result.warnings.push(`CSS filtering failed: ${err.message}`);
185
- }
186
- }
187
-
188
- // Capture viewports
189
- for (const viewport of options.viewports) {
190
- if (!VIEWPORTS[viewport]) {
191
- result.warnings.push(`Invalid viewport: ${viewport}`);
192
- continue;
193
- }
194
-
195
- try {
196
- const screenshotPath = path.join(outputDir, 'analysis', viewport, `${filename}.png`);
197
- const vpResult = await captureViewport(
198
- page,
199
- viewport,
200
- screenshotPath,
201
- options.fullPage,
202
- options.maxSize,
203
- options.scrollDelay
204
- );
205
- result.screenshots[viewport] = {
206
- path: vpResult.path,
207
- size: vpResult.size,
208
- compressed: vpResult.compressed
209
- };
210
- } catch (err) {
211
- result.warnings.push(`${viewport} capture failed: ${err.message}`);
212
- result.screenshots[viewport] = { error: err.message, failed: true };
213
- }
214
- }
215
-
216
- result.success = true;
217
- } catch (err) {
218
- result.success = false;
219
- result.error = err.message;
220
- result.warnings.push(`Page capture failed: ${err.message}`);
221
- }
222
-
223
- return result;
224
- }
225
-
226
- /**
227
- * Capture multiple pages with shared browser session
228
- * @param {Array} pages - Array of { path, name, url }
229
- * @param {Object} options - Capture options
230
- * @returns {Promise<Object>} Complete capture result
231
- */
232
- export async function captureMultiplePages(pages, options = {}) {
233
- const opts = { ...DEFAULT_OPTIONS, ...options };
234
- const startTime = Date.now();
235
-
236
- if (!opts.outputDir) {
237
- throw new Error('outputDir is required');
238
- }
239
-
240
- // Create output directory structure
241
- await createOutputStructure(opts.outputDir, opts.viewports);
242
-
243
- let browser = null;
244
- const results = {
245
- success: true,
246
- baseUrl: pages[0]?.url ? new URL(pages[0].url).origin : null,
247
- outputDir: path.resolve(opts.outputDir),
248
- pages: [],
249
- cssFiles: [], // Raw CSS paths
250
- cssFilesFiltered: [], // Filtered CSS paths
251
- stats: {
252
- totalPages: pages.length,
253
- successfulPages: 0,
254
- failedPages: 0,
255
- totalScreenshots: 0,
256
- totalWarnings: 0
257
- },
258
- capturedAt: new Date().toISOString()
259
- };
260
-
261
- try {
262
- // Launch browser once
263
- browser = await getBrowser({ headless: true });
264
-
265
- for (let i = 0; i < pages.length; i++) {
266
- const pageInfo = pages[i];
267
-
268
- // Progress callback
269
- if (opts.onProgress) {
270
- opts.onProgress(i + 1, pages.length, {
271
- path: pageInfo.path,
272
- name: pageInfo.name,
273
- status: 'capturing'
274
- });
275
- }
276
-
277
- // Get a new page tab
278
- const page = await getPage(browser);
279
-
280
- try {
281
- // Capture this page
282
- const pageResult = await captureSinglePage(page, pageInfo, opts.outputDir, opts);
283
- results.pages.push(pageResult);
284
-
285
- // Track CSS files for merging
286
- if (pageResult.css?.path && !pageResult.css.failed) {
287
- results.cssFiles.push(pageResult.css.path);
288
- }
289
-
290
- // Track filtered CSS files
291
- if (pageResult.cssFiltered?.path) {
292
- results.cssFilesFiltered.push(pageResult.cssFiltered.path);
293
- }
294
-
295
- // Update stats
296
- if (pageResult.success) {
297
- results.stats.successfulPages++;
298
- results.stats.totalScreenshots += Object.keys(pageResult.screenshots)
299
- .filter(vp => !pageResult.screenshots[vp].failed).length;
300
- } else {
301
- results.stats.failedPages++;
302
- }
303
- results.stats.totalWarnings += pageResult.warnings.length;
304
-
305
- // Progress callback - done
306
- if (opts.onProgress) {
307
- opts.onProgress(i + 1, pages.length, {
308
- path: pageInfo.path,
309
- name: pageInfo.name,
310
- status: 'done'
311
- });
312
- }
313
- } finally {
314
- // Close tab, keep browser
315
- await page.close().catch(() => {});
316
- }
317
- }
318
- } catch (err) {
319
- results.success = false;
320
- results.error = err.message;
321
- } finally {
322
- // Disconnect browser
323
- if (browser) {
324
- await disconnectBrowser().catch(() => {});
325
- }
326
- }
327
-
328
- // Calculate total time
329
- results.stats.totalTimeMs = Date.now() - startTime;
330
-
331
- // Write results JSON
332
- const resultsPath = path.join(opts.outputDir, 'capture-results.json');
333
- await fs.writeFile(resultsPath, JSON.stringify(results, null, 2));
334
- results.resultsFile = resultsPath;
335
-
336
- return results;
337
- }
338
-
339
- // CLI support
340
- const isMainModule = process.argv[1] && (
341
- process.argv[1].endsWith('multi-page-screenshot.js') ||
342
- process.argv[1].includes('multi-page-screenshot')
343
- );
344
-
345
- if (isMainModule) {
346
- // Simple CLI: node multi-page-screenshot.js <url> <outputDir>
347
- const url = process.argv[2];
348
- const outputDir = process.argv[3] || './multi-capture-output';
349
-
350
- if (!url) {
351
- console.error('Usage: node multi-page-screenshot.js <url> [outputDir]');
352
- process.exit(1);
353
- }
354
-
355
- // Import discoverPages for CLI mode
356
- import('./discover-pages.js').then(async ({ discoverPages }) => {
357
- console.error(`[INFO] Discovering pages from ${url}...`);
358
- const discovery = await discoverPages(url, { maxPages: 5 });
359
-
360
- if (!discovery.success) {
361
- console.error(`[ERROR] Discovery failed: ${discovery.error}`);
362
- process.exit(1);
363
- }
364
-
365
- console.error(`[INFO] Found ${discovery.pages.length} pages`);
366
-
367
- const result = await captureMultiplePages(discovery.pages, {
368
- outputDir,
369
- onProgress: (current, total, info) => {
370
- console.error(`[${current}/${total}] ${info.status}: ${info.name} (${info.path})`);
371
- }
372
- });
373
-
374
- console.log(JSON.stringify(result, null, 2));
375
- process.exit(result.success ? 0 : 1);
376
- }).catch(err => {
377
- console.error(`[ERROR] ${err.message}`);
378
- process.exit(1);
379
- });
380
- }
@@ -1,226 +0,0 @@
1
- /**
2
- * Link Rewriting Module
3
- *
4
- * Rewrites internal links in HTML to point to local .html files.
5
- * Preserves external links unchanged.
6
- *
7
- * Usage:
8
- * import { rewriteLinks, createPageManifest } from './rewrite-links.js';
9
- * const rewritten = rewriteLinks(html, manifest, { baseUrl });
10
- */
11
-
12
- import { normalizeUrl } from './discover-pages.js';
13
-
14
- /**
15
- * Convert URL path to local filename
16
- * @param {string} urlPath - URL path (e.g., '/about', '/services/consulting')
17
- * @returns {string} Local filename (e.g., 'about.html', 'services-consulting.html')
18
- */
19
- export function pathToFilename(urlPath) {
20
- if (!urlPath || urlPath === '/' || urlPath === '') {
21
- return 'index.html';
22
- }
23
-
24
- const name = urlPath
25
- .replace(/^\//, '') // Remove leading slash
26
- .replace(/\/$/, '') // Remove trailing slash
27
- .replace(/\//g, '-') // Replace slashes with dashes
28
- .replace(/[^a-z0-9-]/gi, '-') // Replace special chars
29
- .replace(/-+/g, '-') // Collapse multiple dashes
30
- .toLowerCase();
31
-
32
- return `${name}.html`;
33
- }
34
-
35
- /**
36
- * Create page manifest from discovered pages
37
- * @param {Array} pages - Array of { path, name, url }
38
- * @param {Object} options - Additional options
39
- * @returns {Object} Page manifest
40
- */
41
- export function createPageManifest(pages, options = {}) {
42
- const baseUrl = pages[0]?.url ? new URL(pages[0].url).origin : '';
43
-
44
- const manifest = {
45
- baseUrl,
46
- capturedAt: new Date().toISOString(),
47
- pages: pages.map(page => ({
48
- path: page.path,
49
- name: page.name,
50
- file: pathToFilename(page.path),
51
- originalUrl: page.url
52
- })),
53
- assets: {
54
- css: 'styles.css',
55
- tokens: options.hasTokens ? 'tokens.css' : null
56
- },
57
- stats: options.stats || {}
58
- };
59
-
60
- return manifest;
61
- }
62
-
63
- /**
64
- * Build URL to filename mapping from manifest
65
- * @param {Object} manifest - Page manifest
66
- * @returns {Map} URL -> filename mapping
67
- */
68
- function buildUrlMap(manifest) {
69
- const urlMap = new Map();
70
-
71
- for (const page of manifest.pages) {
72
- // Map by full URL
73
- if (page.originalUrl) {
74
- urlMap.set(page.originalUrl, page.file);
75
- // Also without trailing slash
76
- const noSlash = page.originalUrl.replace(/\/$/, '');
77
- urlMap.set(noSlash, page.file);
78
- }
79
-
80
- // Map by path
81
- if (page.path) {
82
- urlMap.set(page.path, page.file);
83
- // Also without trailing slash
84
- if (page.path !== '/') {
85
- urlMap.set(page.path.replace(/\/$/, ''), page.file);
86
- }
87
- }
88
- }
89
-
90
- return urlMap;
91
- }
92
-
93
- /**
94
- * Rewrite links in HTML to point to local files
95
- * @param {string} html - HTML content
96
- * @param {Object} manifest - Page manifest
97
- * @param {Object} options - Rewrite options
98
- * @returns {string} HTML with rewritten links
99
- */
100
- export function rewriteLinks(html, manifest, options = {}) {
101
- const { baseUrl, rewriteCss = true, injectTokensCss = false } = options;
102
- const urlMap = buildUrlMap(manifest);
103
-
104
- let result = html;
105
-
106
- // Rewrite <a href="..."> links
107
- result = result.replace(
108
- /(<a\s[^>]*href=["'])([^"']+)(["'][^>]*>)/gi,
109
- (match, prefix, href, suffix) => {
110
- // Skip empty, javascript:, mailto:, tel:, and anchor-only links
111
- if (!href ||
112
- href.startsWith('javascript:') ||
113
- href.startsWith('mailto:') ||
114
- href.startsWith('tel:') ||
115
- href.startsWith('#')) {
116
- return match;
117
- }
118
-
119
- // Try to match against manifest
120
- let filename = null;
121
-
122
- // Direct path match
123
- if (urlMap.has(href)) {
124
- filename = urlMap.get(href);
125
- }
126
- // Normalized URL match
127
- else if (baseUrl) {
128
- const normalized = normalizeUrl(baseUrl, href);
129
- if (normalized && urlMap.has(normalized)) {
130
- filename = urlMap.get(normalized);
131
- }
132
- }
133
-
134
- if (filename) {
135
- // Preserve fragment if present
136
- const fragmentMatch = href.match(/#[^#]*$/);
137
- const fragment = fragmentMatch ? fragmentMatch[0] : '';
138
- return `${prefix}${filename}${fragment}${suffix}`;
139
- }
140
-
141
- // Keep original for external/unknown links
142
- return match;
143
- }
144
- );
145
-
146
- // Rewrite CSS links to use shared styles.css
147
- if (rewriteCss) {
148
- result = result.replace(
149
- /<link([^>]*?)href=["'][^"']*\.css["']([^>]*?)>/gi,
150
- (match, before, after) => {
151
- // Check if it's a stylesheet link
152
- if (match.includes('rel="stylesheet"') || match.includes("rel='stylesheet'") ||
153
- !match.includes('rel=')) {
154
- return `<link${before}href="../styles.css" rel="stylesheet"${after}>`;
155
- }
156
- return match;
157
- }
158
- );
159
-
160
- // Remove duplicate stylesheet links (keep first)
161
- const seenStylesheets = new Set();
162
- result = result.replace(
163
- /<link[^>]*href=["']\.\.\/styles\.css["'][^>]*>/gi,
164
- (match) => {
165
- if (seenStylesheets.has('styles.css')) {
166
- return ''; // Remove duplicate
167
- }
168
- seenStylesheets.add('styles.css');
169
- return match;
170
- }
171
- );
172
-
173
- // Inject tokens.css before styles.css if requested
174
- if (injectTokensCss) {
175
- result = result.replace(
176
- /(<link[^>]*href=["']\.\.\/styles\.css["'][^>]*>)/i,
177
- '<link href="../tokens.css" rel="stylesheet">\n $1'
178
- );
179
- }
180
- }
181
-
182
- return result;
183
- }
184
-
185
- /**
186
- * Rewrite links in all HTML files in a directory
187
- * @param {string} htmlDir - Directory containing HTML files
188
- * @param {Object} manifest - Page manifest
189
- * @param {Object} options - Rewrite options
190
- * @returns {Promise<Object>} Rewrite results
191
- */
192
- export async function rewriteAllLinks(htmlDir, manifest, options = {}) {
193
- const fs = await import('fs/promises');
194
- const path = await import('path');
195
-
196
- const results = {
197
- processed: [],
198
- errors: []
199
- };
200
-
201
- for (const page of manifest.pages) {
202
- const htmlPath = path.join(htmlDir, page.file);
203
-
204
- try {
205
- const html = await fs.readFile(htmlPath, 'utf-8');
206
- const rewritten = rewriteLinks(html, manifest, options);
207
- await fs.writeFile(htmlPath, rewritten, 'utf-8');
208
- results.processed.push(page.file);
209
- } catch (err) {
210
- results.errors.push({ file: page.file, error: err.message });
211
- }
212
- }
213
-
214
- return results;
215
- }
216
-
217
- // CLI support
218
- const isMainModule = process.argv[1] && (
219
- process.argv[1].endsWith('rewrite-links.js') ||
220
- process.argv[1].includes('rewrite-links')
221
- );
222
-
223
- if (isMainModule) {
224
- console.log('rewrite-links.js - Use as module, not CLI');
225
- console.log('Exports: rewriteLinks, createPageManifest, pathToFilename, rewriteAllLinks');
226
- }