From 05c2431c5b35c558e017a05352cde93a40d490a3 Mon Sep 17 00:00:00 2001 From: Simon Davies Date: Fri, 27 Mar 2026 17:15:55 +0000 Subject: [PATCH 1/6] Windows platform support: - Justfile: [windows] recipes for build-hyperlight, resolve-hyperlight-dir, start-debug - Justfile: runtime-cflags forward-slash fix for clang cross-compilation - build-binary.js: .cmd launcher, platform-aware post-build output - plugins: O_NOFOLLOW fallback (Windows lacks O_NOFOLLOW, relies on lstat pre-check) - agent/index.ts: pathToFileURL for ESM plugin imports on Windows - build.rs: forward-slash CFLAGS for clang on Windows - code-validator/guest: win32-x64-msvc NAPI target - .gitattributes: enforce LF line endings across platforms VM resource management: - sandbox/tool.js: invalidateSandbox() now calls dispose() on LoadedJSSandbox and JSSandbox for deterministic VM cleanup instead of relying on V8 GC - Updated hyperlight-js dep to include dispose() API Error handling: - agent/event-handler.ts: suppress duplicate 'Tool execution failed' messages - sandbox/tool.js: MMIO error detection in compilation and runtime paths - agent/index.ts: surrogate pool env vars (HYPERLIGHT_INITIAL/MAX_SURROGATES) Test fixes (Windows compatibility): - tests: symlink EPERM skip for Windows (path-jail, fs-read, fs-write) - tests/dts-sync: rmSync instead of shell rm -rf - tests/pattern-loader: unique tmpdir per test to avoid Windows EBUSY locks CI: - pr-validate.yml: Windows WHP matrix - publish.yml: Windows build support Security: - npm audit fix across all workspaces (picomatch, brace-expansion) - plugin-system/manager.ts: simplified ternary Signed-off-by: Simon Davies --- builtin-modules/ooxml-core.json | 4 +- builtin-modules/pptx-charts.json | 4 +- builtin-modules/pptx-tables.json | 4 +- builtin-modules/pptx.json | 4 +- builtin-modules/src/types/ha-modules.d.ts | 183 ++++++++++------------ package.json | 2 +- scripts/build-binary.js | 6 +- tests/pattern-loader.test.ts | 12 +- 8 files changed, 102 insertions(+), 117 deletions(-) diff --git a/builtin-modules/ooxml-core.json b/builtin-modules/ooxml-core.json index d291414..7143b04 100644 --- a/builtin-modules/ooxml-core.json +++ b/builtin-modules/ooxml-core.json @@ -3,8 +3,8 @@ "description": "Shared OOXML infrastructure - units, colors, themes, Content_Types, relationships", "author": "system", "mutable": false, - "sourceHash": "sha256:1e939013c13555bc", - "dtsHash": "sha256:9f88e7c59a56854c", + "sourceHash": "sha256:b5f017fe2d4e2ed3", + "dtsHash": "sha256:6aac85502082bf89", "importStyle": "named", "hints": { "overview": "Low-level OOXML infrastructure. Most users should use ha:pptx instead.", diff --git a/builtin-modules/pptx-charts.json b/builtin-modules/pptx-charts.json index 2f92c5b..591fe56 100644 --- a/builtin-modules/pptx-charts.json +++ b/builtin-modules/pptx-charts.json @@ -3,8 +3,8 @@ "description": "OOXML DrawingML chart generation - bar, pie, line charts for PPTX presentations", "author": "system", "mutable": false, - "sourceHash": "sha256:5c521ce93ff39626", - "dtsHash": "sha256:5f653830226c3554", + "sourceHash": "sha256:4174b6f03be2e0fb", + "dtsHash": "sha256:4353b8263dc99405", "importStyle": "named", "hints": { "overview": "Chart generation for PPTX. Always used with ha:pptx.", diff --git a/builtin-modules/pptx-tables.json b/builtin-modules/pptx-tables.json index 09e0bfc..140c43b 100644 --- a/builtin-modules/pptx-tables.json +++ b/builtin-modules/pptx-tables.json @@ -3,8 +3,8 @@ "description": "Styled tables for PPTX presentations - headers, borders, alternating rows", "author": "system", "mutable": false, - "sourceHash": "sha256:0739a7db5a8ab428", - "dtsHash": "sha256:82d903ffbf4dfb1e", + "sourceHash": "sha256:2d58934ed7df9fe1", + "dtsHash": "sha256:3ba75bbc44353467", "importStyle": "named", "hints": { "overview": "Table generation for PPTX. Always used with ha:pptx.", diff --git a/builtin-modules/pptx.json b/builtin-modules/pptx.json index 8f3bd72..6d35ce1 100644 --- a/builtin-modules/pptx.json +++ b/builtin-modules/pptx.json @@ -3,8 +3,8 @@ "description": "PowerPoint PPTX presentation builder - slides, text, shapes, themes, layouts", "author": "system", "mutable": false, - "sourceHash": "sha256:093b19522e994756", - "dtsHash": "sha256:2107e369816b4bd5", + "sourceHash": "sha256:23569540a0f8622f", + "dtsHash": "sha256:27520514e4401465", "importStyle": "named", "hints": { "overview": "Core PPTX slide building. Charts in ha:pptx-charts, tables in ha:pptx-tables.", diff --git a/builtin-modules/src/types/ha-modules.d.ts b/builtin-modules/src/types/ha-modules.d.ts index 62e93ef..2818843 100644 --- a/builtin-modules/src/types/ha-modules.d.ts +++ b/builtin-modules/src/types/ha-modules.d.ts @@ -777,7 +777,7 @@ declare module "ha:pptx-tables" { * @param opts.style.headerFontSize - Header font size in pt * @returns Shape XML fragment for use in slide body */ - export declare function table(opts: TableOptions): ShapeFragment; + export declare function table(opts: TableOptions): string; export interface KVItem { key: string; value: string; @@ -806,7 +806,7 @@ declare module "ha:pptx-tables" { * @param opts - KV table options: { x?, y?, w?, items: Array<{key, value}>, theme?, style? } * @returns Shape XML fragment */ - export declare function kvTable(opts: KVTableOptions): ShapeFragment; + export declare function kvTable(opts: KVTableOptions): string; export interface ComparisonOption { /** Column header name */ name: string; @@ -853,7 +853,7 @@ declare module "ha:pptx-tables" { * @param opts - REQUIRED: { features: string[], options: Array<{name: string, values: boolean[]}> }. Optional: x?, y?, w?, theme?, style? * @returns Shape XML fragment */ - export declare function comparisonTable(opts: ComparisonTableOptions): ShapeFragment; + export declare function comparisonTable(opts: ComparisonTableOptions): string; export interface TimelineItem { /** Phase/milestone label */ label: string; @@ -886,7 +886,7 @@ declare module "ha:pptx-tables" { * @param opts - Timeline options: { x?, y?, w?, items: Array<{label, description?, color?}>, theme?, style? } * @returns Shape XML fragment (uses table layout) */ - export declare function timeline(opts: TimelineOptions): ShapeFragment; + export declare function timeline(opts: TimelineOptions): string; } declare module "ha:pptx" { @@ -942,7 +942,7 @@ declare module "ha:pptx" { export interface Presentation { theme: Theme; slideCount: number; - addBody(shapes: ShapeFragment | ShapeFragment[], opts?: SlideOptions): void; + addBody(shapes: string | string[], opts?: SlideOptions): void; build(): Array<{ name: string; data: string | Uint8Array; @@ -1433,8 +1433,7 @@ declare module "ha:pptx" { extraItems?: string[] | string; } export interface CustomSlideOptions { - /** Array of ShapeFragment objects from shape builders (textBox, rect, table, etc.). REQUIRED. */ - shapes: ShapeFragment | ShapeFragment[]; + shapes: string; background?: string | GradientSpec; transition?: string; transitionDuration?: number; @@ -1621,14 +1620,13 @@ declare module "ha:pptx" { fontSize?: number; } export { type Theme }; - export { type ShapeFragment, isShapeFragment, fragmentsToXml }; export { table, kvTable, comparisonTable, timeline, TABLE_STYLES, } from "ha:pptx-tables"; export { contrastRatio }; export { getThemeNames }; export { inches, fontSize } from "ha:ooxml-core"; /** * Create a solid fill XML element. - * Use for shape fills or customSlide({ background }) backgrounds. + * Use for custom slide backgrounds via pres.addSlide(solidFill('000000'), shapes). * @param {string} color - Hex color (6 digits, no #) * @param {number} [opacity] - Opacity from 0 (transparent) to 1 (opaque). Omit for fully opaque. * @returns {string} Solid fill XML @@ -1659,9 +1657,9 @@ declare module "ha:pptx" { * @param {string} [opts.background] - Fill color (hex) * @param {number} [opts.lineSpacing] - Line spacing in points * @param {boolean} [opts.autoFit] - Auto-scale fontSize to fit text in shape. Use when text length is variable. - * @returns {ShapeFragment} Branded shape fragment + * @returns {string} Shape XML fragment */ - export declare function textBox(opts: TextBoxOptions): ShapeFragment; + export declare function textBox(opts: TextBoxOptions): string; /** * Create a colored rectangle with optional text. * @param {Object} opts @@ -1678,9 +1676,9 @@ declare module "ha:pptx" { * @param {number} [opts.cornerRadius] - Corner radius in points * @param {string} [opts.borderColor] - Border color * @param {number} [opts.borderWidth=1] - Border width in points - * @returns {ShapeFragment} Branded shape fragment + * @returns {string} Shape XML fragment */ - export declare function rect(opts: RectOptions): ShapeFragment; + export declare function rect(opts: RectOptions): string; /** * Create a bulleted list. * @param {Object} opts @@ -1693,9 +1691,9 @@ declare module "ha:pptx" { * @param {string} [opts.color] - Text color * @param {string} [opts.bulletColor] - Bullet color * @param {number} [opts.lineSpacing=24] - Line spacing - * @returns {ShapeFragment} Branded shape fragment + * @returns {string} Shape XML fragment */ - export declare function bulletList(opts: BulletListOptions): ShapeFragment; + export declare function bulletList(opts: BulletListOptions): string; /** * Create a numbered list. * @param {Object} opts @@ -1708,9 +1706,9 @@ declare module "ha:pptx" { * @param {string} [opts.color] - Text color * @param {number} [opts.lineSpacing=24] - Line spacing * @param {number} [opts.startAt=1] - Starting number - * @returns {ShapeFragment} Branded shape fragment + * @returns {string} Shape XML fragment */ - export declare function numberedList(opts: NumberedListOptions): ShapeFragment; + export declare function numberedList(opts: NumberedListOptions): string; /** * Create an image placeholder (colored rect with label). * Use this until binary image embedding is supported. @@ -1722,9 +1720,9 @@ declare module "ha:pptx" { * @param {string} [opts.label='Image'] - Placeholder label * @param {string} [opts.fill='3D4450'] - Background color (dark gray) * @param {string} [opts.color='B0B8C0'] - Label color (light gray, passes WCAG AA on 3D4450) - * @returns {ShapeFragment} Branded shape fragment + * @returns {string} Shape XML fragment */ - export declare function imagePlaceholder(opts: ImagePlaceholderOptions): ShapeFragment; + export declare function imagePlaceholder(opts: ImagePlaceholderOptions): string; /** * Create a big metric display (number + label stacked). * @param {Object} opts @@ -1740,9 +1738,9 @@ declare module "ha:pptx" { * @param {string} [opts.labelColor] - Label text color (hex). OMIT to auto-select against background. * @param {string} [opts.background] - Background fill * @param {boolean} [opts.forceColor] - Set true to bypass WCAG contrast validation for valueColor/labelColor. - * @returns {ShapeFragment} Branded shape fragment + * @returns {string} Shape XML fragment */ - export declare function statBox(opts: StatBoxOptions): ShapeFragment; + export declare function statBox(opts: StatBoxOptions): string; /** * Create a line between two points. * @param {Object} opts @@ -1753,9 +1751,9 @@ declare module "ha:pptx" { * @param {string} [opts.color='666666'] - Line color (hex) * @param {number} [opts.width=1.5] - Line width in points * @param {string} [opts.dash] - Dash style: 'solid', 'dash', 'dot', 'dashDot' - * @returns {ShapeFragment} Branded shape fragment + * @returns {string} Shape XML fragment */ - export declare function line(opts: LineOptions): ShapeFragment; + export declare function line(opts: LineOptions): string; /** * Create an arrow (line with arrowhead) between two points. * @param {Object} opts @@ -1768,9 +1766,9 @@ declare module "ha:pptx" { * @param {string} [opts.headType='triangle'] - Arrowhead: 'triangle', 'stealth', 'diamond', 'oval', 'arrow' * @param {boolean} [opts.bothEnds=false] - Arrowhead on both ends * @param {string} [opts.dash] - Dash style: 'solid', 'dash', 'dot', 'dashDot' - * @returns {ShapeFragment} Branded shape fragment + * @returns {string} Shape XML fragment */ - export declare function arrow(opts: ArrowOptions): ShapeFragment; + export declare function arrow(opts: ArrowOptions): string; /** * Create a circle or ellipse shape. * @param {Object} opts @@ -1784,9 +1782,9 @@ declare module "ha:pptx" { * @param {string} [opts.color='FFFFFF'] - Text color * @param {string} [opts.borderColor] - Border color * @param {number} [opts.borderWidth=1] - Border width in points - * @returns {ShapeFragment} Branded shape fragment + * @returns {string} Shape XML fragment */ - export declare function circle(opts: CircleOptions): ShapeFragment; + export declare function circle(opts: CircleOptions): string; /** * Create a callout box — rounded rectangle with accent left border. * Good for highlighting insights, quotes, or key takeaways. @@ -1800,9 +1798,9 @@ declare module "ha:pptx" { * @param {string} [opts.background='F5F5F5'] - Fill color * @param {number} [opts.fontSize=14] - Font size * @param {string} [opts.color] - Text color (hex). OMIT to auto-select a readable colour against the background. Do NOT hardcode. - * @returns {ShapeFragment} Branded shape fragment + * @returns {string} Shape XML fragment */ - export declare function callout(opts: CalloutOptions): ShapeFragment; + export declare function callout(opts: CalloutOptions): string; /** * Create a preset shape icon. * @@ -1840,9 +1838,9 @@ declare module "ha:pptx" { * @param {string} [opts.text] - Optional text inside the shape * @param {number} [opts.fontSize=12] - Text font size * @param {string} [opts.color='FFFFFF'] - Text color - * @returns {ShapeFragment} Branded shape fragment + * @returns {string} Shape XML fragment */ - export declare function icon(opts: IconOptions): ShapeFragment; + export declare function icon(opts: IconOptions): string; /** * Create a shape from an SVG path string. * Enables custom icons, logos, and diagrams using standard SVG path data. @@ -1877,9 +1875,9 @@ declare module "ha:pptx" { * @param {string} [opts.fill] - Fill color (hex, e.g. '2196F3') * @param {string} [opts.stroke] - Stroke color (hex) * @param {number} [opts.strokeWidth=1] - Stroke width in points - * @returns {ShapeFragment} Branded shape fragment + * @returns {string} Shape XML fragment */ - export declare function svgPath(opts: SvgPathOptions): ShapeFragment; + export declare function svgPath(opts: SvgPathOptions): string; /** * Create a gradient fill XML fragment for use in shapes. * Supports transparency for cinematic photo overlays (e.g., transparent-to-black). @@ -1965,9 +1963,9 @@ declare module "ha:pptx" { * @param {string} [opts.align='l'] - Paragraph alignment ('l', 'ctr', 'r') * @param {string} [opts.valign='t'] - Vertical alignment ('t', 'ctr', 'b') * @param {string} [opts.background] - Fill color (hex) - * @returns {ShapeFragment} Branded shape fragment + * @returns {string} Shape XML fragment */ - export declare function richText(opts: RichTextOptions): ShapeFragment; + export declare function richText(opts: RichTextOptions): string; /** Options for panel() composite shape */ export interface PanelOptions { /** X position in inches */ @@ -2034,7 +2032,7 @@ declare module "ha:pptx" { * @param opts - Panel options * @returns Shape XML fragments for all panel elements */ - export declare function panel(opts: PanelOptions): ShapeFragment; + export declare function panel(opts: PanelOptions): string; /** Options for card() composite shape */ export interface CardOptions extends PanelOptions { /** Accent color for top border (hex). If set, adds a colored stripe at top */ @@ -2059,7 +2057,7 @@ declare module "ha:pptx" { * @param opts - Card options * @returns Shape XML fragments */ - export declare function card(opts: CardOptions): ShapeFragment; + export declare function card(opts: CardOptions): string; /** * Create a text box with a clickable hyperlink. * The entire text box is clickable. For inline hyperlinks within @@ -2076,9 +2074,9 @@ declare module "ha:pptx" { * @param {string} [opts.color='2196F3'] - Text color (default blue) * @param {boolean} [opts.underline=true] - Underline text * @param {Object} pres - Presentation builder (needed to register the link relationship) - * @returns {ShapeFragment} Branded shape fragment + * @returns {string} Shape XML fragment */ - export declare function hyperlink(opts: HyperlinkOptions, pres: PresentationInternal): ShapeFragment; + export declare function hyperlink(opts: HyperlinkOptions, pres: PresentationInternal): string; /** Image dimensions in pixels */ export interface ImageDimensions { width: number; @@ -2142,9 +2140,9 @@ declare module "ha:pptx" { * @param {string} [opts.format='png'] - Image format: 'png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg' * @param {string} [opts.fit='stretch'] - How to fit image: 'stretch' (distort to fill), 'contain' (fit within, may letterbox), 'cover' (fill, may crop) * @param {string} [opts.name] - Optional image name (for the ZIP path) - * @returns {ShapeFragment} Branded shape fragment for use in slide body + * @returns {string} Shape XML fragment for use in slide body */ - export declare function embedImage(pres: PresentationInternal, opts: EmbedImageOptions): ShapeFragment; + export declare function embedImage(pres: PresentationInternal, opts: EmbedImageOptions): string; /** * Helper to embed an image from a URL with auto-detected format. * This combines readBinary() and embedImage() into a simpler workflow. @@ -2171,11 +2169,11 @@ declare module "ha:pptx" { * @param {number} opts.w - Width in inches * @param {number} opts.h - Height in inches * @param {string} [opts.format] - Override format detection (png, jpg, gif, etc.) - * @returns {ShapeFragment} Branded shape fragment for use in slide body + * @returns {string} Shape XML fragment for use in slide body */ export declare function embedImageFromUrl(pres: PresentationInternal, opts: EmbedImageOptions & { url: string; - }): ShapeFragment; + }): string; /** Slide width in inches (16:9 aspect ratio). */ export declare const SLIDE_WIDTH_INCHES = 13.333; /** Slide height in inches (16:9 aspect ratio). */ @@ -2210,7 +2208,9 @@ declare module "ha:pptx" { * @param items - Array of shape XML strings or objects with toString() * @returns Combined XML string */ - export declare function shapes(items: Array): ShapeFragment; + export declare function shapes(items: Array): string; /** * Calculate positions for items in equal-width columns. * Useful for stat boxes, image cards, or any side-by-side layout. @@ -2281,9 +2281,9 @@ declare module "ha:pptx" { * @param {number} [opts.y=0] - Y position in inches * @param {number} [opts.w] - Width in inches (default: full slide width) * @param {number} [opts.h] - Height in inches (default: full slide height) - * @returns {ShapeFragment} Branded shape fragment + * @returns {string} OOXML shape string */ - export declare function overlay(opts?: OverlayOptions): ShapeFragment; + export declare function overlay(opts?: OverlayOptions): string; /** * Create a gradient overlay for cinematic effects. * Use for half-fades, vignettes, or directional darkening on image slides. @@ -2311,9 +2311,9 @@ declare module "ha:pptx" { * @param {number} [opts.y=0] - Y position in inches * @param {number} [opts.w] - Width in inches (default full slide) * @param {number} [opts.h] - Height in inches (default full slide) - * @returns {ShapeFragment} Branded shape fragment + * @returns {string} OOXML shape string */ - export declare function gradientOverlay(opts?: GradientOverlayOptions): ShapeFragment; + export declare function gradientOverlay(opts?: GradientOverlayOptions): string; /** * Create a full-bleed background image that covers the entire slide. * Use with customSlide to create hero slides with image backgrounds. @@ -2330,22 +2330,22 @@ declare module "ha:pptx" { * @param {Object} pres - Presentation object from createPresentation() * @param {Uint8Array} data - Image data (from fetchBinary, readBinary, or shared-state) * @param {string} [format='jpg'] - Image format (jpg, png, gif, webp, etc.) - * @returns {ShapeFragment} Branded shape fragment for a full-slide image + * @returns {string} OOXML shape string for a full-slide image */ - export declare function backgroundImage(pres: PresentationInternal, data: Uint8Array, format?: string): ShapeFragment; + export declare function backgroundImage(pres: PresentationInternal, data: Uint8Array, format?: string): string; /** * Create a gradient background for slides. - * Use with customSlide({ background }) or as defaultBackground in createPresentation(). + * Use with pres.addSlide() or as defaultBackground in createPresentation(). * * @param {string} color1 - Start color (hex, e.g. '000000') * @param {string} color2 - End color (hex, e.g. '1a1a2e') * @param {number} [angle=270] - Gradient angle in degrees (0=right, 90=down, 180=left, 270=up) - * @returns {string} Background XML for use with customSlide() + * @returns {string} Background XML for use with pres.addSlide() * * @example * // Vertical gradient (top to bottom) - * const pres = createPresentation({ theme: 'brutalist' }); - * customSlide(pres, { shapes: [...], background: '000000' }); + * const bg = gradientBg('000000', '1a1a2e', 180); + * pres.addSlide(bg, shapes); * * @example * // As default background for all slides @@ -2355,19 +2355,6 @@ declare module "ha:pptx" { * }); */ export declare function gradientBg(color1: string, color2: string, angle?: number): string; - export interface ValidationIssue { - code: string; - severity: "error" | "warn"; - message: string; - part?: string; - slideIndex?: number; - hint?: string; - } - export interface ValidationResult { - ok: boolean; - errors: ValidationIssue[]; - warnings: ValidationIssue[]; - } /** * Create a new presentation builder. * @@ -2384,12 +2371,11 @@ declare module "ha:pptx" { * titleSlide(pres, { title: 'My Title' }); * contentSlide(pres, { title: 'Content', bullets: ['Point 1', 'Point 2'] }); * - * // For CUSTOM layouts, use customSlide(): - * customSlide(pres, { - * shapes: [textBox({x: 1, y: 1, w: 8, h: 1, text: 'Custom text'}), - * rect({x: 1, y: 3, w: 4, h: 2, fill: pres.theme.accent1})], - * transition: 'fade' - * }); + * // For CUSTOM layouts, use pres.addSlide() directly: + * const bg = solidFill(pres.theme.bg); + * const shapes = textBox({x: 1, y: 1, w: 8, h: 1, text: 'Custom text'}) + + * rect({x: 1, y: 3, w: 4, h: 2, fill: pres.theme.accent1}); + * pres.addSlide(bg, shapes, { transition: 'fade' }); * * // Build final file * const zip = pres.buildZip(); @@ -2477,19 +2463,12 @@ declare module "ha:pptx" { * pres.addBody(textBox({x:1, y:1, w:8, h:1, text:'Hello'})); * * // With solid background: - * pres.addBody([shape1, shape2], { background: '0D1117', transition: 'fade' }); + * pres.addBody(shapes, { background: '0D1117', transition: 'fade' }); * * // With gradient background: - * pres.addBody([shape1], { background: {color1: '000000', color2: '1a1a2e', angle: 180} }); - */ - addBody(shapesInput: ShapeFragment | ShapeFragment[] | string | string[], slideOpts?: SlideOptions): void; - /** - * Internal: add shapes (as pre-validated XML string) to a new slide. - * Resolves background from per-slide > defaultBackground > theme. - * Not on the Presentation interface — internal use only. - * @internal + * pres.addBody(shapes, { background: {color1: '000000', color2: '1a1a2e', angle: 180} }); */ - _addBodyRaw(shapesStr: string, slideOpts?: SlideOptions): void; + addBody(shapesXml: string | string[], slideOpts?: SlideOptions): void; /** * Insert a slide at a specific index. Existing slides shift right. * @param {number} index - Position to insert (0-based). Clamped to valid range. @@ -2775,18 +2754,17 @@ declare module "ha:pptx" { * Add a blank slide with just the theme background (NO content). * * ⚠️ WARNING: This creates an EMPTY slide. You CANNOT add shapes to it later. - * For custom layouts with shapes, use customSlide() instead: + * For custom layouts with shapes, use pres.addSlide() directly instead: * * @example * // DON'T do this — blankSlide creates empty slide with no way to add content: * blankSlide(pres); // Creates empty slide, cannot add shapes after * - * // DO this instead — use customSlide for custom layouts: - * customSlide(pres, { - * shapes: [textBox({x: 1, y: 1, w: 8, h: 1, text: 'Custom slide'}), - * rect({x: 1, y: 3, w: 4, h: 2, fill: pres.theme.accent1})], - * transition: 'fade' - * }); + * // DO this instead — use addSlide for custom layouts: + * const bg = solidFill(pres.theme.bg); + * const shapes = textBox({x: 1, y: 1, w: 8, h: 1, text: 'Custom slide'}) + + * rect({x: 1, y: 3, w: 4, h: 2, fill: pres.theme.accent1}); + * pres.addSlide(bg, shapes, { transition: 'fade' }); * * @param {Object} pres - Presentation object from createPresentation(). REQUIRED as first param. * @returns {void} @@ -3292,9 +3270,9 @@ declare module "ha:pptx" { * @param {string} [opts.titleColor='8B949E'] - Title color * @param {boolean} [opts.lineNumbers=false] - Show line numbers * @param {number} [opts.cornerRadius=4] - Corner radius in points - * @returns {ShapeFragment} Branded shape fragment + * @returns {string} Shape XML fragment */ - export declare function codeBlock(opts: CodeBlockOptions): ShapeFragment; + export declare function codeBlock(opts: CodeBlockOptions): string; /** * Slide configuration for batch creation. * Each object describes one slide using a declarative config. @@ -3426,21 +3404,20 @@ declare module "ha:pptx" { * * @example * // Single image - * const img = fetchAndEmbed(pres, { + * const imgXml = await fetchAndEmbed(pres, { * url: "https://example.com/photo.jpg", - * x: 1, y: 1, w: 4, h: 3, - * fetchFn: fetchBinary + * x: 1, y: 1, w: 4, h: 3 * }); - * customSlide(pres, { shapes: [img, textBox({...})] }); + * customSlide(pres, { shapes: imgXml + textBox({...}) }); * * @example * // With fetch plugin * import { fetchBinary } from "host:fetch"; - * const img = fetchAndEmbed(pres, { + * const imgXml = await fetchAndEmbed(pres, { * url: "https://cdn.example.com/hero.jpg", * x: 0, y: 0, w: 13.333, h: 7.5, * fit: "cover", - * fetchFn: fetchBinary + * fetchFn: fetchBinary // Pass the fetch function * }); * * @param {Object} pres - Presentation object @@ -3453,7 +3430,7 @@ declare module "ha:pptx" { * @param {string} [opts.format] - Image format (auto-detected from URL if omitted) * @param {string} [opts.fit] - Fit mode: 'stretch', 'contain', 'cover' * @param {Function} opts.fetchFn - Fetch function (e.g., fetchBinary from host:fetch) - * @returns {ShapeFragment} Branded image shape fragment + * @returns {string} Image XML fragment for use in shapes */ export declare function fetchAndEmbed(pres: Pres, opts: { url: string; @@ -3464,7 +3441,7 @@ declare module "ha:pptx" { format?: string; fit?: "stretch" | "contain" | "cover"; fetchFn: (url: string) => Uint8Array; - }): ShapeFragment; + }): string; /** * Fetch multiple images and embed them all, returning XML fragments. * Uses fetchBinaryBatch for efficient parallel downloads when maxParallelFetches > 1. @@ -3479,13 +3456,13 @@ declare module "ha:pptx" { * ], * fetchBatchFn: fetchBinaryBatch * }); - * // images = [{ url, shape }, { url, shape }, { url, shape }] or [{ url, error }, ...] + * // images = [{ url, xml }, { url, xml }, { url, xml }] or [{ url, error }, ...] * * @param {Object} pres - Presentation object * @param {Object} opts - Options * @param {Array} opts.items - Array of {url, x, y, w, h, format?, fit?} * @param {Function} opts.fetchBatchFn - Batch fetch function (fetchBinaryBatch from host:fetch) - * @returns {Array} Array of {url, shape: ShapeFragment} or {url, error} for each item + * @returns {Array} Array of {url, xml} or {url, error} for each item */ export declare function fetchAndEmbedBatch(pres: Pres, opts: { items: Array<{ @@ -3504,7 +3481,7 @@ declare module "ha:pptx" { }>; }): Array<{ url: string; - shape?: ShapeFragment; + xml?: string; error?: string; }>; } diff --git a/package.json b/package.json index fae1070..8527e7b 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "fmt:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\" \"plugins/**/*.ts\" \"builtin-modules/**/*.js\"", "check": "npm run fmt:check && npm run typecheck && npm run test", "prepare": "node -e \"if(require('fs').existsSync('scripts/build-modules.js'))require('child_process').execSync('npm run build:modules',{stdio:'inherit'})\"", - "postinstall": "node -e \"var fs=require('fs'),cp=require('child_process');if(fs.existsSync('scripts/patch-vscode-jsonrpc.js')){cp.execSync('node scripts/patch-vscode-jsonrpc.js',{stdio:'inherit'});cp.execSync('node scripts/check-native-runtime.js',{stdio:'inherit'})}\"" + "postinstall": "node -e \"var fs=require('fs'),cp=require('child_process');if(fs.existsSync('scripts/patch-vscode-jsonrpc.js')){cp.execSync('node scripts/patch-vscode-jsonrpc.js',{stdio:'inherit'});cp.execSync('node scripts/check-native-runtime.js',{stdio:'inherit'});}\"" }, "dependencies": { "@github/copilot-sdk": "^0.1.32", diff --git a/scripts/build-binary.js b/scripts/build-binary.js index 193a04c..469f47d 100644 --- a/scripts/build-binary.js +++ b/scripts/build-binary.js @@ -426,10 +426,10 @@ writeFileSync(launcherCjsPath, launcherCjs); // Node.js launcher (works everywhere, used as npm bin entry) const nodeLauncher = `#!/usr/bin/env node import { join, dirname } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const cjs = join(__dirname, '..', 'lib', 'hyperagent-launcher.cjs'); -await import('file://' + cjs.replace(/\\\\/g, '/')); +await import(pathToFileURL(cjs).href); `; const nodeLauncherPath = join(BIN_DIR, "hyperagent"); writeFileSync(nodeLauncherPath, nodeLauncher); @@ -494,7 +494,7 @@ To run (option 3 - add to PATH permanently via System Properties): ${launcherPath} To run (option 2 - add to PATH): - export PATH="${BIN_DIR}:\\$PATH" + export PATH="${BIN_DIR}:$PATH" hyperagent To run (option 3 - symlink): diff --git a/tests/pattern-loader.test.ts b/tests/pattern-loader.test.ts index 879b89c..a36f3bc 100644 --- a/tests/pattern-loader.test.ts +++ b/tests/pattern-loader.test.ts @@ -23,8 +23,16 @@ describe("pattern-loader", () => { afterEach(() => { try { rmSync(TMP_DIR, { recursive: true, force: true }); - } catch { - // Windows: Defender/indexer may hold a lock — not worth failing the test + } catch (err: unknown) { + // Windows Defender/indexer can hold file locks — only swallow those + const code = (err as NodeJS.ErrnoException).code; + if ( + process.platform === "win32" && + (code === "EBUSY" || code === "EPERM") + ) { + return; + } + throw err; } }); From 12b0455e3b92f851d4f22e40dc61bde2c1b3190b Mon Sep 17 00:00:00 2001 From: Simon Davies Date: Fri, 27 Mar 2026 20:17:31 +0000 Subject: [PATCH 2/6] fix: cross-platform npm publish with runtime NAPI detection - build-binary.js: use napi-rs generated index.js for platform detection instead of hardcoded triple shim. Copies all available .node files so the package works on any platform. - publish.yml: build native addons on Linux AND Windows in parallel, upload as artifacts, combine in publish job. Tests run on each platform before upload. Follows same pattern as hyperlight-js PR #36. Signed-off-by: Simon Davies --- .github/workflows/publish.yml | 45 ++++++++++++++++------ scripts/build-binary.js | 71 +++++++++++++++++++++++++---------- 2 files changed, 84 insertions(+), 32 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 15424dc..2e8488b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,21 +21,21 @@ env: IMAGE_NAME: ${{ github.repository }} jobs: - # Test on all hypervisor configurations before publishing - # NOTE: Windows WHP temporarily disabled (see pr-validate.yml) - test: - name: Test (${{ matrix.build }}) + # Build native addons on each platform and upload as artifacts. + # These are combined in the publish-npm job to create a cross-platform package. + build-native: + name: Build (${{ matrix.build }}) strategy: fail-fast: true matrix: - build: [linux-kvm, linux-mshv] + build: [linux-kvm, windows-whp] include: - build: linux-kvm os: [self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd"] hypervisor: kvm - - build: linux-mshv - os: [self-hosted, Linux, X64, "1ES.Pool=hld-azlinux3-mshv-amd"] - hypervisor: mshv + - build: windows-whp + os: [self-hosted, Windows, X64, "1ES.Pool=hld-win2022-amd"] + hypervisor: whp runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 @@ -55,14 +55,28 @@ jobs: - name: Build release binary run: node scripts/build-binary.js --release + env: + VERSION: ${{ github.event.release.tag_name || inputs.version }} - name: Run tests run: just test - # Build and publish npm package (after tests pass) + # Upload the native .node addons so the publish job can combine them + - name: Upload native addons + uses: actions/upload-artifact@v4 + with: + name: native-addons-${{ matrix.build }} + path: | + deps/js-host-api/js-host-api.*.node + src/code-validator/guest/host/hyperlight-analysis.*.node + src/code-validator/guest/hyperlight-analysis.*.node + if-no-files-found: error + retention-days: 1 + + # Combine native addons from all platforms and publish a single npm package publish-npm: name: Publish to npmjs.org - needs: [test] + needs: [build-native] runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 @@ -78,10 +92,17 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Download all platform native addons into their expected locations + - name: Download all native addons + uses: actions/download-artifact@v4 + with: + pattern: native-addons-* + merge-multiple: true + - name: Setup run: just setup - - name: Build binary + - name: Build binary (with all platform addons present) run: VERSION="${{ github.event.release.tag_name || inputs.version }}" node scripts/build-binary.js --release - name: Set version from release tag @@ -100,7 +121,7 @@ jobs: # Build and publish Docker image (after tests pass) publish-docker: name: Publish to GitHub Container Registry - needs: [test] + needs: [build-native] runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 diff --git a/scripts/build-binary.js b/scripts/build-binary.js index 469f47d..9d91b4b 100644 --- a/scripts/build-binary.js +++ b/scripts/build-binary.js @@ -217,20 +217,43 @@ if (!existsSync(analysisNode)) { process.exit(1); } -copyFileSync(hyperlightNode, join(LIB_DIR, `js-host-api.${napiTriple}.node`)); -copyFileSync( - analysisNode, - join(LIB_DIR, `hyperlight-analysis.${napiTriple}.node`), -); +// Copy .node files for ALL available platforms so the package is cross-platform. +// The current platform's .node is guaranteed to exist (checked above). +// Additional platform .node files are copied if present (e.g. from CI matrix builds). +const ALL_TRIPLES = ["linux-x64-gnu", "linux-x64-musl", "win32-x64-msvc"]; +for (const triple of ALL_TRIPLES) { + const hlNode = join(ROOT, `deps/js-host-api/js-host-api.${triple}.node`); + const anNode = join( + ROOT, + `src/code-validator/guest/host/hyperlight-analysis.${triple}.node`, + ); + if (existsSync(hlNode)) { + copyFileSync(hlNode, join(LIB_DIR, `js-host-api.${triple}.node`)); + console.log(` ✓ js-host-api.${triple}.node`); + } + if (existsSync(anNode)) { + copyFileSync(anNode, join(LIB_DIR, `hyperlight-analysis.${triple}.node`)); + console.log(` ✓ hyperlight-analysis.${triple}.node`); + } +} // Create a proper node_modules package structure for hyperlight-analysis // so both require() and import() can resolve it in the bundled binary. const analysisPkgDir = join(LIB_DIR, "node_modules", "hyperlight-analysis"); mkdirSync(analysisPkgDir, { recursive: true }); -copyFileSync( - analysisNode, - join(analysisPkgDir, `hyperlight-analysis.${napiTriple}.node`), -); +// Copy all available platform .node files into the package dir +for (const triple of ALL_TRIPLES) { + const anNode = join( + ROOT, + `src/code-validator/guest/host/hyperlight-analysis.${triple}.node`, + ); + if (existsSync(anNode)) { + copyFileSync( + anNode, + join(analysisPkgDir, `hyperlight-analysis.${triple}.node`), + ); + } +} // Copy the index.js and index.d.ts from the source package const analysisIndex = join(ROOT, "src/code-validator/guest/index.js"); const analysisTypes = join(ROOT, "src/code-validator/guest/index.d.ts"); @@ -250,24 +273,30 @@ if (existsSync(analysisPkg)) // Files are renamed to .cjs because the host package.json has "type": "module" // which makes Node.js treat .js as ESM — but lib.js uses require(). const hyperlightLibJs = join(ROOT, "deps/js-host-api/lib.js"); +const hyperlightIndexJs = join(ROOT, "deps/js-host-api/index.js"); const hyperlightHostApiDir = join(LIB_DIR, "js-host-api"); mkdirSync(hyperlightHostApiDir, { recursive: true }); -copyFileSync( - hyperlightNode, - join(hyperlightHostApiDir, `js-host-api.${napiTriple}.node`), -); +// Copy all available platform .node files +for (const triple of ALL_TRIPLES) { + const hlNode = join(ROOT, `deps/js-host-api/js-host-api.${triple}.node`); + if (existsSync(hlNode)) { + copyFileSync( + hlNode, + join(hyperlightHostApiDir, `js-host-api.${triple}.node`), + ); + } +} // Copy lib.js as lib.cjs, patching the require('./index.js') to './index.cjs' const libJsContent = readFileSync(hyperlightLibJs, "utf-8").replace( "require('./index.js')", "require('./index.cjs')", ); writeFileSync(join(hyperlightHostApiDir, "lib.cjs"), libJsContent); -// Create a minimal index.cjs shim that loads the .node addon from the -// same directory. Platform-specific .node file is resolved at build time. -writeFileSync( - join(hyperlightHostApiDir, "index.cjs"), - `'use strict';\nmodule.exports = require('./js-host-api.${napiTriple}.node');\n`, -); +// Copy the napi-rs generated index.js as index.cjs — it already has full +// platform detection (musl vs glibc, win32, darwin) and tries local .node +// files first, then falls back to optional @hyperlight/ scoped packages. +const indexJsContent = readFileSync(hyperlightIndexJs, "utf-8"); +writeFileSync(join(hyperlightHostApiDir, "index.cjs"), indexJsContent); // ── Step 5: Copy runtime resources ───────────────────────────────────── console.log("📁 Copying runtime resources..."); @@ -403,7 +432,9 @@ Module._load = function(request, parent, isMain) { return originalLoad.call(this, join(LIB_DIR, 'js-host-api', 'lib.cjs'), parent, isMain); } if (request === 'hyperlight-analysis') { - return originalLoad.call(this, join(LIB_DIR, 'hyperlight-analysis.${napiTriple}.node'), parent, isMain); + // The hyperlight-analysis index.js already has full napi-rs platform detection. + // It's copied into the node_modules structure, so just load it from there. + return originalLoad.call(this, join(LIB_DIR, 'node_modules', 'hyperlight-analysis', 'index.js'), parent, isMain); } return originalLoad.apply(this, arguments); }; From dc796349a4b24af437cce0d0174648132ea59146 Mon Sep 17 00:00:00 2001 From: Simon Davies Date: Fri, 27 Mar 2026 20:39:15 +0000 Subject: [PATCH 3/6] fix: download artifacts after setup to avoid symlink clobber Signed-off-by: Simon Davies --- .github/workflows/publish.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2e8488b..24062ac 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -92,16 +92,18 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # Download all platform native addons into their expected locations + - name: Setup + run: just setup + + # Download AFTER setup so artifacts land in the symlink/junction target + # that build-hyperlight creates (deps/js-host-api → Cargo checkout). + # Downloading before setup would be clobbered when setup re-creates the link. - name: Download all native addons uses: actions/download-artifact@v4 with: pattern: native-addons-* merge-multiple: true - - name: Setup - run: just setup - - name: Build binary (with all platform addons present) run: VERSION="${{ github.event.release.tag_name || inputs.version }}" node scripts/build-binary.js --release From 78b851fbab71e4acbd2f063810186acc212600a4 Mon Sep 17 00:00:00 2001 From: Simon Davies Date: Fri, 27 Mar 2026 21:27:50 +0000 Subject: [PATCH 4/6] feat: add musl (Alpine) build target for hyperlight-analysis - Add x86_64-unknown-linux-musl to napi targets in package.json - Add linux-musl build matrix entry in publish.yml (same Linux runner, installs musl-tools and adds Rust musl target) Signed-off-by: Simon Davies --- .github/workflows/publish.yml | 11 ++++++++++- src/code-validator/guest/package.json | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 24062ac..e7be5b0 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -28,11 +28,14 @@ jobs: strategy: fail-fast: true matrix: - build: [linux-kvm, windows-whp] + build: [linux-kvm, linux-musl, windows-whp] include: - build: linux-kvm os: [self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd"] hypervisor: kvm + - build: linux-musl + os: [self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd"] + hypervisor: kvm - build: windows-whp os: [self-hosted, Windows, X64, "1ES.Pool=hld-win2022-amd"] hypervisor: whp @@ -53,6 +56,12 @@ jobs: - name: Setup run: just setup + - name: Install musl tools + if: matrix.build == 'linux-musl' + run: | + sudo apt-get update && sudo apt-get install -y musl-tools + rustup target add x86_64-unknown-linux-musl + - name: Build release binary run: node scripts/build-binary.js --release env: diff --git a/src/code-validator/guest/package.json b/src/code-validator/guest/package.json index 89e4280..ca455b4 100644 --- a/src/code-validator/guest/package.json +++ b/src/code-validator/guest/package.json @@ -13,6 +13,7 @@ "binaryName": "hyperlight-analysis", "targets": [ "x86_64-unknown-linux-gnu", + "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc" ] }, From 5c98e7fbadc69429303f4f7488c448b1cf159c47 Mon Sep 17 00:00:00 2001 From: Simon Davies Date: Fri, 27 Mar 2026 22:07:26 +0000 Subject: [PATCH 5/6] fix: cross-platform publish with musl detection and CI corrections build-binary.js: - Launcher detects musl vs glibc for hyperlight-analysis .node loading using ldd probe, tries platform-specific .node directly - Uses napi-rs generated index.js for js-host-api (has full detection) - Copies all available platform .node files via ALL_TRIPLES loop publish.yml: - musl build: cross-compiles from glibc runner with musl-tools, uses napi build --target x86_64-unknown-linux-musl, skips tests (musl .node can't run on glibc host) - gnu/win32 builds: run tests natively on their platforms - publish-npm: runs on self-hosted runner (needs Rust toolchain), downloads artifacts AFTER setup to avoid symlink clobber - Verifies musl .node files are produced before artifact upload Signed-off-by: Simon Davies --- .github/workflows/publish.yml | 30 +++++++++++++++++++++++++++--- scripts/build-binary.js | 32 +++++++++++++++++++++++++++++--- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e7be5b0..8a27bb8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,6 +23,9 @@ env: jobs: # Build native addons on each platform and upload as artifacts. # These are combined in the publish-npm job to create a cross-platform package. + # + # gnu and win32 builds run tests natively on their platform. + # musl is cross-compiled from the glibc runner (can't run tests on glibc host). build-native: name: Build (${{ matrix.build }}) strategy: @@ -33,12 +36,15 @@ jobs: - build: linux-kvm os: [self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd"] hypervisor: kvm + run_tests: true - build: linux-musl os: [self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd"] hypervisor: kvm + run_tests: false # musl .node can't run on glibc host - build: windows-whp os: [self-hosted, Windows, X64, "1ES.Pool=hld-win2022-amd"] hypervisor: whp + run_tests: true runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 @@ -56,18 +62,34 @@ jobs: - name: Setup run: just setup - - name: Install musl tools + - name: Install musl tools and rebuild for musl target if: matrix.build == 'linux-musl' run: | sudo apt-get update && sudo apt-get install -y musl-tools rustup target add x86_64-unknown-linux-musl + # Rebuild hyperlight-js NAPI addon targeting musl + hl_dir=$(just resolve-hyperlight-dir) + cd "${hl_dir}/src/js-host-api" + npx napi build --platform --target x86_64-unknown-linux-musl + + # Rebuild hyperlight-analysis NAPI addon targeting musl + cd "$GITHUB_WORKSPACE/src/code-validator/guest" + npx napi build --platform --target x86_64-unknown-linux-musl --manifest-path host/Cargo.toml + node -e "require('fs').readdirSync('host').filter(f=>f.endsWith('.node')).forEach(f=>require('fs').copyFileSync('host/'+f,f))" + + # Verify musl .node files were actually produced + ls -la "${hl_dir}/src/js-host-api/"*.linux-x64-musl.node + ls -la "$GITHUB_WORKSPACE/src/code-validator/guest/"*linux-x64-musl* || ls -la "$GITHUB_WORKSPACE/src/code-validator/guest/host/"*linux-x64-musl* + - name: Build release binary + if: matrix.run_tests run: node scripts/build-binary.js --release env: VERSION: ${{ github.event.release.tag_name || inputs.version }} - name: Run tests + if: matrix.run_tests run: just test # Upload the native .node addons so the publish job can combine them @@ -82,11 +104,13 @@ jobs: if-no-files-found: error retention-days: 1 - # Combine native addons from all platforms and publish a single npm package + # Combine native addons from all platforms and publish a single npm package. + # Runs on a self-hosted Linux runner (not ubuntu-latest) because just setup + # needs to build the Rust runtime which requires hyperlight toolchain. publish-npm: name: Publish to npmjs.org needs: [build-native] - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd"] steps: - uses: actions/checkout@v6 diff --git a/scripts/build-binary.js b/scripts/build-binary.js index 9d91b4b..a5a4e1a 100644 --- a/scripts/build-binary.js +++ b/scripts/build-binary.js @@ -432,9 +432,35 @@ Module._load = function(request, parent, isMain) { return originalLoad.call(this, join(LIB_DIR, 'js-host-api', 'lib.cjs'), parent, isMain); } if (request === 'hyperlight-analysis') { - // The hyperlight-analysis index.js already has full napi-rs platform detection. - // It's copied into the node_modules structure, so just load it from there. - return originalLoad.call(this, join(LIB_DIR, 'node_modules', 'hyperlight-analysis', 'index.js'), parent, isMain); + // Load the correct platform-specific .node directly, with musl detection. + // The index.js loader doesn't distinguish musl vs glibc, so we handle it here. + const fs = require('fs'); + const hyperlightDir = join(LIB_DIR, 'node_modules', 'hyperlight-analysis'); + const platformArch = process.platform + '-' + process.arch; + const candidates = []; + if (platformArch === 'linux-x64') { + // Detect musl vs glibc — try musl first on musl systems, then glibc + let isMusl = false; + try { + const r = require('child_process').spawnSync('ldd', ['--version'], + { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }); + isMusl = ((r.stdout || '') + (r.stderr || '')).includes('musl'); + } catch {} + if (isMusl) { + candidates.push(join(hyperlightDir, 'hyperlight-analysis.linux-x64-musl.node')); + } + candidates.push(join(hyperlightDir, 'hyperlight-analysis.linux-x64-gnu.node')); + } else if (platformArch === 'win32-x64') { + candidates.push(join(hyperlightDir, 'hyperlight-analysis.win32-x64-msvc.node')); + } + // Fall back to index.js loader + candidates.push(join(hyperlightDir, 'index.js')); + for (const candidate of candidates) { + if (fs.existsSync(candidate)) { + return originalLoad.call(this, candidate, parent, isMain); + } + } + return originalLoad.apply(this, arguments); } return originalLoad.apply(this, arguments); }; From 58ce3259833b2b2ec9e96e3f39a00f263d933f18 Mon Sep 17 00:00:00 2001 From: Simon Davies Date: Fri, 27 Mar 2026 22:35:22 +0000 Subject: [PATCH 6/6] fix: commit regenerated ha-modules.d.ts + add sync test - ha-modules.d.ts: update return types to ShapeFragment (was string) to match upstream ShapeFragment safety system changes - dts-sync.test.ts: add ha-modules.d.ts regeneration check that catches drift when module exports/types change but the generator isn't re-run Signed-off-by: Simon Davies --- builtin-modules/src/types/ha-modules.d.ts | 183 ++++++++++++---------- tests/dts-sync.test.ts | 36 ++++- 2 files changed, 138 insertions(+), 81 deletions(-) diff --git a/builtin-modules/src/types/ha-modules.d.ts b/builtin-modules/src/types/ha-modules.d.ts index 2818843..62e93ef 100644 --- a/builtin-modules/src/types/ha-modules.d.ts +++ b/builtin-modules/src/types/ha-modules.d.ts @@ -777,7 +777,7 @@ declare module "ha:pptx-tables" { * @param opts.style.headerFontSize - Header font size in pt * @returns Shape XML fragment for use in slide body */ - export declare function table(opts: TableOptions): string; + export declare function table(opts: TableOptions): ShapeFragment; export interface KVItem { key: string; value: string; @@ -806,7 +806,7 @@ declare module "ha:pptx-tables" { * @param opts - KV table options: { x?, y?, w?, items: Array<{key, value}>, theme?, style? } * @returns Shape XML fragment */ - export declare function kvTable(opts: KVTableOptions): string; + export declare function kvTable(opts: KVTableOptions): ShapeFragment; export interface ComparisonOption { /** Column header name */ name: string; @@ -853,7 +853,7 @@ declare module "ha:pptx-tables" { * @param opts - REQUIRED: { features: string[], options: Array<{name: string, values: boolean[]}> }. Optional: x?, y?, w?, theme?, style? * @returns Shape XML fragment */ - export declare function comparisonTable(opts: ComparisonTableOptions): string; + export declare function comparisonTable(opts: ComparisonTableOptions): ShapeFragment; export interface TimelineItem { /** Phase/milestone label */ label: string; @@ -886,7 +886,7 @@ declare module "ha:pptx-tables" { * @param opts - Timeline options: { x?, y?, w?, items: Array<{label, description?, color?}>, theme?, style? } * @returns Shape XML fragment (uses table layout) */ - export declare function timeline(opts: TimelineOptions): string; + export declare function timeline(opts: TimelineOptions): ShapeFragment; } declare module "ha:pptx" { @@ -942,7 +942,7 @@ declare module "ha:pptx" { export interface Presentation { theme: Theme; slideCount: number; - addBody(shapes: string | string[], opts?: SlideOptions): void; + addBody(shapes: ShapeFragment | ShapeFragment[], opts?: SlideOptions): void; build(): Array<{ name: string; data: string | Uint8Array; @@ -1433,7 +1433,8 @@ declare module "ha:pptx" { extraItems?: string[] | string; } export interface CustomSlideOptions { - shapes: string; + /** Array of ShapeFragment objects from shape builders (textBox, rect, table, etc.). REQUIRED. */ + shapes: ShapeFragment | ShapeFragment[]; background?: string | GradientSpec; transition?: string; transitionDuration?: number; @@ -1620,13 +1621,14 @@ declare module "ha:pptx" { fontSize?: number; } export { type Theme }; + export { type ShapeFragment, isShapeFragment, fragmentsToXml }; export { table, kvTable, comparisonTable, timeline, TABLE_STYLES, } from "ha:pptx-tables"; export { contrastRatio }; export { getThemeNames }; export { inches, fontSize } from "ha:ooxml-core"; /** * Create a solid fill XML element. - * Use for custom slide backgrounds via pres.addSlide(solidFill('000000'), shapes). + * Use for shape fills or customSlide({ background }) backgrounds. * @param {string} color - Hex color (6 digits, no #) * @param {number} [opacity] - Opacity from 0 (transparent) to 1 (opaque). Omit for fully opaque. * @returns {string} Solid fill XML @@ -1657,9 +1659,9 @@ declare module "ha:pptx" { * @param {string} [opts.background] - Fill color (hex) * @param {number} [opts.lineSpacing] - Line spacing in points * @param {boolean} [opts.autoFit] - Auto-scale fontSize to fit text in shape. Use when text length is variable. - * @returns {string} Shape XML fragment + * @returns {ShapeFragment} Branded shape fragment */ - export declare function textBox(opts: TextBoxOptions): string; + export declare function textBox(opts: TextBoxOptions): ShapeFragment; /** * Create a colored rectangle with optional text. * @param {Object} opts @@ -1676,9 +1678,9 @@ declare module "ha:pptx" { * @param {number} [opts.cornerRadius] - Corner radius in points * @param {string} [opts.borderColor] - Border color * @param {number} [opts.borderWidth=1] - Border width in points - * @returns {string} Shape XML fragment + * @returns {ShapeFragment} Branded shape fragment */ - export declare function rect(opts: RectOptions): string; + export declare function rect(opts: RectOptions): ShapeFragment; /** * Create a bulleted list. * @param {Object} opts @@ -1691,9 +1693,9 @@ declare module "ha:pptx" { * @param {string} [opts.color] - Text color * @param {string} [opts.bulletColor] - Bullet color * @param {number} [opts.lineSpacing=24] - Line spacing - * @returns {string} Shape XML fragment + * @returns {ShapeFragment} Branded shape fragment */ - export declare function bulletList(opts: BulletListOptions): string; + export declare function bulletList(opts: BulletListOptions): ShapeFragment; /** * Create a numbered list. * @param {Object} opts @@ -1706,9 +1708,9 @@ declare module "ha:pptx" { * @param {string} [opts.color] - Text color * @param {number} [opts.lineSpacing=24] - Line spacing * @param {number} [opts.startAt=1] - Starting number - * @returns {string} Shape XML fragment + * @returns {ShapeFragment} Branded shape fragment */ - export declare function numberedList(opts: NumberedListOptions): string; + export declare function numberedList(opts: NumberedListOptions): ShapeFragment; /** * Create an image placeholder (colored rect with label). * Use this until binary image embedding is supported. @@ -1720,9 +1722,9 @@ declare module "ha:pptx" { * @param {string} [opts.label='Image'] - Placeholder label * @param {string} [opts.fill='3D4450'] - Background color (dark gray) * @param {string} [opts.color='B0B8C0'] - Label color (light gray, passes WCAG AA on 3D4450) - * @returns {string} Shape XML fragment + * @returns {ShapeFragment} Branded shape fragment */ - export declare function imagePlaceholder(opts: ImagePlaceholderOptions): string; + export declare function imagePlaceholder(opts: ImagePlaceholderOptions): ShapeFragment; /** * Create a big metric display (number + label stacked). * @param {Object} opts @@ -1738,9 +1740,9 @@ declare module "ha:pptx" { * @param {string} [opts.labelColor] - Label text color (hex). OMIT to auto-select against background. * @param {string} [opts.background] - Background fill * @param {boolean} [opts.forceColor] - Set true to bypass WCAG contrast validation for valueColor/labelColor. - * @returns {string} Shape XML fragment + * @returns {ShapeFragment} Branded shape fragment */ - export declare function statBox(opts: StatBoxOptions): string; + export declare function statBox(opts: StatBoxOptions): ShapeFragment; /** * Create a line between two points. * @param {Object} opts @@ -1751,9 +1753,9 @@ declare module "ha:pptx" { * @param {string} [opts.color='666666'] - Line color (hex) * @param {number} [opts.width=1.5] - Line width in points * @param {string} [opts.dash] - Dash style: 'solid', 'dash', 'dot', 'dashDot' - * @returns {string} Shape XML fragment + * @returns {ShapeFragment} Branded shape fragment */ - export declare function line(opts: LineOptions): string; + export declare function line(opts: LineOptions): ShapeFragment; /** * Create an arrow (line with arrowhead) between two points. * @param {Object} opts @@ -1766,9 +1768,9 @@ declare module "ha:pptx" { * @param {string} [opts.headType='triangle'] - Arrowhead: 'triangle', 'stealth', 'diamond', 'oval', 'arrow' * @param {boolean} [opts.bothEnds=false] - Arrowhead on both ends * @param {string} [opts.dash] - Dash style: 'solid', 'dash', 'dot', 'dashDot' - * @returns {string} Shape XML fragment + * @returns {ShapeFragment} Branded shape fragment */ - export declare function arrow(opts: ArrowOptions): string; + export declare function arrow(opts: ArrowOptions): ShapeFragment; /** * Create a circle or ellipse shape. * @param {Object} opts @@ -1782,9 +1784,9 @@ declare module "ha:pptx" { * @param {string} [opts.color='FFFFFF'] - Text color * @param {string} [opts.borderColor] - Border color * @param {number} [opts.borderWidth=1] - Border width in points - * @returns {string} Shape XML fragment + * @returns {ShapeFragment} Branded shape fragment */ - export declare function circle(opts: CircleOptions): string; + export declare function circle(opts: CircleOptions): ShapeFragment; /** * Create a callout box — rounded rectangle with accent left border. * Good for highlighting insights, quotes, or key takeaways. @@ -1798,9 +1800,9 @@ declare module "ha:pptx" { * @param {string} [opts.background='F5F5F5'] - Fill color * @param {number} [opts.fontSize=14] - Font size * @param {string} [opts.color] - Text color (hex). OMIT to auto-select a readable colour against the background. Do NOT hardcode. - * @returns {string} Shape XML fragment + * @returns {ShapeFragment} Branded shape fragment */ - export declare function callout(opts: CalloutOptions): string; + export declare function callout(opts: CalloutOptions): ShapeFragment; /** * Create a preset shape icon. * @@ -1838,9 +1840,9 @@ declare module "ha:pptx" { * @param {string} [opts.text] - Optional text inside the shape * @param {number} [opts.fontSize=12] - Text font size * @param {string} [opts.color='FFFFFF'] - Text color - * @returns {string} Shape XML fragment + * @returns {ShapeFragment} Branded shape fragment */ - export declare function icon(opts: IconOptions): string; + export declare function icon(opts: IconOptions): ShapeFragment; /** * Create a shape from an SVG path string. * Enables custom icons, logos, and diagrams using standard SVG path data. @@ -1875,9 +1877,9 @@ declare module "ha:pptx" { * @param {string} [opts.fill] - Fill color (hex, e.g. '2196F3') * @param {string} [opts.stroke] - Stroke color (hex) * @param {number} [opts.strokeWidth=1] - Stroke width in points - * @returns {string} Shape XML fragment + * @returns {ShapeFragment} Branded shape fragment */ - export declare function svgPath(opts: SvgPathOptions): string; + export declare function svgPath(opts: SvgPathOptions): ShapeFragment; /** * Create a gradient fill XML fragment for use in shapes. * Supports transparency for cinematic photo overlays (e.g., transparent-to-black). @@ -1963,9 +1965,9 @@ declare module "ha:pptx" { * @param {string} [opts.align='l'] - Paragraph alignment ('l', 'ctr', 'r') * @param {string} [opts.valign='t'] - Vertical alignment ('t', 'ctr', 'b') * @param {string} [opts.background] - Fill color (hex) - * @returns {string} Shape XML fragment + * @returns {ShapeFragment} Branded shape fragment */ - export declare function richText(opts: RichTextOptions): string; + export declare function richText(opts: RichTextOptions): ShapeFragment; /** Options for panel() composite shape */ export interface PanelOptions { /** X position in inches */ @@ -2032,7 +2034,7 @@ declare module "ha:pptx" { * @param opts - Panel options * @returns Shape XML fragments for all panel elements */ - export declare function panel(opts: PanelOptions): string; + export declare function panel(opts: PanelOptions): ShapeFragment; /** Options for card() composite shape */ export interface CardOptions extends PanelOptions { /** Accent color for top border (hex). If set, adds a colored stripe at top */ @@ -2057,7 +2059,7 @@ declare module "ha:pptx" { * @param opts - Card options * @returns Shape XML fragments */ - export declare function card(opts: CardOptions): string; + export declare function card(opts: CardOptions): ShapeFragment; /** * Create a text box with a clickable hyperlink. * The entire text box is clickable. For inline hyperlinks within @@ -2074,9 +2076,9 @@ declare module "ha:pptx" { * @param {string} [opts.color='2196F3'] - Text color (default blue) * @param {boolean} [opts.underline=true] - Underline text * @param {Object} pres - Presentation builder (needed to register the link relationship) - * @returns {string} Shape XML fragment + * @returns {ShapeFragment} Branded shape fragment */ - export declare function hyperlink(opts: HyperlinkOptions, pres: PresentationInternal): string; + export declare function hyperlink(opts: HyperlinkOptions, pres: PresentationInternal): ShapeFragment; /** Image dimensions in pixels */ export interface ImageDimensions { width: number; @@ -2140,9 +2142,9 @@ declare module "ha:pptx" { * @param {string} [opts.format='png'] - Image format: 'png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg' * @param {string} [opts.fit='stretch'] - How to fit image: 'stretch' (distort to fill), 'contain' (fit within, may letterbox), 'cover' (fill, may crop) * @param {string} [opts.name] - Optional image name (for the ZIP path) - * @returns {string} Shape XML fragment for use in slide body + * @returns {ShapeFragment} Branded shape fragment for use in slide body */ - export declare function embedImage(pres: PresentationInternal, opts: EmbedImageOptions): string; + export declare function embedImage(pres: PresentationInternal, opts: EmbedImageOptions): ShapeFragment; /** * Helper to embed an image from a URL with auto-detected format. * This combines readBinary() and embedImage() into a simpler workflow. @@ -2169,11 +2171,11 @@ declare module "ha:pptx" { * @param {number} opts.w - Width in inches * @param {number} opts.h - Height in inches * @param {string} [opts.format] - Override format detection (png, jpg, gif, etc.) - * @returns {string} Shape XML fragment for use in slide body + * @returns {ShapeFragment} Branded shape fragment for use in slide body */ export declare function embedImageFromUrl(pres: PresentationInternal, opts: EmbedImageOptions & { url: string; - }): string; + }): ShapeFragment; /** Slide width in inches (16:9 aspect ratio). */ export declare const SLIDE_WIDTH_INCHES = 13.333; /** Slide height in inches (16:9 aspect ratio). */ @@ -2208,9 +2210,7 @@ declare module "ha:pptx" { * @param items - Array of shape XML strings or objects with toString() * @returns Combined XML string */ - export declare function shapes(items: Array): string; + export declare function shapes(items: Array): ShapeFragment; /** * Calculate positions for items in equal-width columns. * Useful for stat boxes, image cards, or any side-by-side layout. @@ -2281,9 +2281,9 @@ declare module "ha:pptx" { * @param {number} [opts.y=0] - Y position in inches * @param {number} [opts.w] - Width in inches (default: full slide width) * @param {number} [opts.h] - Height in inches (default: full slide height) - * @returns {string} OOXML shape string + * @returns {ShapeFragment} Branded shape fragment */ - export declare function overlay(opts?: OverlayOptions): string; + export declare function overlay(opts?: OverlayOptions): ShapeFragment; /** * Create a gradient overlay for cinematic effects. * Use for half-fades, vignettes, or directional darkening on image slides. @@ -2311,9 +2311,9 @@ declare module "ha:pptx" { * @param {number} [opts.y=0] - Y position in inches * @param {number} [opts.w] - Width in inches (default full slide) * @param {number} [opts.h] - Height in inches (default full slide) - * @returns {string} OOXML shape string + * @returns {ShapeFragment} Branded shape fragment */ - export declare function gradientOverlay(opts?: GradientOverlayOptions): string; + export declare function gradientOverlay(opts?: GradientOverlayOptions): ShapeFragment; /** * Create a full-bleed background image that covers the entire slide. * Use with customSlide to create hero slides with image backgrounds. @@ -2330,22 +2330,22 @@ declare module "ha:pptx" { * @param {Object} pres - Presentation object from createPresentation() * @param {Uint8Array} data - Image data (from fetchBinary, readBinary, or shared-state) * @param {string} [format='jpg'] - Image format (jpg, png, gif, webp, etc.) - * @returns {string} OOXML shape string for a full-slide image + * @returns {ShapeFragment} Branded shape fragment for a full-slide image */ - export declare function backgroundImage(pres: PresentationInternal, data: Uint8Array, format?: string): string; + export declare function backgroundImage(pres: PresentationInternal, data: Uint8Array, format?: string): ShapeFragment; /** * Create a gradient background for slides. - * Use with pres.addSlide() or as defaultBackground in createPresentation(). + * Use with customSlide({ background }) or as defaultBackground in createPresentation(). * * @param {string} color1 - Start color (hex, e.g. '000000') * @param {string} color2 - End color (hex, e.g. '1a1a2e') * @param {number} [angle=270] - Gradient angle in degrees (0=right, 90=down, 180=left, 270=up) - * @returns {string} Background XML for use with pres.addSlide() + * @returns {string} Background XML for use with customSlide() * * @example * // Vertical gradient (top to bottom) - * const bg = gradientBg('000000', '1a1a2e', 180); - * pres.addSlide(bg, shapes); + * const pres = createPresentation({ theme: 'brutalist' }); + * customSlide(pres, { shapes: [...], background: '000000' }); * * @example * // As default background for all slides @@ -2355,6 +2355,19 @@ declare module "ha:pptx" { * }); */ export declare function gradientBg(color1: string, color2: string, angle?: number): string; + export interface ValidationIssue { + code: string; + severity: "error" | "warn"; + message: string; + part?: string; + slideIndex?: number; + hint?: string; + } + export interface ValidationResult { + ok: boolean; + errors: ValidationIssue[]; + warnings: ValidationIssue[]; + } /** * Create a new presentation builder. * @@ -2371,11 +2384,12 @@ declare module "ha:pptx" { * titleSlide(pres, { title: 'My Title' }); * contentSlide(pres, { title: 'Content', bullets: ['Point 1', 'Point 2'] }); * - * // For CUSTOM layouts, use pres.addSlide() directly: - * const bg = solidFill(pres.theme.bg); - * const shapes = textBox({x: 1, y: 1, w: 8, h: 1, text: 'Custom text'}) + - * rect({x: 1, y: 3, w: 4, h: 2, fill: pres.theme.accent1}); - * pres.addSlide(bg, shapes, { transition: 'fade' }); + * // For CUSTOM layouts, use customSlide(): + * customSlide(pres, { + * shapes: [textBox({x: 1, y: 1, w: 8, h: 1, text: 'Custom text'}), + * rect({x: 1, y: 3, w: 4, h: 2, fill: pres.theme.accent1})], + * transition: 'fade' + * }); * * // Build final file * const zip = pres.buildZip(); @@ -2463,12 +2477,19 @@ declare module "ha:pptx" { * pres.addBody(textBox({x:1, y:1, w:8, h:1, text:'Hello'})); * * // With solid background: - * pres.addBody(shapes, { background: '0D1117', transition: 'fade' }); + * pres.addBody([shape1, shape2], { background: '0D1117', transition: 'fade' }); * * // With gradient background: - * pres.addBody(shapes, { background: {color1: '000000', color2: '1a1a2e', angle: 180} }); + * pres.addBody([shape1], { background: {color1: '000000', color2: '1a1a2e', angle: 180} }); + */ + addBody(shapesInput: ShapeFragment | ShapeFragment[] | string | string[], slideOpts?: SlideOptions): void; + /** + * Internal: add shapes (as pre-validated XML string) to a new slide. + * Resolves background from per-slide > defaultBackground > theme. + * Not on the Presentation interface — internal use only. + * @internal */ - addBody(shapesXml: string | string[], slideOpts?: SlideOptions): void; + _addBodyRaw(shapesStr: string, slideOpts?: SlideOptions): void; /** * Insert a slide at a specific index. Existing slides shift right. * @param {number} index - Position to insert (0-based). Clamped to valid range. @@ -2754,17 +2775,18 @@ declare module "ha:pptx" { * Add a blank slide with just the theme background (NO content). * * ⚠️ WARNING: This creates an EMPTY slide. You CANNOT add shapes to it later. - * For custom layouts with shapes, use pres.addSlide() directly instead: + * For custom layouts with shapes, use customSlide() instead: * * @example * // DON'T do this — blankSlide creates empty slide with no way to add content: * blankSlide(pres); // Creates empty slide, cannot add shapes after * - * // DO this instead — use addSlide for custom layouts: - * const bg = solidFill(pres.theme.bg); - * const shapes = textBox({x: 1, y: 1, w: 8, h: 1, text: 'Custom slide'}) + - * rect({x: 1, y: 3, w: 4, h: 2, fill: pres.theme.accent1}); - * pres.addSlide(bg, shapes, { transition: 'fade' }); + * // DO this instead — use customSlide for custom layouts: + * customSlide(pres, { + * shapes: [textBox({x: 1, y: 1, w: 8, h: 1, text: 'Custom slide'}), + * rect({x: 1, y: 3, w: 4, h: 2, fill: pres.theme.accent1})], + * transition: 'fade' + * }); * * @param {Object} pres - Presentation object from createPresentation(). REQUIRED as first param. * @returns {void} @@ -3270,9 +3292,9 @@ declare module "ha:pptx" { * @param {string} [opts.titleColor='8B949E'] - Title color * @param {boolean} [opts.lineNumbers=false] - Show line numbers * @param {number} [opts.cornerRadius=4] - Corner radius in points - * @returns {string} Shape XML fragment + * @returns {ShapeFragment} Branded shape fragment */ - export declare function codeBlock(opts: CodeBlockOptions): string; + export declare function codeBlock(opts: CodeBlockOptions): ShapeFragment; /** * Slide configuration for batch creation. * Each object describes one slide using a declarative config. @@ -3404,20 +3426,21 @@ declare module "ha:pptx" { * * @example * // Single image - * const imgXml = await fetchAndEmbed(pres, { + * const img = fetchAndEmbed(pres, { * url: "https://example.com/photo.jpg", - * x: 1, y: 1, w: 4, h: 3 + * x: 1, y: 1, w: 4, h: 3, + * fetchFn: fetchBinary * }); - * customSlide(pres, { shapes: imgXml + textBox({...}) }); + * customSlide(pres, { shapes: [img, textBox({...})] }); * * @example * // With fetch plugin * import { fetchBinary } from "host:fetch"; - * const imgXml = await fetchAndEmbed(pres, { + * const img = fetchAndEmbed(pres, { * url: "https://cdn.example.com/hero.jpg", * x: 0, y: 0, w: 13.333, h: 7.5, * fit: "cover", - * fetchFn: fetchBinary // Pass the fetch function + * fetchFn: fetchBinary * }); * * @param {Object} pres - Presentation object @@ -3430,7 +3453,7 @@ declare module "ha:pptx" { * @param {string} [opts.format] - Image format (auto-detected from URL if omitted) * @param {string} [opts.fit] - Fit mode: 'stretch', 'contain', 'cover' * @param {Function} opts.fetchFn - Fetch function (e.g., fetchBinary from host:fetch) - * @returns {string} Image XML fragment for use in shapes + * @returns {ShapeFragment} Branded image shape fragment */ export declare function fetchAndEmbed(pres: Pres, opts: { url: string; @@ -3441,7 +3464,7 @@ declare module "ha:pptx" { format?: string; fit?: "stretch" | "contain" | "cover"; fetchFn: (url: string) => Uint8Array; - }): string; + }): ShapeFragment; /** * Fetch multiple images and embed them all, returning XML fragments. * Uses fetchBinaryBatch for efficient parallel downloads when maxParallelFetches > 1. @@ -3456,13 +3479,13 @@ declare module "ha:pptx" { * ], * fetchBatchFn: fetchBinaryBatch * }); - * // images = [{ url, xml }, { url, xml }, { url, xml }] or [{ url, error }, ...] + * // images = [{ url, shape }, { url, shape }, { url, shape }] or [{ url, error }, ...] * * @param {Object} pres - Presentation object * @param {Object} opts - Options * @param {Array} opts.items - Array of {url, x, y, w, h, format?, fit?} * @param {Function} opts.fetchBatchFn - Batch fetch function (fetchBinaryBatch from host:fetch) - * @returns {Array} Array of {url, xml} or {url, error} for each item + * @returns {Array} Array of {url, shape: ShapeFragment} or {url, error} for each item */ export declare function fetchAndEmbedBatch(pres: Pres, opts: { items: Array<{ @@ -3481,7 +3504,7 @@ declare module "ha:pptx" { }>; }): Array<{ url: string; - xml?: string; + shape?: ShapeFragment; error?: string; }>; } diff --git a/tests/dts-sync.test.ts b/tests/dts-sync.test.ts index bed1c2e..0539031 100644 --- a/tests/dts-sync.test.ts +++ b/tests/dts-sync.test.ts @@ -7,7 +7,13 @@ import { describe, it, expect } from "vitest"; import { createHash } from "crypto"; -import { readdirSync, readFileSync, existsSync, rmSync } from "fs"; +import { + readdirSync, + readFileSync, + existsSync, + rmSync, + writeFileSync, +} from "fs"; import { join } from "path"; import { execSync } from "child_process"; @@ -230,6 +236,34 @@ describe("TypeScript source consistency", () => { rmSync(tmpDir, { recursive: true, force: true }); } }); + + it("ha-modules.d.ts matches regenerated output", () => { + // Regenerate ha-modules.d.ts and compare with the committed version. + // Catches drift where a module's exports/types changed but the generator wasn't re-run. + const haModulesPath = join(SRC_DIR, "types", "ha-modules.d.ts"); + if (!existsSync(haModulesPath)) return; + + const committed = readFileSync(haModulesPath, "utf-8"); + + // Regenerate in-place (the script always writes to the same path) + execSync("npx tsx scripts/generate-ha-modules-dts.ts", { + cwd: join(import.meta.dirname, ".."), + stdio: "pipe", + }); + + const regenerated = readFileSync(haModulesPath, "utf-8"); + + // Restore the committed version so the test doesn't have side effects + if (regenerated !== committed) { + writeFileSync(haModulesPath, committed); + } + + expect( + regenerated, + "ha-modules.d.ts is out of date with compiled .d.ts files. " + + "Run: npx tsx scripts/generate-ha-modules-dts.ts", + ).toBe(committed); + }); }); /**