From 0f0c94c12368bb82f4713e6c07d18949505ef20a Mon Sep 17 00:00:00 2001 From: Matthew Davis Date: Thu, 3 Jul 2025 14:00:30 -0500 Subject: [PATCH 01/18] initial commit --- vite-plugin-version.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/vite-plugin-version.ts b/vite-plugin-version.ts index 2a9a724..ce49ad4 100644 --- a/vite-plugin-version.ts +++ b/vite-plugin-version.ts @@ -31,6 +31,14 @@ export default function versionPlugin(): Plugin { return { name: "version-plugin", applyToEnvironment(env) { + console.log(execSync("git status").toString()); + console.log(execSync("git tag -l").toString()); + console.log(execSync("git fetch --all -v ").toString()); + console.log( + execSync( + "git log --graph -10 --branches --remotes --tags --format=format:'%Cgreen%h %Creset• %s (%cN, %cr) %Cred%d' --date-order" + ).toString() + ); env.config.env.VERSION = JSON.stringify({ date: { actual: new Date(execSync("git log -1 --format=%cd").toString()), From f840c14177ff517f690aea4972bf2ec40defe9bf Mon Sep 17 00:00:00 2001 From: Matthew Davis Date: Thu, 3 Jul 2025 14:21:24 -0500 Subject: [PATCH 02/18] initial commit --- vite-plugin-version.ts | 67 ++++++++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/vite-plugin-version.ts b/vite-plugin-version.ts index ce49ad4..1efa7d9 100644 --- a/vite-plugin-version.ts +++ b/vite-plugin-version.ts @@ -1,4 +1,4 @@ -import { execSync, spawnSync } from "child_process"; +import { spawnSync } from "child_process"; import type { Plugin } from "vite"; export const ago = (date: Date, locale = "en"): string => { @@ -27,32 +27,63 @@ export const ago = (date: Date, locale = "en"): string => { return rtf.format(0, "second"); }; -export default function versionPlugin(): Plugin { +export const run = ( + command: string +): { + output: string; + status: number | null; +} => { + const result = spawnSync(command, { + cwd: process.cwd(), + shell: true + }); + if (result.status !== 0) { + return { + output: result.stderr.toString().trim(), + status: result.status + }; + } + return { + output: result.stdout.toString().trim(), + status: result.status + }; +}; + +export const git = { + status: () => run("git status").output, + tag: () => run("git describe --tags --abbrev=0").output, + fetch: () => run("git fetch --all -v ").output, + log: () => + run( + "git log --graph -10 --branches --remotes --tags --format=format:'%Cgreen%h %Creset• %s (%cN, %cr) %Cred%d' --date-order" + ), + date: () => run("git log -1 --format=%cd").output, + commit: { + long: () => run("git rev-parse HEAD").output, + short: () => run("git rev-parse --short HEAD").output + }, + branch: () => run("git rev-parse --abbrev-ref HEAD").output, + dirty: () => run("git diff --quiet").status !== 0 +}; + +export default (): Plugin => { return { name: "version-plugin", applyToEnvironment(env) { - console.log(execSync("git status").toString()); - console.log(execSync("git tag -l").toString()); - console.log(execSync("git fetch --all -v ").toString()); - console.log( - execSync( - "git log --graph -10 --branches --remotes --tags --format=format:'%Cgreen%h %Creset• %s (%cN, %cr) %Cred%d' --date-order" - ).toString() - ); env.config.env.VERSION = JSON.stringify({ date: { - actual: new Date(execSync("git log -1 --format=%cd").toString()), - human: ago(new Date(execSync("git log -1 --format=%cd").toString())) + actual: new Date(git.date()), + human: ago(new Date(git.date())) }, - tag: execSync("git describe --tags --abbrev=0").toString().trim(), + tag: git.tag(), commit: { - long: execSync("git rev-parse HEAD").toString().trim(), - short: execSync("git rev-parse --short HEAD").toString().trim() + long: git.commit.long(), + short: git.commit.short() }, - dirty: spawnSync("git diff --quiet").status !== 0, - branch: execSync("git rev-parse --abbrev-ref HEAD").toString().trim() + dirty: git.dirty(), + branch: git.branch() }); return true; } }; -} +}; From ce4b7e67673d77d81c9ba5b16fa2f386eec78295 Mon Sep 17 00:00:00 2001 From: Matthew Davis Date: Thu, 3 Jul 2025 14:25:27 -0500 Subject: [PATCH 03/18] initial commit --- src/version.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/version.ts b/src/version.ts index c61cf16..2e34915 100644 --- a/src/version.ts +++ b/src/version.ts @@ -13,6 +13,21 @@ export type Version = { }; export const getVersion = (): Version => { - const version = JSON.parse(import.meta.env.VERSION); - return version; + if (!import.meta?.env?.VERSION) { + console.error("VERSION is not set"); + return { + tag: "unknown", + commit: { + long: "unknown", + short: "unknown" + }, + dirty: true, + branch: "unknown", + date: { + actual: new Date(1970, 1, 1), + human: "unknown" + } + }; + } + return JSON.parse(import.meta.env.VERSION) as Version; }; From 897972f088e7140bbeef778a2930b8ea8beac52c Mon Sep 17 00:00:00 2001 From: Matthew Davis Date: Thu, 3 Jul 2025 17:45:36 -0500 Subject: [PATCH 04/18] initial commit --- demo/{build.js => compile-components.js} | 0 demo/src/app.svelte | 44 ++++----- demo/src/components/entry.js | 35 -------- demo/src/components/entry.js.map | 1 - demo/src/components/index.js | 3 - demo/src/components/index.js.map | 1 - demo/src/global.d.ts | 7 ++ demo/src/lib/components/ui/button/index.js | 5 -- .../src/lib/components/ui/button/index.js.map | 1 - demo/src/lib/components/ui/sonner/index.js | 2 - .../src/lib/components/ui/sonner/index.js.map | 1 - demo/src/lib/utils.js | 6 -- demo/src/lib/utils.js.map | 1 - demo/src/main.js | 8 -- demo/src/main.js.map | 1 - demo/src/test-setup.js | 33 ------- demo/src/test-setup.js.map | 1 - demo/src/version/browser.ts | 50 +++++++++++ demo/src/version/git.ts | 15 ++++ demo/src/version/index.ts | 0 demo/src/version/run.ts | 23 +++++ demo/src/version/types.ts | 13 +++ demo/src/version/version.ts | 44 +++++++++ demo/src/vite-env.d.ts | 26 +++++- demo/vite.config.ts | 19 ++-- src/index.ts | 1 - src/version.ts | 33 ------- src/vite-env.d.ts | 32 +++++++ src/vite-plugin-version.ts | 52 +++++++++++ tsconfig.json | 2 +- vite-plugin-version.ts | 89 ------------------- 31 files changed, 283 insertions(+), 266 deletions(-) rename demo/{build.js => compile-components.js} (100%) delete mode 100644 demo/src/components/entry.js delete mode 100644 demo/src/components/entry.js.map delete mode 100644 demo/src/components/index.js delete mode 100644 demo/src/components/index.js.map create mode 100644 demo/src/global.d.ts delete mode 100644 demo/src/lib/components/ui/button/index.js delete mode 100644 demo/src/lib/components/ui/button/index.js.map delete mode 100644 demo/src/lib/components/ui/sonner/index.js delete mode 100644 demo/src/lib/components/ui/sonner/index.js.map delete mode 100644 demo/src/lib/utils.js delete mode 100644 demo/src/lib/utils.js.map delete mode 100644 demo/src/main.js delete mode 100644 demo/src/main.js.map delete mode 100644 demo/src/test-setup.js delete mode 100644 demo/src/test-setup.js.map create mode 100644 demo/src/version/browser.ts create mode 100644 demo/src/version/git.ts create mode 100644 demo/src/version/index.ts create mode 100644 demo/src/version/run.ts create mode 100644 demo/src/version/types.ts create mode 100644 demo/src/version/version.ts delete mode 100644 src/version.ts create mode 100644 src/vite-plugin-version.ts delete mode 100644 vite-plugin-version.ts diff --git a/demo/build.js b/demo/compile-components.js similarity index 100% rename from demo/build.js rename to demo/compile-components.js diff --git a/demo/src/app.svelte b/demo/src/app.svelte index 6ddf03e..b8915dc 100644 --- a/demo/src/app.svelte +++ b/demo/src/app.svelte @@ -4,11 +4,13 @@ import { Toaster } from "$lib/components/ui/sonner"; import Window from "$lib/components/window.svelte"; import { BookHeart, BugPlay, Code, Github, HardDriveDownload, RefreshCcwDot, TerminalIcon } from "@lucide/svelte"; - import { emojis, getVersion, load, Logger, LogLevel, render, type Rendered } from "@mateothegreat/dynamic-component-engine"; + import { emojis, load, Logger, LogLevel, render, type Rendered } from "@mateothegreat/dynamic-component-engine"; import { onDestroy, onMount } from "svelte"; import { toast } from "svelte-sonner"; import type { SimpleProps } from "./components"; import { UseClipboard } from "./extras/hooks/use-clipboard.svelte"; + import { getVersion } from "./version/browser"; + let renderRef: HTMLDivElement | undefined = $state(undefined); let sourceRef: HTMLPreElement | undefined = $state(undefined); let sourceText = $state(""); @@ -67,6 +69,7 @@ }; onMount(async () => create()); + onDestroy(() => { if (component) { logger.info("onDestroy", `${emojis.Trash} destroying dynamic component (${component.name})`); @@ -74,7 +77,6 @@ } }); - console.log(getVersion()); const clipboard = new UseClipboard(); @@ -87,28 +89,6 @@ {#snippet pkg()} {@const version = getVersion()} - `, 1 @@ -59,7 +59,7 @@ function Simple($$anchor, $$props) { } $.delegate(["click"]); -// src/components/entry.ts +// shared-components/simple/entry.ts var factory = (target, props) => { const component = mount(Simple, { target, diff --git a/demo/src/components/entry.ts b/demo/shared-components/simple/entry.ts similarity index 100% rename from demo/src/components/entry.ts rename to demo/shared-components/simple/entry.ts diff --git a/demo/shared-components/simple/simple.svelte b/demo/shared-components/simple/simple.svelte new file mode 100644 index 0000000..84f640a --- /dev/null +++ b/demo/shared-components/simple/simple.svelte @@ -0,0 +1,22 @@ + + +
+
+

Simple.svelte

+

This is a simple component that is rendered on the receiving side.
See the source code for more details.

+
+
+ `name` from $props(): + "{name}" +
+
+ testState: + {testState} +
+ +
diff --git a/demo/src/app.svelte b/demo/src/app.svelte index b8915dc..8afb7b2 100644 --- a/demo/src/app.svelte +++ b/demo/src/app.svelte @@ -1,14 +1,14 @@ {#snippet loading()} @@ -90,10 +86,13 @@ {#snippet pkg()} {@const version = getVersion()}
{#if isLoading} diff --git a/demo/src/components/index.ts b/demo/src/components/index.ts deleted file mode 100644 index d7f5885..0000000 --- a/demo/src/components/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./entry"; -export { default as Simple } from "./simple.svelte"; diff --git a/demo/src/components/simple.svelte b/demo/src/components/simple.svelte deleted file mode 100644 index a572aa2..0000000 --- a/demo/src/components/simple.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - -
- -
- `name` from $props(): - "{name}" -
-
- testState: - {testState} -
- -
diff --git a/demo/src/extras/hooks/README.md b/demo/src/extras/hooks/README.md new file mode 100644 index 0000000..bf7206f --- /dev/null +++ b/demo/src/extras/hooks/README.md @@ -0,0 +1,234 @@ +# Copy to Clipboard Attachment + +A modern, flexible copy to clipboard implementation for Svelte 5 using the new `{@attach}` directive. This replaces the previous class-based approach with a more declarative and feature-rich solution. + +## Features + +- ✨ **Declarative**: Use `{@attach}` directly on any element +- 🎯 **Flexible**: Copy element content, custom text, or dynamic/reactive text +- 🎨 **Visual feedback**: Customizable CSS classes for different states +- ♿ **Accessible**: Built-in keyboard support and ARIA attributes +- 🔄 **Reactive**: Automatically updates when state changes +- 🍞 **Toast integration**: Built-in toast notifications with svelte-sonner +- 🎛️ **Configurable**: Extensive customization options +- 🚀 **TypeScript**: Full type safety and IntelliSense support + +## Installation + +```bash +npm install svelte-sonner # For toast notifications +``` + +## Basic Usage + +### Simple Copy (copies element content) + +```svelte + + +
+ Click me to copy this text! +
+``` + +### Copy with Custom Text + +```svelte + +``` + +### Dynamic/Reactive Copy + +```svelte + + + +``` + +## Advanced Usage + +### Pre-styled Copy Button + +```svelte + + + +``` + +### Code Block Copy + +```svelte + + +
+  console.log('Hello, World!');
+
+``` + +### Custom Styling and Behavior + +```svelte + +``` + +## API Reference + +### `copyToClipboard(options?)` + +Creates a copy to clipboard attachment. + +#### Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `text` | `string` | `undefined` | Static text to copy | +| `getText` | `() => string` | `undefined` | Function to get dynamic text | +| `successMessage` | `string` | `'Copied to clipboard! 📋'` | Success toast message | +| `errorMessage` | `string` | `'Failed to copy to clipboard'` | Error toast message | +| `resetDelay` | `number` | `1000` | Delay before resetting state (ms) | +| `showToast` | `boolean` | `true` | Whether to show toast notifications | +| `classes` | `object` | `{}` | Custom CSS classes for states | +| `preventDefault` | `boolean` | `true` | Prevent default click behavior | +| `onSuccess` | `(text: string) => void` | `undefined` | Success callback | +| `onError` | `(error: Error) => void` | `undefined` | Error callback | + +#### Classes Object + +```typescript +{ + idle?: string; // Default state + copying?: string; // While copying + success?: string; // After successful copy + error?: string; // After copy failure +} +``` + +### `copyButton(options?)` + +Pre-configured copy button with smooth animations. + +```svelte + +``` + +### `copyCodeBlock(options?)` + +Specialized attachment for code blocks that automatically finds and copies code content. + +```svelte +
+  Your code here
+
+``` + +## Migration from UseClipboard Class + +### Before (Class-based) + +```svelte + + + +``` + +### After (Attachment-based) + +```svelte + + + +``` + +## Benefits of the New Approach + +1. **More Declarative**: Attach directly to elements instead of managing class instances +2. **Better Performance**: No need to track state in multiple places +3. **Easier to Use**: Less boilerplate code +4. **More Flexible**: Works with any element, not just buttons +5. **Better Accessibility**: Automatically adds ARIA attributes and keyboard support +6. **Visual Feedback**: Built-in state management with customizable styling +7. **Reactive**: Automatically updates when dependencies change + +## Browser Support + +- Modern browsers with Clipboard API support +- Graceful fallback with error handling for unsupported browsers +- Requires HTTPS in production (Clipboard API requirement) + +## Examples + +See `demo/src/components/copy-demo.svelte` for comprehensive usage examples. + +## TypeScript Support + +Full TypeScript support with proper type definitions: + +```typescript +import type { Attachment } from 'svelte/attachments'; +import { copyToClipboard, type CopyToClipboardOptions } from '$lib/hooks/copy-to-clipboard.svelte'; + +const myAttachment: Attachment = copyToClipboard({ + text: 'Typed text', + onSuccess: (text: string) => console.log('Copied:', text) +}); +``` \ No newline at end of file diff --git a/demo/src/extras/hooks/clipboard.test.ts b/demo/src/extras/hooks/clipboard.test.ts new file mode 100644 index 0000000..21068d6 --- /dev/null +++ b/demo/src/extras/hooks/clipboard.test.ts @@ -0,0 +1,411 @@ +// src/lib/actions/clipboard.test.ts + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { clipboard, createClipboardContext } from "./clipboard"; + +// Mock navigator.clipboard +const mockWriteText = vi.fn(); +Object.defineProperty(navigator, "clipboard", { + value: { + writeText: mockWriteText + }, + writable: true +}); + +describe("clipboard attachment action", () => { + let element: HTMLButtonElement; + + beforeEach(() => { + element = document.createElement("button"); + document.body.appendChild(element); + vi.useFakeTimers(); + mockWriteText.mockClear(); + }); + + afterEach(() => { + document.body.removeChild(element); + vi.useRealTimers(); + }); + + it("should copy text to clipboard on click", async () => { + const text = "Hello, World!"; + const onSuccess = vi.fn(); + + mockWriteText.mockResolvedValueOnce(undefined); + + const action = clipboard(element, { + text, + onSuccess + }); + + // Simulate click + element.click(); + + await vi.waitFor(() => { + expect(mockWriteText).toHaveBeenCalledWith(text); + expect(onSuccess).toHaveBeenCalledWith(text); + }); + + action.destroy(); + }); + + it("should handle copy failure", async () => { + const text = "Failed text"; + const onFailure = vi.fn(); + const error = new Error("Clipboard access denied"); + + mockWriteText.mockRejectedValueOnce(error); + + const action = clipboard(element, { + text, + onFailure + }); + + element.click(); + + await vi.waitFor(() => { + expect(mockWriteText).toHaveBeenCalledWith(text); + expect(onFailure).toHaveBeenCalledWith(error); + }); + + action.destroy(); + }); + + it("should update status through onStatusChange callback", async () => { + const text = "Status test"; + const onStatusChange = vi.fn(); + + mockWriteText.mockResolvedValueOnce(undefined); + + const action = clipboard(element, { + text, + onStatusChange + }); + + element.click(); + + await vi.waitFor(() => { + expect(onStatusChange).toHaveBeenCalledWith("success"); + }); + + // Fast forward to check status reset + vi.advanceTimersByTime(2000); + + expect(onStatusChange).toHaveBeenCalledWith("idle"); + + action.destroy(); + }); + + it("should use custom delay for status reset", async () => { + const text = "Custom delay"; + const onStatusChange = vi.fn(); + const customDelay = 500; + + mockWriteText.mockResolvedValueOnce(undefined); + + const action = clipboard(element, { + text, + delay: customDelay, + onStatusChange + }); + + element.click(); + + await vi.waitFor(() => { + expect(onStatusChange).toHaveBeenCalledWith("success"); + }); + + // Advance less than custom delay - status should not reset + vi.advanceTimersByTime(400); + expect(onStatusChange).toHaveBeenCalledTimes(1); + + // Advance past custom delay - status should reset + vi.advanceTimersByTime(100); + expect(onStatusChange).toHaveBeenCalledWith("idle"); + + action.destroy(); + }); + + it("should clear previous timeout on rapid clicks", async () => { + const text = "Rapid clicks"; + const onStatusChange = vi.fn(); + + mockWriteText.mockResolvedValue(undefined); + + const action = clipboard(element, { + text, + delay: 1000, + onStatusChange + }); + + // First click + element.click(); + await vi.waitFor(() => { + expect(onStatusChange).toHaveBeenCalledWith("success"); + }); + + // Advance 500ms + vi.advanceTimersByTime(500); + + // Second click before timeout + onStatusChange.mockClear(); + element.click(); + + await vi.waitFor(() => { + expect(onStatusChange).toHaveBeenCalledWith("success"); + }); + + // Advance 1500ms total - should only have one idle call + vi.advanceTimersByTime(1000); + expect(onStatusChange).toHaveBeenCalledWith("idle"); + expect(onStatusChange).toHaveBeenCalledTimes(2); // success + idle + + action.destroy(); + }); + + it("should add appropriate ARIA attributes", () => { + const action = clipboard(element, { + text: "ARIA test" + }); + + expect(element.getAttribute("role")).toBe("button"); + expect(element.getAttribute("aria-label")).toBe("Copy to clipboard"); + expect(element.style.cursor).toBe("pointer"); + + action.destroy(); + + expect(element.getAttribute("role")).toBeNull(); + expect(element.getAttribute("aria-label")).toBeNull(); + }); + + it("should prevent default for buttons inside forms", async () => { + const form = document.createElement("form"); + const button = document.createElement("button"); + form.appendChild(button); + document.body.appendChild(form); + + mockWriteText.mockResolvedValueOnce(undefined); + + const action = clipboard(button, { + text: "Form button test" + }); + + const event = new MouseEvent("click", { bubbles: true, cancelable: true }); + const preventDefault = vi.spyOn(event, "preventDefault"); + + button.dispatchEvent(event); + + expect(preventDefault).toHaveBeenCalled(); + + action.destroy(); + document.body.removeChild(form); + }); + + it("should handle update method", () => { + const initialText = "Initial"; + const updatedText = "Updated"; + const onStatusChange = vi.fn(); + + mockWriteText.mockResolvedValue(undefined); + + const action = clipboard(element, { + text: initialText, + onStatusChange + }); + + // Update the action + action.update({ + text: updatedText, + onStatusChange + }); + + element.click(); + + // Should use updated text + expect(mockWriteText).toHaveBeenCalledWith(updatedText); + + action.destroy(); + }); + + it("should convert non-Error objects to Error instances", async () => { + const text = "Error conversion test"; + const onFailure = vi.fn(); + + // Reject with a string instead of Error + mockWriteText.mockRejectedValueOnce("Not an error object"); + + const action = clipboard(element, { + text, + onFailure + }); + + element.click(); + + await vi.waitFor(() => { + expect(onFailure).toHaveBeenCalled(); + const error = onFailure.mock.calls[0][0]; + expect(error).toBeInstanceOf(Error); + expect(error.message).toBe("Failed to copy to clipboard"); + }); + + action.destroy(); + }); +}); + +describe("createClipboardContext", () => { + let element: HTMLButtonElement; + + beforeEach(() => { + element = document.createElement("button"); + document.body.appendChild(element); + vi.useFakeTimers(); + mockWriteText.mockClear(); + }); + + afterEach(() => { + document.body.removeChild(element); + vi.useRealTimers(); + }); + + it("should create a clipboard context with shared state", async () => { + const context = createClipboardContext(); + + expect(context.status).toBe("idle"); + expect(context.lastCopiedText).toBeNull(); + + mockWriteText.mockResolvedValueOnce(undefined); + + const action = context.action(element, { + text: "Context test" + }); + + element.click(); + + await vi.waitFor(() => { + expect(context.status).toBe("success"); + expect(context.lastCopiedText).toBe("Context test"); + }); + + action.destroy(); + }); + + it("should use default delay from context", async () => { + const customDelay = 300; + const context = createClipboardContext(customDelay); + + mockWriteText.mockResolvedValueOnce(undefined); + + const action = context.action(element, { + text: "Delay test" + }); + + element.click(); + + await vi.waitFor(() => { + expect(context.status).toBe("success"); + }); + + // Should reset after custom delay + vi.advanceTimersByTime(customDelay); + expect(context.status).toBe("idle"); + + action.destroy(); + }); + + it("should override context delay with action-specific delay", async () => { + const contextDelay = 1000; + const actionDelay = 200; + const context = createClipboardContext(contextDelay); + + mockWriteText.mockResolvedValueOnce(undefined); + + const action = context.action(element, { + text: "Override delay", + delay: actionDelay + }); + + element.click(); + + await vi.waitFor(() => { + expect(context.status).toBe("success"); + }); + + // Should use action-specific delay + vi.advanceTimersByTime(actionDelay); + expect(context.status).toBe("idle"); + + action.destroy(); + }); + + it("should propagate callbacks from context action", async () => { + const context = createClipboardContext(); + const onSuccess = vi.fn(); + const onFailure = vi.fn(); + + // Test success callback + mockWriteText.mockResolvedValueOnce(undefined); + + const successAction = context.action(element, { + text: "Success callback", + onSuccess + }); + + element.click(); + + await vi.waitFor(() => { + expect(onSuccess).toHaveBeenCalledWith("Success callback"); + }); + + successAction.destroy(); + + // Test failure callback + const error = new Error("Test error"); + mockWriteText.mockRejectedValueOnce(error); + + const failureAction = context.action(element, { + text: "Failure callback", + onFailure + }); + + element.click(); + + await vi.waitFor(() => { + expect(onFailure).toHaveBeenCalledWith(error); + }); + + failureAction.destroy(); + }); + + it("should handle multiple actions with shared state", async () => { + const context = createClipboardContext(); + const button1 = document.createElement("button"); + const button2 = document.createElement("button"); + document.body.appendChild(button1); + document.body.appendChild(button2); + + mockWriteText.mockResolvedValue(undefined); + + const action1 = context.action(button1, { text: "Button 1" }); + const action2 = context.action(button2, { text: "Button 2" }); + + // Click first button + button1.click(); + + await vi.waitFor(() => { + expect(context.status).toBe("success"); + expect(context.lastCopiedText).toBe("Button 1"); + }); + + // Click second button + button2.click(); + + await vi.waitFor(() => { + expect(context.lastCopiedText).toBe("Button 2"); + }); + + action1.destroy(); + action2.destroy(); + document.body.removeChild(button1); + document.body.removeChild(button2); + }); +}); diff --git a/demo/src/extras/hooks/clipboard.ts b/demo/src/extras/hooks/clipboard.ts new file mode 100644 index 0000000..106eb21 --- /dev/null +++ b/demo/src/extras/hooks/clipboard.ts @@ -0,0 +1,169 @@ +// src/lib/actions/clipboard.ts + +import type { Attachment } from "svelte/attachments"; + +export type ClipboardStatus = "idle" | "success" | "failure"; + +export interface ClipboardOptions { + /** The text to copy to clipboard */ + text: string; + /** The time in milliseconds before the status resets to idle (default: 2000) */ + delay?: number; + /** Callback when copy succeeds */ + onSuccess?: (text: string) => void; + /** Callback when copy fails */ + onFailure?: (error: Error) => void; + /** Callback when status changes */ + onStatusChange?: (status: ClipboardStatus) => void; +} + +interface ClipboardState { + status: ClipboardStatus; + timeout?: ReturnType; +} + +/** + * Svelte 5 attachment action for copying text to clipboard + * + * @example + * ```svelte + * + * + * + * ``` + */ +export function clipboard(options: ClipboardOptions): Attachment { + return (node: HTMLElement) => { + const state: ClipboardState = { + status: "idle", + timeout: undefined + }; + + const updateStatus = (newStatus: ClipboardStatus) => { + state.status = newStatus; + options.onStatusChange?.(newStatus); + }; + + const resetStatus = () => { + if (state.timeout) { + clearTimeout(state.timeout); + state.timeout = undefined; + } + + const delay = options.delay ?? 2000; + + state.timeout = setTimeout(() => { + updateStatus("idle"); + state.timeout = undefined; + }, delay); + }; + + const handleClick = async (event: MouseEvent) => { + // Prevent default if it's a button inside a form + if (node.tagName === "BUTTON" && node.closest("form")) { + event.preventDefault(); + } + + // Clear any existing timeout + if (state.timeout) { + clearTimeout(state.timeout); + state.timeout = undefined; + } + + try { + await navigator.clipboard.writeText(options.text); + updateStatus("success"); + options.onSuccess?.(options.text); + resetStatus(); + } catch (error) { + const err = error instanceof Error ? error : new Error("Failed to copy to clipboard"); + updateStatus("failure"); + options.onFailure?.(err); + resetStatus(); + } + }; + + // Add click listener + node.addEventListener("click", handleClick); + + // Add appropriate ARIA attributes + node.setAttribute("role", "button"); + node.setAttribute("aria-label", "Copy to clipboard"); + + // Add cursor style if not already set + if (!node.style.cursor) { + node.style.cursor = "pointer"; + } + + return () => { + if (state.timeout) { + clearTimeout(state.timeout); + } + node.removeEventListener("click", handleClick); + node.removeAttribute("role"); + node.removeAttribute("aria-label"); + }; + }; +} + +/** + * Creates a clipboard context with shared state + * Useful for managing multiple clipboard actions with shared status + * + * @example + * ```svelte + * + * + * + * ``` + */ +export function createClipboardContext(defaultDelay = 2000) { + let status = $state("idle"); + let lastCopiedText = $state(null); + + const action = (node: HTMLElement, options: Omit) => { + return clipboard(node, { + ...options, + delay: options.delay ?? defaultDelay, + onStatusChange: (s) => { + status = s; + if (s === "success") { + lastCopiedText = options.text; + } + }, + onSuccess: (text) => { + options.onSuccess?.(text); + }, + onFailure: (error) => { + options.onFailure?.(error); + } + }); + }; + + return { + get status() { + return status; + }, + get lastCopiedText() { + return lastCopiedText; + }, + action + }; +} diff --git a/demo/src/extras/hooks/copy-to-clipboard.svelte.ts b/demo/src/extras/hooks/copy-to-clipboard.svelte.ts new file mode 100644 index 0000000..848bf74 --- /dev/null +++ b/demo/src/extras/hooks/copy-to-clipboard.svelte.ts @@ -0,0 +1,315 @@ +import { toast } from "svelte-sonner"; +import type { Attachment } from "svelte/attachments"; + +/** + * Configuration options for the copy to clipboard attachment. + */ +export interface CopyToClipboardOptions { + /** The text to copy to clipboard. If not provided, will use element's textContent */ + text?: string; + /** Function to get dynamic text content */ + getText?: () => string; + /** Success message to show in toast */ + successMessage?: string; + /** Error message to show in toast */ + errorMessage?: string; + /** Delay before resetting visual state (in milliseconds) */ + resetDelay?: number; + /** Whether to show toast notifications */ + showToast?: boolean; + /** Custom CSS classes to apply during different states */ + classes?: { + idle?: string; + copying?: string; + success?: string; + error?: string; + }; + /** Whether to prevent default click behavior */ + preventDefault?: boolean; + /** Custom success callback */ + onSuccess?: (text: string) => void; + /** Custom error callback */ + onError?: (error: Error) => void; +} + +/** + * Creates a copy to clipboard attachment that can be used with {@attach} directive. + * + * ## Usage + * ```svelte + * + * + * + * + * + * + * + * + * + * + * + * + * + * ``` + */ +export function copyToClipboard(options: CopyToClipboardOptions = {}): Attachment { + const { text, getText, successMessage = "Copied to clipboard! 📋", errorMessage = "Failed to copy to clipboard", resetDelay = 1000, showToast = true, classes = {}, preventDefault = true, onSuccess, onError } = options; + + return (element: HTMLElement) => { + let currentState: "idle" | "copying" | "success" | "error" = "idle"; + let resetTimeout: ReturnType | undefined; + let originalClasses: string = ""; + let originalCursor: string = ""; + + // Store original styles + const storeOriginalStyles = () => { + originalClasses = element.className; + originalCursor = element.style.cursor; + }; + + // Apply state classes + const applyStateClasses = (state: typeof currentState) => { + // Reset to original classes + element.className = originalClasses; + + // Add state-specific classes + if (classes[state]) { + element.className += ` ${classes[state]}`; + } + + // Update cursor for copying state + if (state === "copying") { + element.style.cursor = "wait"; + } else { + element.style.cursor = originalCursor; + } + }; + + // Set up initial state + const initialize = () => { + storeOriginalStyles(); + + // Make element appear interactive if it's not already + if (!element.style.cursor && element.tagName !== "BUTTON") { + element.style.cursor = "pointer"; + } + + // Add role for accessibility + if (!element.getAttribute("role")) { + element.setAttribute("role", "button"); + } + + // Add aria-label for accessibility + if (!element.getAttribute("aria-label")) { + element.setAttribute("aria-label", "Copy to clipboard"); + } + }; + + // Get text to copy + const getTextToCopy = (): string => { + if (getText) { + return getText(); + } + if (text) { + return text; + } + return element.textContent?.trim() || ""; + }; + + // Handle copy operation + const handleCopy = async (event: Event) => { + if (preventDefault) { + event.preventDefault(); + } + + // Prevent multiple simultaneous copies + if (currentState === "copying") { + return; + } + + const textToCopy = getTextToCopy(); + + if (!textToCopy) { + console.warn("copyToClipboard: No text to copy"); + return; + } + + // Clear any existing timeout + if (resetTimeout) { + clearTimeout(resetTimeout); + } + + // Set copying state + currentState = "copying"; + applyStateClasses("copying"); + + try { + // Check if clipboard API is available + if (!navigator.clipboard) { + throw new Error("Clipboard API not available"); + } + + await navigator.clipboard.writeText(textToCopy); + + // Success state + currentState = "success"; + applyStateClasses("success"); + + if (showToast) { + toast.success(successMessage); + } + + if (onSuccess) { + onSuccess(textToCopy); + } + } catch (error) { + // Error state + currentState = "error"; + applyStateClasses("error"); + + const errorObj = error instanceof Error ? error : new Error("Unknown error"); + + if (showToast) { + toast.error(errorMessage); + } + + if (onError) { + onError(errorObj); + } else { + console.error("copyToClipboard error:", errorObj); + } + } finally { + // Reset to idle state after delay + resetTimeout = setTimeout(() => { + currentState = "idle"; + applyStateClasses("idle"); + }, resetDelay); + } + }; + + // Set up event listeners + const setupEventListeners = () => { + element.addEventListener("click", handleCopy); + + // Add keyboard support for accessibility + element.addEventListener("keydown", (event) => { + if (event.key === "Enter" || event.key === " ") { + handleCopy(event); + } + }); + + // Add visual feedback for hover/focus states + element.addEventListener("mouseenter", () => { + if (currentState === "idle") { + element.style.opacity = "0.8"; + } + }); + + element.addEventListener("mouseleave", () => { + if (currentState === "idle") { + element.style.opacity = ""; + } + }); + }; + + // Initialize + initialize(); + setupEventListeners(); + + // Cleanup function + return () => { + if (resetTimeout) { + clearTimeout(resetTimeout); + } + + element.removeEventListener("click", handleCopy); + element.style.cursor = originalCursor; + element.className = originalClasses; + }; + }; +} + +/** + * Creates a copy button attachment with predefined styling and behavior. + * Perfect for code blocks and similar use cases. + * + * ## Usage + * ```svelte + * + * + *
+ *
+ *     {code}
+ *   
+ * + *
+ * ``` + */ +export function copyButton(options: CopyToClipboardOptions = {}): Attachment { + return copyToClipboard({ + classes: { + idle: "transition-all duration-200", + copying: "opacity-50 cursor-wait scale-95", + success: "bg-green-500 text-white scale-105", + error: "bg-red-500 text-white" + }, + ...options + }); +} + +/** + * Creates a copy attachment specifically designed for code blocks. + * Automatically finds and copies the code content. + * + * ## Usage + * ```svelte + * + * + *
+ *   console.log('Hello, World!');
+ * 
+ * ``` + */ +export function copyCodeBlock(options: Omit = {}): Attachment { + return copyToClipboard({ + getText: () => { + const codeElement = document.activeElement?.querySelector("code") || document.activeElement?.closest("pre")?.querySelector("code"); + return codeElement?.textContent?.trim() || ""; + }, + successMessage: "Code copied to clipboard! 👨‍💻", + classes: { + idle: "cursor-pointer transition-all duration-200", + copying: "opacity-50", + success: "bg-green-100 border border-green-300", + error: "bg-red-100 border border-red-300" + }, + ...options + }); +} diff --git a/demo/src/version/browser.ts b/demo/src/version/browser.ts index 90b8317..e863dc6 100644 --- a/demo/src/version/browser.ts +++ b/demo/src/version/browser.ts @@ -1,7 +1,6 @@ import type { VersionConfig } from "../../../src/vite-plugin-version"; import type { Version } from "./types"; -// Declare the global constant that will be replaced at build time declare const __VERSION__: Version | undefined; export const getVersion = (config?: VersionConfig): Version => { @@ -9,7 +8,6 @@ export const getVersion = (config?: VersionConfig): Version => { console.log("getVersion()", "checking available version sources"); } - // Try build-time constant first (production builds) if (typeof __VERSION__ !== "undefined") { if (config?.debug) { console.log("getVersion()", "Found __VERSION__ from build-time replacement:", __VERSION__); @@ -17,7 +15,6 @@ export const getVersion = (config?: VersionConfig): Version => { return __VERSION__; } - // Try import.meta.env (available in dev and production) if (import.meta.env.VITE_VERSION) { try { const parsed = JSON.parse(import.meta.env.VITE_VERSION as string); @@ -35,6 +32,7 @@ export const getVersion = (config?: VersionConfig): Version => { console.error("getVersion()", "no version information available"); } return { + location: null, tag: "unknown", commit: { long: "unknown", diff --git a/demo/src/version/run.ts b/demo/src/version/exec.ts similarity index 95% rename from demo/src/version/run.ts rename to demo/src/version/exec.ts index 64aac0d..65ac7c9 100644 --- a/demo/src/version/run.ts +++ b/demo/src/version/exec.ts @@ -1,6 +1,6 @@ import { spawnSync } from "node:child_process"; -export const run = ( +export const exec = ( command: string ): { output: string; diff --git a/demo/src/version/git.ts b/demo/src/version/git.ts index d29c4fa..24b5515 100644 --- a/demo/src/version/git.ts +++ b/demo/src/version/git.ts @@ -1,15 +1,58 @@ -import { run } from "./run"; +import { exec } from "./exec"; +import type { Version } from "./types"; + +export type GitLookup = "tag" | "commit" | "branch" | "dirty" | "date"; export const git = { - status: () => run("git status")?.output, - tag: () => run("git describe --tags --abbrev=0")?.output, - fetch: () => run("git fetch --all -v ")?.output, - log: () => run("git log --graph -10 --branches --remotes --tags --format=format:'%Cgreen%h %Creset• %s (%cN, %cr) %Cred%d' --date-order")?.output, - date: () => run("git log -1 --format=%cd")?.output, + status: () => exec("git status")?.output, + tag: () => exec("git describe --tags --abbrev=0")?.output, + fetch: () => exec("git fetch --all -v ")?.output, + log: () => exec("git log --graph -10 --branches --remotes --tags --format=format:'%Cgreen%h %Creset• %s (%cN, %cr) %Cred%d' --date-order")?.output, + date: () => exec("git log -1 --format=%cd")?.output, commit: { - long: () => run("git rev-parse HEAD")?.output, - short: () => run("git rev-parse --short HEAD")?.output + long: () => exec("git rev-parse HEAD")?.output, + short: () => exec("git rev-parse --short HEAD")?.output }, - branch: () => run("git rev-parse --abbrev-ref HEAD")?.output, - dirty: () => run("git diff --quiet")?.status !== 0 + branch: () => exec("git rev-parse --abbrev-ref HEAD")?.output, + dirty: () => exec("git diff --quiet")?.status !== 0, + lookup: (lookups: GitLookup[]): Version => { + for (const lookup of lookups) { + switch (lookup) { + case "tag": + return { + location: "git-tag", + tag: git.tag() + }; + case "commit": + return { + location: "git-commit", + commit: { + long: git.commit.long() ?? "unknown", + short: git.commit.short() ?? "unknown" + } + }; + case "branch": + return { + location: "git-branch", + branch: git.branch() ?? "unknown" + }; + case "dirty": + return { + location: "git-dirty", + dirty: git.dirty() + }; + case "date": + return { + location: "git-date", + date: { + actual: new Date(git.date() ?? "unknown"), + human: git.date() ?? "unknown" + } + }; + + default: + throw new Error(`Unknown git lookup: ${lookup}`); + } + } + } }; diff --git a/demo/src/version/types.ts b/demo/src/version/types.ts index 1eef949..9685375 100644 --- a/demo/src/version/types.ts +++ b/demo/src/version/types.ts @@ -1,12 +1,15 @@ +import type { VersionLocation } from "../../../src/vite-plugin-version"; + export type Version = { - tag: string; - commit: { + location: VersionLocation; + tag?: string; + commit?: { long: string; short: string; }; - dirty: boolean; - branch: string; - date: { + dirty?: boolean; + branch?: string; + date?: { actual: Date; human: string; }; diff --git a/demo/src/version/version.ts b/demo/src/version/version.ts index f45e73d..a3d0a56 100644 --- a/demo/src/version/version.ts +++ b/demo/src/version/version.ts @@ -1,3 +1,5 @@ +import fs from "node:fs"; +import type { VersionConfig } from "../../../src/vite-plugin-version"; import { git } from "./git"; import type { Version } from "./types"; @@ -27,18 +29,19 @@ export const ago = (date: Date, locale = "en"): string => { return rtf.format(0, "second"); }; -export const setVersion = (): Version => { - return { - tag: git.tag(), - commit: { - long: git.commit.long(), - short: git.commit.short() - }, - dirty: git.dirty(), - branch: git.branch(), - date: { - actual: new Date(git.date()), - human: ago(new Date(git.date())) +export const setVersion = (config?: VersionConfig): Version => { + for (const location of config?.locations ?? ["git-tag", "package.json"]) { + if (location === "git-tag") { + return { + location: "git-tag", + tag: git.tag() + }; + } else if (location === "package.json") { + const pkg = JSON.parse(fs.readFileSync("package.json", "utf8")); + return { + location: "package.json", + tag: pkg.version + }; } - }; + } }; diff --git a/demo/tsconfig.json b/demo/tsconfig.json index 2652022..bedac5a 100644 --- a/demo/tsconfig.json +++ b/demo/tsconfig.json @@ -24,6 +24,6 @@ "$components/*": ["./src/lib/components/ui/*"] } }, - "include": ["./src/**/*.ts", "./src/**/*.js", "./src/**/*.svelte"], + "include": ["./src/**/*.ts", "./src/**/*.js", "./src/**/*.svelte", "shared-components/simple.svelte", "shared-components/simple/entry.ts", "shared-components/entry.ts"], "exclude": ["src/lib/components/ui"] } diff --git a/demo/vite.config.ts b/demo/vite.config.ts index 23dcc1a..ae253f3 100644 --- a/demo/vite.config.ts +++ b/demo/vite.config.ts @@ -9,7 +9,9 @@ import { versionPlugin } from "../src/vite-plugin-version"; export default defineConfig({ logLevel: "info", plugins: [ - versionPlugin({}), + versionPlugin({ + locations: ["package.json"] + }), tsconfigPaths(), svelte(), svelteInspector({ diff --git a/docs/readme.md b/docs/readme.md index afe6044..9f858eb 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -86,7 +86,30 @@ Here's an example of how to use the dynamic component engine to render a compone You must first compile your Svelte component(s) down to a string and serve it to the client (http endpoint, websockets, etc.) then you can use the `load` and `render` functions to dynamically render the component(s) in the browser. -### `esbuild-svelte` +### Using the [`compile-components`](../demo/bin/compile-components) script + +I've provided an easy button to compile your components down to a single file that can be served to the client. + +Simply run `npm run compile` and it will compile all the components in the `shared-components` directory down to a single files that can be served to the client. +Your output will look like this: + +```sh +➜ npm run compile + +> demo@0.0.1 compile +> node bin/compile-components + +Discovering components via ./shared-components/**/entry.ts ++ Discovered component entrypoint: shared-components/simple/entry.ts + +Compiling (1) component... + + public/entry.js 2.7kb + +⚡ Done in 191ms +``` + +### Manually using `esbuild-svelte` ```typescript import esbuild from "esbuild"; @@ -116,28 +139,12 @@ async function bundleSvelte(entry) { return build.outputFiles; } -bundleSvelte(["./src/components/entry.ts"]); -``` - -Now you're ready to compile your svelte component(s) down to an esm module: - -```bash -node build.js -``` - -Should show something like this in your terminal: - -```shell -$ node build.js - - public/entry.js 1.2kb - -⚡ Done in 156ms +bundleSvelte(["./shared-components/simple/entry.ts"]); ``` ### Output -After running `node build.js` the output will be a single file in the `public` directory and will look like this: +After running `npm run compile` the output will be a single file in the `public` directory and will look like this: ```js const compiledComponentSource = ` diff --git a/src/vite-plugin-version.ts b/src/vite-plugin-version.ts index 8248b03..e4a1ef0 100644 --- a/src/vite-plugin-version.ts +++ b/src/vite-plugin-version.ts @@ -1,8 +1,24 @@ -import type { PluginOption } from "vite"; import { setVersion } from "../demo/src/version/version"; +export type VersionLocation = + | "git-tag" + | "git-commit" + | "git-branch" + | "git-dirty" + | "git-date" + | "package.json"; + export interface VersionConfig { debug?: boolean; + /** + * The locations to check for version information. + * + * The order is important, the first location that is found will be used + * otherwise the next (fallback) location will be used. + * + * @default ["git-tag", "package.json"] + */ + locations?: VersionLocation[]; } /** @@ -10,7 +26,7 @@ export interface VersionConfig { * * @param {VersionConfig} pluginConfig The plugin configuration. * - * @returns {PluginOption} The plugin option. + * @returns {any} The plugin option. * * @example * ```ts @@ -21,14 +37,14 @@ export interface VersionConfig { * }); * ``` */ -export const versionPlugin = (pluginConfig: VersionConfig): PluginOption => { - const versionData = setVersion(); +export const versionPlugin = (pluginConfig?: VersionConfig): any => { + const versionData = setVersion(pluginConfig); const versionString = JSON.stringify(versionData); return { name: "version-plugin", - config(config, env) { - if (pluginConfig.debug) { + config(config: any) { + if (pluginConfig?.debug) { console.log("versionPlugin.config() versionData:", versionData); } @@ -42,7 +58,7 @@ export const versionPlugin = (pluginConfig: VersionConfig): PluginOption => { // For import.meta.env.VITE_VERSION access: process.env.VITE_VERSION = versionString; - if (pluginConfig.debug) { + if (pluginConfig?.debug) { console.log("versionPlugin.config() config.define:", config.define); } From dc3e89b5e0dc6bcd92eb05c19d28ec46648d44a2 Mon Sep 17 00:00:00 2001 From: Matthew Davis Date: Thu, 3 Jul 2025 18:31:43 -0500 Subject: [PATCH 06/18] initial commit --- docs/readme.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/readme.md b/docs/readme.md index 9f858eb..097ed58 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -2,6 +2,9 @@ A powerful, secure, and flexible runtime component compiler for Svelte 5+ applications. +> [!NOTE] +> Live demo is available at [dynamic-component-engine.matthewdavis.io](https://dynamic-component-engine.matthewdavis.io)! + ## ✨ Features - **Runtime Component Compilation**: Transform Svelte component strings into fully functional components on the fly. From 13780ce1f5494dacc5692d51137615ddac96b229 Mon Sep 17 00:00:00 2001 From: Matthew Davis Date: Thu, 3 Jul 2025 18:32:05 -0500 Subject: [PATCH 07/18] initial commit --- docs/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/readme.md b/docs/readme.md index 097ed58..b030f6b 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -3,7 +3,7 @@ A powerful, secure, and flexible runtime component compiler for Svelte 5+ applications. > [!NOTE] -> Live demo is available at [dynamic-component-engine.matthewdavis.io](https://dynamic-component-engine.matthewdavis.io)! +> Live demo is available at [https://dynamic-component-engine.matthewdavis.io](https://dynamic-component-engine.matthewdavis.io)! ## ✨ Features From 9422fcbeacdcd634e15bd1902cfa4f93b0df0bf5 Mon Sep 17 00:00:00 2001 From: Matthew Davis Date: Mon, 7 Jul 2025 11:56:59 -0500 Subject: [PATCH 08/18] fix: duplicate backdrops --- demo/src/app.svelte | 1 - tsconfig.json | 1 - 2 files changed, 2 deletions(-) diff --git a/demo/src/app.svelte b/demo/src/app.svelte index 8afb7b2..d40e2fc 100644 --- a/demo/src/app.svelte +++ b/demo/src/app.svelte @@ -8,7 +8,6 @@ import { toast } from "svelte-sonner"; import type { SimpleProps } from "./components"; import { clipboard } from "./extras/hooks/clipboard"; - import { copyButton } from "./extras/hooks/copy-to-clipboard.svelte"; import { getVersion } from "./version/browser"; let renderRef: HTMLDivElement | undefined = $state(undefined); diff --git a/tsconfig.json b/tsconfig.json index 2062314..7394bad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,6 @@ { "compilerOptions": { "module": "esnext", - "moduleResolution": "bundler", "target": "es2023", /** Svelte Preprocess cannot figure out whether you have a value or a type, so tell TypeScript From 77606d4484e146b2f0a2ef3a0fc4208ba87a456e Mon Sep 17 00:00:00 2001 From: Matthew Davis Date: Tue, 8 Jul 2025 12:04:30 -0500 Subject: [PATCH 09/18] fix: import was dumb dumb --- demo/package-lock.json | 4 +- demo/src/app.svelte | 3 +- package-lock.json | 333 +---------------------------------------- 3 files changed, 8 insertions(+), 332 deletions(-) diff --git a/demo/package-lock.json b/demo/package-lock.json index 1d7dba2..8343c97 100644 --- a/demo/package-lock.json +++ b/demo/package-lock.json @@ -1,12 +1,12 @@ { "name": "demo", - "version": "0.0.1", + "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "demo", - "version": "0.0.1", + "version": "0.0.0", "dependencies": { "tailwindcss": "^4.1.11" }, diff --git a/demo/src/app.svelte b/demo/src/app.svelte index d40e2fc..a02e3c8 100644 --- a/demo/src/app.svelte +++ b/demo/src/app.svelte @@ -6,8 +6,9 @@ import { emojis, load, Logger, LogLevel, render, type Rendered } from "@mateothegreat/dynamic-component-engine"; import { onDestroy, onMount } from "svelte"; import { toast } from "svelte-sonner"; - import type { SimpleProps } from "./components"; + import type { SimpleProps } from "../shared-components/simple/entry"; import { clipboard } from "./extras/hooks/clipboard"; + import { copyButton } from "./extras/hooks/copy-to-clipboard.svelte"; import { getVersion } from "./version/browser"; let renderRef: HTMLDivElement | undefined = $state(undefined); diff --git a/package-lock.json b/package-lock.json index 5cdee2b..e402786 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,6 @@ "svelte": "^5.35.1", "svelte-check": "^4.2.2", "svelte-preprocess": "^6.0.3", - "tsup": "^8.5.0", "typescript": "~5.8.3", "vite": "^6.3.5", "vite-tsconfig-paths": "^5.1.4", @@ -1490,13 +1489,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -1570,22 +1562,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/bundle-require": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", - "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "load-tsconfig": "^0.2.3" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "peerDependencies": { - "esbuild": ">=0.18" - } - }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -1710,33 +1686,6 @@ "node": ">= 0.8" } }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2121,18 +2070,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fix-dts-default-cjs-exports": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", - "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "magic-string": "^0.30.17", - "mlly": "^1.7.4", - "rollup": "^4.34.8" - } - }, "node_modules/flatted": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", @@ -2506,16 +2443,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2579,6 +2506,8 @@ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=14" }, @@ -2586,23 +2515,6 @@ "url": "https://github.com/sponsors/antonk52" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/load-tsconfig": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", - "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", @@ -2617,13 +2529,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", - "dev": true, - "license": "MIT" - }, "node_modules/loupe": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", @@ -2755,19 +2660,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/mlly": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", - "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.14.0", - "pathe": "^2.0.1", - "pkg-types": "^1.3.0", - "ufo": "^1.5.4" - } - }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -2795,18 +2687,6 @@ "dev": true, "license": "MIT" }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -2879,16 +2759,6 @@ "dev": true, "license": "MIT" }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -2973,28 +2843,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -3040,6 +2888,8 @@ } ], "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "lilconfig": "^3.1.1" }, @@ -3246,16 +3096,6 @@ "node": ">=8" } }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/rollup": { "version": "4.44.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.1.tgz", @@ -3407,19 +3247,6 @@ "node": ">=18" } }, - "node_modules/source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "whatwg-url": "^7.0.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3430,35 +3257,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map/node_modules/tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/source-map/node_modules/webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/source-map/node_modules/whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -3603,29 +3401,6 @@ "dev": true, "license": "MIT" }, - "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "^10.3.10", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3767,29 +3542,6 @@ "node": ">=18" } }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -3907,23 +3659,6 @@ "node": ">=18" } }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/tsconfck": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", @@ -3945,59 +3680,6 @@ } } }, - "node_modules/tsup": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.0.tgz", - "integrity": "sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bundle-require": "^5.1.0", - "cac": "^6.7.14", - "chokidar": "^4.0.3", - "consola": "^3.4.0", - "debug": "^4.4.0", - "esbuild": "^0.25.0", - "fix-dts-default-cjs-exports": "^1.0.0", - "joycon": "^3.1.1", - "picocolors": "^1.1.1", - "postcss-load-config": "^6.0.1", - "resolve-from": "^5.0.0", - "rollup": "^4.34.8", - "source-map": "0.8.0-beta.0", - "sucrase": "^3.35.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.11", - "tree-kill": "^1.2.2" - }, - "bin": { - "tsup": "dist/cli-default.js", - "tsup-node": "dist/cli-node.js" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@microsoft/api-extractor": "^7.36.0", - "@swc/core": "^1", - "postcss": "^8.4.12", - "typescript": ">=4.5.0" - }, - "peerDependenciesMeta": { - "@microsoft/api-extractor": { - "optional": true - }, - "@swc/core": { - "optional": true - }, - "postcss": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -4012,13 +3694,6 @@ "node": ">=14.17" } }, - "node_modules/ufo": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", - "dev": true, - "license": "MIT" - }, "node_modules/undici-types": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", From 8a19fd0c876dac1e323541bb8b315234050a4d11 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 12:49:35 +0000 Subject: [PATCH 10/18] Bump the npm_and_yarn group across 2 directories with 1 update Bumps the npm_and_yarn group with 1 update in the / directory: [form-data](https://github.com/form-data/form-data). Bumps the npm_and_yarn group with 1 update in the /demo directory: [form-data](https://github.com/form-data/form-data). Updates `form-data` from 4.0.3 to 4.0.4 - [Release notes](https://github.com/form-data/form-data/releases) - [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md) - [Commits](https://github.com/form-data/form-data/compare/v4.0.3...v4.0.4) Updates `form-data` from 4.0.3 to 4.0.4 - [Release notes](https://github.com/form-data/form-data/releases) - [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md) - [Commits](https://github.com/form-data/form-data/compare/v4.0.3...v4.0.4) --- updated-dependencies: - dependency-name: form-data dependency-version: 4.0.4 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: form-data dependency-version: 4.0.4 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- demo/package-lock.json | 69 +++++++++++++++++++++++++++++++++++++++--- package-lock.json | 6 ++-- 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/demo/package-lock.json b/demo/package-lock.json index 8343c97..57a3c83 100644 --- a/demo/package-lock.json +++ b/demo/package-lock.json @@ -1420,6 +1420,66 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.4.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.4.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.11", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.9.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.9.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.0", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", @@ -2552,9 +2612,9 @@ } }, "node_modules/form-data": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", - "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "dev": true, "license": "MIT", "dependencies": { @@ -4561,7 +4621,8 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tw-animate-css": { "version": "1.3.4", diff --git a/package-lock.json b/package-lock.json index e402786..075a468 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2095,9 +2095,9 @@ } }, "node_modules/form-data": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", - "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "dev": true, "license": "MIT", "dependencies": { From 5d8759a236453e5f0699cae8b58116aa70f50062 Mon Sep 17 00:00:00 2001 From: Matthew Davis Date: Thu, 14 Aug 2025 22:52:34 -0500 Subject: [PATCH 11/18] pre string compiler --- build.js => build.ts | 0 demo/bin/compile-components | 46 -- demo/package-lock.json | 181 +++++++- demo/package.json | 7 +- demo/public/entry.js | 8 +- demo/shared-components/simple/simple.js | 87 ++++ demo/src/app.svelte | 30 +- demo/src/extras/hooks/README.md | 234 ---------- demo/src/extras/hooks/clipboard.test.ts | 411 ------------------ .../extras/hooks/copy-to-clipboard.svelte.ts | 315 -------------- demo/src/extras/hooks/use-clipboard.svelte.ts | 87 ---- demo/src/extras/ui/button/button.svelte | 111 ----- demo/src/extras/ui/button/index.ts | 19 - .../extras/ui/copy-button/copy-button.svelte | 58 --- demo/src/extras/ui/copy-button/index.ts | 7 - demo/src/extras/ui/copy-button/types.ts | 21 - demo/src/extras/utils/utils.ts | 17 - demo/src/global.d.ts | 2 +- demo/src/{extras/hooks => lib}/clipboard.ts | 66 +-- {src => demo/src}/lib/logger.ts | 0 demo/src/{ => lib}/version/browser.ts | 2 +- demo/src/{ => lib}/version/exec.ts | 0 demo/src/{ => lib}/version/git.ts | 0 demo/src/{ => lib}/version/index.ts | 0 demo/src/{ => lib}/version/types.ts | 2 +- demo/src/{ => lib}/version/version.ts | 2 +- .../src/lib/version}/vite-plugin-version.ts | 10 +- demo/src/test-setup.ts | 16 +- demo/src/vite-env.d.ts | 32 +- demo/tsconfig.json | 13 +- demo/vite.config.ts | 2 +- out/entry.js | 107 +++++ package-lock.json | 188 +++++++- package.json | 16 +- src/compiler.ts | 181 ++++++++ src/{lib => }/dynamic-components.ts | 6 - src/entry.ts | 44 ++ src/index.ts | 4 +- src/test-setup.ts | 45 -- src/vite-env.d.ts | 34 -- svelte.config.js | 5 - tsconfig.json | 31 +- vite.config.ts | 12 +- 43 files changed, 863 insertions(+), 1596 deletions(-) rename build.js => build.ts (100%) delete mode 100755 demo/bin/compile-components create mode 100644 demo/shared-components/simple/simple.js delete mode 100644 demo/src/extras/hooks/README.md delete mode 100644 demo/src/extras/hooks/clipboard.test.ts delete mode 100644 demo/src/extras/hooks/copy-to-clipboard.svelte.ts delete mode 100644 demo/src/extras/hooks/use-clipboard.svelte.ts delete mode 100644 demo/src/extras/ui/button/button.svelte delete mode 100644 demo/src/extras/ui/button/index.ts delete mode 100644 demo/src/extras/ui/copy-button/copy-button.svelte delete mode 100644 demo/src/extras/ui/copy-button/index.ts delete mode 100644 demo/src/extras/ui/copy-button/types.ts delete mode 100644 demo/src/extras/utils/utils.ts rename demo/src/{extras/hooks => lib}/clipboard.ts (63%) rename {src => demo/src}/lib/logger.ts (100%) rename demo/src/{ => lib}/version/browser.ts (94%) rename demo/src/{ => lib}/version/exec.ts (100%) rename demo/src/{ => lib}/version/git.ts (100%) rename demo/src/{ => lib}/version/index.ts (100%) rename demo/src/{ => lib}/version/types.ts (75%) rename demo/src/{ => lib}/version/version.ts (94%) rename {src => demo/src/lib/version}/vite-plugin-version.ts (88%) create mode 100644 out/entry.js create mode 100755 src/compiler.ts rename src/{lib => }/dynamic-components.ts (91%) create mode 100644 src/entry.ts delete mode 100644 src/test-setup.ts delete mode 100644 src/vite-env.d.ts delete mode 100644 svelte.config.js diff --git a/build.js b/build.ts similarity index 100% rename from build.js rename to build.ts diff --git a/demo/bin/compile-components b/demo/bin/compile-components deleted file mode 100755 index 8889b6d..0000000 --- a/demo/bin/compile-components +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env node - -import esbuild from "esbuild"; -import esbuildSvelte from "esbuild-svelte"; -import { sveltePreprocess } from "svelte-preprocess"; - -console.log("Discovering components via ./shared-components/**/entry.ts"); - -import { glob } from "glob"; - -const entries = glob.sync("./shared-components/**/entry.ts"); -for (const entry of entries) { - console.log(`+ Discovered component entrypoint: ${entry}`); -} - -bundleSvelte(entries); - -async function bundleSvelte(entry) { - console.log(`\nCompiling (${entry.length}) component${entry.length > 1 ? "s" : ""}...`); - const build = await esbuild.build({ - logLevel: "debug", - entryPoints: Array.isArray(entry) ? entry : [entry], - target: "esnext", - format: "esm", - splitting: false, - packages: "external", - banner: { - js: "// I'm compiled from entry.ts which imports simple.svelte using esbuild-svelte." - }, - bundle: true, - outdir: "./public", - plugins: [ - esbuildSvelte({ - preprocess: sveltePreprocess(), - css: true, - compilerOptions: { - css: "injected", - preserveComments: true, - preserveWhitespace: true - } - }) - ] - }); - - return build.outputFiles; -} diff --git a/demo/package-lock.json b/demo/package-lock.json index 57a3c83..60b3e2d 100644 --- a/demo/package-lock.json +++ b/demo/package-lock.json @@ -8,7 +8,10 @@ "name": "demo", "version": "0.0.0", "dependencies": { - "tailwindcss": "^4.1.11" + "yargs": "^18.0.0" + }, + "bin": { + "compile-components": "bin/compile-components" }, "devDependencies": { "@lucide/svelte": "^0.525.0", @@ -19,6 +22,7 @@ "@testing-library/svelte": "^5.2.8", "@tsconfig/svelte": "^5.0.4", "@types/node": "^24.0.10", + "@types/yargs": "^17.0.33", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", "bits-ui": "^2.8.10", @@ -36,6 +40,7 @@ "svelte-sonner": "^1.0.5", "tailwind-merge": "^3.3.1", "tailwind-variants": "^1.0.0", + "tailwindcss": "^4.1.11", "tw-animate-css": "^1.3.4", "typescript": "~5.8.3", "vite": "^6.3.5", @@ -1689,6 +1694,23 @@ "form-data": "^4.0.0" } }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@vitest/coverage-v8": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", @@ -2136,6 +2158,72 @@ "node": ">=18" } }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2528,6 +2616,15 @@ "svelte": ">=4.2.1 <6" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/esm-env": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", @@ -2653,6 +2750,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -4108,7 +4226,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -4138,7 +4255,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -4433,6 +4549,7 @@ "version": "4.1.11", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", + "dev": true, "license": "MIT" }, "node_modules/tapable": { @@ -5093,6 +5210,15 @@ "dev": true, "license": "MIT" }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -5103,6 +5229,55 @@ "node": ">=18" } }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zimmerframe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", diff --git a/demo/package.json b/demo/package.json index ba3f29b..63182f4 100644 --- a/demo/package.json +++ b/demo/package.json @@ -1,6 +1,9 @@ { "name": "demo", "version": "0.0.0", + "bin": { + "compile-components": "./bin/compile-components" + }, "type": "module", "scripts": { "dev": "vite", @@ -17,6 +20,7 @@ "@testing-library/svelte": "^5.2.8", "@tsconfig/svelte": "^5.0.4", "@types/node": "^24.0.10", + "@types/yargs": "^17.0.33", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", "bits-ui": "^2.8.10", @@ -34,6 +38,7 @@ "svelte-sonner": "^1.0.5", "tailwind-merge": "^3.3.1", "tailwind-variants": "^1.0.0", + "tailwindcss": "^4.1.11", "tw-animate-css": "^1.3.4", "typescript": "~5.8.3", "vite": "^6.3.5", @@ -42,6 +47,6 @@ "vitest": "^3.2.4" }, "dependencies": { - "tailwindcss": "^4.1.11" + "yargs": "^18.0.0" } } diff --git a/demo/public/entry.js b/demo/public/entry.js index 059d9b4..f29fc74 100644 --- a/demo/public/entry.js +++ b/demo/public/entry.js @@ -1,9 +1,7 @@ -// I'm compiled from entry.ts which imports simple.svelte using esbuild-svelte. - -// shared-components/simple/entry.ts +// demo/shared-components/simple/entry.ts import { mount, unmount } from "svelte"; -// shared-components/simple/simple.svelte +// demo/shared-components/simple/simple.svelte import "svelte/internal/disclose-version"; import * as $ from "svelte/internal/client"; var on_click = (_, testState) => $.update(testState); @@ -59,7 +57,7 @@ function Simple($$anchor, $$props) { } $.delegate(["click"]); -// shared-components/simple/entry.ts +// demo/shared-components/simple/entry.ts var factory = (target, props) => { const component = mount(Simple, { target, diff --git a/demo/shared-components/simple/simple.js b/demo/shared-components/simple/simple.js new file mode 100644 index 0000000..90d35c9 --- /dev/null +++ b/demo/shared-components/simple/simple.js @@ -0,0 +1,87 @@ +"use strict"; +(() => { + var __create = Object.create; + var __defProp = Object.defineProperty; + var __getOwnPropDesc = Object.getOwnPropertyDescriptor; + var __getOwnPropNames = Object.getOwnPropertyNames; + var __getProtoOf = Object.getPrototypeOf; + var __hasOwnProp = Object.prototype.hasOwnProperty; + var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { + get: (a, b) => (typeof require !== "undefined" ? require : a)[b] + }) : x)(function(x) { + if (typeof require !== "undefined") return require.apply(this, arguments); + throw Error('Dynamic require of "' + x + '" is not supported'); + }); + var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; + }; + var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod + )); + + // demo/shared-components/simple/simple.svelte + var import_disclose_version = __require("svelte/internal/disclose-version"); + var $ = __toESM(__require("svelte/internal/client")); + var on_click = (_, testState) => $.update(testState); + var root = $.from_html( + ` + +
+
+

Simple.svelte

+

This is a simple component that is rendered on the receiving side.
See the source code for more details.

+
+
+ \`name\` from $props(): + +
+
+ testState: + +
+ +
`, + 1 + ); + function Simple($$anchor, $$props) { + "use strict"; + let testState = $.state(1); + $.next(); + var fragment = root(); + var div = $.sibling($.first_child(fragment)); + var div_1 = $.sibling($.child(div), 3); + var span = $.sibling($.child(div_1), 3); + var text = $.child(span); + $.reset(span); + $.next(); + $.reset(div_1); + var div_2 = $.sibling(div_1, 2); + var span_1 = $.sibling($.child(div_2), 3); + var text_1 = $.child(span_1, true); + $.reset(span_1); + $.next(); + $.reset(div_2); + var button = $.sibling(div_2, 2); + button.__click = [on_click, testState]; + $.next(); + $.reset(div); + $.template_effect(() => { + $.set_text(text, `"${$$props.name ?? ""}"`); + $.set_text(text_1, $.get(testState)); + }); + $.append($$anchor, fragment); + } + $.delegate(["click"]); +})(); diff --git a/demo/src/app.svelte b/demo/src/app.svelte index a02e3c8..b84933a 100644 --- a/demo/src/app.svelte +++ b/demo/src/app.svelte @@ -3,17 +3,17 @@ import { Toaster } from "$lib/components/ui/sonner"; import Window from "$lib/components/window.svelte"; import { BookHeart, Code, Github, HardDriveDownload, RefreshCcwDot, TerminalIcon } from "@lucide/svelte"; - import { emojis, load, Logger, LogLevel, render, type Rendered } from "@mateothegreat/dynamic-component-engine"; + import { load, render, type Rendered } from "@mateothegreat/dynamic-component-engine"; import { onDestroy, onMount } from "svelte"; import { toast } from "svelte-sonner"; - import type { SimpleProps } from "../shared-components/simple/entry"; - import { clipboard } from "./extras/hooks/clipboard"; - import { copyButton } from "./extras/hooks/copy-to-clipboard.svelte"; - import { getVersion } from "./version/browser"; + import type { SimpleProps } from "../../src/entry"; + import { clipboard } from "./lib/clipboard"; + import { emojis, Logger, LogLevel } from "./lib/logger"; + import { getVersion } from "./lib/version/browser"; let renderRef: HTMLDivElement | undefined = $state(undefined); let sourceRef: HTMLPreElement | undefined = $state(undefined); - let sourceText = $state(""); + let code = $state(""); let isLoading = $state(true); let component: Rendered; let loadTime = $state(0); @@ -33,7 +33,7 @@ isLoading = true; try { const source = await fetch("/entry.js").then((res) => res.text()); - sourceText = source; + code = source; logger.info("createComponent", `⬇️ downloaded component source code (${source.length} bytes)`); const fn = await load(source); @@ -86,8 +86,7 @@ {#snippet pkg()} {@const version = getVersion()}
@@ -173,7 +167,7 @@ {#if isLoading} {@render loading()} {:else} -
{sourceText}
+
{code}
{/if} diff --git a/demo/src/extras/hooks/README.md b/demo/src/extras/hooks/README.md deleted file mode 100644 index bf7206f..0000000 --- a/demo/src/extras/hooks/README.md +++ /dev/null @@ -1,234 +0,0 @@ -# Copy to Clipboard Attachment - -A modern, flexible copy to clipboard implementation for Svelte 5 using the new `{@attach}` directive. This replaces the previous class-based approach with a more declarative and feature-rich solution. - -## Features - -- ✨ **Declarative**: Use `{@attach}` directly on any element -- 🎯 **Flexible**: Copy element content, custom text, or dynamic/reactive text -- 🎨 **Visual feedback**: Customizable CSS classes for different states -- ♿ **Accessible**: Built-in keyboard support and ARIA attributes -- 🔄 **Reactive**: Automatically updates when state changes -- 🍞 **Toast integration**: Built-in toast notifications with svelte-sonner -- 🎛️ **Configurable**: Extensive customization options -- 🚀 **TypeScript**: Full type safety and IntelliSense support - -## Installation - -```bash -npm install svelte-sonner # For toast notifications -``` - -## Basic Usage - -### Simple Copy (copies element content) - -```svelte - - -
- Click me to copy this text! -
-``` - -### Copy with Custom Text - -```svelte - -``` - -### Dynamic/Reactive Copy - -```svelte - - - -``` - -## Advanced Usage - -### Pre-styled Copy Button - -```svelte - - - -``` - -### Code Block Copy - -```svelte - - -
-  console.log('Hello, World!');
-
-``` - -### Custom Styling and Behavior - -```svelte - -``` - -## API Reference - -### `copyToClipboard(options?)` - -Creates a copy to clipboard attachment. - -#### Options - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `text` | `string` | `undefined` | Static text to copy | -| `getText` | `() => string` | `undefined` | Function to get dynamic text | -| `successMessage` | `string` | `'Copied to clipboard! 📋'` | Success toast message | -| `errorMessage` | `string` | `'Failed to copy to clipboard'` | Error toast message | -| `resetDelay` | `number` | `1000` | Delay before resetting state (ms) | -| `showToast` | `boolean` | `true` | Whether to show toast notifications | -| `classes` | `object` | `{}` | Custom CSS classes for states | -| `preventDefault` | `boolean` | `true` | Prevent default click behavior | -| `onSuccess` | `(text: string) => void` | `undefined` | Success callback | -| `onError` | `(error: Error) => void` | `undefined` | Error callback | - -#### Classes Object - -```typescript -{ - idle?: string; // Default state - copying?: string; // While copying - success?: string; // After successful copy - error?: string; // After copy failure -} -``` - -### `copyButton(options?)` - -Pre-configured copy button with smooth animations. - -```svelte - -``` - -### `copyCodeBlock(options?)` - -Specialized attachment for code blocks that automatically finds and copies code content. - -```svelte -
-  Your code here
-
-``` - -## Migration from UseClipboard Class - -### Before (Class-based) - -```svelte - - - -``` - -### After (Attachment-based) - -```svelte - - - -``` - -## Benefits of the New Approach - -1. **More Declarative**: Attach directly to elements instead of managing class instances -2. **Better Performance**: No need to track state in multiple places -3. **Easier to Use**: Less boilerplate code -4. **More Flexible**: Works with any element, not just buttons -5. **Better Accessibility**: Automatically adds ARIA attributes and keyboard support -6. **Visual Feedback**: Built-in state management with customizable styling -7. **Reactive**: Automatically updates when dependencies change - -## Browser Support - -- Modern browsers with Clipboard API support -- Graceful fallback with error handling for unsupported browsers -- Requires HTTPS in production (Clipboard API requirement) - -## Examples - -See `demo/src/components/copy-demo.svelte` for comprehensive usage examples. - -## TypeScript Support - -Full TypeScript support with proper type definitions: - -```typescript -import type { Attachment } from 'svelte/attachments'; -import { copyToClipboard, type CopyToClipboardOptions } from '$lib/hooks/copy-to-clipboard.svelte'; - -const myAttachment: Attachment = copyToClipboard({ - text: 'Typed text', - onSuccess: (text: string) => console.log('Copied:', text) -}); -``` \ No newline at end of file diff --git a/demo/src/extras/hooks/clipboard.test.ts b/demo/src/extras/hooks/clipboard.test.ts deleted file mode 100644 index 21068d6..0000000 --- a/demo/src/extras/hooks/clipboard.test.ts +++ /dev/null @@ -1,411 +0,0 @@ -// src/lib/actions/clipboard.test.ts - -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { clipboard, createClipboardContext } from "./clipboard"; - -// Mock navigator.clipboard -const mockWriteText = vi.fn(); -Object.defineProperty(navigator, "clipboard", { - value: { - writeText: mockWriteText - }, - writable: true -}); - -describe("clipboard attachment action", () => { - let element: HTMLButtonElement; - - beforeEach(() => { - element = document.createElement("button"); - document.body.appendChild(element); - vi.useFakeTimers(); - mockWriteText.mockClear(); - }); - - afterEach(() => { - document.body.removeChild(element); - vi.useRealTimers(); - }); - - it("should copy text to clipboard on click", async () => { - const text = "Hello, World!"; - const onSuccess = vi.fn(); - - mockWriteText.mockResolvedValueOnce(undefined); - - const action = clipboard(element, { - text, - onSuccess - }); - - // Simulate click - element.click(); - - await vi.waitFor(() => { - expect(mockWriteText).toHaveBeenCalledWith(text); - expect(onSuccess).toHaveBeenCalledWith(text); - }); - - action.destroy(); - }); - - it("should handle copy failure", async () => { - const text = "Failed text"; - const onFailure = vi.fn(); - const error = new Error("Clipboard access denied"); - - mockWriteText.mockRejectedValueOnce(error); - - const action = clipboard(element, { - text, - onFailure - }); - - element.click(); - - await vi.waitFor(() => { - expect(mockWriteText).toHaveBeenCalledWith(text); - expect(onFailure).toHaveBeenCalledWith(error); - }); - - action.destroy(); - }); - - it("should update status through onStatusChange callback", async () => { - const text = "Status test"; - const onStatusChange = vi.fn(); - - mockWriteText.mockResolvedValueOnce(undefined); - - const action = clipboard(element, { - text, - onStatusChange - }); - - element.click(); - - await vi.waitFor(() => { - expect(onStatusChange).toHaveBeenCalledWith("success"); - }); - - // Fast forward to check status reset - vi.advanceTimersByTime(2000); - - expect(onStatusChange).toHaveBeenCalledWith("idle"); - - action.destroy(); - }); - - it("should use custom delay for status reset", async () => { - const text = "Custom delay"; - const onStatusChange = vi.fn(); - const customDelay = 500; - - mockWriteText.mockResolvedValueOnce(undefined); - - const action = clipboard(element, { - text, - delay: customDelay, - onStatusChange - }); - - element.click(); - - await vi.waitFor(() => { - expect(onStatusChange).toHaveBeenCalledWith("success"); - }); - - // Advance less than custom delay - status should not reset - vi.advanceTimersByTime(400); - expect(onStatusChange).toHaveBeenCalledTimes(1); - - // Advance past custom delay - status should reset - vi.advanceTimersByTime(100); - expect(onStatusChange).toHaveBeenCalledWith("idle"); - - action.destroy(); - }); - - it("should clear previous timeout on rapid clicks", async () => { - const text = "Rapid clicks"; - const onStatusChange = vi.fn(); - - mockWriteText.mockResolvedValue(undefined); - - const action = clipboard(element, { - text, - delay: 1000, - onStatusChange - }); - - // First click - element.click(); - await vi.waitFor(() => { - expect(onStatusChange).toHaveBeenCalledWith("success"); - }); - - // Advance 500ms - vi.advanceTimersByTime(500); - - // Second click before timeout - onStatusChange.mockClear(); - element.click(); - - await vi.waitFor(() => { - expect(onStatusChange).toHaveBeenCalledWith("success"); - }); - - // Advance 1500ms total - should only have one idle call - vi.advanceTimersByTime(1000); - expect(onStatusChange).toHaveBeenCalledWith("idle"); - expect(onStatusChange).toHaveBeenCalledTimes(2); // success + idle - - action.destroy(); - }); - - it("should add appropriate ARIA attributes", () => { - const action = clipboard(element, { - text: "ARIA test" - }); - - expect(element.getAttribute("role")).toBe("button"); - expect(element.getAttribute("aria-label")).toBe("Copy to clipboard"); - expect(element.style.cursor).toBe("pointer"); - - action.destroy(); - - expect(element.getAttribute("role")).toBeNull(); - expect(element.getAttribute("aria-label")).toBeNull(); - }); - - it("should prevent default for buttons inside forms", async () => { - const form = document.createElement("form"); - const button = document.createElement("button"); - form.appendChild(button); - document.body.appendChild(form); - - mockWriteText.mockResolvedValueOnce(undefined); - - const action = clipboard(button, { - text: "Form button test" - }); - - const event = new MouseEvent("click", { bubbles: true, cancelable: true }); - const preventDefault = vi.spyOn(event, "preventDefault"); - - button.dispatchEvent(event); - - expect(preventDefault).toHaveBeenCalled(); - - action.destroy(); - document.body.removeChild(form); - }); - - it("should handle update method", () => { - const initialText = "Initial"; - const updatedText = "Updated"; - const onStatusChange = vi.fn(); - - mockWriteText.mockResolvedValue(undefined); - - const action = clipboard(element, { - text: initialText, - onStatusChange - }); - - // Update the action - action.update({ - text: updatedText, - onStatusChange - }); - - element.click(); - - // Should use updated text - expect(mockWriteText).toHaveBeenCalledWith(updatedText); - - action.destroy(); - }); - - it("should convert non-Error objects to Error instances", async () => { - const text = "Error conversion test"; - const onFailure = vi.fn(); - - // Reject with a string instead of Error - mockWriteText.mockRejectedValueOnce("Not an error object"); - - const action = clipboard(element, { - text, - onFailure - }); - - element.click(); - - await vi.waitFor(() => { - expect(onFailure).toHaveBeenCalled(); - const error = onFailure.mock.calls[0][0]; - expect(error).toBeInstanceOf(Error); - expect(error.message).toBe("Failed to copy to clipboard"); - }); - - action.destroy(); - }); -}); - -describe("createClipboardContext", () => { - let element: HTMLButtonElement; - - beforeEach(() => { - element = document.createElement("button"); - document.body.appendChild(element); - vi.useFakeTimers(); - mockWriteText.mockClear(); - }); - - afterEach(() => { - document.body.removeChild(element); - vi.useRealTimers(); - }); - - it("should create a clipboard context with shared state", async () => { - const context = createClipboardContext(); - - expect(context.status).toBe("idle"); - expect(context.lastCopiedText).toBeNull(); - - mockWriteText.mockResolvedValueOnce(undefined); - - const action = context.action(element, { - text: "Context test" - }); - - element.click(); - - await vi.waitFor(() => { - expect(context.status).toBe("success"); - expect(context.lastCopiedText).toBe("Context test"); - }); - - action.destroy(); - }); - - it("should use default delay from context", async () => { - const customDelay = 300; - const context = createClipboardContext(customDelay); - - mockWriteText.mockResolvedValueOnce(undefined); - - const action = context.action(element, { - text: "Delay test" - }); - - element.click(); - - await vi.waitFor(() => { - expect(context.status).toBe("success"); - }); - - // Should reset after custom delay - vi.advanceTimersByTime(customDelay); - expect(context.status).toBe("idle"); - - action.destroy(); - }); - - it("should override context delay with action-specific delay", async () => { - const contextDelay = 1000; - const actionDelay = 200; - const context = createClipboardContext(contextDelay); - - mockWriteText.mockResolvedValueOnce(undefined); - - const action = context.action(element, { - text: "Override delay", - delay: actionDelay - }); - - element.click(); - - await vi.waitFor(() => { - expect(context.status).toBe("success"); - }); - - // Should use action-specific delay - vi.advanceTimersByTime(actionDelay); - expect(context.status).toBe("idle"); - - action.destroy(); - }); - - it("should propagate callbacks from context action", async () => { - const context = createClipboardContext(); - const onSuccess = vi.fn(); - const onFailure = vi.fn(); - - // Test success callback - mockWriteText.mockResolvedValueOnce(undefined); - - const successAction = context.action(element, { - text: "Success callback", - onSuccess - }); - - element.click(); - - await vi.waitFor(() => { - expect(onSuccess).toHaveBeenCalledWith("Success callback"); - }); - - successAction.destroy(); - - // Test failure callback - const error = new Error("Test error"); - mockWriteText.mockRejectedValueOnce(error); - - const failureAction = context.action(element, { - text: "Failure callback", - onFailure - }); - - element.click(); - - await vi.waitFor(() => { - expect(onFailure).toHaveBeenCalledWith(error); - }); - - failureAction.destroy(); - }); - - it("should handle multiple actions with shared state", async () => { - const context = createClipboardContext(); - const button1 = document.createElement("button"); - const button2 = document.createElement("button"); - document.body.appendChild(button1); - document.body.appendChild(button2); - - mockWriteText.mockResolvedValue(undefined); - - const action1 = context.action(button1, { text: "Button 1" }); - const action2 = context.action(button2, { text: "Button 2" }); - - // Click first button - button1.click(); - - await vi.waitFor(() => { - expect(context.status).toBe("success"); - expect(context.lastCopiedText).toBe("Button 1"); - }); - - // Click second button - button2.click(); - - await vi.waitFor(() => { - expect(context.lastCopiedText).toBe("Button 2"); - }); - - action1.destroy(); - action2.destroy(); - document.body.removeChild(button1); - document.body.removeChild(button2); - }); -}); diff --git a/demo/src/extras/hooks/copy-to-clipboard.svelte.ts b/demo/src/extras/hooks/copy-to-clipboard.svelte.ts deleted file mode 100644 index 848bf74..0000000 --- a/demo/src/extras/hooks/copy-to-clipboard.svelte.ts +++ /dev/null @@ -1,315 +0,0 @@ -import { toast } from "svelte-sonner"; -import type { Attachment } from "svelte/attachments"; - -/** - * Configuration options for the copy to clipboard attachment. - */ -export interface CopyToClipboardOptions { - /** The text to copy to clipboard. If not provided, will use element's textContent */ - text?: string; - /** Function to get dynamic text content */ - getText?: () => string; - /** Success message to show in toast */ - successMessage?: string; - /** Error message to show in toast */ - errorMessage?: string; - /** Delay before resetting visual state (in milliseconds) */ - resetDelay?: number; - /** Whether to show toast notifications */ - showToast?: boolean; - /** Custom CSS classes to apply during different states */ - classes?: { - idle?: string; - copying?: string; - success?: string; - error?: string; - }; - /** Whether to prevent default click behavior */ - preventDefault?: boolean; - /** Custom success callback */ - onSuccess?: (text: string) => void; - /** Custom error callback */ - onError?: (error: Error) => void; -} - -/** - * Creates a copy to clipboard attachment that can be used with {@attach} directive. - * - * ## Usage - * ```svelte - * - * - * - * - * - * - * - * - * - * - * - * - * - * ``` - */ -export function copyToClipboard(options: CopyToClipboardOptions = {}): Attachment { - const { text, getText, successMessage = "Copied to clipboard! 📋", errorMessage = "Failed to copy to clipboard", resetDelay = 1000, showToast = true, classes = {}, preventDefault = true, onSuccess, onError } = options; - - return (element: HTMLElement) => { - let currentState: "idle" | "copying" | "success" | "error" = "idle"; - let resetTimeout: ReturnType | undefined; - let originalClasses: string = ""; - let originalCursor: string = ""; - - // Store original styles - const storeOriginalStyles = () => { - originalClasses = element.className; - originalCursor = element.style.cursor; - }; - - // Apply state classes - const applyStateClasses = (state: typeof currentState) => { - // Reset to original classes - element.className = originalClasses; - - // Add state-specific classes - if (classes[state]) { - element.className += ` ${classes[state]}`; - } - - // Update cursor for copying state - if (state === "copying") { - element.style.cursor = "wait"; - } else { - element.style.cursor = originalCursor; - } - }; - - // Set up initial state - const initialize = () => { - storeOriginalStyles(); - - // Make element appear interactive if it's not already - if (!element.style.cursor && element.tagName !== "BUTTON") { - element.style.cursor = "pointer"; - } - - // Add role for accessibility - if (!element.getAttribute("role")) { - element.setAttribute("role", "button"); - } - - // Add aria-label for accessibility - if (!element.getAttribute("aria-label")) { - element.setAttribute("aria-label", "Copy to clipboard"); - } - }; - - // Get text to copy - const getTextToCopy = (): string => { - if (getText) { - return getText(); - } - if (text) { - return text; - } - return element.textContent?.trim() || ""; - }; - - // Handle copy operation - const handleCopy = async (event: Event) => { - if (preventDefault) { - event.preventDefault(); - } - - // Prevent multiple simultaneous copies - if (currentState === "copying") { - return; - } - - const textToCopy = getTextToCopy(); - - if (!textToCopy) { - console.warn("copyToClipboard: No text to copy"); - return; - } - - // Clear any existing timeout - if (resetTimeout) { - clearTimeout(resetTimeout); - } - - // Set copying state - currentState = "copying"; - applyStateClasses("copying"); - - try { - // Check if clipboard API is available - if (!navigator.clipboard) { - throw new Error("Clipboard API not available"); - } - - await navigator.clipboard.writeText(textToCopy); - - // Success state - currentState = "success"; - applyStateClasses("success"); - - if (showToast) { - toast.success(successMessage); - } - - if (onSuccess) { - onSuccess(textToCopy); - } - } catch (error) { - // Error state - currentState = "error"; - applyStateClasses("error"); - - const errorObj = error instanceof Error ? error : new Error("Unknown error"); - - if (showToast) { - toast.error(errorMessage); - } - - if (onError) { - onError(errorObj); - } else { - console.error("copyToClipboard error:", errorObj); - } - } finally { - // Reset to idle state after delay - resetTimeout = setTimeout(() => { - currentState = "idle"; - applyStateClasses("idle"); - }, resetDelay); - } - }; - - // Set up event listeners - const setupEventListeners = () => { - element.addEventListener("click", handleCopy); - - // Add keyboard support for accessibility - element.addEventListener("keydown", (event) => { - if (event.key === "Enter" || event.key === " ") { - handleCopy(event); - } - }); - - // Add visual feedback for hover/focus states - element.addEventListener("mouseenter", () => { - if (currentState === "idle") { - element.style.opacity = "0.8"; - } - }); - - element.addEventListener("mouseleave", () => { - if (currentState === "idle") { - element.style.opacity = ""; - } - }); - }; - - // Initialize - initialize(); - setupEventListeners(); - - // Cleanup function - return () => { - if (resetTimeout) { - clearTimeout(resetTimeout); - } - - element.removeEventListener("click", handleCopy); - element.style.cursor = originalCursor; - element.className = originalClasses; - }; - }; -} - -/** - * Creates a copy button attachment with predefined styling and behavior. - * Perfect for code blocks and similar use cases. - * - * ## Usage - * ```svelte - * - * - *
- *
- *     {code}
- *   
- * - *
- * ``` - */ -export function copyButton(options: CopyToClipboardOptions = {}): Attachment { - return copyToClipboard({ - classes: { - idle: "transition-all duration-200", - copying: "opacity-50 cursor-wait scale-95", - success: "bg-green-500 text-white scale-105", - error: "bg-red-500 text-white" - }, - ...options - }); -} - -/** - * Creates a copy attachment specifically designed for code blocks. - * Automatically finds and copies the code content. - * - * ## Usage - * ```svelte - * - * - *
- *   console.log('Hello, World!');
- * 
- * ``` - */ -export function copyCodeBlock(options: Omit = {}): Attachment { - return copyToClipboard({ - getText: () => { - const codeElement = document.activeElement?.querySelector("code") || document.activeElement?.closest("pre")?.querySelector("code"); - return codeElement?.textContent?.trim() || ""; - }, - successMessage: "Code copied to clipboard! 👨‍💻", - classes: { - idle: "cursor-pointer transition-all duration-200", - copying: "opacity-50", - success: "bg-green-100 border border-green-300", - error: "bg-red-100 border border-red-300" - }, - ...options - }); -} diff --git a/demo/src/extras/hooks/use-clipboard.svelte.ts b/demo/src/extras/hooks/use-clipboard.svelte.ts deleted file mode 100644 index 3bcad7c..0000000 --- a/demo/src/extras/hooks/use-clipboard.svelte.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - Installed from @ieedan/shadcn-svelte-extras -*/ - -type Options = { - /** The time before the copied status is reset. */ - delay: number; -}; - -/** Use this hook to copy text to the clipboard and show a copied state. - * - * ## Usage - * ```svelte - * - * - * - * ``` - * - */ -export class UseClipboard { - #copiedStatus = $state<"success" | "failure">(); - private delay: number; - private timeout: ReturnType | undefined = undefined; - - constructor({ delay = 500 }: Partial = {}) { - this.delay = delay; - } - - /** Copies the given text to the users clipboard. - * - * ## Usage - * ```ts - * clipboard.copy('Hello, World!'); - * ``` - * - * @param text - * @returns - */ - async copy(text: string) { - if (this.timeout) { - this.#copiedStatus = undefined; - clearTimeout(this.timeout); - } - - try { - await navigator.clipboard.writeText(text); - - this.#copiedStatus = "success"; - - this.timeout = setTimeout(() => { - this.#copiedStatus = undefined; - }, this.delay); - } catch { - // an error can occur when not in the browser or if the user hasn't given clipboard access - this.#copiedStatus = "failure"; - - this.timeout = setTimeout(() => { - this.#copiedStatus = undefined; - }, this.delay); - } - - return this.#copiedStatus; - } - - /** true when the user has just copied to the clipboard. */ - get copied() { - return this.#copiedStatus === "success"; - } - - /** Indicates whether a copy has occurred - * and gives a status of either `success` or `failure`. */ - get status() { - return this.#copiedStatus; - } -} diff --git a/demo/src/extras/ui/button/button.svelte b/demo/src/extras/ui/button/button.svelte deleted file mode 100644 index b78cc58..0000000 --- a/demo/src/extras/ui/button/button.svelte +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - { - onclick?.(e); - - if (type === undefined) return; - - if (onClickPromise) { - loading = true; - - await onClickPromise(e); - - loading = false; - } - }}> - {#if type !== undefined && loading} -
-
- -
-
- Loading - {/if} - {@render children?.()} -
diff --git a/demo/src/extras/ui/button/index.ts b/demo/src/extras/ui/button/index.ts deleted file mode 100644 index bd53eb3..0000000 --- a/demo/src/extras/ui/button/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - Installed from @ieedan/shadcn-svelte-extras -*/ - -import Root, { type ButtonProps, type ButtonSize, type ButtonVariant, type AnchorElementProps, type ButtonElementProps, type ButtonPropsWithoutHTML, buttonVariants } from "./button.svelte"; - -export { - Root, - type ButtonProps as Props, - // - Root as Button, - buttonVariants, - type ButtonProps, - type ButtonSize, - type ButtonVariant, - type AnchorElementProps, - type ButtonElementProps, - type ButtonPropsWithoutHTML -}; diff --git a/demo/src/extras/ui/copy-button/copy-button.svelte b/demo/src/extras/ui/copy-button/copy-button.svelte deleted file mode 100644 index 7ca761e..0000000 --- a/demo/src/extras/ui/copy-button/copy-button.svelte +++ /dev/null @@ -1,58 +0,0 @@ - - - - - diff --git a/demo/src/extras/ui/copy-button/index.ts b/demo/src/extras/ui/copy-button/index.ts deleted file mode 100644 index 20047fa..0000000 --- a/demo/src/extras/ui/copy-button/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - Installed from @ieedan/shadcn-svelte-extras -*/ - -import CopyButton from "./copy-button.svelte"; - -export { CopyButton }; diff --git a/demo/src/extras/ui/copy-button/types.ts b/demo/src/extras/ui/copy-button/types.ts deleted file mode 100644 index 7683a1d..0000000 --- a/demo/src/extras/ui/copy-button/types.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - Installed from @ieedan/shadcn-svelte-extras -*/ - -import type { Snippet } from "svelte"; -import type { ButtonPropsWithoutHTML } from "../button"; -import type { UseClipboard } from "../../hooks/use-clipboard.svelte"; -import type { HTMLAttributes } from "svelte/elements"; -import type { WithChildren, WithoutChildren } from "bits-ui"; - -export type CopyButtonPropsWithoutHTML = WithChildren< - Pick & { - ref?: HTMLButtonElement | null; - text: string; - icon?: Snippet<[]>; - animationDuration?: number; - onCopy?: (status: UseClipboard["status"]) => void; - } ->; - -export type CopyButtonProps = CopyButtonPropsWithoutHTML & WithoutChildren>; diff --git a/demo/src/extras/utils/utils.ts b/demo/src/extras/utils/utils.ts deleted file mode 100644 index 1ff3140..0000000 --- a/demo/src/extras/utils/utils.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - Installed from @ieedan/shadcn-svelte-extras -*/ - -import { type ClassValue, clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type WithoutChild = T extends { child?: any } ? Omit : T; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type WithoutChildren = T extends { children?: any } ? Omit : T; -export type WithoutChildrenOrChild = WithoutChildren>; -export type WithElementRef = T & { ref?: U | null }; diff --git a/demo/src/global.d.ts b/demo/src/global.d.ts index 4586ad3..766ac9d 100644 --- a/demo/src/global.d.ts +++ b/demo/src/global.d.ts @@ -1,4 +1,4 @@ -import type { Version } from "./version/types"; +import type { Version } from "./lib/version/types"; declare global { const __VERSION__: Version | undefined; diff --git a/demo/src/extras/hooks/clipboard.ts b/demo/src/lib/clipboard.ts similarity index 63% rename from demo/src/extras/hooks/clipboard.ts rename to demo/src/lib/clipboard.ts index 106eb21..c3d2a07 100644 --- a/demo/src/extras/hooks/clipboard.ts +++ b/demo/src/lib/clipboard.ts @@ -5,8 +5,6 @@ import type { Attachment } from "svelte/attachments"; export type ClipboardStatus = "idle" | "success" | "failure"; export interface ClipboardOptions { - /** The text to copy to clipboard */ - text: string; /** The time in milliseconds before the status resets to idle (default: 2000) */ delay?: number; /** Callback when copy succeeds */ @@ -32,9 +30,8 @@ interface ClipboardState { * * * * ``` */ -export function clipboard(options: ClipboardOptions): Attachment { +export const clipboard = (value: any, options: ClipboardOptions): Attachment => { return (node: HTMLElement) => { const state: ClipboardState = { status: "idle", @@ -82,9 +79,9 @@ export function clipboard(options: ClipboardOptions): Attachment { } try { - await navigator.clipboard.writeText(options.text); + await navigator.clipboard.writeText(value); updateStatus("success"); - options.onSuccess?.(options.text); + options.onSuccess?.(value); resetStatus(); } catch (error) { const err = error instanceof Error ? error : new Error("Failed to copy to clipboard"); @@ -115,55 +112,4 @@ export function clipboard(options: ClipboardOptions): Attachment { node.removeAttribute("aria-label"); }; }; -} - -/** - * Creates a clipboard context with shared state - * Useful for managing multiple clipboard actions with shared status - * - * @example - * ```svelte - * - * - * - * ``` - */ -export function createClipboardContext(defaultDelay = 2000) { - let status = $state("idle"); - let lastCopiedText = $state(null); - - const action = (node: HTMLElement, options: Omit) => { - return clipboard(node, { - ...options, - delay: options.delay ?? defaultDelay, - onStatusChange: (s) => { - status = s; - if (s === "success") { - lastCopiedText = options.text; - } - }, - onSuccess: (text) => { - options.onSuccess?.(text); - }, - onFailure: (error) => { - options.onFailure?.(error); - } - }); - }; - - return { - get status() { - return status; - }, - get lastCopiedText() { - return lastCopiedText; - }, - action - }; -} +}; diff --git a/src/lib/logger.ts b/demo/src/lib/logger.ts similarity index 100% rename from src/lib/logger.ts rename to demo/src/lib/logger.ts diff --git a/demo/src/version/browser.ts b/demo/src/lib/version/browser.ts similarity index 94% rename from demo/src/version/browser.ts rename to demo/src/lib/version/browser.ts index e863dc6..a56e3d8 100644 --- a/demo/src/version/browser.ts +++ b/demo/src/lib/version/browser.ts @@ -1,5 +1,5 @@ -import type { VersionConfig } from "../../../src/vite-plugin-version"; import type { Version } from "./types"; +import type { VersionConfig } from "./vite-plugin-version"; declare const __VERSION__: Version | undefined; diff --git a/demo/src/version/exec.ts b/demo/src/lib/version/exec.ts similarity index 100% rename from demo/src/version/exec.ts rename to demo/src/lib/version/exec.ts diff --git a/demo/src/version/git.ts b/demo/src/lib/version/git.ts similarity index 100% rename from demo/src/version/git.ts rename to demo/src/lib/version/git.ts diff --git a/demo/src/version/index.ts b/demo/src/lib/version/index.ts similarity index 100% rename from demo/src/version/index.ts rename to demo/src/lib/version/index.ts diff --git a/demo/src/version/types.ts b/demo/src/lib/version/types.ts similarity index 75% rename from demo/src/version/types.ts rename to demo/src/lib/version/types.ts index 9685375..1b88519 100644 --- a/demo/src/version/types.ts +++ b/demo/src/lib/version/types.ts @@ -1,4 +1,4 @@ -import type { VersionLocation } from "../../../src/vite-plugin-version"; +import type { VersionLocation } from "./vite-plugin-version"; export type Version = { location: VersionLocation; diff --git a/demo/src/version/version.ts b/demo/src/lib/version/version.ts similarity index 94% rename from demo/src/version/version.ts rename to demo/src/lib/version/version.ts index a3d0a56..6baa9a0 100644 --- a/demo/src/version/version.ts +++ b/demo/src/lib/version/version.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; -import type { VersionConfig } from "../../../src/vite-plugin-version"; import { git } from "./git"; import type { Version } from "./types"; +import type { VersionConfig } from "./vite-plugin-version"; export const ago = (date: Date, locale = "en"): string => { const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" }); diff --git a/src/vite-plugin-version.ts b/demo/src/lib/version/vite-plugin-version.ts similarity index 88% rename from src/vite-plugin-version.ts rename to demo/src/lib/version/vite-plugin-version.ts index e4a1ef0..1b3677c 100644 --- a/src/vite-plugin-version.ts +++ b/demo/src/lib/version/vite-plugin-version.ts @@ -1,12 +1,6 @@ -import { setVersion } from "../demo/src/version/version"; +import { setVersion } from "./version"; -export type VersionLocation = - | "git-tag" - | "git-commit" - | "git-branch" - | "git-dirty" - | "git-date" - | "package.json"; +export type VersionLocation = "git-tag" | "git-commit" | "git-branch" | "git-dirty" | "git-date" | "package.json"; export interface VersionConfig { debug?: boolean; diff --git a/demo/src/test-setup.ts b/demo/src/test-setup.ts index fabd443..6710c5b 100644 --- a/demo/src/test-setup.ts +++ b/demo/src/test-setup.ts @@ -1,18 +1,24 @@ import "@testing-library/jest-dom"; import { vi } from "vitest"; -// Mock URL.createObjectURL and URL.revokeObjectURL for tests +/** + * Mock URL.createObjectURL and URL.revokeObjectURL for tests. + */ globalThis.URL.createObjectURL = vi.fn(() => "blob:test-url"); globalThis.URL.revokeObjectURL = vi.fn(); -// Mock console methods to reduce test noise +/** + * Mock console methods to reduce test noise. + */ globalThis.console = { ...console, warn: vi.fn(), error: vi.fn() }; -// Setup DOM environment +/** + * Setup DOM environment. + */ Object.defineProperty(window, "location", { value: { href: "http://localhost:3000" @@ -20,7 +26,9 @@ Object.defineProperty(window, "location", { writable: true }); -// Create a mock component class for testing +/** + * Create a mock component class for testing. + */ export class MockSvelteComponent { public target: HTMLElement; public props: any; diff --git a/demo/src/vite-env.d.ts b/demo/src/vite-env.d.ts index bca3f65..ddd6bdf 100644 --- a/demo/src/vite-env.d.ts +++ b/demo/src/vite-env.d.ts @@ -1,35 +1,5 @@ -/// /// -interface ViteTypeOptions { - // By adding this line, you can make the type of ImportMetaEnv strict - // to disallow unknown keys. - // strictImportMetaEnv: unknown -} - interface ImportMetaEnv { - readonly VITE_VERSION: string; // JSON stringified Version object + readonly VITE_VERSION: string; // JSON stringified Version object from my plugin. } -// declare module "virtual:version" { -// export interface Version { -// date: { -// actual: Date; -// human: string; -// }; -// tag: string; -// commit: { -// long: string; -// short: string; -// }; -// dirty: boolean; -// branch: string; -// } - -// const version: Version; -// export default version; -// export const tag: string; -// export const commit: Version["commit"]; -// export const dirty: boolean; -// export const branch: string; -// export const date: Version["date"]; -// } diff --git a/demo/tsconfig.json b/demo/tsconfig.json index bedac5a..7fbfdeb 100644 --- a/demo/tsconfig.json +++ b/demo/tsconfig.json @@ -2,16 +2,8 @@ "compilerOptions": { "module": "esnext", "moduleResolution": "bundler", - "target": "es2023", - /** - Svelte Preprocess cannot figure out whether you have a value or a type, so tell TypeScript - to enforce using `import type` instead of `import` for Types. - */ + "target": "es2022", "verbatimModuleSyntax": true, - /** - To have warnings/errors of the Svelte compiler at the correct position, - enable source maps by default. - */ "sourceMap": true, "strict": true, "strictNullChecks": false, @@ -24,6 +16,5 @@ "$components/*": ["./src/lib/components/ui/*"] } }, - "include": ["./src/**/*.ts", "./src/**/*.js", "./src/**/*.svelte", "shared-components/simple.svelte", "shared-components/simple/entry.ts", "shared-components/entry.ts"], - "exclude": ["src/lib/components/ui"] + "include": ["./src/**/*.ts", "./src/**/*.svelte", "./src/**/*.svelte.ts"] } diff --git a/demo/vite.config.ts b/demo/vite.config.ts index ae253f3..c2067cd 100644 --- a/demo/vite.config.ts +++ b/demo/vite.config.ts @@ -4,7 +4,7 @@ import tailwindcss from "@tailwindcss/vite"; import path from "path"; import { defineConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; -import { versionPlugin } from "../src/vite-plugin-version"; +import { versionPlugin } from "./src/lib/version/vite-plugin-version"; export default defineConfig({ logLevel: "info", diff --git a/out/entry.js b/out/entry.js new file mode 100644 index 0000000..0c0345c --- /dev/null +++ b/out/entry.js @@ -0,0 +1,107 @@ +"use strict"; +(() => { + var __create = Object.create; + var __defProp = Object.defineProperty; + var __getOwnPropDesc = Object.getOwnPropertyDescriptor; + var __getOwnPropNames = Object.getOwnPropertyNames; + var __getProtoOf = Object.getPrototypeOf; + var __hasOwnProp = Object.prototype.hasOwnProperty; + var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { + get: (a, b) => (typeof require !== "undefined" ? require : a)[b] + }) : x)(function(x) { + if (typeof require !== "undefined") return require.apply(this, arguments); + throw Error('Dynamic require of "' + x + '" is not supported'); + }); + var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; + }; + var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod + )); + + // demo/shared-components/simple/entry.ts + var import_svelte = __require("svelte"); + + // demo/shared-components/simple/simple.svelte + var import_disclose_version = __require("svelte/internal/disclose-version"); + var $ = __toESM(__require("svelte/internal/client")); + var on_click = (_, testState) => $.update(testState); + var root = $.from_html( + ` + +
+
+

Simple.svelte

+

This is a simple component that is rendered on the receiving side.
See the source code for more details.

+
+
+ \`name\` from $props(): + +
+
+ testState: + +
+ +
`, + 1 + ); + function Simple($$anchor, $$props) { + "use strict"; + let testState = $.state(1); + $.next(); + var fragment = root(); + var div = $.sibling($.first_child(fragment)); + var div_1 = $.sibling($.child(div), 3); + var span = $.sibling($.child(div_1), 3); + var text = $.child(span); + $.reset(span); + $.next(); + $.reset(div_1); + var div_2 = $.sibling(div_1, 2); + var span_1 = $.sibling($.child(div_2), 3); + var text_1 = $.child(span_1, true); + $.reset(span_1); + $.next(); + $.reset(div_2); + var button = $.sibling(div_2, 2); + button.__click = [on_click, testState]; + $.next(); + $.reset(div); + $.template_effect(() => { + $.set_text(text, `"${$$props.name ?? ""}"`); + $.set_text(text_1, $.get(testState)); + }); + $.append($$anchor, fragment); + } + $.delegate(["click"]); + + // demo/shared-components/simple/entry.ts + var factory = (target, props) => { + const component = (0, import_svelte.mount)(Simple, { + target, + props + }); + return { + component, + name: "Simple", + props, + destroy: () => { + console.log("entry.ts -> simple.svelte", "destroying component", component); + (0, import_svelte.unmount)(component); + } + }; + }; +})(); diff --git a/package-lock.json b/package-lock.json index 075a468..080f100 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@testing-library/svelte": "^5.2.8", "@tsconfig/svelte": "^5.0.4", "@types/node": "^24.0.10", + "@types/yargs": "^17.0.33", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", "esbuild-plugin-cache": "^0.2.10", @@ -28,7 +29,8 @@ "typescript": "~5.8.3", "vite": "^6.3.5", "vite-tsconfig-paths": "^5.1.4", - "vitest": "^3.2.4" + "vitest": "^3.2.4", + "yargs": "^18.0.0" }, "peerDependencies": { "svelte": "^5.0.0" @@ -1269,6 +1271,23 @@ "form-data": "^4.0.0" } }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@vitest/coverage-v8": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", @@ -1643,6 +1662,77 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2011,6 +2101,16 @@ "svelte": ">=4.2.1 <6" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/esm-env": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", @@ -2136,6 +2236,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -4129,6 +4252,69 @@ "dev": true, "license": "MIT" }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zimmerframe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", diff --git a/package.json b/package.json index 72e0912..8253c74 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,21 @@ { "name": "@mateothegreat/dynamic-component-engine", "version": "0.0.1", - "type": "module", - "moduleResolution": "bundler", "description": "A powerful, secure, and flexible runtime component compiler for Svelte 5+ applications.", "author": "Matthew Davis ", "license": "MIT", + "type": "module", + "moduleResolution": "bundler", + "types": "./index.d.ts", "exports": { ".": { - "import": "./src/index.ts", - "types": "./src/index.ts" + "types": "./index.d.ts", + "import": "./index.js" } }, + "files": [ + "./**/*" + ], "scripts": { "dev": "vite", "build": "vite build", @@ -27,6 +31,7 @@ "@testing-library/svelte": "^5.2.8", "@tsconfig/svelte": "^5.0.4", "@types/node": "^24.0.10", + "@types/yargs": "^17.0.33", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", "esbuild-plugin-cache": "^0.2.10", @@ -41,7 +46,8 @@ "typescript": "~5.8.3", "vite": "^6.3.5", "vite-tsconfig-paths": "^5.1.4", - "vitest": "^3.2.4" + "vitest": "^3.2.4", + "yargs": "^18.0.0" }, "peerDependencies": { "svelte": "^5.0.0" diff --git a/src/compiler.ts b/src/compiler.ts new file mode 100755 index 0000000..4c82e3a --- /dev/null +++ b/src/compiler.ts @@ -0,0 +1,181 @@ +#!/usr/bin/env node + +import type { Format } from "esbuild"; +import esbuild, { type BuildOptions } from "esbuild"; +import esbuildSvelte from "esbuild-svelte"; +import { glob } from "glob"; +import { sveltePreprocess } from "svelte-preprocess"; +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; + +const template = ` +import type { Rendered } from "@mateothegreat/dynamic-component-engine"; +import { mount, unmount, type ComponentProps } from "svelte"; +import NAME from "./NAME.svelte"; + +/** + * Handy type alias for the props of the NAME component that can + * be used all over and anywhere. + */ +export type NAMEProps = ComponentProps; + +/** + * The factory function is used to create a new instance of the component + * when being rendered on the receiving side. + * + * This is important because it allows us to have granular control over the component + * lifecycle and not require the receiving side to bear that burden. + * + * @param {HTMLElement} target The target element to mount the component on. + * @param {NAMEProps} props The props to pass to the component. + * + * @returns {Rendered} A Rendered object that contains the component, name, props, and destroy function. + */ +const factory = (target: HTMLElement, props?: NAMEProps): Rendered => { + const component = mount(NAME, { + target, + props: props as NAMEProps + }); + + return { + component, + name: "NAME", + props: props as NAMEProps, + destroy: () => { + console.log("entry.ts -> NAME.svelte", "destroying component", component); + unmount(component); + } + }; +}; + +/** + * Export the factory function as the default export to make it easier + * on the receiving side performing the dynamic import. + */ +export { factory as default }; +`; +// Define the options interface +interface CompilerOptions { + input: string; + output: string; + target: string; + format: string; + debug: boolean; + banner: string; +} + +/** + * Bundle Svelte components using esbuild and esbuild-svelte plugin. + * + * @param entry - Array of entry file paths or a single entry path. + * @param options - Compiler options containing build configuration. + * + * @returns The output files from the build process. + */ +const bundleSvelte = async ( + entry: string[] | string, + options: CompilerOptions +): Promise => { + if (options.debug) { + console.log( + `\nCompiling (${Array.isArray(entry) ? entry.length : 1}) component${Array.isArray(entry) && entry.length > 1 ? "s" : ""}...` + ); + } + + const buildOptions: BuildOptions = { + logLevel: options.debug ? "debug" : "info", + entryPoints: Array.isArray(entry) ? entry : [entry], + target: options.target, + format: options.format as Format, + splitting: false, + packages: "external", + banner: { + js: options.banner + }, + bundle: true, + outdir: options.output, + plugins: [ + esbuildSvelte({ + preprocess: sveltePreprocess(), + compilerOptions: { + css: "injected", + preserveComments: true, + preserveWhitespace: true + } + }) + ] + }; + + const build = await esbuild.build(buildOptions); + + return build.outputFiles; +}; + +const argv = yargs(hideBin(process.argv)) + .option("debug", { + alias: "d", + type: "boolean", + description: "Enable debug logging.", + default: false + }) + .option("input", { + alias: "i", + type: "string", + description: "Input glob pattern for component entry files." + }) + .demandOption("input") + .option("output", { + alias: "o", + type: "string", + description: "Path to output the compiled components." + }) + .demandOption("output") + .option("target", { + alias: "t", + type: "string", + description: "Documentation: https://esbuild.github.io/api/#target", + default: "esnext" + }) + .option("format", { + alias: "f", + type: "string", + description: "Documentation: https://esbuild.github.io/api/#format" + }) + .choices("format", ["esm", "cjs"]) + .option("banner", { + alias: "b", + type: "string", + description: + "Banner text to add at the top of compiled files.\nExample:\n1 | // Compiled with esbuild-svelte 🕺\n2 | ..javascript output follows now..", + default: "" + }) + .example([ + ["compile --input=src/**/*.svelte --output=public"], + [ + 'compile --input=src/**/*.svelte --output=public --banner="// Compiled with esbuild-svelte" --debug' + ] + ]) + .version(false) + .help(false) + .wrap(Math.min(120, process.stdout.columns || 120)) + .parseSync(); + +console.log(`Searching for components with "${argv.input}"...`); + +const entries = glob.sync(argv.input); + +entries.forEach((entry) => { + const t = template.replaceAll("NAME", "Tester"); + console.log(entry, t); +}); + +if (entries.length === 0) { + console.error(`\nNo components found with "${argv.input}"`); + process.exit(1); +} + +console.log(`\nCompiling components...`); + +const output = await bundleSvelte(entries, argv as CompilerOptions); + +console.log(`\nCompiled ${output?.length} components.`); diff --git a/src/lib/dynamic-components.ts b/src/dynamic-components.ts similarity index 91% rename from src/lib/dynamic-components.ts rename to src/dynamic-components.ts index f59c6c0..6129d17 100644 --- a/src/lib/dynamic-components.ts +++ b/src/dynamic-components.ts @@ -1,10 +1,4 @@ import type { mount } from "svelte"; -import { Logger, LogLevel } from "./logger"; - -/** - * The logger for the dynamic-components module. - */ -const logger = new Logger("dynamic-components.ts", { level: LogLevel.DEBUG }); /** * The type of the component instance returned by the `mount` function. diff --git a/src/entry.ts b/src/entry.ts new file mode 100644 index 0000000..0284487 --- /dev/null +++ b/src/entry.ts @@ -0,0 +1,44 @@ +import type { Rendered } from "@mateothegreat/dynamic-component-engine"; +import { mount, unmount, type ComponentProps } from "svelte"; +import NAME from "./NAME.svelte"; + +/** + * Handy type alias for the props of the NAME component that can + * be used all over and anywhere. + */ +export type NAMEProps = ComponentProps; + +/** + * The factory function is used to create a new instance of the component + * when being rendered on the receiving side. + * + * This is important because it allows us to have granular control over the component + * lifecycle and not require the receiving side to bear that burden. + * + * @param {HTMLElement} target The target element to mount the component on. + * @param {NAMEProps} props The props to pass to the component. + * + * @returns {Rendered} A Rendered object that contains the component, name, props, and destroy function. + */ +const factory = (target: HTMLElement, props?: NAMEProps): Rendered => { + const component = mount(NAME, { + target, + props: props as NAMEProps + }); + + return { + component, + name: "NAME", + props: props as NAMEProps, + destroy: () => { + console.log("entry.ts -> NAME.svelte", "destroying component", component); + unmount(component); + } + }; +}; + +/** + * Export the factory function as the default export to make it easier + * on the receiving side performing the dynamic import. + */ +export { factory as default }; diff --git a/src/index.ts b/src/index.ts index 4dcf4e8..d116a90 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,2 @@ -export * from "./lib/dynamic-components"; -export * from "./lib/logger"; +// Make it easier to import when through package.json (cjs and esm are the usual suspects 🤸‍♀️ ofc). +export * from "./dynamic-components"; diff --git a/src/test-setup.ts b/src/test-setup.ts deleted file mode 100644 index fabd443..0000000 --- a/src/test-setup.ts +++ /dev/null @@ -1,45 +0,0 @@ -import "@testing-library/jest-dom"; -import { vi } from "vitest"; - -// Mock URL.createObjectURL and URL.revokeObjectURL for tests -globalThis.URL.createObjectURL = vi.fn(() => "blob:test-url"); -globalThis.URL.revokeObjectURL = vi.fn(); - -// Mock console methods to reduce test noise -globalThis.console = { - ...console, - warn: vi.fn(), - error: vi.fn() -}; - -// Setup DOM environment -Object.defineProperty(window, "location", { - value: { - href: "http://localhost:3000" - }, - writable: true -}); - -// Create a mock component class for testing -export class MockSvelteComponent { - public target: HTMLElement; - public props: any; - - constructor({ target, props }: { target: HTMLElement; props?: any }) { - this.target = target; - this.props = props; - this.render(); - } - - render() { - if (this.target) { - this.target.innerHTML = "
Mock Dynamic Component
"; - } - } - - $destroy() { - if (this.target) { - this.target.innerHTML = ""; - } - } -} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts deleted file mode 100644 index 8da0ddd..0000000 --- a/src/vite-env.d.ts +++ /dev/null @@ -1,34 +0,0 @@ -/// -/// -interface ViteTypeOptions { - // By adding this line, you can make the type of ImportMetaEnv strict - // to disallow unknown keys. - // strictImportMetaEnv: unknown -} - -interface ImportMetaEnv { - readonly CURRENT_VERSION: Version; -} -declare module "virtual:version" { - export interface Version { - date: { - actual: Date; - human: string; - }; - tag: string; - commit: { - long: string; - short: string; - }; - dirty: boolean; - branch: string; - } - - const version: Version; - export default version; - export const tag: string; - export const commit: Version["commit"]; - export const dirty: boolean; - export const branch: string; - export const date: Version["date"]; -} diff --git a/svelte.config.js b/svelte.config.js deleted file mode 100644 index 447c400..0000000 --- a/svelte.config.js +++ /dev/null @@ -1,5 +0,0 @@ -import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; - -export default { - preprocess: [vitePreprocess({})] -}; diff --git a/tsconfig.json b/tsconfig.json index 7394bad..458d361 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,22 +1,23 @@ { + "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { - "module": "esnext", - "target": "es2023", - /** - Svelte Preprocess cannot figure out whether you have a value or a type, so tell TypeScript - to enforce using `import type` instead of `import` for Types. - */ + "rootDir": "./src", + "outDir": "./dist", + "module": "es2022", + "moduleResolution": "bundler", + "target": "es2022", "verbatimModuleSyntax": true, - /** - To have warnings/errors of the Svelte compiler at the correct position, - enable source maps by default. - */ - "noEmit": true, "sourceMap": true, "strict": true, - "strictNullChecks": false, - "esModuleInterop": true, - "skipLibCheck": true + "isolatedModules": true, + "noImplicitAny": true, + "noUncheckedIndexedAccess": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "skipLibCheck": true, + "paths": { + "@mateothegreat/dynamic-component-engine": ["./src/dynamic-components.ts"] + } }, - "include": ["./src/**/*", "demo/src/version/version.ts"] + "include": ["./src/*"] } diff --git a/vite.config.ts b/vite.config.ts index 11f61c5..03f6f1f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,16 +1,6 @@ -import { svelte } from "@sveltejs/vite-plugin-svelte"; - import { defineConfig } from "vite"; - import tsconfigPaths from "vite-tsconfig-paths"; -import { sveltePreprocess } from "svelte-preprocess"; - export default defineConfig({ - plugins: [ - tsconfigPaths(), - svelte({ - preprocess: [sveltePreprocess({ typescript: true })] - }) - ] + plugins: [tsconfigPaths()] }); From 239137b3599aa218f332a591ae23677c9460434e Mon Sep 17 00:00:00 2001 From: Matthew Davis Date: Thu, 14 Aug 2025 22:54:34 -0500 Subject: [PATCH 12/18] pre string compiler --- src/compiler.ts | 128 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 122 insertions(+), 6 deletions(-) diff --git a/src/compiler.ts b/src/compiler.ts index 4c82e3a..41469be 100755 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node -import type { Format } from "esbuild"; -import esbuild, { type BuildOptions } from "esbuild"; +import type { Format, BuildOptions } from "esbuild"; +import * as esbuild from "esbuild"; import esbuildSvelte from "esbuild-svelte"; import { glob } from "glob"; import { sveltePreprocess } from "svelte-preprocess"; @@ -64,21 +64,104 @@ interface CompilerOptions { banner: string; } +// Interface for component source input +interface ComponentSource { + name: string; + source: string; + filename?: string; +} + /** * Bundle Svelte components using esbuild and esbuild-svelte plugin. + * Now accepts component source as strings instead of file paths. * - * @param entry - Array of entry file paths or a single entry path. + * @param components - Array of component sources or entry file paths. * @param options - Compiler options containing build configuration. * * @returns The output files from the build process. */ const bundleSvelte = async ( + components: ComponentSource[] | string[] | string, + options: CompilerOptions +): Promise => { + // Handle backward compatibility with file paths + if (typeof components === 'string' || (Array.isArray(components) && typeof components[0] === 'string')) { + return bundleSvelteFromFiles(components as string[] | string, options); + } + + const componentArray = Array.isArray(components) ? components : [components]; + + if (options.debug) { + console.log(`\nCompiling (${componentArray.length}) component${componentArray.length > 1 ? "s" : ""} from source...`); + } + + // Create virtual file system plugin + const virtualFilePlugin: esbuild.Plugin = { + name: 'virtual-file', + setup(build) { + componentArray.forEach((component) => { + const comp = component as ComponentSource; + const filename = comp.filename || `${comp.name}.ts`; + build.onResolve({ filter: new RegExp(`^virtual:${comp.name}$`) }, () => ({ + path: filename, + namespace: 'virtual' + })); + + build.onLoad({ filter: /.*/, namespace: 'virtual' }, (args) => { + const matchingComponent = componentArray.find((c) => { + const compSource = c as ComponentSource; + const compFilename = compSource.filename || `${compSource.name}.ts`; + return args.path === compFilename; + }) as ComponentSource; + + return { + contents: matchingComponent.source, + loader: filename.endsWith('.svelte') ? 'ts' : 'ts' + }; + }); + }); + } + }; + + const buildOptions: BuildOptions = { + logLevel: options.debug ? "debug" : "info", + entryPoints: componentArray.map(comp => `virtual:${(comp as ComponentSource).name}`), + target: options.target, + format: options.format as Format, + splitting: false, + packages: "external", + banner: { + js: options.banner + }, + bundle: true, + write: false, // Don't write to filesystem, return output files + plugins: [ + virtualFilePlugin, + esbuildSvelte({ + preprocess: sveltePreprocess(), + compilerOptions: { + css: "injected", + preserveComments: true, + preserveWhitespace: true + } + }) + ] + }; + + const build = await esbuild.build(buildOptions); + return build.outputFiles; +}; + +/** + * Bundle Svelte components from file paths (backward compatibility). + */ +const bundleSvelteFromFiles = async ( entry: string[] | string, options: CompilerOptions ): Promise => { if (options.debug) { console.log( - `\nCompiling (${Array.isArray(entry) ? entry.length : 1}) component${Array.isArray(entry) && entry.length > 1 ? "s" : ""}...` + `\nCompiling (${Array.isArray(entry) ? entry.length : 1}) component${Array.isArray(entry) && entry.length > 1 ? "s" : ""} from files...` ); } @@ -107,7 +190,6 @@ const bundleSvelte = async ( }; const build = await esbuild.build(buildOptions); - return build.outputFiles; }; @@ -165,7 +247,7 @@ console.log(`Searching for components with "${argv.input}"...`); const entries = glob.sync(argv.input); entries.forEach((entry) => { - const t = template.replaceAll("NAME", "Tester"); + const t = template.replace(/NAME/g, "Tester"); console.log(entry, t); }); @@ -179,3 +261,37 @@ console.log(`\nCompiling components...`); const output = await bundleSvelte(entries, argv as CompilerOptions); console.log(`\nCompiled ${output?.length} components.`); + +// Export functions for programmatic use +export { bundleSvelte, bundleSvelteFromFiles, type ComponentSource, type CompilerOptions }; + +/** + * Convenience function to compile a single component from source string. + * + * @param name - Component name + * @param source - Component source code + * @param options - Compiler options + * @returns The compiled output + */ +export const compileComponentFromSource = async ( + name: string, + source: string, + options: Omit +): Promise => { + const component: ComponentSource = { name, source }; + return bundleSvelte([component], { ...options, input: '' }); +}; + +/** + * Compile multiple components from source strings. + * + * @param components - Array of component sources + * @param options - Compiler options + * @returns The compiled outputs + */ +export const compileComponentsFromSource = async ( + components: ComponentSource[], + options: Omit +): Promise => { + return bundleSvelte(components, { ...options, input: '' }); +}; From aeb30b22ca755619d5f3fb92daa4035e7dd74cd3 Mon Sep 17 00:00:00 2001 From: Matthew Davis Date: Fri, 15 Aug 2025 10:00:05 -0500 Subject: [PATCH 13/18] initial v2 --- USAGE.md | 353 +++++++++++++ demo/index.html | 7 +- demo/package-lock.json | 967 +++++++++++++++++++++++++---------- demo/package.json | 32 +- demo/public/entry.js | 78 --- demo/src/app.svelte | 222 +++----- demo/src/lib/dyn.ts | 87 ++++ demo/tsconfig.json | 1 + package-lock.json | 199 +++---- package.json | 17 +- src/compiler.ts | 297 ----------- src/compiler/index.ts | 2 + src/compiler/runtime.1.ts | 361 +++++++++++++ src/compiler/runtime.test.ts | 240 +++++++++ src/compiler/runtime.ts | 205 ++++++++ src/dynamic-components.ts | 1 + src/entry.ts | 44 -- src/index.ts | 2 + src/loader.ts | 19 + tsconfig.json | 7 +- 20 files changed, 2166 insertions(+), 975 deletions(-) create mode 100644 USAGE.md delete mode 100644 demo/public/entry.js create mode 100644 demo/src/lib/dyn.ts delete mode 100755 src/compiler.ts create mode 100644 src/compiler/index.ts create mode 100644 src/compiler/runtime.1.ts create mode 100644 src/compiler/runtime.test.ts create mode 100644 src/compiler/runtime.ts delete mode 100644 src/entry.ts create mode 100644 src/loader.ts diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 0000000..a85fe12 --- /dev/null +++ b/USAGE.md @@ -0,0 +1,353 @@ +# Svelte Dynamic Component Engine - Usage Guide + +## Overview + +The Svelte Dynamic Component Engine now supports both file-based and string-based compilation of Svelte components. This allows you to compile Svelte components directly from source code strings without requiring physical files on the filesystem. + +## Installation + +```bash +npm install @mateothegreat/dynamic-component-engine +``` + +## API Reference + +### Types + +```typescript +interface ComponentSource { + name: string; // Component name + source: string; // Component source code + filename?: string; // Optional custom filename + type?: 'svelte' | 'ts' | 'js'; // Component type (auto-detected if not specified) +} + +interface CompilerOptions { + output: string; // Output directory + target: string; // Build target (default: 'esnext') + format: string; // Output format (default: 'esm') + debug: boolean; // Enable debug logging + banner: string; // Banner text for compiled files +} +``` + +## String-Based Compilation + +### Single Component + +```typescript +import { compileComponentFromSource } from '@mateothegreat/dynamic-component-engine/compiler'; + +const svelteSource = ` + + +

Hello {name}!

+

Count: {count}

+ + + +`; + +const result = await compileComponentFromSource( + 'HelloComponent', + svelteSource, + { + output: './dist', + target: 'esnext', + format: 'esm', + debug: true, + banner: '// Compiled with Svelte Dynamic Component Engine' + }, + 'svelte' // Component type (optional, auto-detected) +); + +console.log('Compiled files:', result?.length); +``` + +### Multiple Components + +```typescript +import { compileComponentsFromSource } from '@mateothegreat/dynamic-component-engine/compiler'; + +const components = [ + { + name: 'Button', + source: ` + + + + + + `, + type: 'svelte' + }, + { + name: 'Card', + source: ` + + +
+

{title}

+
+ +
+
+ + + `, + type: 'svelte' + } +]; + +const results = await compileComponentsFromSource(components, { + output: './dist/components', + target: 'esnext', + format: 'esm', + debug: false, + banner: '' +}); + +console.log(`Compiled ${results?.length} component files`); +``` + +## File-Based Compilation (Backward Compatible) + +### CLI Usage + +```bash +# Compile components from files +node src/compiler.ts --input="src/components/**/*.svelte" --output="./dist" --debug + +# With custom options +node src/compiler.ts \ + --input="src/components/**/*.svelte" \ + --output="./public/components" \ + --target="es2020" \ + --format="esm" \ + --banner="// My Custom Banner" \ + --debug +``` + +### Programmatic Usage + +```typescript +import { bundleSvelteFromFiles } from '@mateothegreat/dynamic-component-engine/compiler'; + +// Single file +const result = await bundleSvelteFromFiles( + 'src/components/MyComponent.svelte', + { + input: '', // Not used for file-based compilation + output: './dist', + target: 'esnext', + format: 'esm', + debug: true, + banner: '// Compiled component' + } +); + +// Multiple files +const results = await bundleSvelteFromFiles( + ['src/components/Button.svelte', 'src/components/Card.svelte'], + options +); +``` + +## Advanced Usage + +### Dynamic Component Loading + +After compilation, you can dynamically load and use your components: + +```typescript +// Assuming you've compiled a component and it's available +const componentModule = await import('./dist/HelloComponent.js'); +const componentFactory = componentModule.default; + +// Mount the component +const rendered = componentFactory( + document.getElementById('app'), // target element + { name: 'Svelte', count: 5 } // props +); + +// Later, destroy the component +rendered.destroy(); +``` + +### Runtime Component Generation + +```typescript +// Generate component source dynamically +function generateComponentSource(componentName: string, props: string[]): string { + const propsDeclaration = props.map(prop => `export let ${prop};`).join('\n '); + const propsDisplay = props.map(prop => `

{${prop}}

`).join('\n '); + + return ` + + +
+

${componentName}

+ ${propsDisplay} +
+ + + `; +} + +// Compile the dynamically generated component +const dynamicSource = generateComponentSource('UserCard', ['name', 'email', 'role']); +const result = await compileComponentFromSource('UserCard', dynamicSource, options); +``` + +### Error Handling + +```typescript +try { + const result = await compileComponentFromSource('MyComponent', source, options); + + if (!result || result.length === 0) { + console.error('Compilation failed: No output files generated'); + return; + } + + console.log('Compilation successful:', result.map(f => f.path)); +} catch (error) { + console.error('Compilation error:', error.message); + + if (error.errors) { + error.errors.forEach(err => { + console.error(` ${err.location?.file}:${err.location?.line} - ${err.text}`); + }); + } +} +``` + +## Configuration Options + +### Build Targets + +- `'es5'` - ES5 compatible output +- `'es2015'` - ES2015/ES6 compatible output +- `'es2017'` - ES2017 compatible output (default for most use cases) +- `'es2020'` - ES2020 compatible output +- `'esnext'` - Latest ECMAScript features (default) + +### Output Formats + +- `'esm'` - ES modules (default, recommended) +- `'cjs'` - CommonJS modules + +### Debug Mode + +When `debug: true` is enabled: +- Detailed build logs are shown +- Compilation timing information +- Plugin processing details +- Source map generation (if supported) + +## Best Practices + +1. **Component Naming**: Use PascalCase for component names (`MyComponent`, not `myComponent`) + +2. **Type Safety**: Explicitly specify component type when known: + ```typescript + const component = { name: 'Button', source: '...', type: 'svelte' as const }; + ``` + +3. **Error Boundaries**: Always wrap compilation in try-catch blocks + +4. **Performance**: For batch compilation, use `compileComponentsFromSource` instead of multiple `compileComponentFromSource` calls + +5. **Memory Management**: Remember to call `destroy()` on rendered components to prevent memory leaks + +## Troubleshooting + +### Common Issues + +**Issue**: `Expected ">" but found "lang"` +**Solution**: Ensure component type is set to `'svelte'` or let auto-detection work by including Svelte syntax in the source. + +**Issue**: `Virtual file not found` +**Solution**: This usually indicates a problem with the virtual file system. Use the new string-based compilation which uses `stdin` instead. + +**Issue**: CSS not being injected +**Solution**: Ensure `css: "injected"` is set in the Svelte compiler options (this is the default in the new version). + +### Debug Information + +Enable debug mode to get detailed information about the compilation process: + +```typescript +const result = await compileComponentFromSource(name, source, { + ...options, + debug: true +}); +``` + +This will show: +- Component detection results +- Build configuration +- Plugin execution order +- Compilation timing +- Output file details \ No newline at end of file diff --git a/demo/index.html b/demo/index.html index 6893ea7..7329a60 100644 --- a/demo/index.html +++ b/demo/index.html @@ -7,9 +7,10 @@ diff --git a/demo/package-lock.json b/demo/package-lock.json index 60b3e2d..ced8572 100644 --- a/demo/package-lock.json +++ b/demo/package-lock.json @@ -14,18 +14,18 @@ "compile-components": "bin/compile-components" }, "devDependencies": { - "@lucide/svelte": "^0.525.0", - "@sveltejs/vite-plugin-svelte": "^5.1.0", - "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", - "@tailwindcss/vite": "^4.1.11", - "@testing-library/jest-dom": "^6.6.3", + "@lucide/svelte": "^0.539.0", + "@sveltejs/vite-plugin-svelte": "^6.1.2", + "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", + "@tailwindcss/vite": "^4.1.12", + "@testing-library/jest-dom": "^6.7.0", "@testing-library/svelte": "^5.2.8", "@tsconfig/svelte": "^5.0.4", - "@types/node": "^24.0.10", + "@types/node": "^24.3.0", "@types/yargs": "^17.0.33", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", - "bits-ui": "^2.8.10", + "bits-ui": "^2.9.3", "clsx": "^2.1.1", "esbuild-plugin-cache": "^0.2.10", "esbuild-svelte": "^0.9.3", @@ -33,18 +33,18 @@ "mode-watcher": "^1.1.0", "prettier": "^3.6.2", "prettier-plugin-svelte": "^3.4.0", - "prettier-plugin-tailwindcss": "^0.6.13", - "svelte": "^5.35.1", - "svelte-check": "^4.2.2", + "prettier-plugin-tailwindcss": "^0.6.14", + "svelte": "^5.38.1", + "svelte-check": "^4.3.1", "svelte-preprocess": "^6.0.3", "svelte-sonner": "^1.0.5", "tailwind-merge": "^3.3.1", - "tailwind-variants": "^1.0.0", - "tailwindcss": "^4.1.11", - "tw-animate-css": "^1.3.4", - "typescript": "~5.8.3", - "vite": "^6.3.5", - "vite-plugin-version-mark": "^0.1.4", + "tailwind-variants": "^2.1.0", + "tailwindcss": "^4.1.12", + "tw-animate-css": "^1.3.6", + "typescript": "~5.9.2", + "vite": "^7.1.2", + "vite-plugin-version-mark": "^0.2.2", "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.2.4" } @@ -800,6 +800,17 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -829,15 +840,68 @@ } }, "node_modules/@lucide/svelte": { - "version": "0.525.0", - "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-0.525.0.tgz", - "integrity": "sha512-dyUxkXzepagLUzL8jHQNdeH286nC66ClLACsg+Neu/bjkRJWPWMzkT+H0DKlE70QdkicGCfs1ZGmXCc351hmZA==", + "version": "0.539.0", + "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-0.539.0.tgz", + "integrity": "sha512-OWhw4BhHO+owmOE/ijSNLnw/flbW2/DsLzMHAeM8oEjLsO0xE6glX0ADCDwxKItTs5ZJYssfyGNXxMXrea173w==", "dev": true, "license": "ISC", "peerDependencies": { "svelte": "^5" } }, + "node_modules/@nuxt/kit": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.0.3.tgz", + "integrity": "sha512-9+lwvP4n8KhO91azoebO0o39smESGzEV4HU6nef9HIFyt04YwlVMY37Pk63GgZn0WhWVjyPWcQWs0rUdZUYcPw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "c12": "^3.2.0", + "consola": "^3.4.2", + "defu": "^6.1.4", + "destr": "^2.0.5", + "errx": "^0.1.0", + "exsolve": "^1.0.7", + "ignore": "^7.0.5", + "jiti": "^2.5.1", + "klona": "^2.0.6", + "mlly": "^1.7.4", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "pkg-types": "^2.2.0", + "scule": "^1.3.0", + "semver": "^7.7.2", + "std-env": "^3.9.0", + "tinyglobby": "^0.2.14", + "ufo": "^1.6.1", + "unctx": "^2.4.1", + "unimport": "^5.2.0", + "untyped": "^2.0.0" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/@nuxt/schema": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-4.0.3.tgz", + "integrity": "sha512-acDigyy8tF8xDCMFee00mt5u2kE5Qx5Y34ButBlibLzhguQjc+6f6FpMGdieN07oahjpegWIQG66yQywjw+sKw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/shared": "^3.5.18", + "consola": "^3.4.2", + "defu": "^6.1.4", + "pathe": "^2.0.3", + "std-env": "^3.9.0", + "ufo": "1.6.1" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1147,43 +1211,43 @@ } }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.0.tgz", - "integrity": "sha512-wojIS/7GYnJDYIg1higWj2ROA6sSRWvcR1PO/bqEyFr/5UZah26c8Cz4u0NaqjPeVltzsVpt2Tm8d2io0V+4Tw==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.1.2.tgz", + "integrity": "sha512-7v+7OkUYelC2dhhYDAgX1qO2LcGscZ18Hi5kKzJQq7tQeXpH215dd0+J/HnX2zM5B3QKcIrTVqCGkZXAy5awYw==", "dev": true, "license": "MIT", "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.17", - "vitefu": "^1.0.6" + "vitefu": "^1.1.1" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" + "node": "^20.19 || ^22.12 || >=24" }, "peerDependencies": { "svelte": "^5.0.0", - "vite": "^6.0.0" + "vite": "^6.3.0 || ^7.0.0" } }, "node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", - "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.0.tgz", + "integrity": "sha512-iwQ8Z4ET6ZFSt/gC+tVfcsSBHwsqc6RumSaiLUkAurW3BCpJam65cmHw0oOlDMTO0u+PZi9hilBRYN+LZNHTUQ==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.3.7" + "debug": "^4.4.1" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" + "node": "^20.19 || ^22.12 || >=24" }, "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", - "vite": "^6.0.0" + "vite": "^6.3.0 || ^7.0.0" } }, "node_modules/@swc/helpers": { @@ -1198,25 +1262,25 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", - "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.12.tgz", + "integrity": "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.3.0", - "enhanced-resolve": "^5.18.1", - "jiti": "^2.4.2", + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.5.1", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.11" + "tailwindcss": "4.1.12" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", - "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.12.tgz", + "integrity": "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1228,24 +1292,24 @@ "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.11", - "@tailwindcss/oxide-darwin-arm64": "4.1.11", - "@tailwindcss/oxide-darwin-x64": "4.1.11", - "@tailwindcss/oxide-freebsd-x64": "4.1.11", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", - "@tailwindcss/oxide-linux-x64-musl": "4.1.11", - "@tailwindcss/oxide-wasm32-wasi": "4.1.11", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" + "@tailwindcss/oxide-android-arm64": "4.1.12", + "@tailwindcss/oxide-darwin-arm64": "4.1.12", + "@tailwindcss/oxide-darwin-x64": "4.1.12", + "@tailwindcss/oxide-freebsd-x64": "4.1.12", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.12", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.12", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.12", + "@tailwindcss/oxide-linux-x64-musl": "4.1.12", + "@tailwindcss/oxide-wasm32-wasi": "4.1.12", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.12", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.12" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", - "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.12.tgz", + "integrity": "sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==", "cpu": [ "arm64" ], @@ -1260,9 +1324,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", - "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.12.tgz", + "integrity": "sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==", "cpu": [ "arm64" ], @@ -1277,9 +1341,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", - "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.12.tgz", + "integrity": "sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==", "cpu": [ "x64" ], @@ -1294,9 +1358,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", - "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.12.tgz", + "integrity": "sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==", "cpu": [ "x64" ], @@ -1311,9 +1375,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", - "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.12.tgz", + "integrity": "sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==", "cpu": [ "arm" ], @@ -1328,9 +1392,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", - "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.12.tgz", + "integrity": "sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==", "cpu": [ "arm64" ], @@ -1345,9 +1409,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", - "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.12.tgz", + "integrity": "sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==", "cpu": [ "arm64" ], @@ -1362,9 +1426,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", - "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.12.tgz", + "integrity": "sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==", "cpu": [ "x64" ], @@ -1379,9 +1443,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", - "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.12.tgz", + "integrity": "sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==", "cpu": [ "x64" ], @@ -1396,9 +1460,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", - "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.12.tgz", + "integrity": "sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -1414,81 +1478,21 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@emnapi/wasi-threads": "^1.0.2", - "@napi-rs/wasm-runtime": "^0.2.11", - "@tybys/wasm-util": "^0.9.0", + "@emnapi/core": "^1.4.5", + "@emnapi/runtime": "^1.4.5", + "@emnapi/wasi-threads": "^1.0.4", + "@napi-rs/wasm-runtime": "^0.2.12", + "@tybys/wasm-util": "^0.10.0", "tslib": "^2.8.0" }, "engines": { "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.4.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.4.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.0.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.11", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.9.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.9.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.0", - "dev": true, - "inBundle": true, - "license": "0BSD", - "optional": true - }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", - "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz", + "integrity": "sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==", "cpu": [ "arm64" ], @@ -1503,9 +1507,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", - "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.12.tgz", + "integrity": "sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==", "cpu": [ "x64" ], @@ -1520,15 +1524,15 @@ } }, "node_modules/@tailwindcss/vite": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.11.tgz", - "integrity": "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.12.tgz", + "integrity": "sha512-4pt0AMFDx7gzIrAOIYgYP0KCBuKWqyW8ayrdiLEjoJTT4pKTjrzG/e4uzWtTLDziC+66R9wbUqZBccJalSE5vQ==", "dev": true, "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.1.11", - "@tailwindcss/oxide": "4.1.11", - "tailwindcss": "4.1.11" + "@tailwindcss/node": "4.1.12", + "@tailwindcss/oxide": "4.1.12", + "tailwindcss": "4.1.12" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" @@ -1589,18 +1593,17 @@ "license": "MIT" }, "node_modules/@testing-library/jest-dom": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", - "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.7.0.tgz", + "integrity": "sha512-RI2e97YZ7MRa+vxP4UUnMuMFL2buSsf0ollxUbTgrbPLKhMn8KVTx7raS6DYjC7v1NDVrioOvaShxsguLNISCA==", "dev": true, "license": "MIT", "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", - "chalk": "^3.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", - "lodash": "^4.17.21", + "picocolors": "^1.1.1", "redent": "^3.0.0" }, "engines": { @@ -1674,13 +1677,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.0.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz", - "integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==", + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.8.0" + "undici-types": "~7.10.0" } }, "node_modules/@types/node-fetch": { @@ -1882,6 +1885,14 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vue/shared": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.18.tgz", + "integrity": "sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1995,9 +2006,9 @@ "license": "MIT" }, "node_modules/bits-ui": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.8.10.tgz", - "integrity": "sha512-MOobkqapDZNrpcNmeL2g664xFmH4tZBOKBTxFmsQYMZQuybSZHQnPXy+AjM5XZEXRmCFx5+XRmo6+fC3vHh1hQ==", + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.9.3.tgz", + "integrity": "sha512-jSoi3U1pfhLQVKZ3j+1GbeuvT1w6cMbnrVWhObVE+DRuV+zcpKeUnd6zpz7NYF47HsSXVNjI1rJdZH+A5sNsOQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2067,6 +2078,36 @@ "balanced-match": "^1.0.0" } }, + "node_modules/c12": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.2.0.tgz", + "integrity": "sha512-ixkEtbYafL56E6HiFuonMm1ZjoKtIo7TH68/uiEq4DAwv9NcUX2nJ95F8TrbMeNjqIkZpruo3ojXQJ+MGG5gcQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^17.2.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.5.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -2108,20 +2149,6 @@ "node": ">=12" } }, - "node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -2158,6 +2185,17 @@ "node": ">=18" } }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "consola": "^3.2.3" + } + }, "node_modules/cliui": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", @@ -2267,6 +2305,25 @@ "node": ">= 0.8" } }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2362,6 +2419,14 @@ "node": ">=0.10.0" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2418,6 +2483,14 @@ "node": ">=6" } }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -2435,6 +2508,20 @@ "dev": true, "license": "MIT" }, + "node_modules/dotenv": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", + "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2465,9 +2552,9 @@ "license": "MIT" }, "node_modules/enhanced-resolve": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", - "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "dev": true, "license": "MIT", "dependencies": { @@ -2491,6 +2578,14 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/errx": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/errx/-/errx-0.1.0.tgz", + "integrity": "sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2625,6 +2720,20 @@ "node": ">=6" } }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/esm-env": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", @@ -2633,9 +2742,9 @@ "license": "MIT" }, "node_modules/esrap": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.0.1.tgz", - "integrity": "sha512-6n1JodkxeMvyTDCog7J//t8Yti//fGicZgtFLko6h/aEpc54BK9O8k9cZgC2J8+2Dh1U5uYIxuJWSsylybvFBA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz", + "integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==", "dev": true, "license": "MIT", "dependencies": { @@ -2662,6 +2771,14 @@ "node": ">=12.0.0" } }, + "node_modules/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/fdir": { "version": "6.4.6", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", @@ -2810,6 +2927,25 @@ "node": ">= 0.4" } }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -2971,6 +3107,17 @@ "node": ">=0.10.0" } }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -3093,9 +3240,9 @@ } }, "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", "dev": true, "license": "MIT", "bin": { @@ -3159,6 +3306,25 @@ "node": ">=6" } }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/knitwork": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/knitwork/-/knitwork-1.2.0.tgz", + "integrity": "sha512-xYSH7AvuQ6nXkq42x0v5S8/Iry+cfulBz/DJQzhIyESdLD7425jXsPy4vn5cCXU+HhRN2kVw51Vd1K6/By4BQg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/lightningcss": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", @@ -3398,6 +3564,25 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/local-pkg": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.1.tgz", + "integrity": "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.0.1", + "quansync": "^0.2.8" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", @@ -3405,13 +3590,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, - "license": "MIT" - }, "node_modules/loupe": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", @@ -3572,6 +3750,41 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/mlly": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", + "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "acorn": "^8.14.0", + "pathe": "^2.0.1", + "pkg-types": "^1.3.0", + "ufo": "^1.5.4" + } + }, + "node_modules/mlly/node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/mlly/node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, "node_modules/mode-watcher": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-1.1.0.tgz", @@ -3653,6 +3866,14 @@ } } }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/node-fetch/node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -3685,6 +3906,43 @@ "dev": true, "license": "MIT" }, + "node_modules/nypm": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.1.tgz", + "integrity": "sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.2", + "pathe": "^2.0.3", + "pkg-types": "^2.2.0", + "tinyexec": "^1.0.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "node_modules/nypm/node_modules/tinyexec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -3749,6 +4007,14 @@ "node": ">= 14.16" } }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3757,9 +4023,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -3769,6 +4035,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.2.0.tgz", + "integrity": "sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -3826,9 +4105,9 @@ } }, "node_modules/prettier-plugin-tailwindcss": { - "version": "0.6.13", - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.13.tgz", - "integrity": "sha512-uQ0asli1+ic8xrrSmIOaElDu0FacR4x69GynTh2oZjFY10JUt6EEumTQl5tB4fMeD6I1naKd+4rXQQ7esT2i1g==", + "version": "0.6.14", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.14.tgz", + "integrity": "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==", "dev": true, "license": "MIT", "engines": { @@ -3836,6 +4115,8 @@ }, "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-hermes": "*", + "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", @@ -3857,6 +4138,12 @@ "@ianvs/prettier-plugin-sort-imports": { "optional": true }, + "@prettier/plugin-hermes": { + "optional": true + }, + "@prettier/plugin-oxc": { + "optional": true + }, "@prettier/plugin-pug": { "optional": true }, @@ -3942,6 +4229,36 @@ "node": ">=6" } }, + "node_modules/quansync": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz", + "integrity": "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -4073,6 +4390,14 @@ "node": ">=v12.22.7" } }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -4320,13 +4645,13 @@ } }, "node_modules/svelte": { - "version": "5.35.1", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.35.1.tgz", - "integrity": "sha512-3kNMwstMB2VUBod63H6BQPzBr7NiPRR9qFVzrWqW/nU4ulqqAYZsJ6E7m8LYFoRzuW6yylx+uzdgKVhFKZVf4Q==", + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.38.1.tgz", + "integrity": "sha512-fO6CLDfJYWHgfo6lQwkQU2vhCiHc2MBl6s3vEhK+sSZru17YL4R5s1v14ndRpqKAIkq8nCz6MTk1yZbESZWeyQ==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.3.0", + "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", @@ -4335,7 +4660,7 @@ "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", - "esrap": "^2.0.0", + "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", @@ -4346,9 +4671,9 @@ } }, "node_modules/svelte-check": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.2.2.tgz", - "integrity": "sha512-1+31EOYZ7NKN0YDMKusav2hhEoA51GD9Ws6o//0SphMT0ve9mBTsTUEX7OmDMadUP3KjNHsSKtJrqdSaD8CrGQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.1.tgz", + "integrity": "sha512-lkh8gff5gpHLjxIV+IaApMxQhTGnir2pNUAqcNgeKkvK5bT/30Ey/nzBxNLDlkztCH4dP7PixkMt9SWEKFPBWg==", "dev": true, "license": "MIT", "dependencies": { @@ -4518,37 +4843,29 @@ } }, "node_modules/tailwind-variants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-1.0.0.tgz", - "integrity": "sha512-2WSbv4ulEEyuBKomOunut65D8UZwxrHoRfYnxGcQNnHqlSCp2+B7Yz2W+yrNDrxRodOXtGD/1oCcKGNBnUqMqA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-2.1.0.tgz", + "integrity": "sha512-82m0eRex0z6A3GpvfoTCpHr+wWJmbecfVZfP3mqLoDxeya5tN4mYJQZwa5Aw1hRZTedwpu1D2JizYenoEdyD8w==", "dev": true, "license": "MIT", - "dependencies": { - "tailwind-merge": "3.0.2" - }, "engines": { "node": ">=16.x", "pnpm": ">=7.x" }, "peerDependencies": { + "tailwind-merge": ">=3.0.0", "tailwindcss": "*" - } - }, - "node_modules/tailwind-variants/node_modules/tailwind-merge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.0.2.tgz", - "integrity": "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" + }, + "peerDependenciesMeta": { + "tailwind-merge": { + "optional": true + } } }, "node_modules/tailwindcss": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", - "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz", + "integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==", "dev": true, "license": "MIT" }, @@ -4738,13 +5055,12 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tw-animate-css": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.4.tgz", - "integrity": "sha512-dd1Ht6/YQHcNbq0znIT6dG8uhO7Ce+VIIhZUhjsryXsMPJQz3bZg7Q2eNzLwipb25bRZslGb2myio5mScd1TFg==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.6.tgz", + "integrity": "sha512-9dy0R9UsYEGmgf26L8UcHiLmSFTHa9+D7+dAt/G/sF5dCnPePZbfgDYinc7/UzAM7g/baVrmS6m9yEpU46d+LA==", "dev": true, "license": "MIT", "funding": { @@ -4752,9 +5068,9 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4765,32 +5081,133 @@ "node": ">=14.17" } }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/unctx": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/unctx/-/unctx-2.4.1.tgz", + "integrity": "sha512-AbaYw0Nm4mK4qjhns67C+kgxR2YWiwlDBPzxrN8h8C6VtAdCgditAY5Dezu3IJy4XVqAnbrXt9oQJvsn3fyozg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "acorn": "^8.14.0", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17", + "unplugin": "^2.1.0" + } + }, "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", "dev": true, "license": "MIT" }, + "node_modules/unimport": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/unimport/-/unimport-5.2.0.tgz", + "integrity": "sha512-bTuAMMOOqIAyjV4i4UH7P07pO+EsVxmhOzQ2YJ290J6mkLUdozNhb5I/YoOEheeNADC03ent3Qj07X0fWfUpmw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "acorn": "^8.15.0", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "local-pkg": "^1.1.1", + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "pkg-types": "^2.2.0", + "scule": "^1.3.0", + "strip-literal": "^3.0.0", + "tinyglobby": "^0.2.14", + "unplugin": "^2.3.5", + "unplugin-utils": "^0.2.4" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/unplugin": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.5.tgz", + "integrity": "sha512-RyWSb5AHmGtjjNQ6gIlA67sHOsWpsbWpwDokLwTcejVdOjEkJZh7QKu14J00gDDVSh8kGH4KYC/TNBceXFZhtw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "acorn": "^8.14.1", + "picomatch": "^4.0.2", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/unplugin-utils": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.2.5.tgz", + "integrity": "sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/untyped": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/untyped/-/untyped-2.0.0.tgz", + "integrity": "sha512-nwNCjxJTjNuLCgFr42fEak5OcLuB3ecca+9ksPFNvtfYSLpjf+iJqSIaSnIile6ZPbKYxI5k2AfXqeopGudK/g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "citty": "^0.1.6", + "defu": "^6.1.4", + "jiti": "^2.4.2", + "knitwork": "^1.2.0", + "scule": "^1.3.0" + }, + "bin": { + "untyped": "dist/cli.mjs" + } + }, "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.2.tgz", + "integrity": "sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "fdir": "^6.4.6", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.14" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -4799,14 +5216,14 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", - "less": "*", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -4871,12 +5288,14 @@ } }, "node_modules/vite-plugin-version-mark": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/vite-plugin-version-mark/-/vite-plugin-version-mark-0.1.4.tgz", - "integrity": "sha512-0uoEWDnrH9ho4IwTzyFKosA+XnOWK7uT8WwCskPb2DVeFrCSn4sfPG7b+8l9EFWHgCx0n52iqwQEbs3w5FFzpw==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/vite-plugin-version-mark/-/vite-plugin-version-mark-0.2.2.tgz", + "integrity": "sha512-qFxfnlpK++zBX7sWdOOmRFE+Yd7G/6vZ7mJFvlEhp/vLtqMU9cN0LWX+VcfVXubnyiGTpSzt/fzoejHRyTrbIQ==", "dev": true, "license": "MIT", "peerDependencies": { + "@nuxt/kit": ">3.3.0", + "@nuxt/schema": ">3.3.0", "vite": "*" } }, @@ -4901,9 +5320,9 @@ } }, "node_modules/vitefu": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.0.tgz", - "integrity": "sha512-AiG/L9DVsEYHWQ9jAEnke0nKiASlPw+JYwDl6Z4l6a6/IqT1tKseEl6R5+rVnKJt/K3jCTWiQvgoIh5MuqBJJQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", "dev": true, "license": "MIT", "workspaces": [ @@ -5016,6 +5435,14 @@ "node": ">=12" } }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", diff --git a/demo/package.json b/demo/package.json index 63182f4..538d3f2 100644 --- a/demo/package.json +++ b/demo/package.json @@ -12,18 +12,18 @@ "compile": "node bin/compile-components" }, "devDependencies": { - "@lucide/svelte": "^0.525.0", - "@sveltejs/vite-plugin-svelte": "^5.1.0", - "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", - "@tailwindcss/vite": "^4.1.11", - "@testing-library/jest-dom": "^6.6.3", + "@lucide/svelte": "^0.539.0", + "@sveltejs/vite-plugin-svelte": "^6.1.2", + "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", + "@tailwindcss/vite": "^4.1.12", + "@testing-library/jest-dom": "^6.7.0", "@testing-library/svelte": "^5.2.8", "@tsconfig/svelte": "^5.0.4", - "@types/node": "^24.0.10", + "@types/node": "^24.3.0", "@types/yargs": "^17.0.33", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", - "bits-ui": "^2.8.10", + "bits-ui": "^2.9.3", "clsx": "^2.1.1", "esbuild-plugin-cache": "^0.2.10", "esbuild-svelte": "^0.9.3", @@ -31,18 +31,18 @@ "mode-watcher": "^1.1.0", "prettier": "^3.6.2", "prettier-plugin-svelte": "^3.4.0", - "prettier-plugin-tailwindcss": "^0.6.13", - "svelte": "^5.35.1", - "svelte-check": "^4.2.2", + "prettier-plugin-tailwindcss": "^0.6.14", + "svelte": "^5.38.1", + "svelte-check": "^4.3.1", "svelte-preprocess": "^6.0.3", "svelte-sonner": "^1.0.5", "tailwind-merge": "^3.3.1", - "tailwind-variants": "^1.0.0", - "tailwindcss": "^4.1.11", - "tw-animate-css": "^1.3.4", - "typescript": "~5.8.3", - "vite": "^6.3.5", - "vite-plugin-version-mark": "^0.1.4", + "tailwind-variants": "^2.1.0", + "tailwindcss": "^4.1.12", + "tw-animate-css": "^1.3.6", + "typescript": "~5.9.2", + "vite": "^7.1.2", + "vite-plugin-version-mark": "^0.2.2", "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.2.4" }, diff --git a/demo/public/entry.js b/demo/public/entry.js deleted file mode 100644 index f29fc74..0000000 --- a/demo/public/entry.js +++ /dev/null @@ -1,78 +0,0 @@ -// demo/shared-components/simple/entry.ts -import { mount, unmount } from "svelte"; - -// demo/shared-components/simple/simple.svelte -import "svelte/internal/disclose-version"; -import * as $ from "svelte/internal/client"; -var on_click = (_, testState) => $.update(testState); -var root = $.from_html( - ` - -
-
-

Simple.svelte

-

This is a simple component that is rendered on the receiving side.
See the source code for more details.

-
-
- \`name\` from $props(): - -
-
- testState: - -
- -
`, - 1 -); -function Simple($$anchor, $$props) { - "use strict"; - let testState = $.state(1); - $.next(); - var fragment = root(); - var div = $.sibling($.first_child(fragment)); - var div_1 = $.sibling($.child(div), 3); - var span = $.sibling($.child(div_1), 3); - var text = $.child(span); - $.reset(span); - $.next(); - $.reset(div_1); - var div_2 = $.sibling(div_1, 2); - var span_1 = $.sibling($.child(div_2), 3); - var text_1 = $.child(span_1, true); - $.reset(span_1); - $.next(); - $.reset(div_2); - var button = $.sibling(div_2, 2); - button.__click = [on_click, testState]; - $.next(); - $.reset(div); - $.template_effect(() => { - $.set_text(text, `"${$$props.name ?? ""}"`); - $.set_text(text_1, $.get(testState)); - }); - $.append($$anchor, fragment); -} -$.delegate(["click"]); - -// demo/shared-components/simple/entry.ts -var factory = (target, props) => { - const component = mount(Simple, { - target, - props - }); - return { - component, - name: "Simple", - props, - destroy: () => { - console.log("entry.ts -> simple.svelte", "destroying component", component); - unmount(component); - } - }; -}; -export { - factory as default -}; diff --git a/demo/src/app.svelte b/demo/src/app.svelte index b84933a..79a791d 100644 --- a/demo/src/app.svelte +++ b/demo/src/app.svelte @@ -1,176 +1,78 @@ -{#snippet loading()} -
-
-
Compiling component...
+
+
+ +
-{/snippet} - -{#snippet pkg()} - {@const version = getVersion()} - -{/snippet} -
-
-
Svelte Dynamic Component Engine
-

This demo shows how to use the Svelte Dynamic Component Engine to render a component at runtime in the browser using Svelte 5.

-
- - - - - - - {@render pkg()} -
- -
-
-
-
-
- - Rendered Component -
- -
-
- - { - console.error(" trapped an error:", e); - reset(); - }}> -
- {#if isLoading} - {@render loading()} - {/if} -
-
-
-
-
-

🚀 Load + Render:

-

{loadTime.toFixed(2)}ms

+
+
-
-
-
- - Source Code -
- -
-
- {#if isLoading} - {@render loading()} - {:else} -
{code}
- {/if} -
-
-
- - +
diff --git a/demo/src/lib/dyn.ts b/demo/src/lib/dyn.ts new file mode 100644 index 0000000..b00af8a --- /dev/null +++ b/demo/src/lib/dyn.ts @@ -0,0 +1,87 @@ +import { compile } from "svelte/compiler"; + +export interface DynamicComponent { + component: any; + destroy: () => void; +} + +export class SvelteDynamicRenderer { + private static internalsScript: string | null = null; + + /** + * Initialize by loading Svelte internals + */ + static async initialize() { + if (this.internalsScript) return; + + // Load Svelte internal modules + const modules = ["svelte/internal/client", "svelte/internal/disclose-version"]; + + const scripts = await Promise.all( + modules.map(async (m) => { + console.log(m); + const response = await fetch(`https://esm.sh/svelte/internal`); + return response.text(); + }) + ); + + this.internalsScript = scripts.join("\n"); + } + + /** + * Compile and render a Svelte component from source + */ + static async render(source: string, target: HTMLElement, props?: Record): Promise { + await this.initialize(); + + // Compile the Svelte component + const compiled = compile(source, { + generate: "client", + css: "injected", + modernAst: true, + runes: true, + filename: "DynamicComponent.svelte" + }); + + // Create a self-contained module + const moduleCode = ` + (function() { + // Inject Svelte internals + ${this.internalsScript} + + // Component code + ${compiled.js.code} + + // Return the component + return typeof Component !== 'undefined' ? Component : + typeof default !== 'undefined' ? default : + null; + })() + `; + + // Create and execute the module + console.log(moduleCode); + const ComponentConstructor = new Function("return " + moduleCode)(); + + if (!ComponentConstructor) { + throw new Error("Failed to compile component"); + } + + // Mount the component + const instance = new ComponentConstructor({ + target, + props + }); + + return { + component: instance, + destroy: () => { + if (instance && typeof instance.$destroy === "function") { + instance.$destroy(); + } else if (instance && typeof instance.destroy === "function") { + instance.destroy(); + } + } + }; + } +} diff --git a/demo/tsconfig.json b/demo/tsconfig.json index 7fbfdeb..0c91dd4 100644 --- a/demo/tsconfig.json +++ b/demo/tsconfig.json @@ -11,6 +11,7 @@ "skipLibCheck": true, "paths": { "@mateothegreat/dynamic-component-engine": ["../src/index.ts"], + "@mateothegreat/dynamic-component-engine/compiler": ["../src/compiler/index.ts"], "$lib": ["./src/lib"], "$lib/*": ["./src/lib/*"], "$components/*": ["./src/lib/components/ui/*"] diff --git a/package-lock.json b/package-lock.json index 080f100..5b24c98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,25 +9,26 @@ "version": "0.0.1", "license": "MIT", "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^5.1.0", - "@testing-library/jest-dom": "^6.6.3", + "@sveltejs/vite-plugin-svelte": "^6.1.2", + "@testing-library/jest-dom": "^6.7.0", "@testing-library/svelte": "^5.2.8", "@tsconfig/svelte": "^5.0.4", - "@types/node": "^24.0.10", + "@types/node": "^24.3.0", "@types/yargs": "^17.0.33", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", "esbuild-plugin-cache": "^0.2.10", "esbuild-svelte": "^0.9.3", + "esbuild-wasm": "^0.25.9", "jsdom": "^26.1.0", "prettier": "^3.6.2", "prettier-plugin-svelte": "^3.4.0", - "prettier-plugin-tailwindcss": "^0.6.13", - "svelte": "^5.35.1", - "svelte-check": "^4.2.2", + "prettier-plugin-tailwindcss": "^0.6.14", + "svelte": "^5.38.1", + "svelte-check": "^4.3.1", "svelte-preprocess": "^6.0.3", - "typescript": "~5.8.3", - "vite": "^6.3.5", + "typescript": "~5.9.2", + "vite": "^7.1.2", "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.2.4", "yargs": "^18.0.0" @@ -735,6 +736,17 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1072,43 +1084,43 @@ } }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.0.tgz", - "integrity": "sha512-wojIS/7GYnJDYIg1higWj2ROA6sSRWvcR1PO/bqEyFr/5UZah26c8Cz4u0NaqjPeVltzsVpt2Tm8d2io0V+4Tw==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.1.2.tgz", + "integrity": "sha512-7v+7OkUYelC2dhhYDAgX1qO2LcGscZ18Hi5kKzJQq7tQeXpH215dd0+J/HnX2zM5B3QKcIrTVqCGkZXAy5awYw==", "dev": true, "license": "MIT", "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.17", - "vitefu": "^1.0.6" + "vitefu": "^1.1.1" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" + "node": "^20.19 || ^22.12 || >=24" }, "peerDependencies": { "svelte": "^5.0.0", - "vite": "^6.0.0" + "vite": "^6.3.0 || ^7.0.0" } }, - "node_modules/@sveltejs/vite-plugin-svelte/node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", - "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.0.tgz", + "integrity": "sha512-iwQ8Z4ET6ZFSt/gC+tVfcsSBHwsqc6RumSaiLUkAurW3BCpJam65cmHw0oOlDMTO0u+PZi9hilBRYN+LZNHTUQ==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.3.7" + "debug": "^4.4.1" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" + "node": "^20.19 || ^22.12 || >=24" }, "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", - "vite": "^6.0.0" + "vite": "^6.3.0 || ^7.0.0" } }, "node_modules/@testing-library/dom": { @@ -1166,18 +1178,17 @@ "license": "MIT" }, "node_modules/@testing-library/jest-dom": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", - "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.7.0.tgz", + "integrity": "sha512-RI2e97YZ7MRa+vxP4UUnMuMFL2buSsf0ollxUbTgrbPLKhMn8KVTx7raS6DYjC7v1NDVrioOvaShxsguLNISCA==", "dev": true, "license": "MIT", "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", - "chalk": "^3.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", - "lodash": "^4.17.21", + "picocolors": "^1.1.1", "redent": "^3.0.0" }, "engines": { @@ -1251,13 +1262,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.0.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz", - "integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==", + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.8.0" + "undici-types": "~7.10.0" } }, "node_modules/@types/node-fetch": { @@ -1622,20 +1633,6 @@ "node": ">=12" } }, - "node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -2101,6 +2098,19 @@ "svelte": ">=4.2.1 <6" } }, + "node_modules/esbuild-wasm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.25.9.tgz", + "integrity": "sha512-Jpv5tCSwQg18aCqCRD3oHIX/prBhXMDapIoG//A+6+dV0e7KQMGFg85ihJ5T1EeMjbZjON3TqFy0VrGAnIHLDA==", + "dev": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2119,9 +2129,9 @@ "license": "MIT" }, "node_modules/esrap": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.0.1.tgz", - "integrity": "sha512-6n1JodkxeMvyTDCog7J//t8Yti//fGicZgtFLko6h/aEpc54BK9O8k9cZgC2J8+2Dh1U5uYIxuJWSsylybvFBA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz", + "integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==", "dev": true, "license": "MIT", "dependencies": { @@ -2645,13 +2655,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, - "license": "MIT" - }, "node_modules/loupe": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", @@ -2954,9 +2957,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -3068,9 +3071,9 @@ } }, "node_modules/prettier-plugin-tailwindcss": { - "version": "0.6.13", - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.13.tgz", - "integrity": "sha512-uQ0asli1+ic8xrrSmIOaElDu0FacR4x69GynTh2oZjFY10JUt6EEumTQl5tB4fMeD6I1naKd+4rXQQ7esT2i1g==", + "version": "0.6.14", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.14.tgz", + "integrity": "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==", "dev": true, "license": "MIT", "engines": { @@ -3078,6 +3081,8 @@ }, "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-hermes": "*", + "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", @@ -3099,6 +3104,12 @@ "@ianvs/prettier-plugin-sort-imports": { "optional": true }, + "@prettier/plugin-hermes": { + "optional": true + }, + "@prettier/plugin-oxc": { + "optional": true + }, "@prettier/plugin-pug": { "optional": true }, @@ -3538,13 +3549,13 @@ } }, "node_modules/svelte": { - "version": "5.35.1", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.35.1.tgz", - "integrity": "sha512-3kNMwstMB2VUBod63H6BQPzBr7NiPRR9qFVzrWqW/nU4ulqqAYZsJ6E7m8LYFoRzuW6yylx+uzdgKVhFKZVf4Q==", + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.38.1.tgz", + "integrity": "sha512-fO6CLDfJYWHgfo6lQwkQU2vhCiHc2MBl6s3vEhK+sSZru17YL4R5s1v14ndRpqKAIkq8nCz6MTk1yZbESZWeyQ==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.3.0", + "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", @@ -3553,7 +3564,7 @@ "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", - "esrap": "^2.0.0", + "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", @@ -3564,9 +3575,9 @@ } }, "node_modules/svelte-check": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.2.2.tgz", - "integrity": "sha512-1+31EOYZ7NKN0YDMKusav2hhEoA51GD9Ws6o//0SphMT0ve9mBTsTUEX7OmDMadUP3KjNHsSKtJrqdSaD8CrGQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.1.tgz", + "integrity": "sha512-lkh8gff5gpHLjxIV+IaApMxQhTGnir2pNUAqcNgeKkvK5bT/30Ey/nzBxNLDlkztCH4dP7PixkMt9SWEKFPBWg==", "dev": true, "license": "MIT", "dependencies": { @@ -3804,9 +3815,9 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3818,31 +3829,31 @@ } }, "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", "dev": true, "license": "MIT" }, "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.2.tgz", + "integrity": "sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "fdir": "^6.4.6", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.14" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -3851,14 +3862,14 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", - "less": "*", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -3943,9 +3954,9 @@ } }, "node_modules/vitefu": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.0.tgz", - "integrity": "sha512-AiG/L9DVsEYHWQ9jAEnke0nKiASlPw+JYwDl6Z4l6a6/IqT1tKseEl6R5+rVnKJt/K3jCTWiQvgoIh5MuqBJJQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", "dev": true, "license": "MIT", "workspaces": [ diff --git a/package.json b/package.json index 8253c74..2590dcf 100644 --- a/package.json +++ b/package.json @@ -26,25 +26,26 @@ "test:coverage": "vitest --coverage" }, "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^5.1.0", - "@testing-library/jest-dom": "^6.6.3", + "@sveltejs/vite-plugin-svelte": "^6.1.2", + "@testing-library/jest-dom": "^6.7.0", "@testing-library/svelte": "^5.2.8", "@tsconfig/svelte": "^5.0.4", - "@types/node": "^24.0.10", + "@types/node": "^24.3.0", "@types/yargs": "^17.0.33", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", "esbuild-plugin-cache": "^0.2.10", "esbuild-svelte": "^0.9.3", + "esbuild-wasm": "^0.25.9", "jsdom": "^26.1.0", "prettier": "^3.6.2", "prettier-plugin-svelte": "^3.4.0", - "prettier-plugin-tailwindcss": "^0.6.13", - "svelte": "^5.35.1", - "svelte-check": "^4.2.2", + "prettier-plugin-tailwindcss": "^0.6.14", + "svelte": "^5.38.1", + "svelte-check": "^4.3.1", "svelte-preprocess": "^6.0.3", - "typescript": "~5.8.3", - "vite": "^6.3.5", + "typescript": "~5.9.2", + "vite": "^7.1.2", "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.2.4", "yargs": "^18.0.0" diff --git a/src/compiler.ts b/src/compiler.ts deleted file mode 100755 index 41469be..0000000 --- a/src/compiler.ts +++ /dev/null @@ -1,297 +0,0 @@ -#!/usr/bin/env node - -import type { Format, BuildOptions } from "esbuild"; -import * as esbuild from "esbuild"; -import esbuildSvelte from "esbuild-svelte"; -import { glob } from "glob"; -import { sveltePreprocess } from "svelte-preprocess"; -import yargs from "yargs"; -import { hideBin } from "yargs/helpers"; - -const template = ` -import type { Rendered } from "@mateothegreat/dynamic-component-engine"; -import { mount, unmount, type ComponentProps } from "svelte"; -import NAME from "./NAME.svelte"; - -/** - * Handy type alias for the props of the NAME component that can - * be used all over and anywhere. - */ -export type NAMEProps = ComponentProps; - -/** - * The factory function is used to create a new instance of the component - * when being rendered on the receiving side. - * - * This is important because it allows us to have granular control over the component - * lifecycle and not require the receiving side to bear that burden. - * - * @param {HTMLElement} target The target element to mount the component on. - * @param {NAMEProps} props The props to pass to the component. - * - * @returns {Rendered} A Rendered object that contains the component, name, props, and destroy function. - */ -const factory = (target: HTMLElement, props?: NAMEProps): Rendered => { - const component = mount(NAME, { - target, - props: props as NAMEProps - }); - - return { - component, - name: "NAME", - props: props as NAMEProps, - destroy: () => { - console.log("entry.ts -> NAME.svelte", "destroying component", component); - unmount(component); - } - }; -}; - -/** - * Export the factory function as the default export to make it easier - * on the receiving side performing the dynamic import. - */ -export { factory as default }; -`; -// Define the options interface -interface CompilerOptions { - input: string; - output: string; - target: string; - format: string; - debug: boolean; - banner: string; -} - -// Interface for component source input -interface ComponentSource { - name: string; - source: string; - filename?: string; -} - -/** - * Bundle Svelte components using esbuild and esbuild-svelte plugin. - * Now accepts component source as strings instead of file paths. - * - * @param components - Array of component sources or entry file paths. - * @param options - Compiler options containing build configuration. - * - * @returns The output files from the build process. - */ -const bundleSvelte = async ( - components: ComponentSource[] | string[] | string, - options: CompilerOptions -): Promise => { - // Handle backward compatibility with file paths - if (typeof components === 'string' || (Array.isArray(components) && typeof components[0] === 'string')) { - return bundleSvelteFromFiles(components as string[] | string, options); - } - - const componentArray = Array.isArray(components) ? components : [components]; - - if (options.debug) { - console.log(`\nCompiling (${componentArray.length}) component${componentArray.length > 1 ? "s" : ""} from source...`); - } - - // Create virtual file system plugin - const virtualFilePlugin: esbuild.Plugin = { - name: 'virtual-file', - setup(build) { - componentArray.forEach((component) => { - const comp = component as ComponentSource; - const filename = comp.filename || `${comp.name}.ts`; - build.onResolve({ filter: new RegExp(`^virtual:${comp.name}$`) }, () => ({ - path: filename, - namespace: 'virtual' - })); - - build.onLoad({ filter: /.*/, namespace: 'virtual' }, (args) => { - const matchingComponent = componentArray.find((c) => { - const compSource = c as ComponentSource; - const compFilename = compSource.filename || `${compSource.name}.ts`; - return args.path === compFilename; - }) as ComponentSource; - - return { - contents: matchingComponent.source, - loader: filename.endsWith('.svelte') ? 'ts' : 'ts' - }; - }); - }); - } - }; - - const buildOptions: BuildOptions = { - logLevel: options.debug ? "debug" : "info", - entryPoints: componentArray.map(comp => `virtual:${(comp as ComponentSource).name}`), - target: options.target, - format: options.format as Format, - splitting: false, - packages: "external", - banner: { - js: options.banner - }, - bundle: true, - write: false, // Don't write to filesystem, return output files - plugins: [ - virtualFilePlugin, - esbuildSvelte({ - preprocess: sveltePreprocess(), - compilerOptions: { - css: "injected", - preserveComments: true, - preserveWhitespace: true - } - }) - ] - }; - - const build = await esbuild.build(buildOptions); - return build.outputFiles; -}; - -/** - * Bundle Svelte components from file paths (backward compatibility). - */ -const bundleSvelteFromFiles = async ( - entry: string[] | string, - options: CompilerOptions -): Promise => { - if (options.debug) { - console.log( - `\nCompiling (${Array.isArray(entry) ? entry.length : 1}) component${Array.isArray(entry) && entry.length > 1 ? "s" : ""} from files...` - ); - } - - const buildOptions: BuildOptions = { - logLevel: options.debug ? "debug" : "info", - entryPoints: Array.isArray(entry) ? entry : [entry], - target: options.target, - format: options.format as Format, - splitting: false, - packages: "external", - banner: { - js: options.banner - }, - bundle: true, - outdir: options.output, - plugins: [ - esbuildSvelte({ - preprocess: sveltePreprocess(), - compilerOptions: { - css: "injected", - preserveComments: true, - preserveWhitespace: true - } - }) - ] - }; - - const build = await esbuild.build(buildOptions); - return build.outputFiles; -}; - -const argv = yargs(hideBin(process.argv)) - .option("debug", { - alias: "d", - type: "boolean", - description: "Enable debug logging.", - default: false - }) - .option("input", { - alias: "i", - type: "string", - description: "Input glob pattern for component entry files." - }) - .demandOption("input") - .option("output", { - alias: "o", - type: "string", - description: "Path to output the compiled components." - }) - .demandOption("output") - .option("target", { - alias: "t", - type: "string", - description: "Documentation: https://esbuild.github.io/api/#target", - default: "esnext" - }) - .option("format", { - alias: "f", - type: "string", - description: "Documentation: https://esbuild.github.io/api/#format" - }) - .choices("format", ["esm", "cjs"]) - .option("banner", { - alias: "b", - type: "string", - description: - "Banner text to add at the top of compiled files.\nExample:\n1 | // Compiled with esbuild-svelte 🕺\n2 | ..javascript output follows now..", - default: "" - }) - .example([ - ["compile --input=src/**/*.svelte --output=public"], - [ - 'compile --input=src/**/*.svelte --output=public --banner="// Compiled with esbuild-svelte" --debug' - ] - ]) - .version(false) - .help(false) - .wrap(Math.min(120, process.stdout.columns || 120)) - .parseSync(); - -console.log(`Searching for components with "${argv.input}"...`); - -const entries = glob.sync(argv.input); - -entries.forEach((entry) => { - const t = template.replace(/NAME/g, "Tester"); - console.log(entry, t); -}); - -if (entries.length === 0) { - console.error(`\nNo components found with "${argv.input}"`); - process.exit(1); -} - -console.log(`\nCompiling components...`); - -const output = await bundleSvelte(entries, argv as CompilerOptions); - -console.log(`\nCompiled ${output?.length} components.`); - -// Export functions for programmatic use -export { bundleSvelte, bundleSvelteFromFiles, type ComponentSource, type CompilerOptions }; - -/** - * Convenience function to compile a single component from source string. - * - * @param name - Component name - * @param source - Component source code - * @param options - Compiler options - * @returns The compiled output - */ -export const compileComponentFromSource = async ( - name: string, - source: string, - options: Omit -): Promise => { - const component: ComponentSource = { name, source }; - return bundleSvelte([component], { ...options, input: '' }); -}; - -/** - * Compile multiple components from source strings. - * - * @param components - Array of component sources - * @param options - Compiler options - * @returns The compiled outputs - */ -export const compileComponentsFromSource = async ( - components: ComponentSource[], - options: Omit -): Promise => { - return bundleSvelte(components, { ...options, input: '' }); -}; diff --git a/src/compiler/index.ts b/src/compiler/index.ts new file mode 100644 index 0000000..2de1098 --- /dev/null +++ b/src/compiler/index.ts @@ -0,0 +1,2 @@ +export * from "./runtime"; +export { ComponentCompiler1 } from "./runtime.1"; diff --git a/src/compiler/runtime.1.ts b/src/compiler/runtime.1.ts new file mode 100644 index 0000000..a67cbec --- /dev/null +++ b/src/compiler/runtime.1.ts @@ -0,0 +1,361 @@ +// ComponentCompiler.ts +// Runtime Svelte 5 compiler/runner for browser environments. +// +// Design goals: +// - No brittle source rewriting of compiled output. +// - Work natively with Svelte 5's mount/unmount APIs. +// - Allow prop "updates" via predictable remounts (stable, future-proof). +// - Handle CSS for both injected and external modes. +// - Surface warnings and basic metadata to the caller. +// - Provide bounded caching to avoid memory bloat in long-running sessions. + +import { mount, unmount } from "svelte"; +import { compile, type CompileOptions, type CompileResult } from "svelte/compiler"; + +/** + * The result returned by `render()`, including lifecycle controls and metadata. + */ +export interface CompiledComponent { + /** The opaque handle returned by `mount` (not guaranteed stable API). */ + handle: unknown | null; + /** Programmatic teardown of the component and any injected CSS. */ + destroy: () => void; + /** + * Update props. Implementation performs a safe re-mount to avoid reliance on internal instance APIs. + * Designed for live previews—if you need micro-optimized updates, prefer a fixed component tree. + */ + update: (props: Record) => void; + /** Compilation/runtime metadata useful for tooling and diagnostics. */ + result: { + filename?: string; + hasCSS: boolean; + length: number; + warnings: string[]; + /** Whether CSS was injected automatically by Svelte compiler. */ + cssMode: "injected" | "external"; + /** Cache hit (true) or compiled anew (false). */ + fromCache: boolean; + }; +} + +/** + * Options that influence compilation and runtime behavior. + */ +export interface CompilerOptions { + /** + * CSS mode: + * - 'injected': Svelte injects CSS via JS at runtime (best for playgrounds). + * - 'external': Compiler emits CSS separately; we attach/detach a +// `; + +// const component = await SvelteRuntimeCompiler.render( +// source, +// container, +// { color: "red" }, +// { injectCss: true } +// ); + +// expect(component).toBeDefined(); +// }); + +// it("should handle component destruction", async () => { +// const source = ` +// +//

{text}

+// `; + +// const component = await SvelteRuntimeCompiler.render(source, container, { text: "Test" }); + +// expect(container.innerHTML).not.toBe(""); + +// component.destroy(); + +// expect(container.innerHTML).toBe(""); +// }); + +// it("should cache compiled components", async () => { +// const source = ` +// +// {value} +// `; + +// // First render +// const component1 = await SvelteRuntimeCompiler.render(source, container, { value: 1 }); + +// // Destroy first component +// component1.destroy(); + +// // Create new container +// const container2 = document.createElement("div"); +// document.body.appendChild(container2); + +// // Second render (should use cache) +// const component2 = await SvelteRuntimeCompiler.render(source, container2, { value: 2 }); + +// expect(component2).toBeDefined(); + +// // Cleanup +// component2.destroy(); +// container2.parentNode?.removeChild(container2); +// }); + +// it("should handle compilation errors gracefully", async () => { +// const invalidSource = ` +// +//

Test

+// `; + +// await expect(SvelteRuntimeCompiler.render(invalidSource, container)).rejects.toThrow( +// "Component compilation failed" +// ); +// }); + +// it("should use custom component name when provided", async () => { +// const source = ` +// +//

{greeting}

+// `; + +// const component = await SvelteRuntimeCompiler.render( +// source, +// container, +// { greeting: "Hello" }, +// { name: "CustomGreeting" } +// ); + +// expect(component).toBeDefined(); +// // The component name is used internally in the factory +// }); + +// it("should handle components with effects", async () => { +// const source = ` +// +//
+//

Count: {count}

+//

Doubled: {doubled}

+//
+// `; + +// const component = await SvelteRuntimeCompiler.render(source, container); + +// expect(component).toBeDefined(); +// }); + +// it("should log warning when update is called", async () => { +// const consoleSpy = vi.spyOn(console, "warn"); + +// const source = ` +// +//

{text}

+// `; + +// const component = await SvelteRuntimeCompiler.render(source, container, { text: "Test" }); + +// component.update?.({ text: "Updated" }); + +// expect(consoleSpy).toHaveBeenCalledWith( +// "Direct prop updates not supported in runtime compilation. Recreate component instead." +// ); + +// consoleSpy.mockRestore(); +// }); +// }); + +// describe("clearCache", () => { +// it("should clear the component cache", async () => { +// const consoleSpy = vi.spyOn(console, "log"); + +// const source = ` +// +// {value} +// `; + +// // Compile once to populate cache +// const component = await SvelteRuntimeCompiler.render(source, container, { value: 1 }); +// component.destroy(); + +// // Clear cache +// SvelteRuntimeCompiler.clearCache(); + +// expect(consoleSpy).toHaveBeenCalledWith("Component cache cleared"); + +// consoleSpy.mockRestore(); +// }); +// }); +// }); diff --git a/src/compiler/runtime.ts b/src/compiler/runtime.ts new file mode 100644 index 0000000..8f6ce44 --- /dev/null +++ b/src/compiler/runtime.ts @@ -0,0 +1,205 @@ +import { compile, type CompileOptions, type CompileResult } from "svelte/compiler"; + +export interface CompiledComponent { + component: any; + destroy: () => void; + update: (props: Record) => void; + result?: { + name?: string; + filename?: string; + hasCSS?: boolean; + length: number; + }; +} + +export interface CompilerOptions { + css?: boolean; + sourcemap?: boolean; + name?: string; + cache?: boolean; + onMount?: (component: any) => void; + onDestroy?: () => void; + onError?: (error: Error) => void; + useSandbox?: boolean; // Optional: render inside iframe for isolation +} + +/** + * Runtime compiler for Svelte 5 components with reactive props and optional sandboxing. + * Compiles Svelte source strings into live components using mount/unmount APIs. + */ +export class ComponentCompiler { + private static componentCache = new Map(); + + /** + * Dynamically load compiled JavaScript as an ES module. + * Uses Blob URLs to simulate module imports at runtime. + * + * @param source - Transformed JavaScript code. + * + * @returns The component factory function. + */ + private static async loadModule(source: string): Promise { + const blob = new Blob([source], { type: "application/javascript" }); + const url = URL.createObjectURL(blob); + try { + const module = await import(/* @vite-ignore */ url); + URL.revokeObjectURL(url); + return module.default; + } finally { + URL.revokeObjectURL(url); + } + } + + /** + * Compile Svelte source code into JavaScript using Svelte 5 compiler. + * + * @param source - Svelte component source. + * @param options - Compiler options. + * + * @returns CompileResult containing JS and CSS. + */ + private static compileSource(source: string, options: CompilerOptions = {}): CompileResult { + const compileOptions: CompileOptions = { + generate: "client", + css: options.css ? "injected" : "external", + // If true, returns the modern version of the AST. Will become true by default in Svelte 6, and the option will be removed in Svelte 7. + modernAst: true, + // Set to true to force the compiler into runes mode, even if there are no indications of runes usage. Set to false to force the compiler into ignoring runes, even if there are indications of runes usage. Set to undefined (the default) to infer runes mode from the component code. Is always true for JS/TS modules compiled with Svelte. Will be true by default in Svelte 6. Note that setting this to true in your svelte.config.js will force runes mode for your entire project, including components in node_modules, which is likely not what you want. If you're using Vite, consider using dynamicCompileOptions instead. + runes: true, + filename: options.name || "DynamicComponent.svelte" + }; + + return compile(source, compileOptions); + } + + /** + * Transforms compiled JS to wrap the component in a factory using mount/unmount to + * enable lifecycle control and prop injection. + * + * @param code - Compiled JS code. + * @param componentName - Name of the component. + * + * @returns Transformed JS code as string. + */ + private static transformCompiledCode(code: string, componentName: string): string { + const lines = code.split("\n"); + + /** + * Inject mount and unmount imports so that the wrapper can do it's job. + */ + lines.unshift(`import { mount, unmount } from "svelte";`); + + /** + * Replace default export with named component so that when the component is rendered, + * the component is not exported as default but as a named component. + */ + const exportDefault = lines.findIndex((line) => line.startsWith("export default")); + if (exportDefault !== -1) { + const line = lines[exportDefault]; + if (line) { + lines[exportDefault] = line.replace(/export default\s+/, `const ${componentName} = `); + } + } + + /** + * This is to wrap the component in a factory function so that we can: + * 1. Create a new instance of the component. + * 2. Mount the component into the target element. + * 3. Return the component and the destroy function. + */ + lines.push(` + const factory = (target, props) => { + const component = mount(${componentName}, { target, props }); + return { + component, + destroy: () => unmount(component) + }; + }; + export { factory as default }; + `); + + return lines.join("\n"); + } + + /** + * Renders a compiled component into a target DOM element. + * + * @param source - Svelte source code. + * @param target - DOM element to mount into. + * @param props - Initial props. + * @param options - Compiler and lifecycle options. + * + * @returns A compiled component instance. + */ + static async render( + source: string, + target: HTMLElement, + props: Record = {}, + options: CompilerOptions = {} + ): Promise { + try { + const cacheKey = source + JSON.stringify(options); + let fn: Function | undefined; + + if (options.cache) { + fn = this.componentCache.get(cacheKey); + } + + const compiled = this.compileSource(source, options); + /** + * Strip the file extension from the name. + * + * This is to prevent the following error (the name of function is not valid because of the dot): + * ```ts + * const DynamicComponent.svelte = function DynamicComponent($$anchor, $$props) { + * SyntaxError: Missing initializer in const declaration (at 21242b81-0fce-4eb9-8a92-d21cf4e83631:11:7) + * ``` + */ + const componentName = options.name?.replace(/\.(svelte|js)$/, "") || "DynamicComponent"; + const transformedCode = this.transformCompiledCode(compiled.js.code, componentName); + + fn = await this.loadModule(transformedCode); + this.componentCache.set(cacheKey, fn); + + const instance = fn(target, props); + + const component = { + component: instance.component, + destroy: () => { + instance.destroy(); + target.innerHTML = ""; + options.onDestroy?.(); + }, + update: (props: Record) => { + console.log("needs to propagate the update to the component", props); + // instance.update(props); + }, + result: { + name: componentName, + filename: options.name, + hasCSS: !!compiled.css, + length: compiled.js.code.length + } + }; + + /** + * If the caller provides an onMount callback, call it with the component instance + * so that the caller can do something with the newcomponent instance. + */ + options.onMount?.(instance.component); + + return component; + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + options.onError?.(err); + throw err; + } + } + + /** + * Clears the internal component cache. + */ + static clearCache(): void { + this.componentCache.clear(); + } +} diff --git a/src/dynamic-components.ts b/src/dynamic-components.ts index 6129d17..940f2fb 100644 --- a/src/dynamic-components.ts +++ b/src/dynamic-components.ts @@ -47,6 +47,7 @@ export interface RenderOptions { export const load = async (source: string): Promise => { const url = URL.createObjectURL(new Blob([source], { type: "application/javascript" })); const module = await import(/* @vite-ignore */ url); + URL.revokeObjectURL(url); return module.default; }; diff --git a/src/entry.ts b/src/entry.ts deleted file mode 100644 index 0284487..0000000 --- a/src/entry.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { Rendered } from "@mateothegreat/dynamic-component-engine"; -import { mount, unmount, type ComponentProps } from "svelte"; -import NAME from "./NAME.svelte"; - -/** - * Handy type alias for the props of the NAME component that can - * be used all over and anywhere. - */ -export type NAMEProps = ComponentProps; - -/** - * The factory function is used to create a new instance of the component - * when being rendered on the receiving side. - * - * This is important because it allows us to have granular control over the component - * lifecycle and not require the receiving side to bear that burden. - * - * @param {HTMLElement} target The target element to mount the component on. - * @param {NAMEProps} props The props to pass to the component. - * - * @returns {Rendered} A Rendered object that contains the component, name, props, and destroy function. - */ -const factory = (target: HTMLElement, props?: NAMEProps): Rendered => { - const component = mount(NAME, { - target, - props: props as NAMEProps - }); - - return { - component, - name: "NAME", - props: props as NAMEProps, - destroy: () => { - console.log("entry.ts -> NAME.svelte", "destroying component", component); - unmount(component); - } - }; -}; - -/** - * Export the factory function as the default export to make it easier - * on the receiving side performing the dynamic import. - */ -export { factory as default }; diff --git a/src/index.ts b/src/index.ts index d116a90..99e5027 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,4 @@ // Make it easier to import when through package.json (cjs and esm are the usual suspects 🤸‍♀️ ofc). +export * from "./compiler"; export * from "./dynamic-components"; +export * from "./loader"; diff --git a/src/loader.ts b/src/loader.ts new file mode 100644 index 0000000..513f194 --- /dev/null +++ b/src/loader.ts @@ -0,0 +1,19 @@ +import { load, render, type Rendered } from "./dynamic-components"; + +export const loadComponent = async (renderRef: HTMLElement, props: T): Promise> => { + const source = await fetch("/entry.js").then((res) => res.text()); + const fn = await load(source); + + /** + * We pass type type to the render function to infer the type of the props for the + * component. This is useful because we can then use the props type for accessing the + * props of the renderedcomponent. + */ + const component = await render(fn, { + source: source, + target: renderRef!, + props: props + }); + + return component; +}; diff --git a/tsconfig.json b/tsconfig.json index 458d361..59b107e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,10 +14,7 @@ "noUncheckedIndexedAccess": true, "noImplicitReturns": true, "noImplicitThis": true, - "skipLibCheck": true, - "paths": { - "@mateothegreat/dynamic-component-engine": ["./src/dynamic-components.ts"] - } + "skipLibCheck": true }, - "include": ["./src/*"] + "include": ["./src/**/*.ts", "runtime-orig.ts"] } From 642a2ceb5056c28e607c49a15e2f599343a8b185 Mon Sep 17 00:00:00 2001 From: Matthew Davis Date: Fri, 15 Aug 2025 10:00:45 -0500 Subject: [PATCH 14/18] bump --- src/compiler/index.ts | 1 - src/compiler/runtime.1.ts | 361 -------------------------------------- 2 files changed, 362 deletions(-) delete mode 100644 src/compiler/runtime.1.ts diff --git a/src/compiler/index.ts b/src/compiler/index.ts index 2de1098..e3ce6fd 100644 --- a/src/compiler/index.ts +++ b/src/compiler/index.ts @@ -1,2 +1 @@ export * from "./runtime"; -export { ComponentCompiler1 } from "./runtime.1"; diff --git a/src/compiler/runtime.1.ts b/src/compiler/runtime.1.ts deleted file mode 100644 index a67cbec..0000000 --- a/src/compiler/runtime.1.ts +++ /dev/null @@ -1,361 +0,0 @@ -// ComponentCompiler.ts -// Runtime Svelte 5 compiler/runner for browser environments. -// -// Design goals: -// - No brittle source rewriting of compiled output. -// - Work natively with Svelte 5's mount/unmount APIs. -// - Allow prop "updates" via predictable remounts (stable, future-proof). -// - Handle CSS for both injected and external modes. -// - Surface warnings and basic metadata to the caller. -// - Provide bounded caching to avoid memory bloat in long-running sessions. - -import { mount, unmount } from "svelte"; -import { compile, type CompileOptions, type CompileResult } from "svelte/compiler"; - -/** - * The result returned by `render()`, including lifecycle controls and metadata. - */ -export interface CompiledComponent { - /** The opaque handle returned by `mount` (not guaranteed stable API). */ - handle: unknown | null; - /** Programmatic teardown of the component and any injected CSS. */ - destroy: () => void; - /** - * Update props. Implementation performs a safe re-mount to avoid reliance on internal instance APIs. - * Designed for live previews—if you need micro-optimized updates, prefer a fixed component tree. - */ - update: (props: Record) => void; - /** Compilation/runtime metadata useful for tooling and diagnostics. */ - result: { - filename?: string; - hasCSS: boolean; - length: number; - warnings: string[]; - /** Whether CSS was injected automatically by Svelte compiler. */ - cssMode: "injected" | "external"; - /** Cache hit (true) or compiled anew (false). */ - fromCache: boolean; - }; -} - -/** - * Options that influence compilation and runtime behavior. - */ -export interface CompilerOptions { - /** - * CSS mode: - * - 'injected': Svelte injects CSS via JS at runtime (best for playgrounds). - * - 'external': Compiler emits CSS separately; we attach/detach a