@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.
- package/.tool-versions +1 -0
- package/README.md +230 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +39 -0
- package/dist/cli.js.map +1 -0
- package/dist/engine/hash.d.ts +20 -0
- package/dist/engine/hash.d.ts.map +1 -0
- package/dist/engine/hash.js +53 -0
- package/dist/engine/hash.js.map +1 -0
- package/dist/engine/lock.d.ts +18 -0
- package/dist/engine/lock.d.ts.map +1 -0
- package/dist/engine/lock.js +54 -0
- package/dist/engine/lock.js.map +1 -0
- package/dist/engine/manifest.d.ts +14 -0
- package/dist/engine/manifest.d.ts.map +1 -0
- package/dist/engine/manifest.js +39 -0
- package/dist/engine/manifest.js.map +1 -0
- package/dist/engine/run.d.ts +15 -0
- package/dist/engine/run.d.ts.map +1 -0
- package/dist/engine/run.js +110 -0
- package/dist/engine/run.js.map +1 -0
- package/dist/generators/og-image.d.ts +16 -0
- package/dist/generators/og-image.d.ts.map +1 -0
- package/dist/generators/og-image.js +46 -0
- package/dist/generators/og-image.js.map +1 -0
- package/dist/generators/robots.d.ts +9 -0
- package/dist/generators/robots.d.ts.map +1 -0
- package/dist/generators/robots.js +29 -0
- package/dist/generators/robots.js.map +1 -0
- package/dist/generators/sitemap.d.ts +9 -0
- package/dist/generators/sitemap.d.ts.map +1 -0
- package/dist/generators/sitemap.js +24 -0
- package/dist/generators/sitemap.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/templates/og-default.html +100 -0
- package/dist/types/lock.d.ts +11 -0
- package/dist/types/lock.d.ts.map +1 -0
- package/dist/types/lock.js +2 -0
- package/dist/types/lock.js.map +1 -0
- package/dist/types/manifest.d.ts +7 -0
- package/dist/types/manifest.d.ts.map +1 -0
- package/dist/types/manifest.js +2 -0
- package/dist/types/manifest.js.map +1 -0
- package/dist/types/spec.d.ts +47 -0
- package/dist/types/spec.d.ts.map +1 -0
- package/dist/types/spec.js +2 -0
- package/dist/types/spec.js.map +1 -0
- package/dist/utils/fs.d.ts +13 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +32 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/paths.d.ts +14 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +30 -0
- package/dist/utils/paths.js.map +1 -0
- package/package.json +37 -0
- package/src/cli.ts +47 -0
- package/src/engine/hash.ts +71 -0
- package/src/engine/lock.ts +72 -0
- package/src/engine/manifest.ts +52 -0
- package/src/engine/run.ts +156 -0
- package/src/generators/og-image.ts +65 -0
- package/src/generators/robots.ts +44 -0
- package/src/generators/sitemap.ts +36 -0
- package/src/index.ts +8 -0
- package/src/templates/og-default.html +100 -0
- package/src/types/lock.ts +11 -0
- package/src/types/manifest.ts +6 -0
- package/src/types/spec.ts +52 -0
- package/src/utils/fs.ts +32 -0
- package/src/utils/paths.ts +33 -0
- package/tsconfig.json +20 -0
- 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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/types/lock.ts"],"names":[],"mappings":""}
|
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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"}
|
package/dist/utils/fs.js
ADDED
|
@@ -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
|
+
}
|