@whittakertech/virgil 0.1.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 (77) hide show
  1. package/.tool-versions +1 -0
  2. package/README.md +230 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +39 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/engine/hash.d.ts +20 -0
  8. package/dist/engine/hash.d.ts.map +1 -0
  9. package/dist/engine/hash.js +53 -0
  10. package/dist/engine/hash.js.map +1 -0
  11. package/dist/engine/lock.d.ts +18 -0
  12. package/dist/engine/lock.d.ts.map +1 -0
  13. package/dist/engine/lock.js +54 -0
  14. package/dist/engine/lock.js.map +1 -0
  15. package/dist/engine/manifest.d.ts +14 -0
  16. package/dist/engine/manifest.d.ts.map +1 -0
  17. package/dist/engine/manifest.js +39 -0
  18. package/dist/engine/manifest.js.map +1 -0
  19. package/dist/engine/run.d.ts +15 -0
  20. package/dist/engine/run.d.ts.map +1 -0
  21. package/dist/engine/run.js +110 -0
  22. package/dist/engine/run.js.map +1 -0
  23. package/dist/generators/og-image.d.ts +16 -0
  24. package/dist/generators/og-image.d.ts.map +1 -0
  25. package/dist/generators/og-image.js +46 -0
  26. package/dist/generators/og-image.js.map +1 -0
  27. package/dist/generators/robots.d.ts +9 -0
  28. package/dist/generators/robots.d.ts.map +1 -0
  29. package/dist/generators/robots.js +29 -0
  30. package/dist/generators/robots.js.map +1 -0
  31. package/dist/generators/sitemap.d.ts +9 -0
  32. package/dist/generators/sitemap.d.ts.map +1 -0
  33. package/dist/generators/sitemap.js +24 -0
  34. package/dist/generators/sitemap.js.map +1 -0
  35. package/dist/index.d.ts +8 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +5 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/templates/og-default.html +100 -0
  40. package/dist/types/lock.d.ts +11 -0
  41. package/dist/types/lock.d.ts.map +1 -0
  42. package/dist/types/lock.js +2 -0
  43. package/dist/types/lock.js.map +1 -0
  44. package/dist/types/manifest.d.ts +7 -0
  45. package/dist/types/manifest.d.ts.map +1 -0
  46. package/dist/types/manifest.js +2 -0
  47. package/dist/types/manifest.js.map +1 -0
  48. package/dist/types/spec.d.ts +47 -0
  49. package/dist/types/spec.d.ts.map +1 -0
  50. package/dist/types/spec.js +2 -0
  51. package/dist/types/spec.js.map +1 -0
  52. package/dist/utils/fs.d.ts +13 -0
  53. package/dist/utils/fs.d.ts.map +1 -0
  54. package/dist/utils/fs.js +32 -0
  55. package/dist/utils/fs.js.map +1 -0
  56. package/dist/utils/paths.d.ts +14 -0
  57. package/dist/utils/paths.d.ts.map +1 -0
  58. package/dist/utils/paths.js +30 -0
  59. package/dist/utils/paths.js.map +1 -0
  60. package/package.json +37 -0
  61. package/src/cli.ts +47 -0
  62. package/src/engine/hash.ts +71 -0
  63. package/src/engine/lock.ts +72 -0
  64. package/src/engine/manifest.ts +52 -0
  65. package/src/engine/run.ts +156 -0
  66. package/src/generators/og-image.ts +65 -0
  67. package/src/generators/robots.ts +44 -0
  68. package/src/generators/sitemap.ts +36 -0
  69. package/src/index.ts +8 -0
  70. package/src/templates/og-default.html +100 -0
  71. package/src/types/lock.ts +11 -0
  72. package/src/types/manifest.ts +6 -0
  73. package/src/types/spec.ts +52 -0
  74. package/src/utils/fs.ts +32 -0
  75. package/src/utils/paths.ts +33 -0
  76. package/tsconfig.json +20 -0
  77. package/virgil.spec.example.json +65 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"og-image.d.ts","sourceRoot":"","sources":["../../src/generators/og-image.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAK7E,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,aAAa,CAAC;IACvB,MAAM,EAAE,aAAa,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,cAAc,EACnB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAkCf;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAG1D"}
@@ -0,0 +1,46 @@
1
+ import { chromium } from 'playwright';
2
+ import { readFile } from 'fs/promises';
3
+ import { join, dirname } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { ensureParentDir } from '../utils/fs.js';
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ /**
8
+ * Generate OG image using Playwright
9
+ */
10
+ export async function generateOGImage(ctx, outputPath) {
11
+ // Load template
12
+ const templatePath = ctx.templatePath || join(__dirname, '../templates/og-default.html');
13
+ let html = await readFile(templatePath, 'utf-8');
14
+ // Replace placeholders
15
+ html = html
16
+ .replace(/{{brand.name}}/g, ctx.brand.name)
17
+ .replace(/{{brand.color}}/g, ctx.brand.color)
18
+ .replace(/{{product.name}}/g, ctx.product.name)
19
+ .replace(/{{product.version}}/g, ctx.product.version)
20
+ .replace(/{{page.title}}/g, ctx.output.page.title)
21
+ .replace(/{{page.url}}/g, ctx.output.page.url)
22
+ .replace(/{{page.description}}/g, ctx.output.page.description || '');
23
+ // Launch headless browser
24
+ const browser = await chromium.launch();
25
+ const page = await browser.newPage({
26
+ viewport: { width: 1200, height: 630 }
27
+ });
28
+ // Set content and wait for fonts/assets
29
+ await page.setContent(html, { waitUntil: 'networkidle' });
30
+ // Ensure output directory exists
31
+ await ensureParentDir(outputPath);
32
+ // Screenshot
33
+ await page.screenshot({
34
+ path: outputPath,
35
+ type: 'png'
36
+ });
37
+ await browser.close();
38
+ }
39
+ /**
40
+ * Generate filename for OG image with cache-busting timestamp
41
+ */
42
+ export function generateOGImageFilename(id) {
43
+ const timestamp = Math.floor(Date.now() / 1000);
44
+ return `${id}.${timestamp}.png`;
45
+ }
46
+ //# sourceMappingURL=og-image.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"og-image.js","sourceRoot":"","sources":["../../src/generators/og-image.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAa,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAEpC,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAS1D;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,GAAmB,EACnB,UAAkB;IAElB,gBAAgB;IAChB,MAAM,YAAY,GAAG,GAAG,CAAC,YAAY,IAAI,IAAI,CAAC,SAAS,EAAE,8BAA8B,CAAC,CAAC;IACzF,IAAI,IAAI,GAAG,MAAM,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IAEjD,uBAAuB;IACvB,IAAI,GAAG,IAAI;SACR,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;SAC1C,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC;SAC5C,OAAO,CAAC,mBAAmB,EAAE,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC;SAC9C,OAAO,CAAC,sBAAsB,EAAE,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC;SACpD,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC;SACjD,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;SAC7C,OAAO,CAAC,uBAAuB,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC;IAEvE,0BAA0B;IAC1B,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,MAAM,EAAE,CAAC;IACxC,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC;QACjC,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE;KACvC,CAAC,CAAC;IAEH,wCAAwC;IACxC,MAAM,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,aAAa,EAAE,CAAC,CAAC;IAE1D,iCAAiC;IACjC,MAAM,eAAe,CAAC,UAAU,CAAC,CAAC;IAElC,aAAa;IACb,MAAM,IAAI,CAAC,UAAU,CAAC;QACpB,IAAI,EAAE,UAAU;QAChB,IAAI,EAAE,KAAK;KACZ,CAAC,CAAC;IAEH,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;AACxB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,uBAAuB,CAAC,EAAU;IAChD,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IAChD,OAAO,GAAG,EAAE,IAAI,SAAS,MAAM,CAAC;AAClC,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { RobotsOutput } from '../types/spec.js';
2
+ export interface RobotsContext {
3
+ output: RobotsOutput;
4
+ }
5
+ /**
6
+ * Generate robots.txt
7
+ */
8
+ export declare function generateRobots(ctx: RobotsContext, outputPath: string): Promise<void>;
9
+ //# sourceMappingURL=robots.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"robots.d.ts","sourceRoot":"","sources":["../../src/generators/robots.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAGhD,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,YAAY,CAAC;CACtB;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,aAAa,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CA6Bf"}
@@ -0,0 +1,29 @@
1
+ import { writeFile } from 'fs/promises';
2
+ import { ensureParentDir } from '../utils/fs.js';
3
+ /**
4
+ * Generate robots.txt
5
+ */
6
+ export async function generateRobots(ctx, outputPath) {
7
+ const lines = [];
8
+ for (const rule of ctx.output.rules) {
9
+ lines.push(`User-agent: ${rule.userAgent}`);
10
+ if (rule.allow) {
11
+ for (const path of rule.allow) {
12
+ lines.push(`Allow: ${path}`);
13
+ }
14
+ }
15
+ if (rule.disallow) {
16
+ for (const path of rule.disallow) {
17
+ lines.push(`Disallow: ${path}`);
18
+ }
19
+ }
20
+ lines.push(''); // Blank line between rules
21
+ }
22
+ if (ctx.output.sitemap) {
23
+ lines.push(`Sitemap: ${ctx.output.sitemap}`);
24
+ }
25
+ const content = lines.join('\n');
26
+ await ensureParentDir(outputPath);
27
+ await writeFile(outputPath, content, 'utf-8');
28
+ }
29
+ //# sourceMappingURL=robots.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"robots.js","sourceRoot":"","sources":["../../src/generators/robots.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAMjD;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,GAAkB,EAClB,UAAkB;IAElB,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QACpC,KAAK,CAAC,IAAI,CAAC,eAAe,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;QAE5C,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;gBAC9B,KAAK,CAAC,IAAI,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC;YAC/B,CAAC;QACH,CAAC;QAED,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACjC,KAAK,CAAC,IAAI,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC;YAClC,CAAC;QACH,CAAC;QAED,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,2BAA2B;IAC7C,CAAC;IAED,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACvB,KAAK,CAAC,IAAI,CAAC,YAAY,GAAG,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;IAC/C,CAAC;IAED,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEjC,MAAM,eAAe,CAAC,UAAU,CAAC,CAAC;IAClC,MAAM,SAAS,CAAC,UAAU,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AAChD,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { SitemapOutput } from '../types/spec.js';
2
+ export interface SitemapContext {
3
+ output: SitemapOutput;
4
+ }
5
+ /**
6
+ * Generate sitemap.xml
7
+ */
8
+ export declare function generateSitemap(ctx: SitemapContext, outputPath: string): Promise<void>;
9
+ //# sourceMappingURL=sitemap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sitemap.d.ts","sourceRoot":"","sources":["../../src/generators/sitemap.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGjD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,aAAa,CAAC;CACvB;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,cAAc,EACnB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAqBf"}
@@ -0,0 +1,24 @@
1
+ import { writeFile } from 'fs/promises';
2
+ import { ensureParentDir } from '../utils/fs.js';
3
+ /**
4
+ * Generate sitemap.xml
5
+ */
6
+ export async function generateSitemap(ctx, outputPath) {
7
+ const { baseUrl, pages } = ctx.output;
8
+ const urls = pages.map(page => {
9
+ const loc = `${baseUrl}${page.path}`;
10
+ const lastmod = page.lastmod ? `<lastmod>${page.lastmod}</lastmod>` : '';
11
+ const changefreq = page.changefreq ? `<changefreq>${page.changefreq}</changefreq>` : '';
12
+ const priority = page.priority !== undefined ? `<priority>${page.priority}</priority>` : '';
13
+ return ` <url>
14
+ <loc>${loc}</loc>${lastmod}${changefreq}${priority}
15
+ </url>`;
16
+ }).join('\n');
17
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
18
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
19
+ ${urls}
20
+ </urlset>`;
21
+ await ensureParentDir(outputPath);
22
+ await writeFile(outputPath, xml, 'utf-8');
23
+ }
24
+ //# sourceMappingURL=sitemap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sitemap.js","sourceRoot":"","sources":["../../src/generators/sitemap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAMjD;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,GAAmB,EACnB,UAAkB;IAElB,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC;IAEtC,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;QAC5B,MAAM,GAAG,GAAG,GAAG,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACrC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC;QACzE,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,eAAe,IAAI,CAAC,UAAU,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC;QACxF,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,aAAa,IAAI,CAAC,QAAQ,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC;QAE5F,OAAO;WACA,GAAG,SAAS,OAAO,GAAG,UAAU,GAAG,QAAQ;SAC7C,CAAC;IACR,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEd,MAAM,GAAG,GAAG;;EAEZ,IAAI;UACI,CAAC;IAET,MAAM,eAAe,CAAC,UAAU,CAAC,CAAC;IAClC,MAAM,SAAS,CAAC,UAAU,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;AAC5C,CAAC"}
@@ -0,0 +1,8 @@
1
+ export { run } from './engine/run.js';
2
+ export { computeHash, hashFile } from './engine/hash.js';
3
+ export { loadLock, saveLock } from './engine/lock.js';
4
+ export { loadManifest, saveManifest } from './engine/manifest.js';
5
+ export type { VirgilSpec, OutputConfig } from './types/spec.js';
6
+ export type { VirgilLock, LockEntry } from './types/lock.js';
7
+ export type { VirgilManifest } from './types/manifest.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAElE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAChE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC7D,YAAY,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { run } from './engine/run.js';
2
+ export { computeHash, hashFile } from './engine/hash.js';
3
+ export { loadLock, saveLock } from './engine/lock.js';
4
+ export { loadManifest, saveManifest } from './engine/manifest.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,100 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <style>
6
+ * {
7
+ margin: 0;
8
+ padding: 0;
9
+ box-sizing: border-box;
10
+ }
11
+
12
+ body {
13
+ width: 1200px;
14
+ height: 630px;
15
+ background: linear-gradient(135deg, {{brand.color}} 0%, #1a1a2e 100%);
16
+ display: flex;
17
+ align-items: center;
18
+ justify-content: center;
19
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
20
+ padding: 60px;
21
+ }
22
+
23
+ .container {
24
+ width: 100%;
25
+ height: 100%;
26
+ display: flex;
27
+ flex-direction: column;
28
+ justify-content: space-between;
29
+ color: white;
30
+ }
31
+
32
+ .header {
33
+ display: flex;
34
+ align-items: center;
35
+ gap: 20px;
36
+ }
37
+
38
+ .brand {
39
+ font-size: 24px;
40
+ font-weight: 600;
41
+ opacity: 0.9;
42
+ }
43
+
44
+ .content {
45
+ flex: 1;
46
+ display: flex;
47
+ flex-direction: column;
48
+ justify-content: center;
49
+ }
50
+
51
+ .title {
52
+ font-size: 72px;
53
+ font-weight: 700;
54
+ line-height: 1.2;
55
+ margin-bottom: 20px;
56
+ }
57
+
58
+ .description {
59
+ font-size: 28px;
60
+ opacity: 0.8;
61
+ line-height: 1.4;
62
+ }
63
+
64
+ .footer {
65
+ display: flex;
66
+ justify-content: space-between;
67
+ align-items: center;
68
+ font-size: 20px;
69
+ opacity: 0.7;
70
+ }
71
+
72
+ .product {
73
+ font-weight: 600;
74
+ }
75
+
76
+ .version {
77
+ background: rgba(255, 255, 255, 0.2);
78
+ padding: 4px 12px;
79
+ border-radius: 4px;
80
+ }
81
+ </style>
82
+ </head>
83
+ <body>
84
+ <div class="container">
85
+ <div class="header">
86
+ <div class="brand">{{brand.name}}</div>
87
+ </div>
88
+
89
+ <div class="content">
90
+ <div class="title">{{page.title}}</div>
91
+ <div class="description">{{page.description}}</div>
92
+ </div>
93
+
94
+ <div class="footer">
95
+ <div class="product">{{product.name}}</div>
96
+ <div class="version">v{{product.version}}</div>
97
+ </div>
98
+ </div>
99
+ </body>
100
+ </html>
@@ -0,0 +1,11 @@
1
+ export interface VirgilLock {
2
+ version: '0.1';
3
+ entries: Record<string, LockEntry>;
4
+ }
5
+ export interface LockEntry {
6
+ hash: string;
7
+ generatedAt: string;
8
+ generator: string;
9
+ generatorVersion: string;
10
+ }
11
+ //# sourceMappingURL=lock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock.d.ts","sourceRoot":"","sources":["../../src/types/lock.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,KAAK,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;CAC1B"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/types/lock.ts"],"names":[],"mappings":""}
@@ -0,0 +1,7 @@
1
+ export interface VirgilManifest {
2
+ version: '0.1';
3
+ og?: Record<string, string>;
4
+ sitemap?: Record<string, string>;
5
+ robots?: Record<string, string>;
6
+ }
7
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/types/manifest.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,KAAK,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/types/manifest.ts"],"names":[],"mappings":""}
@@ -0,0 +1,47 @@
1
+ export interface VirgilSpec {
2
+ brand: BrandConfig;
3
+ product: ProductConfig;
4
+ outputs: OutputConfig[];
5
+ }
6
+ export interface BrandConfig {
7
+ name: string;
8
+ logo: string;
9
+ color: string;
10
+ }
11
+ export interface ProductConfig {
12
+ name: string;
13
+ logo: string;
14
+ version: string;
15
+ }
16
+ export type OutputConfig = OGImageOutput | SitemapOutput | RobotsOutput;
17
+ export interface OGImageOutput {
18
+ type: 'og-image';
19
+ id: string;
20
+ page: {
21
+ title: string;
22
+ url: string;
23
+ description?: string;
24
+ };
25
+ }
26
+ export interface SitemapOutput {
27
+ type: 'sitemap';
28
+ id: string;
29
+ baseUrl: string;
30
+ pages: Array<{
31
+ path: string;
32
+ lastmod?: string;
33
+ changefreq?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
34
+ priority?: number;
35
+ }>;
36
+ }
37
+ export interface RobotsOutput {
38
+ type: 'robots';
39
+ id: string;
40
+ rules: Array<{
41
+ userAgent: string;
42
+ allow?: string[];
43
+ disallow?: string[];
44
+ }>;
45
+ sitemap?: string;
46
+ }
47
+ //# sourceMappingURL=spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spec.d.ts","sourceRoot":"","sources":["../../src/types/spec.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,aAAa,CAAC;IACvB,OAAO,EAAE,YAAY,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,aAAa,GAAG,YAAY,CAAC;AAExE,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,UAAU,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE;QACJ,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;QACZ,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;CACH;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,SAAS,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,KAAK,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;QACvF,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC,CAAC;CACJ;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,QAAQ,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,KAAK,CAAC;QACX,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QACjB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC,CAAC;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spec.js","sourceRoot":"","sources":["../../src/types/spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Ensure directory exists, create if needed
3
+ */
4
+ export declare function ensureDir(dirPath: string): Promise<void>;
5
+ /**
6
+ * Ensure parent directory exists for a file path
7
+ */
8
+ export declare function ensureParentDir(filePath: string): Promise<void>;
9
+ /**
10
+ * Check if file exists
11
+ */
12
+ export declare function fileExists(filePath: string): Promise<boolean>;
13
+ //# sourceMappingURL=fs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../../src/utils/fs.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAM9D;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAErE;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOnE"}
@@ -0,0 +1,32 @@
1
+ import { mkdir, access } from 'fs/promises';
2
+ import { dirname } from 'path';
3
+ /**
4
+ * Ensure directory exists, create if needed
5
+ */
6
+ export async function ensureDir(dirPath) {
7
+ try {
8
+ await access(dirPath);
9
+ }
10
+ catch {
11
+ await mkdir(dirPath, { recursive: true });
12
+ }
13
+ }
14
+ /**
15
+ * Ensure parent directory exists for a file path
16
+ */
17
+ export async function ensureParentDir(filePath) {
18
+ await ensureDir(dirname(filePath));
19
+ }
20
+ /**
21
+ * Check if file exists
22
+ */
23
+ export async function fileExists(filePath) {
24
+ try {
25
+ await access(filePath);
26
+ return true;
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ //# sourceMappingURL=fs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs.js","sourceRoot":"","sources":["../../src/utils/fs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAE/B;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAAe;IAC7C,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,OAAO,CAAC,CAAC;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,KAAK,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,QAAgB;IACpD,MAAM,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;AACrC,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,QAAgB;IAC/C,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;QACvB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Resolve paths relative to project root
3
+ */
4
+ export declare class PathResolver {
5
+ private rootDir;
6
+ constructor(rootDir: string);
7
+ resolve(...segments: string[]): string;
8
+ join(...segments: string[]): string;
9
+ relative(from: string, to: string): string;
10
+ get spec(): string;
11
+ get lock(): string;
12
+ get manifest(): string;
13
+ }
14
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/utils/paths.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,qBAAa,YAAY;IACX,OAAO,CAAC,OAAO;gBAAP,OAAO,EAAE,MAAM;IAEnC,OAAO,CAAC,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM;IAItC,IAAI,CAAC,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM;IAInC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM;IAK1C,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,IAAI,QAAQ,IAAI,MAAM,CAErB;CACF"}
@@ -0,0 +1,30 @@
1
+ import { resolve, join, relative } from 'path';
2
+ /**
3
+ * Resolve paths relative to project root
4
+ */
5
+ export class PathResolver {
6
+ rootDir;
7
+ constructor(rootDir) {
8
+ this.rootDir = rootDir;
9
+ }
10
+ resolve(...segments) {
11
+ return resolve(this.rootDir, ...segments);
12
+ }
13
+ join(...segments) {
14
+ return join(this.rootDir, ...segments);
15
+ }
16
+ relative(from, to) {
17
+ return relative(from, to);
18
+ }
19
+ // Standard Virgil paths
20
+ get spec() {
21
+ return this.resolve('virgil.spec.json');
22
+ }
23
+ get lock() {
24
+ return this.resolve('virgil.lock.json');
25
+ }
26
+ get manifest() {
27
+ return this.resolve('virgil.manifest.json');
28
+ }
29
+ }
30
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/utils/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AAE/C;;GAEG;AACH,MAAM,OAAO,YAAY;IACH;IAApB,YAAoB,OAAe;QAAf,YAAO,GAAP,OAAO,CAAQ;IAAG,CAAC;IAEvC,OAAO,CAAC,GAAG,QAAkB;QAC3B,OAAO,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,QAAQ,CAAC,CAAC;IAC5C,CAAC;IAED,IAAI,CAAC,GAAG,QAAkB;QACxB,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,QAAQ,CAAC,CAAC;IACzC,CAAC;IAED,QAAQ,CAAC,IAAY,EAAE,EAAU;QAC/B,OAAO,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAC5B,CAAC;IAED,wBAAwB;IACxB,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAC1C,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAC1C,CAAC;IAED,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC9C,CAAC;CACF"}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@whittakertech/virgil",
3
+ "version": "0.1.0",
4
+ "description": "Build-phase sigil forge for static site artifacts",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "virgil": "dist/cli.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc && npm run copy:assets",
13
+ "prepare": "npm run build",
14
+ "copy:assets": "cp -R src/templates dist",
15
+ "dev": "tsc --watch",
16
+ "test": "node --test dist/**/*.test.js"
17
+ },
18
+ "keywords": [
19
+ "static-site",
20
+ "og-image",
21
+ "sitemap",
22
+ "build-tool"
23
+ ],
24
+ "author": "Lee Whittaker",
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "commander": "^12.0.0",
28
+ "playwright": "^1.40.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^20.10.0",
32
+ "typescript": "^5.3.0"
33
+ },
34
+ "engines": {
35
+ "node": ">=18.0.0"
36
+ }
37
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { run } from './engine/run.js';
5
+ import { resolve } from 'path';
6
+
7
+ const program = new Command();
8
+
9
+ program
10
+ .name('virgil')
11
+ .description('Build-phase sigil forge for static site artifacts')
12
+ .version('0.1.0');
13
+
14
+ program
15
+ .command('build')
16
+ .description('Generate artifacts from virgil.spec.json')
17
+ .option('-r, --root <dir>', 'Project root directory', process.cwd())
18
+ .option('-o, --output <dir>', 'Output directory', 'public')
19
+ .option('-v, --verbose', 'Verbose output', false)
20
+ .action(async (options) => {
21
+ const rootDir = resolve(options.root);
22
+ const outputDir = resolve(rootDir, options.output);
23
+
24
+ console.log('šŸ”® Virgil v0.1.0');
25
+ console.log(`šŸ“ Root: ${rootDir}`);
26
+ console.log(`šŸ“¦ Output: ${outputDir}`);
27
+ console.log('');
28
+
29
+ const result = await run({
30
+ rootDir,
31
+ outputDir,
32
+ verbose: options.verbose
33
+ });
34
+
35
+ if (result.errors.length > 0) {
36
+ console.error('\nāŒ Errors:');
37
+ for (const error of result.errors) {
38
+ console.error(` ${error}`);
39
+ }
40
+ process.exit(1);
41
+ }
42
+
43
+ console.log('\n✨ Done');
44
+ process.exit(0);
45
+ });
46
+
47
+ program.parse();
@@ -0,0 +1,71 @@
1
+ import { createHash } from 'crypto';
2
+ import { readFile } from 'fs/promises';
3
+
4
+ export const GENERATOR_VERSION = '0.1.0';
5
+
6
+ export interface HashInput {
7
+ data: unknown;
8
+ templatePath?: string;
9
+ generatorName: string;
10
+ }
11
+
12
+ function stableStringify(value: unknown): string {
13
+ if (value === null || typeof value !== 'object') {
14
+ return JSON.stringify(value);
15
+ }
16
+
17
+ if (Array.isArray(value)) {
18
+ return JSON.stringify(value.map(stableStringify));
19
+ }
20
+
21
+ const obj = value as Record<string, unknown>;
22
+ const sortedKeys = Object.keys(obj).sort();
23
+
24
+ const normalized: Record<string, unknown> = {};
25
+ for (const key of sortedKeys) {
26
+ normalized[key] = obj[key];
27
+ }
28
+
29
+ return JSON.stringify(normalized);
30
+ }
31
+
32
+ /**
33
+ * Compute deterministic hash from input data, template contents, and generator version.
34
+ *
35
+ * This ensures:
36
+ * - Data changes trigger regeneration
37
+ * - Template changes trigger regeneration
38
+ * - Generator version changes trigger regeneration
39
+ */
40
+ export async function computeHash(input: HashInput): Promise<string> {
41
+ const parts: string[] = [];
42
+
43
+ // 1. Serialize data (stable JSON)
44
+ parts.push(stableStringify(input.data));
45
+
46
+ // 2. Include template contents if specified
47
+ if (input.templatePath) {
48
+ const templateContents = await readFile(input.templatePath, 'utf-8');
49
+ parts.push(templateContents);
50
+ }
51
+
52
+ // 3. Include generator identity
53
+ parts.push(input.generatorName);
54
+ parts.push(GENERATOR_VERSION);
55
+
56
+ // Compute SHA-256
57
+ const hash = createHash('sha256');
58
+ hash.update(parts.join('\n'));
59
+
60
+ return `sha256:${hash.digest('hex')}`;
61
+ }
62
+
63
+ /**
64
+ * Compute hash for a file's contents
65
+ */
66
+ export async function hashFile(filepath: string): Promise<string> {
67
+ const contents = await readFile(filepath);
68
+ const hash = createHash('sha256');
69
+ hash.update(contents);
70
+ return `sha256:${hash.digest('hex')}`;
71
+ }