@useavalon/avalon 0.1.12 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mod.ts +302 -0
- package/package.json +9 -17
- package/src/build/integration-bundler-plugin.ts +116 -0
- package/src/build/integration-config.ts +168 -0
- package/src/build/integration-detection-plugin.ts +117 -0
- package/src/build/integration-resolver-plugin.ts +90 -0
- package/src/build/island-manifest.ts +269 -0
- package/src/build/island-types-generator.ts +476 -0
- package/src/build/mdx-island-transform.ts +464 -0
- package/src/build/mdx-plugin.ts +98 -0
- package/src/build/page-island-transform.ts +598 -0
- package/src/build/prop-extractors/index.ts +21 -0
- package/src/build/prop-extractors/lit.ts +140 -0
- package/src/build/prop-extractors/qwik.ts +16 -0
- package/src/build/prop-extractors/solid.ts +125 -0
- package/src/build/prop-extractors/svelte.ts +194 -0
- package/src/build/prop-extractors/vue.ts +111 -0
- package/src/build/sidecar-file-manager.ts +104 -0
- package/src/build/sidecar-renderer.ts +30 -0
- package/src/client/adapters/index.ts +21 -0
- package/src/client/components.ts +35 -0
- package/src/client/css-hmr-handler.ts +344 -0
- package/src/client/framework-adapter.ts +462 -0
- package/src/client/hmr-coordinator.ts +396 -0
- package/src/client/hmr-error-overlay.js +533 -0
- package/src/client/main.js +824 -0
- package/src/components/Image.tsx +123 -0
- package/src/components/IslandErrorBoundary.tsx +145 -0
- package/src/components/LayoutDataErrorBoundary.tsx +141 -0
- package/src/components/LayoutErrorBoundary.tsx +127 -0
- package/src/components/PersistentIsland.tsx +52 -0
- package/src/components/StreamingErrorBoundary.tsx +233 -0
- package/src/components/StreamingLayout.tsx +538 -0
- package/src/core/components/component-analyzer.ts +192 -0
- package/src/core/components/component-detection.ts +508 -0
- package/src/core/components/enhanced-framework-detector.ts +500 -0
- package/src/core/components/framework-registry.ts +563 -0
- package/src/core/content/mdx-processor.ts +46 -0
- package/src/core/integrations/index.ts +19 -0
- package/src/core/integrations/loader.ts +125 -0
- package/src/core/integrations/registry.ts +175 -0
- package/src/core/islands/island-persistence.ts +325 -0
- package/src/core/islands/island-state-serializer.ts +258 -0
- package/src/core/islands/persistent-island-context.tsx +80 -0
- package/src/core/islands/use-persistent-state.ts +68 -0
- package/src/core/layout/enhanced-layout-resolver.ts +322 -0
- package/src/core/layout/layout-cache-manager.ts +485 -0
- package/src/core/layout/layout-composer.ts +357 -0
- package/src/core/layout/layout-data-loader.ts +516 -0
- package/src/core/layout/layout-discovery.ts +243 -0
- package/src/core/layout/layout-matcher.ts +299 -0
- package/src/core/layout/layout-types.ts +110 -0
- package/src/core/modules/framework-module-resolver.ts +273 -0
- package/src/islands/component-analysis.ts +213 -0
- package/src/islands/css-utils.ts +565 -0
- package/src/islands/discovery/index.ts +80 -0
- package/src/islands/discovery/registry.ts +340 -0
- package/src/islands/discovery/resolver.ts +477 -0
- package/src/islands/discovery/scanner.ts +386 -0
- package/src/islands/discovery/types.ts +117 -0
- package/src/islands/discovery/validator.ts +544 -0
- package/src/islands/discovery/watcher.ts +368 -0
- package/src/islands/framework-detection.ts +428 -0
- package/src/islands/integration-loader.ts +490 -0
- package/src/islands/island.tsx +565 -0
- package/src/islands/render-cache.ts +550 -0
- package/src/islands/types.ts +80 -0
- package/src/islands/universal-css-collector.ts +157 -0
- package/src/islands/universal-head-collector.ts +137 -0
- package/src/layout-system.ts +218 -0
- package/src/middleware/discovery.ts +268 -0
- package/src/middleware/executor.ts +315 -0
- package/src/middleware/index.ts +76 -0
- package/src/middleware/types.ts +99 -0
- package/src/nitro/build-config.ts +576 -0
- package/src/nitro/config.ts +483 -0
- package/src/nitro/error-handler.ts +636 -0
- package/src/nitro/index.ts +173 -0
- package/src/nitro/island-manifest.ts +584 -0
- package/src/nitro/middleware-adapter.ts +260 -0
- package/src/nitro/renderer.ts +1471 -0
- package/src/nitro/route-discovery.ts +439 -0
- package/src/nitro/types.ts +321 -0
- package/src/render/collect-css.ts +198 -0
- package/src/render/error-pages.ts +79 -0
- package/src/render/isolated-ssr-renderer.ts +654 -0
- package/src/render/ssr.ts +1030 -0
- package/src/schemas/api.ts +30 -0
- package/src/schemas/core.ts +64 -0
- package/src/schemas/index.ts +212 -0
- package/src/schemas/layout.ts +279 -0
- package/src/schemas/routing/index.ts +38 -0
- package/src/schemas/routing.ts +376 -0
- package/src/types/as-island.ts +20 -0
- package/src/types/layout.ts +285 -0
- package/src/types/routing.ts +555 -0
- package/src/types/types.ts +5 -0
- package/src/utils/dev-logger.ts +299 -0
- package/src/utils/fs.ts +151 -0
- package/src/vite-plugin/auto-discover.ts +551 -0
- package/src/vite-plugin/config.ts +266 -0
- package/src/vite-plugin/errors.ts +127 -0
- package/src/vite-plugin/image-optimization.ts +156 -0
- package/src/vite-plugin/integration-activator.ts +126 -0
- package/src/vite-plugin/island-sidecar-plugin.ts +176 -0
- package/src/vite-plugin/module-discovery.ts +189 -0
- package/src/vite-plugin/nitro-integration.ts +1354 -0
- package/src/vite-plugin/plugin.ts +403 -0
- package/src/vite-plugin/types.ts +327 -0
- package/src/vite-plugin/validation.ts +228 -0
- package/dist/mod.js +0 -1
- package/dist/src/build/integration-bundler-plugin.js +0 -1
- package/dist/src/build/integration-config.js +0 -1
- package/dist/src/build/integration-detection-plugin.js +0 -1
- package/dist/src/build/integration-resolver-plugin.js +0 -1
- package/dist/src/build/island-manifest.js +0 -1
- package/dist/src/build/island-types-generator.js +0 -5
- package/dist/src/build/mdx-island-transform.js +0 -2
- package/dist/src/build/mdx-plugin.js +0 -1
- package/dist/src/build/page-island-transform.js +0 -3
- package/dist/src/build/prop-extractors/index.js +0 -1
- package/dist/src/build/prop-extractors/lit.js +0 -1
- package/dist/src/build/prop-extractors/qwik.js +0 -1
- package/dist/src/build/prop-extractors/solid.js +0 -1
- package/dist/src/build/prop-extractors/svelte.js +0 -1
- package/dist/src/build/prop-extractors/vue.js +0 -1
- package/dist/src/build/sidecar-file-manager.js +0 -1
- package/dist/src/build/sidecar-renderer.js +0 -6
- package/dist/src/client/adapters/index.js +0 -1
- package/dist/src/client/components.js +0 -1
- package/dist/src/client/css-hmr-handler.js +0 -1
- package/dist/src/client/framework-adapter.js +0 -13
- package/dist/src/client/hmr-coordinator.js +0 -1
- package/dist/src/client/hmr-error-overlay.js +0 -214
- package/dist/src/client/main.js +0 -39
- package/dist/src/components/Image.js +0 -1
- package/dist/src/components/IslandErrorBoundary.js +0 -1
- package/dist/src/components/LayoutDataErrorBoundary.js +0 -1
- package/dist/src/components/LayoutErrorBoundary.js +0 -1
- package/dist/src/components/PersistentIsland.js +0 -1
- package/dist/src/components/StreamingErrorBoundary.js +0 -1
- package/dist/src/components/StreamingLayout.js +0 -29
- package/dist/src/core/components/component-analyzer.js +0 -1
- package/dist/src/core/components/component-detection.js +0 -5
- package/dist/src/core/components/enhanced-framework-detector.js +0 -1
- package/dist/src/core/components/framework-registry.js +0 -1
- package/dist/src/core/content/mdx-processor.js +0 -1
- package/dist/src/core/integrations/index.js +0 -1
- package/dist/src/core/integrations/loader.js +0 -1
- package/dist/src/core/integrations/registry.js +0 -1
- package/dist/src/core/islands/island-persistence.js +0 -1
- package/dist/src/core/islands/island-state-serializer.js +0 -1
- package/dist/src/core/islands/persistent-island-context.js +0 -1
- package/dist/src/core/islands/use-persistent-state.js +0 -1
- package/dist/src/core/layout/enhanced-layout-resolver.js +0 -1
- package/dist/src/core/layout/layout-cache-manager.js +0 -1
- package/dist/src/core/layout/layout-composer.js +0 -1
- package/dist/src/core/layout/layout-data-loader.js +0 -1
- package/dist/src/core/layout/layout-discovery.js +0 -1
- package/dist/src/core/layout/layout-matcher.js +0 -1
- package/dist/src/core/layout/layout-types.js +0 -1
- package/dist/src/core/modules/framework-module-resolver.js +0 -1
- package/dist/src/islands/component-analysis.js +0 -1
- package/dist/src/islands/css-utils.js +0 -17
- package/dist/src/islands/discovery/index.js +0 -1
- package/dist/src/islands/discovery/registry.js +0 -1
- package/dist/src/islands/discovery/resolver.js +0 -2
- package/dist/src/islands/discovery/scanner.js +0 -1
- package/dist/src/islands/discovery/types.js +0 -1
- package/dist/src/islands/discovery/validator.js +0 -18
- package/dist/src/islands/discovery/watcher.js +0 -1
- package/dist/src/islands/framework-detection.js +0 -1
- package/dist/src/islands/integration-loader.js +0 -1
- package/dist/src/islands/island.js +0 -1
- package/dist/src/islands/render-cache.js +0 -1
- package/dist/src/islands/types.js +0 -1
- package/dist/src/islands/universal-css-collector.js +0 -5
- package/dist/src/islands/universal-head-collector.js +0 -2
- package/dist/src/layout-system.js +0 -1
- package/dist/src/middleware/discovery.js +0 -1
- package/dist/src/middleware/executor.js +0 -1
- package/dist/src/middleware/index.js +0 -1
- package/dist/src/middleware/types.js +0 -1
- package/dist/src/nitro/build-config.js +0 -1
- package/dist/src/nitro/config.js +0 -1
- package/dist/src/nitro/error-handler.js +0 -198
- package/dist/src/nitro/index.js +0 -1
- package/dist/src/nitro/island-manifest.js +0 -2
- package/dist/src/nitro/middleware-adapter.js +0 -1
- package/dist/src/nitro/renderer.js +0 -183
- package/dist/src/nitro/route-discovery.js +0 -1
- package/dist/src/nitro/types.js +0 -1
- package/dist/src/render/collect-css.js +0 -3
- package/dist/src/render/error-pages.js +0 -48
- package/dist/src/render/isolated-ssr-renderer.js +0 -1
- package/dist/src/render/ssr.js +0 -90
- package/dist/src/schemas/api.js +0 -1
- package/dist/src/schemas/core.js +0 -1
- package/dist/src/schemas/index.js +0 -1
- package/dist/src/schemas/layout.js +0 -1
- package/dist/src/schemas/routing/index.js +0 -1
- package/dist/src/schemas/routing.js +0 -1
- package/dist/src/types/as-island.js +0 -1
- package/dist/src/types/layout.js +0 -1
- package/dist/src/types/routing.js +0 -1
- package/dist/src/types/types.js +0 -1
- package/dist/src/utils/dev-logger.js +0 -12
- package/dist/src/utils/fs.js +0 -1
- package/dist/src/vite-plugin/auto-discover.js +0 -1
- package/dist/src/vite-plugin/config.js +0 -1
- package/dist/src/vite-plugin/errors.js +0 -1
- package/dist/src/vite-plugin/image-optimization.js +0 -45
- package/dist/src/vite-plugin/integration-activator.js +0 -1
- package/dist/src/vite-plugin/island-sidecar-plugin.js +0 -1
- package/dist/src/vite-plugin/module-discovery.js +0 -1
- package/dist/src/vite-plugin/nitro-integration.js +0 -42
- package/dist/src/vite-plugin/plugin.js +0 -1
- package/dist/src/vite-plugin/types.js +0 -1
- package/dist/src/vite-plugin/validation.js +0 -2
- /package/{dist/src → src}/client/types/framework-runtime.d.ts +0 -0
- /package/{dist/src → src}/client/types/vite-hmr.d.ts +0 -0
- /package/{dist/src → src}/client/types/vite-virtual-modules.d.ts +0 -0
- /package/{dist/src → src}/layout-system.d.ts +0 -0
- /package/{dist/src → src}/types/image.d.ts +0 -0
- /package/{dist/src → src}/types/index.d.ts +0 -0
- /package/{dist/src → src}/types/island-jsx.d.ts +0 -0
- /package/{dist/src → src}/types/island-prop.d.ts +0 -0
- /package/{dist/src → src}/types/mdx.d.ts +0 -0
- /package/{dist/src → src}/types/urlpattern.d.ts +0 -0
- /package/{dist/src → src}/types/vite-env.d.ts +0 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { FALLBACK_PROPS, type PropExtractionResult } from "./vue.ts";
|
|
2
|
+
|
|
3
|
+
/** Mapping from Lit type constructors to TypeScript type strings */
|
|
4
|
+
const LIT_TYPE_MAP: Record<string, string> = {
|
|
5
|
+
String: "string",
|
|
6
|
+
Number: "number",
|
|
7
|
+
Boolean: "boolean",
|
|
8
|
+
Array: "unknown[]",
|
|
9
|
+
Object: "Record<string, unknown>",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extract props from a Lit element source string.
|
|
14
|
+
*
|
|
15
|
+
* Parses `static properties = { ... }` blocks and maps Lit type
|
|
16
|
+
* constructors to TypeScript types. Properties with `state: true`
|
|
17
|
+
* are excluded (internal state, not public props).
|
|
18
|
+
*
|
|
19
|
+
* Never throws — returns fallback on any failure.
|
|
20
|
+
*/
|
|
21
|
+
export function extractLitProps(source: string): PropExtractionResult {
|
|
22
|
+
try {
|
|
23
|
+
const block = extractStaticPropertiesBlock(source);
|
|
24
|
+
if (block === null) {
|
|
25
|
+
return { propsType: FALLBACK_PROPS, fallback: true };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const props = parsePropertyEntries(block);
|
|
29
|
+
if (props.length === 0) {
|
|
30
|
+
return { propsType: FALLBACK_PROPS, fallback: true };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const fields = props.map((p) => p.name + "?: " + p.tsType).join("; ");
|
|
34
|
+
const propsType = "{ " + fields + " }";
|
|
35
|
+
return { propsType, fallback: false };
|
|
36
|
+
} catch {
|
|
37
|
+
console.warn(
|
|
38
|
+
"[avalon] Failed to extract Lit props — falling back to Record<string, unknown>",
|
|
39
|
+
);
|
|
40
|
+
return { propsType: FALLBACK_PROPS, fallback: true };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extract the content inside `static properties = { ... }`.
|
|
46
|
+
* Uses brace-counting to handle nested objects.
|
|
47
|
+
* Returns the inner content (without outer braces), or null if not found.
|
|
48
|
+
*/
|
|
49
|
+
function extractStaticPropertiesBlock(source: string): string | null {
|
|
50
|
+
const marker = /static\s+properties\s*=\s*\{/;
|
|
51
|
+
const match = marker.exec(source);
|
|
52
|
+
if (!match) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Position of the opening brace
|
|
57
|
+
const openBrace = match.index + match[0].length - 1;
|
|
58
|
+
let depth = 1;
|
|
59
|
+
let i = openBrace + 1;
|
|
60
|
+
|
|
61
|
+
while (i < source.length && depth > 0) {
|
|
62
|
+
if (source[i] === "{") depth++;
|
|
63
|
+
else if (source[i] === "}") depth--;
|
|
64
|
+
i++;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (depth !== 0) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Return content between the outer braces
|
|
72
|
+
return source.slice(openBrace + 1, i - 1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface ParsedProp {
|
|
76
|
+
name: string;
|
|
77
|
+
tsType: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Parse individual property entries from the static properties block content.
|
|
82
|
+
* Each entry looks like: `propName: { type: Constructor, ... }`
|
|
83
|
+
* Filters out entries with `state: true`.
|
|
84
|
+
*/
|
|
85
|
+
function parsePropertyEntries(block: string): ParsedProp[] {
|
|
86
|
+
const props: ParsedProp[] = [];
|
|
87
|
+
|
|
88
|
+
// Match each property entry: name: { ... }
|
|
89
|
+
// We use a regex to find property names followed by `{`, then brace-count
|
|
90
|
+
const entryRegex = /(\w+)\s*:\s*\{/g;
|
|
91
|
+
let entryMatch: RegExpExecArray | null;
|
|
92
|
+
|
|
93
|
+
while ((entryMatch = entryRegex.exec(block)) !== null) {
|
|
94
|
+
const name = entryMatch[1];
|
|
95
|
+
const openIdx = entryMatch.index + entryMatch[0].length - 1;
|
|
96
|
+
|
|
97
|
+
// Extract the balanced { ... } for this entry
|
|
98
|
+
const entryBody = extractBalancedBraces(block, openIdx);
|
|
99
|
+
if (entryBody === null) continue;
|
|
100
|
+
|
|
101
|
+
// Skip state properties
|
|
102
|
+
if (/\bstate\s*:\s*true\b/.test(entryBody)) continue;
|
|
103
|
+
|
|
104
|
+
// Extract the type constructor
|
|
105
|
+
const typeMatch = new RegExp(/\btype\s*:\s*(\w+)/).exec(entryBody);
|
|
106
|
+
const litType = typeMatch ? typeMatch[1] : null;
|
|
107
|
+
const tsType = litType && litType in LIT_TYPE_MAP
|
|
108
|
+
? LIT_TYPE_MAP[litType]
|
|
109
|
+
: "unknown";
|
|
110
|
+
|
|
111
|
+
props.push({ name, tsType });
|
|
112
|
+
|
|
113
|
+
// Advance regex past this entry to avoid re-matching nested braces
|
|
114
|
+
entryRegex.lastIndex = openIdx + (entryBody.length);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return props;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Extract a balanced `{ ... }` block starting at the given index.
|
|
122
|
+
* Returns the content between the braces (excluding outer braces), or null.
|
|
123
|
+
*/
|
|
124
|
+
function extractBalancedBraces(source: string, startIdx: number): string | null {
|
|
125
|
+
if (source[startIdx] !== "{") return null;
|
|
126
|
+
|
|
127
|
+
let depth = 0;
|
|
128
|
+
let i = startIdx;
|
|
129
|
+
|
|
130
|
+
while (i < source.length) {
|
|
131
|
+
if (source[i] === "{") depth++;
|
|
132
|
+
else if (source[i] === "}") depth--;
|
|
133
|
+
if (depth === 0) {
|
|
134
|
+
return source.slice(startIdx + 1, i);
|
|
135
|
+
}
|
|
136
|
+
i++;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PropExtractionResult } from "./vue.ts";
|
|
2
|
+
import { FALLBACK_PROPS } from "./vue.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Extract props type from a Qwik component file.
|
|
6
|
+
* Qwik components use component$(() => ...) — props are typed via the
|
|
7
|
+
* generic parameter or a Props interface. For now we fall back to
|
|
8
|
+
* Record<string, unknown> since Qwik's JSX types aren't Preact-compatible
|
|
9
|
+
* and the sidecar just needs to expose the island prop.
|
|
10
|
+
*/
|
|
11
|
+
export function extractQwikProps(_source: string): PropExtractionResult {
|
|
12
|
+
return {
|
|
13
|
+
propsType: FALLBACK_PROPS,
|
|
14
|
+
fallback: false
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { FALLBACK_PROPS, type PropExtractionResult } from "./vue.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract props from a Solid component source string.
|
|
5
|
+
*
|
|
6
|
+
* Supports two patterns:
|
|
7
|
+
* 1. `export default function Name(props: { ... })` — named function export
|
|
8
|
+
* 2. `export default (props: { ... }) =>` — arrow function export
|
|
9
|
+
*
|
|
10
|
+
* Uses brace-counting to handle nested types in the props parameter.
|
|
11
|
+
* Never throws — returns fallback on any failure.
|
|
12
|
+
*/
|
|
13
|
+
export function extractSolidProps(source: string): PropExtractionResult {
|
|
14
|
+
try {
|
|
15
|
+
// Try named function pattern first
|
|
16
|
+
const namedResult = extractFromNamedFunction(source);
|
|
17
|
+
if (namedResult !== null) {
|
|
18
|
+
return { propsType: namedResult, fallback: false };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Try arrow function pattern
|
|
22
|
+
const arrowResult = extractFromArrowFunction(source);
|
|
23
|
+
if (arrowResult !== null) {
|
|
24
|
+
return { propsType: arrowResult, fallback: false };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return { propsType: FALLBACK_PROPS, fallback: true };
|
|
28
|
+
} catch {
|
|
29
|
+
console.warn(
|
|
30
|
+
"[avalon] Failed to extract Solid props — falling back to Record<string, unknown>",
|
|
31
|
+
);
|
|
32
|
+
return { propsType: FALLBACK_PROPS, fallback: true };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extract props type from `export default function Name(props: { ... })`.
|
|
38
|
+
* Returns the type literal string, or null if not found.
|
|
39
|
+
*/
|
|
40
|
+
function extractFromNamedFunction(source: string): string | null {
|
|
41
|
+
// Match: export default function <Name>(props:
|
|
42
|
+
const regex = /export\s+default\s+function\s+\w+\s*\(\s*props\s*:\s*/;
|
|
43
|
+
const match = regex.exec(source);
|
|
44
|
+
if (!match) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const typeStart = match.index + match[0].length;
|
|
49
|
+
return extractPropsType(source, typeStart);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Extract props type from `export default (props: { ... }) =>`.
|
|
54
|
+
* Returns the type literal string, or null if not found.
|
|
55
|
+
*/
|
|
56
|
+
function extractFromArrowFunction(source: string): string | null {
|
|
57
|
+
// Match: export default (props:
|
|
58
|
+
const regex = /export\s+default\s+\(\s*props\s*:\s*/;
|
|
59
|
+
const match = regex.exec(source);
|
|
60
|
+
if (!match) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const typeStart = match.index + match[0].length;
|
|
65
|
+
return extractPropsType(source, typeStart);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Extract a props type starting at the given index in the source.
|
|
70
|
+
* Handles both inline type literals `{ ... }` using brace-counting
|
|
71
|
+
* and simple type references like `Props`.
|
|
72
|
+
* Returns the trimmed type string, or null if extraction fails.
|
|
73
|
+
*/
|
|
74
|
+
function extractPropsType(source: string, startIdx: number): string | null {
|
|
75
|
+
// Skip leading whitespace
|
|
76
|
+
let i = startIdx;
|
|
77
|
+
while (i < source.length && /\s/.test(source[i])) {
|
|
78
|
+
i++;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (i >= source.length) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// If it starts with `{`, use brace-counting for inline type literal
|
|
86
|
+
if (source[i] === "{") {
|
|
87
|
+
return extractBalancedBraces(source, i);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Otherwise it's a type reference — read until `)` or `,`
|
|
91
|
+
const remaining = source.slice(i);
|
|
92
|
+
const refMatch = new RegExp(/^([A-Za-z_$][\w$]*(?:<[^>]*>)?)/).exec(remaining);
|
|
93
|
+
if (refMatch) {
|
|
94
|
+
return refMatch[1].trim();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Extract a balanced `{ ... }` block starting at the given index.
|
|
102
|
+
* Returns the full string including the outer braces, or null if unbalanced.
|
|
103
|
+
*/
|
|
104
|
+
function extractBalancedBraces(source: string, startIdx: number): string | null {
|
|
105
|
+
if (source[startIdx] !== "{") {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let depth = 0;
|
|
110
|
+
let i = startIdx;
|
|
111
|
+
|
|
112
|
+
while (i < source.length) {
|
|
113
|
+
if (source[i] === "{") {
|
|
114
|
+
depth++;
|
|
115
|
+
} else if (source[i] === "}") {
|
|
116
|
+
depth--;
|
|
117
|
+
if (depth === 0) {
|
|
118
|
+
return source.slice(startIdx, i + 1).trim();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
i++;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return null; // unbalanced
|
|
125
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { FALLBACK_PROPS, type PropExtractionResult } from "./vue.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract props from a Svelte component source string.
|
|
5
|
+
*
|
|
6
|
+
* Supports three patterns:
|
|
7
|
+
* 1. `let { ... }: { ... } = $props()` — inline type literal
|
|
8
|
+
* 2. `let { ... }: TypeName = $props()` or `let name: TypeName = $props()`
|
|
9
|
+
* — named type resolved from an interface/type declaration in the script block
|
|
10
|
+
* 3. `export let name: type` — Svelte 4 style, collected into a type literal
|
|
11
|
+
*
|
|
12
|
+
* Never throws — returns fallback on any failure.
|
|
13
|
+
*/
|
|
14
|
+
export function extractSvelteProps(source: string): PropExtractionResult {
|
|
15
|
+
try {
|
|
16
|
+
const scriptContent = extractScriptContent(source);
|
|
17
|
+
if (scriptContent === null) {
|
|
18
|
+
return { propsType: FALLBACK_PROPS, fallback: true };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Try $props() patterns first (Svelte 5)
|
|
22
|
+
const propsResult = extractFromDollarProps(scriptContent);
|
|
23
|
+
if (propsResult !== null) {
|
|
24
|
+
return { propsType: propsResult, fallback: false };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Try export let pattern (Svelte 4)
|
|
28
|
+
const exportLetResult = extractFromExportLet(scriptContent);
|
|
29
|
+
if (exportLetResult !== null) {
|
|
30
|
+
return { propsType: exportLetResult, fallback: false };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { propsType: FALLBACK_PROPS, fallback: true };
|
|
34
|
+
} catch {
|
|
35
|
+
console.warn(
|
|
36
|
+
"[avalon] Failed to extract Svelte props — falling back to Record<string, unknown>",
|
|
37
|
+
);
|
|
38
|
+
return { propsType: FALLBACK_PROPS, fallback: true };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Extract the content of the `<script>` or `<script lang="ts">` block.
|
|
44
|
+
* Returns `null` if no script block is found.
|
|
45
|
+
*/
|
|
46
|
+
function extractScriptContent(source: string): string | null {
|
|
47
|
+
const scriptRegex = /<script\b[^>]*>([\s\S]*?)<\/script>/i;
|
|
48
|
+
const match = new RegExp(scriptRegex).exec(source);
|
|
49
|
+
return match ? match[1] : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Extract props type from `$props()` call patterns.
|
|
54
|
+
*
|
|
55
|
+
* Handles:
|
|
56
|
+
* - `let { ... }: { ... } = $props()` (inline type literal)
|
|
57
|
+
* - `let { ... }: TypeName = $props()` (named type, destructuring)
|
|
58
|
+
* - `let name: TypeName = $props()` (named type, non-destructuring)
|
|
59
|
+
*/
|
|
60
|
+
function extractFromDollarProps(scriptContent: string): string | null {
|
|
61
|
+
// Match: let <binding> : <type> = $props()
|
|
62
|
+
// The binding can be `{ ... }` (destructuring) or a simple identifier
|
|
63
|
+
const propsCallRegex =
|
|
64
|
+
/let\s+(?:\{[^}]*\}|\w+)\s*:\s*([\s\S]*?)\s*=\s*\$props\s*\(\s*\)/;
|
|
65
|
+
const match = new RegExp(propsCallRegex).exec(scriptContent);
|
|
66
|
+
if (!match) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const typeAnnotation = match[1].trim();
|
|
71
|
+
if (typeAnnotation.length === 0) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// If it starts with `{`, it's an inline type literal — return as-is
|
|
76
|
+
if (typeAnnotation.startsWith("{")) {
|
|
77
|
+
// Validate balanced braces
|
|
78
|
+
if (!areBracesBalanced(typeAnnotation)) {
|
|
79
|
+
console.warn(
|
|
80
|
+
"[avalon] Unbalanced braces in Svelte $props() type — falling back",
|
|
81
|
+
);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return typeAnnotation;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Otherwise it's a named type — resolve from interface/type in the script
|
|
88
|
+
return resolveNamedType(scriptContent, typeAnnotation);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Resolve a named type (interface or type alias) from the script content.
|
|
93
|
+
* Returns the body as a type literal string, or null if not found.
|
|
94
|
+
*/
|
|
95
|
+
function resolveNamedType(
|
|
96
|
+
scriptContent: string,
|
|
97
|
+
typeName: string,
|
|
98
|
+
): string | null {
|
|
99
|
+
// Try interface first: `interface TypeName { ... }`
|
|
100
|
+
const interfaceRegex = new RegExp(
|
|
101
|
+
String.raw`interface\s+${escapeRegex(typeName)}\s*\{`,
|
|
102
|
+
);
|
|
103
|
+
const interfaceMatch = interfaceRegex.exec(scriptContent);
|
|
104
|
+
if (interfaceMatch) {
|
|
105
|
+
const startIdx = interfaceMatch.index + interfaceMatch[0].length - 1; // position of `{`
|
|
106
|
+
const body = extractBalancedBraces(scriptContent, startIdx);
|
|
107
|
+
if (body !== null) {
|
|
108
|
+
return body;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Try type alias: `type TypeName = { ... }`
|
|
113
|
+
const typeAliasRegex = new RegExp(
|
|
114
|
+
String.raw`type\s+${escapeRegex(typeName)}\s*=\s*\{`,
|
|
115
|
+
);
|
|
116
|
+
const typeAliasMatch = typeAliasRegex.exec(scriptContent);
|
|
117
|
+
if (typeAliasMatch) {
|
|
118
|
+
const startIdx =
|
|
119
|
+
typeAliasMatch.index + typeAliasMatch[0].length - 1; // position of `{`
|
|
120
|
+
const body = extractBalancedBraces(scriptContent, startIdx);
|
|
121
|
+
if (body !== null) {
|
|
122
|
+
return body;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Extract a balanced `{ ... }` block starting at the given index.
|
|
131
|
+
* Returns the full string including the outer braces, or null if unbalanced.
|
|
132
|
+
*/
|
|
133
|
+
function extractBalancedBraces(source: string, startIdx: number): string | null {
|
|
134
|
+
if (source[startIdx] !== "{") {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let depth = 0;
|
|
139
|
+
let i = startIdx;
|
|
140
|
+
|
|
141
|
+
while (i < source.length) {
|
|
142
|
+
if (source[i] === "{") {
|
|
143
|
+
depth++;
|
|
144
|
+
} else if (source[i] === "}") {
|
|
145
|
+
depth--;
|
|
146
|
+
if (depth === 0) {
|
|
147
|
+
return source.slice(startIdx, i + 1).trim();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
i++;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return null; // unbalanced
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Extract props from `export let` declarations (Svelte 4 pattern).
|
|
158
|
+
* Collects all `export let name: type` and builds a type literal.
|
|
159
|
+
*/
|
|
160
|
+
function extractFromExportLet(scriptContent: string): string | null {
|
|
161
|
+
const exportLetRegex = /export\s+let\s+(\w+)\s*:\s*([^;=]+)/g;
|
|
162
|
+
const props: string[] = [];
|
|
163
|
+
let match: RegExpExecArray | null;
|
|
164
|
+
|
|
165
|
+
while ((match = exportLetRegex.exec(scriptContent)) !== null) {
|
|
166
|
+
const name = match[1].trim();
|
|
167
|
+
const type = match[2].trim();
|
|
168
|
+
if (name && type) {
|
|
169
|
+
props.push(`${name}: ${type}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (props.length === 0) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return `{ ${props.join("; ")} }`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Check if braces are balanced in a string */
|
|
181
|
+
function areBracesBalanced(str: string): boolean {
|
|
182
|
+
let depth = 0;
|
|
183
|
+
for (const ch of str) {
|
|
184
|
+
if (ch === "{") depth++;
|
|
185
|
+
else if (ch === "}") depth--;
|
|
186
|
+
if (depth < 0) return false;
|
|
187
|
+
}
|
|
188
|
+
return depth === 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Escape special regex characters in a string */
|
|
192
|
+
function escapeRegex(str: string): string {
|
|
193
|
+
return str.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
194
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/** Result of prop extraction */
|
|
2
|
+
export interface PropExtractionResult {
|
|
3
|
+
/** The TypeScript type literal for props, e.g. "{ count?: number }" */
|
|
4
|
+
propsType: string;
|
|
5
|
+
/** Whether extraction succeeded or fell back */
|
|
6
|
+
fallback: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Fallback props type used when extraction fails or no props are found */
|
|
10
|
+
export const FALLBACK_PROPS = "Record<string, unknown>";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extract props from a Vue SFC source string.
|
|
14
|
+
*
|
|
15
|
+
* Looks for `defineProps<{...}>()` inside a `<script setup>` or
|
|
16
|
+
* `<script setup lang="ts">` block. Uses brace-counting to handle
|
|
17
|
+
* nested types within the angle brackets.
|
|
18
|
+
*
|
|
19
|
+
* Never throws — returns fallback on any failure.
|
|
20
|
+
*/
|
|
21
|
+
export function extractVueProps(source: string): PropExtractionResult {
|
|
22
|
+
try {
|
|
23
|
+
// 1. Extract the <script setup ...> block content
|
|
24
|
+
const scriptContent = extractScriptSetupContent(source);
|
|
25
|
+
if (scriptContent === null) {
|
|
26
|
+
return { propsType: FALLBACK_PROPS, fallback: true };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 2. Find defineProps<...>() and extract the type argument
|
|
30
|
+
const propsType = extractDefinePropsType(scriptContent);
|
|
31
|
+
if (propsType === null) {
|
|
32
|
+
return { propsType: FALLBACK_PROPS, fallback: true };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { propsType, fallback: false };
|
|
36
|
+
} catch {
|
|
37
|
+
console.warn(
|
|
38
|
+
"[avalon] Failed to extract Vue props — falling back to Record<string, unknown>",
|
|
39
|
+
);
|
|
40
|
+
return { propsType: FALLBACK_PROPS, fallback: true };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extract the content of the `<script setup>` block from a Vue SFC source.
|
|
46
|
+
* Returns `null` if no `<script setup>` block is found.
|
|
47
|
+
*/
|
|
48
|
+
function extractScriptSetupContent(source: string): string | null {
|
|
49
|
+
// Match <script setup> or <script setup lang="ts"> (and other attrs)
|
|
50
|
+
// The 's' flag makes . match newlines
|
|
51
|
+
const scriptSetupRegex =
|
|
52
|
+
/<script\b[^>]*\bsetup\b[^>]*>([\s\S]*?)<\/script>/i;
|
|
53
|
+
const match = new RegExp(scriptSetupRegex).exec(source);
|
|
54
|
+
return match ? match[1] : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Check that `{` and `}` are balanced in a string */
|
|
58
|
+
function hasBracesBalanced(str: string): boolean {
|
|
59
|
+
let depth = 0;
|
|
60
|
+
for (const ch of str) {
|
|
61
|
+
if (ch === "{") depth++;
|
|
62
|
+
else if (ch === "}") depth--;
|
|
63
|
+
if (depth < 0) return false;
|
|
64
|
+
}
|
|
65
|
+
return depth === 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Extract the type argument from `defineProps<TYPE>()` using angle-bracket
|
|
70
|
+
* counting to handle nested generics and object types.
|
|
71
|
+
*
|
|
72
|
+
* After extraction, validates that braces are balanced in the result.
|
|
73
|
+
* Returns the trimmed type string, or `null` if not found.
|
|
74
|
+
*/
|
|
75
|
+
function extractDefinePropsType(scriptContent: string): string | null {
|
|
76
|
+
const marker = "defineProps<";
|
|
77
|
+
const idx = scriptContent.indexOf(marker);
|
|
78
|
+
if (idx === -1) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const start = idx + marker.length;
|
|
83
|
+
let depth = 1;
|
|
84
|
+
let i = start;
|
|
85
|
+
|
|
86
|
+
while (i < scriptContent.length && depth > 0) {
|
|
87
|
+
const ch = scriptContent[i];
|
|
88
|
+
if (ch === "<") depth++;
|
|
89
|
+
else if (ch === ">") depth--;
|
|
90
|
+
if (depth > 0) i++;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (depth !== 0) {
|
|
94
|
+
console.warn("[avalon] Unbalanced angle brackets in defineProps<...> — falling back");
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const typeStr = scriptContent.slice(start, i).trim();
|
|
99
|
+
if (typeStr.length === 0) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!hasBracesBalanced(typeStr)) {
|
|
104
|
+
console.warn("[avalon] Unbalanced braces in defineProps type — falling back");
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return typeStr;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readFile, writeFile, unlink, stat } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Supported compound extensions for island files, ordered longest-first
|
|
6
|
+
* so that `.solid.tsx` matches before `.tsx`.
|
|
7
|
+
*
|
|
8
|
+
* Sidecar naming strategy per extension type:
|
|
9
|
+
* - `.vue`, `.svelte` → `Name.d.vue.ts`, `Name.d.svelte.ts`
|
|
10
|
+
* (foreign extensions — allowArbitraryExtensions picks these up)
|
|
11
|
+
* - `.lit.ts` → `Name.lit.d.ts`
|
|
12
|
+
* (TypeScript declaration file lookup: for `Name.lit.ts` TS looks for `Name.lit.d.ts`)
|
|
13
|
+
* - `.solid.tsx` → `Name.solid.d.ts`
|
|
14
|
+
* (TypeScript declaration file lookup: for `Name.solid.tsx` TS looks for `Name.solid.d.ts`)
|
|
15
|
+
* - `.qwik.tsx` → `Name.qwik.d.ts`
|
|
16
|
+
* (same as solid.tsx)
|
|
17
|
+
*/
|
|
18
|
+
const COMPOUND_EXTENSIONS = [".solid.tsx", ".qwik.tsx", ".lit.ts", ".svelte", ".vue"];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Extensions where TypeScript's native `.d.ts` declaration lookup applies.
|
|
22
|
+
* For `Name.lit.ts`, TS looks for `Name.lit.d.ts`.
|
|
23
|
+
* For `Name.solid.tsx` / `Name.qwik.tsx`, TS looks for `Name.solid.d.ts` / `Name.qwik.d.ts`.
|
|
24
|
+
*/
|
|
25
|
+
const NATIVE_DECL_EXTENSIONS = new Set([".lit.ts", ".solid.tsx", ".qwik.tsx"]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Compute the sidecar declaration file path for a given island file path.
|
|
29
|
+
*
|
|
30
|
+
* For simple extensions like `.vue`, the sidecar is `Name.d.vue.ts`.
|
|
31
|
+
* For compound extensions like `.lit.ts`, the sidecar is `Name.d.lit.ts`.
|
|
32
|
+
* For `.solid.tsx`, the sidecar is `Name.d.solid.tsx.ts`.
|
|
33
|
+
*/
|
|
34
|
+
export function getSidecarPath(islandFilePath: string): string {
|
|
35
|
+
const dir = path.dirname(islandFilePath);
|
|
36
|
+
const basename = path.basename(islandFilePath);
|
|
37
|
+
|
|
38
|
+
for (const ext of COMPOUND_EXTENSIONS) {
|
|
39
|
+
if (basename.endsWith(ext)) {
|
|
40
|
+
const name = basename.slice(0, -ext.length);
|
|
41
|
+
if (NATIVE_DECL_EXTENSIONS.has(ext)) {
|
|
42
|
+
// For .lit.ts → Name.lit.d.ts (TS declaration file lookup)
|
|
43
|
+
// For .solid.tsx / .qwik.tsx → Name.solid.d.ts / Name.qwik.d.ts
|
|
44
|
+
const innerExt = ext.endsWith(".ts") ? ext.slice(0, -3) : ext.slice(0, -4); // strip .ts or .tsx
|
|
45
|
+
return path.join(dir, `${name}${innerExt}.d.ts`);
|
|
46
|
+
}
|
|
47
|
+
// For .vue/.svelte → Name.d.vue.ts / Name.d.svelte.ts (allowArbitraryExtensions)
|
|
48
|
+
return path.join(dir, `${name}.d${ext}.ts`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fallback
|
|
53
|
+
const ext = path.extname(islandFilePath);
|
|
54
|
+
const name = basename.slice(0, -ext.length);
|
|
55
|
+
return path.join(dir, `${name}.d${ext}.ts`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if a sidecar file is up-to-date by comparing mtimes.
|
|
60
|
+
* Returns `true` if the sidecar exists and is newer than the source file.
|
|
61
|
+
*/
|
|
62
|
+
export async function isSidecarFresh(sourcePath: string, sidecarPath: string): Promise<boolean> {
|
|
63
|
+
try {
|
|
64
|
+
const [sourceStat, sidecarStat] = await Promise.all([
|
|
65
|
+
stat(sourcePath),
|
|
66
|
+
stat(sidecarPath),
|
|
67
|
+
]);
|
|
68
|
+
return sidecarStat.mtimeMs >= sourceStat.mtimeMs;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Write sidecar content only if it differs from the existing file.
|
|
76
|
+
* Returns `true` if a write was performed, `false` if content was already up-to-date.
|
|
77
|
+
*/
|
|
78
|
+
export async function writeSidecarIfChanged(sidecarPath: string, content: string): Promise<boolean> {
|
|
79
|
+
try {
|
|
80
|
+
const existing = await readFile(sidecarPath, "utf-8");
|
|
81
|
+
if (existing === content) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// File doesn't exist yet — that's fine, we'll write it
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await writeFile(sidecarPath, content, "utf-8");
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Delete a sidecar file if it exists.
|
|
94
|
+
* Returns `true` if a file was deleted, `false` if it didn't exist.
|
|
95
|
+
*/
|
|
96
|
+
export async function deleteSidecar(sidecarPath: string): Promise<boolean> {
|
|
97
|
+
try {
|
|
98
|
+
await unlink(sidecarPath);
|
|
99
|
+
return true;
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|