@aprovan/patchwork-compiler 0.1.0 → 0.1.2-dev.6bd527d

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/dist/index.js CHANGED
@@ -182,7 +182,7 @@ async function loadLocalImage(name) {
182
182
  try {
183
183
  const { createRequire } = await import("module");
184
184
  const { readFile } = await import("fs/promises");
185
- const { dirname: dirname2, join } = await import("path");
185
+ const { dirname: dirname3, join: join2 } = await import("path");
186
186
  const require2 = createRequire(import.meta.url);
187
187
  let packageJsonPath;
188
188
  try {
@@ -195,10 +195,10 @@ async function loadLocalImage(name) {
195
195
  const config = safeParseImageConfig(packageJson.patchwork) || DEFAULT_IMAGE_CONFIG;
196
196
  let setup;
197
197
  let mount;
198
- const packageDir = dirname2(packageJsonPath);
198
+ const packageDir = dirname3(packageJsonPath);
199
199
  if (packageJson.main) {
200
200
  try {
201
- const mainPath = join(packageDir, packageJson.main);
201
+ const mainPath = join2(packageDir, packageJson.main);
202
202
  const imageModule = await import(
203
203
  /* webpackIgnore: true */
204
204
  /* @vite-ignore */
@@ -1627,70 +1627,634 @@ var PatchworkCompiler = class {
1627
1627
  }
1628
1628
  };
1629
1629
 
1630
- // src/vfs/store.ts
1631
- var VFSStore = class {
1632
- constructor(backend, root = "") {
1633
- this.backend = backend;
1634
- this.root = root;
1630
+ // src/vfs/core/utils.ts
1631
+ function createFileStats(size, mtime, isDir = false) {
1632
+ return {
1633
+ size,
1634
+ mtime,
1635
+ isFile: () => !isDir,
1636
+ isDirectory: () => isDir
1637
+ };
1638
+ }
1639
+ function createDirEntry(name, isDir) {
1640
+ return {
1641
+ name,
1642
+ isFile: () => !isDir,
1643
+ isDirectory: () => isDir
1644
+ };
1645
+ }
1646
+ function normalizePath2(path) {
1647
+ return path.replace(/\/+/g, "/").replace(/^\/|\/$/g, "");
1648
+ }
1649
+ function dirname2(path) {
1650
+ const normalized = normalizePath2(path);
1651
+ const lastSlash = normalized.lastIndexOf("/");
1652
+ return lastSlash === -1 ? "" : normalized.slice(0, lastSlash);
1653
+ }
1654
+ function join(...parts) {
1655
+ return normalizePath2(parts.filter(Boolean).join("/"));
1656
+ }
1657
+
1658
+ // src/vfs/backends/memory.ts
1659
+ var MemoryBackend = class {
1660
+ files = /* @__PURE__ */ new Map();
1661
+ dirs = /* @__PURE__ */ new Set([""]);
1662
+ watchers = /* @__PURE__ */ new Map();
1663
+ async readFile(path) {
1664
+ const entry = this.files.get(normalizePath2(path));
1665
+ if (!entry) throw new Error(`ENOENT: ${path}`);
1666
+ return entry.content;
1667
+ }
1668
+ async writeFile(path, content) {
1669
+ const normalized = normalizePath2(path);
1670
+ const dir = dirname2(normalized);
1671
+ if (dir && !this.dirs.has(dir)) {
1672
+ throw new Error(`ENOENT: ${dir}`);
1673
+ }
1674
+ const isNew = !this.files.has(normalized);
1675
+ this.files.set(normalized, { content, mtime: /* @__PURE__ */ new Date() });
1676
+ this.emit(isNew ? "create" : "update", normalized);
1677
+ }
1678
+ async unlink(path) {
1679
+ const normalized = normalizePath2(path);
1680
+ if (!this.files.delete(normalized)) {
1681
+ throw new Error(`ENOENT: ${path}`);
1682
+ }
1683
+ this.emit("delete", normalized);
1635
1684
  }
1636
- key(path) {
1637
- return this.root ? `${this.root}/${path}` : path;
1685
+ async stat(path) {
1686
+ const normalized = normalizePath2(path);
1687
+ const entry = this.files.get(normalized);
1688
+ if (entry) {
1689
+ return createFileStats(entry.content.length, entry.mtime, false);
1690
+ }
1691
+ if (this.dirs.has(normalized)) {
1692
+ return createFileStats(0, /* @__PURE__ */ new Date(), true);
1693
+ }
1694
+ throw new Error(`ENOENT: ${path}`);
1695
+ }
1696
+ async mkdir(path, options) {
1697
+ const normalized = normalizePath2(path);
1698
+ if (this.dirs.has(normalized)) return;
1699
+ const parent = dirname2(normalized);
1700
+ if (parent && !this.dirs.has(parent)) {
1701
+ if (options?.recursive) {
1702
+ await this.mkdir(parent, options);
1703
+ } else {
1704
+ throw new Error(`ENOENT: ${parent}`);
1705
+ }
1706
+ }
1707
+ this.dirs.add(normalized);
1708
+ }
1709
+ async readdir(path) {
1710
+ const normalized = normalizePath2(path);
1711
+ if (!this.dirs.has(normalized)) {
1712
+ throw new Error(`ENOENT: ${path}`);
1713
+ }
1714
+ const prefix = normalized ? `${normalized}/` : "";
1715
+ const entries = /* @__PURE__ */ new Map();
1716
+ for (const filePath of this.files.keys()) {
1717
+ if (filePath.startsWith(prefix)) {
1718
+ const rest = filePath.slice(prefix.length);
1719
+ const name = rest.split("/")[0];
1720
+ if (name) entries.set(name, false);
1721
+ }
1722
+ }
1723
+ for (const dirPath of this.dirs) {
1724
+ if (dirPath.startsWith(prefix) && dirPath !== normalized) {
1725
+ const rest = dirPath.slice(prefix.length);
1726
+ const name = rest.split("/")[0];
1727
+ if (name) entries.set(name, true);
1728
+ }
1729
+ }
1730
+ return Array.from(entries).map(
1731
+ ([name, isDir]) => createDirEntry(name, isDir)
1732
+ );
1733
+ }
1734
+ async rmdir(path, options) {
1735
+ const normalized = normalizePath2(path);
1736
+ if (!this.dirs.has(normalized)) {
1737
+ throw new Error(`ENOENT: ${path}`);
1738
+ }
1739
+ const prefix = `${normalized}/`;
1740
+ const hasChildren = [...this.files.keys()].some((p) => p.startsWith(prefix)) || [...this.dirs].some((d) => d.startsWith(prefix));
1741
+ if (hasChildren && !options?.recursive) {
1742
+ throw new Error(`ENOTEMPTY: ${path}`);
1743
+ }
1744
+ if (options?.recursive) {
1745
+ for (const filePath of this.files.keys()) {
1746
+ if (filePath.startsWith(prefix)) {
1747
+ this.files.delete(filePath);
1748
+ this.emit("delete", filePath);
1749
+ }
1750
+ }
1751
+ for (const dirPath of this.dirs) {
1752
+ if (dirPath.startsWith(prefix)) {
1753
+ this.dirs.delete(dirPath);
1754
+ }
1755
+ }
1756
+ }
1757
+ this.dirs.delete(normalized);
1758
+ }
1759
+ async exists(path) {
1760
+ const normalized = normalizePath2(path);
1761
+ return this.files.has(normalized) || this.dirs.has(normalized);
1762
+ }
1763
+ watch(path, callback) {
1764
+ const normalized = normalizePath2(path);
1765
+ let callbacks = this.watchers.get(normalized);
1766
+ if (!callbacks) {
1767
+ callbacks = /* @__PURE__ */ new Set();
1768
+ this.watchers.set(normalized, callbacks);
1769
+ }
1770
+ callbacks.add(callback);
1771
+ return () => callbacks.delete(callback);
1772
+ }
1773
+ emit(event, path) {
1774
+ let current = path;
1775
+ while (true) {
1776
+ const callbacks = this.watchers.get(current);
1777
+ if (callbacks) {
1778
+ for (const cb of callbacks) cb(event, path);
1779
+ }
1780
+ if (!current) break;
1781
+ current = dirname2(current);
1782
+ }
1638
1783
  }
1639
- async getFile(path) {
1640
- const content = await this.backend.get(this.key(path));
1641
- if (!content) return null;
1642
- return { path, content };
1784
+ };
1785
+
1786
+ // src/vfs/core/virtual-fs.ts
1787
+ var VirtualFS = class {
1788
+ changes = /* @__PURE__ */ new Map();
1789
+ listeners = /* @__PURE__ */ new Set();
1790
+ backend;
1791
+ constructor(backend) {
1792
+ this.backend = backend ?? new MemoryBackend();
1793
+ }
1794
+ async readFile(path, encoding) {
1795
+ return this.backend.readFile(path, encoding);
1796
+ }
1797
+ async writeFile(path, content) {
1798
+ const existed = await this.backend.exists(path);
1799
+ await this.backend.writeFile(path, content);
1800
+ this.recordChange(path, existed ? "update" : "create");
1801
+ }
1802
+ async applyRemoteFile(path, content) {
1803
+ await this.backend.writeFile(path, content);
1804
+ }
1805
+ async applyRemoteDelete(path) {
1806
+ try {
1807
+ if (await this.backend.exists(path)) {
1808
+ await this.backend.unlink(path);
1809
+ }
1810
+ } catch {
1811
+ return;
1812
+ }
1643
1813
  }
1644
- async putFile(file) {
1645
- await this.backend.put(this.key(file.path), file.content);
1814
+ async unlink(path) {
1815
+ await this.backend.unlink(path);
1816
+ this.recordChange(path, "delete");
1646
1817
  }
1647
- async deleteFile(path) {
1648
- await this.backend.delete(this.key(path));
1818
+ async stat(path) {
1819
+ return this.backend.stat(path);
1649
1820
  }
1650
- async listFiles(prefix) {
1651
- const fullPrefix = prefix ? this.key(prefix) : this.root;
1652
- const paths = await this.backend.list(fullPrefix);
1653
- return paths.map((p) => this.root ? p.slice(this.root.length + 1) : p);
1821
+ async mkdir(path, options) {
1822
+ return this.backend.mkdir(path, options);
1654
1823
  }
1655
- async loadProject(id) {
1656
- const paths = await this.listFiles(id);
1657
- if (paths.length === 0) return null;
1658
- const files = /* @__PURE__ */ new Map();
1659
- await Promise.all(
1660
- paths.map(async (path) => {
1661
- const file = await this.getFile(path);
1662
- if (file) files.set(path.slice(id.length + 1), file);
1663
- })
1664
- );
1665
- return { id, entry: resolveEntry(files), files };
1824
+ async readdir(path) {
1825
+ return this.backend.readdir(path);
1666
1826
  }
1667
- async saveProject(project) {
1668
- await Promise.all(
1669
- Array.from(project.files.values()).map(
1670
- (file) => this.putFile({ ...file, path: `${project.id}/${file.path}` })
1671
- )
1672
- );
1827
+ async rmdir(path, options) {
1828
+ return this.backend.rmdir(path, options);
1829
+ }
1830
+ async exists(path) {
1831
+ return this.backend.exists(path);
1832
+ }
1833
+ watch(path, callback) {
1834
+ if (this.backend.watch) {
1835
+ return this.backend.watch(path, callback);
1836
+ }
1837
+ return () => {
1838
+ };
1839
+ }
1840
+ /**
1841
+ * Get all pending changes since last sync
1842
+ */
1843
+ getChanges() {
1844
+ return Array.from(this.changes.values());
1845
+ }
1846
+ /**
1847
+ * Clear change tracking (after successful sync)
1848
+ */
1849
+ clearChanges() {
1850
+ this.changes.clear();
1851
+ }
1852
+ /**
1853
+ * Mark specific paths as synced
1854
+ */
1855
+ markSynced(paths) {
1856
+ for (const path of paths) {
1857
+ this.changes.delete(path);
1858
+ }
1859
+ }
1860
+ /**
1861
+ * Subscribe to change events
1862
+ */
1863
+ onChange(listener) {
1864
+ this.listeners.add(listener);
1865
+ return () => this.listeners.delete(listener);
1866
+ }
1867
+ recordChange(path, type) {
1868
+ const record = { path, type, mtime: /* @__PURE__ */ new Date() };
1869
+ this.changes.set(path, record);
1870
+ for (const listener of this.listeners) {
1871
+ listener(record);
1872
+ }
1873
+ }
1874
+ };
1875
+
1876
+ // src/vfs/sync/differ.ts
1877
+ function hashContent2(content) {
1878
+ let hash = 2166136261;
1879
+ for (let i = 0; i < content.length; i += 1) {
1880
+ hash ^= content.charCodeAt(i);
1881
+ hash = hash + (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24) >>> 0;
1882
+ }
1883
+ return hash.toString(16).padStart(8, "0");
1884
+ }
1885
+ async function readChecksum(provider, path) {
1886
+ try {
1887
+ const content = await provider.readFile(path);
1888
+ return hashContent2(content);
1889
+ } catch {
1890
+ return void 0;
1891
+ }
1892
+ }
1893
+ async function readChecksums(local, localPath, remote, remotePath) {
1894
+ const [localChecksum, remoteChecksum] = await Promise.all([
1895
+ readChecksum(local, localPath),
1896
+ readChecksum(remote, remotePath)
1897
+ ]);
1898
+ return { local: localChecksum, remote: remoteChecksum };
1899
+ }
1900
+
1901
+ // src/vfs/sync/resolver.ts
1902
+ function resolveConflict(input) {
1903
+ if (input.remoteMtime <= input.changeMtime) return null;
1904
+ if (input.localChecksum && input.remoteChecksum && input.localChecksum === input.remoteChecksum) {
1905
+ return null;
1906
+ }
1907
+ const conflict = {
1908
+ path: input.path,
1909
+ localMtime: input.changeMtime,
1910
+ remoteMtime: input.remoteMtime
1911
+ };
1912
+ switch (input.strategy) {
1913
+ case "local-wins":
1914
+ conflict.resolved = "local";
1915
+ break;
1916
+ case "remote-wins":
1917
+ conflict.resolved = "remote";
1918
+ break;
1919
+ case "newest-wins":
1920
+ conflict.resolved = input.remoteMtime > input.changeMtime ? "remote" : "local";
1921
+ break;
1922
+ case "manual":
1923
+ break;
1924
+ }
1925
+ return conflict;
1926
+ }
1927
+
1928
+ // src/vfs/sync/engine.ts
1929
+ var SyncEngineImpl = class {
1930
+ constructor(local, remote, config = {}) {
1931
+ this.local = local;
1932
+ this.remote = remote;
1933
+ this.conflictStrategy = config.conflictStrategy ?? "local-wins";
1934
+ this.basePath = config.basePath ?? "";
1935
+ this.startRemoteWatch();
1936
+ }
1937
+ status = "idle";
1938
+ intervalId;
1939
+ listeners = /* @__PURE__ */ new Map();
1940
+ conflictStrategy;
1941
+ basePath;
1942
+ async sync() {
1943
+ if (this.status === "syncing") {
1944
+ return { pushed: 0, pulled: 0, conflicts: [] };
1945
+ }
1946
+ this.setStatus("syncing");
1947
+ const result = { pushed: 0, pulled: 0, conflicts: [] };
1948
+ try {
1949
+ const localChanges = this.local.getChanges();
1950
+ const localChangeMap = new Map(
1951
+ localChanges.map((change) => [change.path, change])
1952
+ );
1953
+ const syncedPaths = [];
1954
+ const remoteFiles = await this.listFiles(this.remote, this.basePath);
1955
+ const localFiles = await this.listFiles(this.local, "");
1956
+ const remoteLocalPaths = new Set(
1957
+ remoteFiles.map((path) => this.localPath(path))
1958
+ );
1959
+ for (const remotePath of remoteFiles) {
1960
+ const localPath = this.localPath(remotePath);
1961
+ const localChange = localChangeMap.get(localPath);
1962
+ if (localChange) {
1963
+ const conflict = await this.checkConflict(localChange, remotePath);
1964
+ if (conflict) {
1965
+ result.conflicts.push(conflict);
1966
+ this.emit("conflict", conflict);
1967
+ if (conflict.resolved === "remote") {
1968
+ if (await this.pullRemoteFile(localPath, remotePath)) {
1969
+ result.pulled++;
1970
+ this.emit("change", {
1971
+ path: localPath,
1972
+ type: "update",
1973
+ mtime: /* @__PURE__ */ new Date()
1974
+ });
1975
+ }
1976
+ syncedPaths.push(localPath);
1977
+ }
1978
+ }
1979
+ continue;
1980
+ }
1981
+ if (await this.pullRemoteFile(localPath, remotePath)) {
1982
+ result.pulled++;
1983
+ this.emit("change", {
1984
+ path: localPath,
1985
+ type: "update",
1986
+ mtime: /* @__PURE__ */ new Date()
1987
+ });
1988
+ }
1989
+ }
1990
+ for (const localPath of localFiles) {
1991
+ if (remoteLocalPaths.has(localPath)) continue;
1992
+ if (localChangeMap.has(localPath)) continue;
1993
+ await this.local.applyRemoteDelete(localPath);
1994
+ result.pulled++;
1995
+ this.emit("change", {
1996
+ path: localPath,
1997
+ type: "delete",
1998
+ mtime: /* @__PURE__ */ new Date()
1999
+ });
2000
+ }
2001
+ for (const change of localChanges) {
2002
+ if (syncedPaths.includes(change.path)) continue;
2003
+ const remotePath = this.remotePath(change.path);
2004
+ try {
2005
+ const conflict = await this.checkConflict(change, remotePath);
2006
+ if (conflict) {
2007
+ result.conflicts.push(conflict);
2008
+ this.emit("conflict", conflict);
2009
+ if (conflict.resolved === "remote") {
2010
+ if (await this.pullRemoteFile(change.path, remotePath)) {
2011
+ result.pulled++;
2012
+ this.emit("change", {
2013
+ path: change.path,
2014
+ type: "update",
2015
+ mtime: /* @__PURE__ */ new Date()
2016
+ });
2017
+ }
2018
+ syncedPaths.push(change.path);
2019
+ }
2020
+ if (conflict.resolved !== "local") continue;
2021
+ }
2022
+ if (change.type === "delete") {
2023
+ if (await this.remote.exists(remotePath)) {
2024
+ await this.remote.unlink(remotePath);
2025
+ }
2026
+ result.pushed++;
2027
+ syncedPaths.push(change.path);
2028
+ this.emit("change", change);
2029
+ continue;
2030
+ }
2031
+ const content = await this.local.readFile(change.path);
2032
+ await this.remote.writeFile(remotePath, content);
2033
+ result.pushed++;
2034
+ syncedPaths.push(change.path);
2035
+ this.emit("change", change);
2036
+ } catch (err) {
2037
+ this.emit(
2038
+ "error",
2039
+ err instanceof Error ? err : new Error(String(err))
2040
+ );
2041
+ }
2042
+ }
2043
+ if (syncedPaths.length > 0) {
2044
+ this.local.markSynced(syncedPaths);
2045
+ }
2046
+ this.setStatus("idle");
2047
+ } catch (err) {
2048
+ this.setStatus("error");
2049
+ this.emit("error", err instanceof Error ? err : new Error(String(err)));
2050
+ }
2051
+ return result;
2052
+ }
2053
+ startAutoSync(intervalMs) {
2054
+ this.stopAutoSync();
2055
+ this.intervalId = setInterval(() => this.sync(), intervalMs);
2056
+ }
2057
+ stopAutoSync() {
2058
+ if (this.intervalId) {
2059
+ clearInterval(this.intervalId);
2060
+ this.intervalId = void 0;
2061
+ }
2062
+ }
2063
+ on(event, callback) {
2064
+ let set = this.listeners.get(event);
2065
+ if (!set) {
2066
+ set = /* @__PURE__ */ new Set();
2067
+ this.listeners.set(event, set);
2068
+ }
2069
+ set.add(callback);
2070
+ return () => set.delete(callback);
2071
+ }
2072
+ emit(event, data) {
2073
+ const set = this.listeners.get(event);
2074
+ if (set) {
2075
+ for (const cb of set) cb(data);
2076
+ }
2077
+ }
2078
+ setStatus(status) {
2079
+ this.status = status;
2080
+ this.emit("status", status);
2081
+ }
2082
+ remotePath(localPath) {
2083
+ return this.basePath ? join(this.basePath, localPath) : localPath;
2084
+ }
2085
+ localPath(remotePath) {
2086
+ if (!this.basePath) return normalizePath2(remotePath);
2087
+ const normalized = normalizePath2(remotePath);
2088
+ const base = normalizePath2(this.basePath);
2089
+ if (normalized === base) return "";
2090
+ if (normalized.startsWith(`${base}/`)) {
2091
+ return normalized.slice(base.length + 1);
2092
+ }
2093
+ return normalized;
2094
+ }
2095
+ async listFiles(provider, basePath) {
2096
+ const normalized = normalizePath2(basePath);
2097
+ let entries = [];
2098
+ try {
2099
+ entries = await provider.readdir(normalized);
2100
+ } catch {
2101
+ return [];
2102
+ }
2103
+ const results = [];
2104
+ for (const entry of entries) {
2105
+ const entryPath = normalized ? `${normalized}/${entry.name}` : entry.name;
2106
+ if (entry.isDirectory()) {
2107
+ results.push(...await this.listFiles(provider, entryPath));
2108
+ } else {
2109
+ results.push(entryPath);
2110
+ }
2111
+ }
2112
+ return results;
2113
+ }
2114
+ async pullRemoteFile(localPath, remotePath) {
2115
+ let localContent = null;
2116
+ try {
2117
+ if (await this.local.exists(localPath)) {
2118
+ localContent = await this.local.readFile(localPath);
2119
+ }
2120
+ } catch {
2121
+ localContent = null;
2122
+ }
2123
+ const remoteContent = await this.remote.readFile(remotePath);
2124
+ if (localContent === remoteContent) return false;
2125
+ await this.local.applyRemoteFile(localPath, remoteContent);
2126
+ return true;
2127
+ }
2128
+ startRemoteWatch() {
2129
+ if (!this.remote.watch) return;
2130
+ this.remote.watch(this.basePath, (event, path) => {
2131
+ void this.handleRemoteEvent(event, path);
2132
+ });
2133
+ }
2134
+ async handleRemoteEvent(event, remotePath) {
2135
+ const localPath = this.localPath(remotePath);
2136
+ const localChange = this.local.getChanges().find((change) => change.path === localPath);
2137
+ if (localChange) {
2138
+ const conflict = await this.checkRemoteEventConflict(
2139
+ localChange,
2140
+ remotePath,
2141
+ event
2142
+ );
2143
+ if (conflict) {
2144
+ this.emit("conflict", conflict);
2145
+ if (conflict.resolved === "remote") {
2146
+ await this.applyRemoteEvent(event, localPath, remotePath);
2147
+ this.local.markSynced([localPath]);
2148
+ this.emit("change", {
2149
+ path: localPath,
2150
+ type: event,
2151
+ mtime: /* @__PURE__ */ new Date()
2152
+ });
2153
+ }
2154
+ return;
2155
+ }
2156
+ return;
2157
+ }
2158
+ try {
2159
+ await this.applyRemoteEvent(event, localPath, remotePath);
2160
+ this.emit("change", {
2161
+ path: localPath,
2162
+ type: event,
2163
+ mtime: /* @__PURE__ */ new Date()
2164
+ });
2165
+ } catch (err) {
2166
+ this.emit("error", err instanceof Error ? err : new Error(String(err)));
2167
+ }
2168
+ }
2169
+ async checkConflict(change, remotePath) {
2170
+ try {
2171
+ const remoteStat = await this.remote.stat(remotePath);
2172
+ if (remoteStat.mtime <= change.mtime) return null;
2173
+ const checksums = await readChecksums(
2174
+ this.local,
2175
+ change.path,
2176
+ this.remote,
2177
+ remotePath
2178
+ );
2179
+ return resolveConflict({
2180
+ path: change.path,
2181
+ changeMtime: change.mtime,
2182
+ remoteMtime: remoteStat.mtime,
2183
+ localChecksum: checksums.local,
2184
+ remoteChecksum: checksums.remote,
2185
+ strategy: this.conflictStrategy
2186
+ });
2187
+ } catch {
2188
+ return null;
2189
+ }
2190
+ }
2191
+ async checkRemoteEventConflict(change, remotePath, event) {
2192
+ if (event === "delete") {
2193
+ if (change.type === "delete") return null;
2194
+ return resolveConflict({
2195
+ path: change.path,
2196
+ changeMtime: change.mtime,
2197
+ remoteMtime: /* @__PURE__ */ new Date(),
2198
+ strategy: this.conflictStrategy
2199
+ });
2200
+ }
2201
+ try {
2202
+ const remoteStat = await this.remote.stat(remotePath);
2203
+ if (remoteStat.mtime <= change.mtime) return null;
2204
+ const checksums = await readChecksums(
2205
+ this.local,
2206
+ change.path,
2207
+ this.remote,
2208
+ remotePath
2209
+ );
2210
+ return resolveConflict({
2211
+ path: change.path,
2212
+ changeMtime: change.mtime,
2213
+ remoteMtime: remoteStat.mtime,
2214
+ localChecksum: checksums.local,
2215
+ remoteChecksum: checksums.remote,
2216
+ strategy: this.conflictStrategy
2217
+ });
2218
+ } catch {
2219
+ return null;
2220
+ }
2221
+ }
2222
+ async applyRemoteEvent(event, localPath, remotePath) {
2223
+ if (event === "delete") {
2224
+ await this.local.applyRemoteDelete(localPath);
2225
+ return;
2226
+ }
2227
+ const content = await this.remote.readFile(remotePath);
2228
+ await this.local.applyRemoteFile(localPath, content);
1673
2229
  }
1674
2230
  };
1675
2231
 
1676
2232
  // src/vfs/backends/indexeddb.ts
1677
2233
  var DB_NAME = "patchwork-vfs";
1678
- var STORE_NAME = "files";
2234
+ var DB_VERSION = 2;
2235
+ var FILES_STORE = "files";
2236
+ var DIRS_STORE = "dirs";
1679
2237
  function openDB() {
1680
2238
  return new Promise((resolve, reject) => {
1681
- const request = indexedDB.open(DB_NAME, 1);
2239
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
1682
2240
  request.onerror = () => reject(request.error);
1683
2241
  request.onsuccess = () => resolve(request.result);
1684
- request.onupgradeneeded = () => {
1685
- request.result.createObjectStore(STORE_NAME);
2242
+ request.onupgradeneeded = (event) => {
2243
+ const db = request.result;
2244
+ if (!db.objectStoreNames.contains(FILES_STORE)) {
2245
+ db.createObjectStore(FILES_STORE);
2246
+ }
2247
+ if (!db.objectStoreNames.contains(DIRS_STORE)) {
2248
+ db.createObjectStore(DIRS_STORE);
2249
+ }
1686
2250
  };
1687
2251
  });
1688
2252
  }
1689
- function withStore(mode, fn) {
2253
+ function withStore(storeName, mode, fn) {
1690
2254
  return openDB().then(
1691
2255
  (db) => new Promise((resolve, reject) => {
1692
- const tx = db.transaction(STORE_NAME, mode);
1693
- const store = tx.objectStore(STORE_NAME);
2256
+ const tx = db.transaction(storeName, mode);
2257
+ const store = tx.objectStore(storeName);
1694
2258
  const request = fn(store);
1695
2259
  request.onerror = () => reject(request.error);
1696
2260
  request.onsuccess = () => resolve(request.result);
@@ -1702,107 +2266,419 @@ var IndexedDBBackend = class {
1702
2266
  this.prefix = prefix;
1703
2267
  }
1704
2268
  key(path) {
1705
- return `${this.prefix}:${path}`;
2269
+ return `${this.prefix}:${normalizePath2(path)}`;
1706
2270
  }
1707
- async get(path) {
1708
- const result = await withStore(
2271
+ async readFile(path) {
2272
+ const record = await withStore(
2273
+ FILES_STORE,
1709
2274
  "readonly",
1710
2275
  (store) => store.get(this.key(path))
1711
2276
  );
1712
- return result ?? null;
2277
+ if (!record) throw new Error(`ENOENT: ${path}`);
2278
+ return record.content;
1713
2279
  }
1714
- async put(path, content) {
1715
- await withStore("readwrite", (store) => store.put(content, this.key(path)));
2280
+ async writeFile(path, content) {
2281
+ const dir = dirname2(normalizePath2(path));
2282
+ if (dir && !await this.dirExists(dir)) {
2283
+ throw new Error(`ENOENT: ${dir}`);
2284
+ }
2285
+ const record = { content, mtime: Date.now() };
2286
+ await withStore(
2287
+ FILES_STORE,
2288
+ "readwrite",
2289
+ (store) => store.put(record, this.key(path))
2290
+ );
2291
+ }
2292
+ async unlink(path) {
2293
+ await withStore(
2294
+ FILES_STORE,
2295
+ "readwrite",
2296
+ (store) => store.delete(this.key(path))
2297
+ );
2298
+ }
2299
+ async stat(path) {
2300
+ const normalized = normalizePath2(path);
2301
+ const record = await withStore(
2302
+ FILES_STORE,
2303
+ "readonly",
2304
+ (store) => store.get(this.key(normalized))
2305
+ );
2306
+ if (record) {
2307
+ return createFileStats(
2308
+ record.content.length,
2309
+ new Date(record.mtime),
2310
+ false
2311
+ );
2312
+ }
2313
+ if (await this.dirExists(normalized)) {
2314
+ return createFileStats(0, /* @__PURE__ */ new Date(), true);
2315
+ }
2316
+ throw new Error(`ENOENT: ${path}`);
2317
+ }
2318
+ async mkdir(path, options) {
2319
+ const normalized = normalizePath2(path);
2320
+ if (await this.dirExists(normalized)) return;
2321
+ const parent = dirname2(normalized);
2322
+ if (parent && !await this.dirExists(parent)) {
2323
+ if (options?.recursive) {
2324
+ await this.mkdir(parent, options);
2325
+ } else {
2326
+ throw new Error(`ENOENT: ${parent}`);
2327
+ }
2328
+ }
2329
+ await withStore(
2330
+ DIRS_STORE,
2331
+ "readwrite",
2332
+ (store) => store.put(Date.now(), this.key(normalized))
2333
+ );
1716
2334
  }
1717
- async delete(path) {
1718
- await withStore("readwrite", (store) => store.delete(this.key(path)));
2335
+ async readdir(path) {
2336
+ const normalized = normalizePath2(path);
2337
+ if (normalized && !await this.dirExists(normalized)) {
2338
+ throw new Error(`ENOENT: ${path}`);
2339
+ }
2340
+ const prefix = normalized ? `${this.key(normalized)}/` : `${this.prefix}:`;
2341
+ const entries = /* @__PURE__ */ new Map();
2342
+ const fileKeys = await withStore(
2343
+ FILES_STORE,
2344
+ "readonly",
2345
+ (store) => store.getAllKeys()
2346
+ );
2347
+ for (const key of fileKeys) {
2348
+ if (key.startsWith(prefix)) {
2349
+ const rest = key.slice(prefix.length);
2350
+ const name = rest.split("/")[0];
2351
+ if (name && !rest.includes("/")) entries.set(name, false);
2352
+ }
2353
+ }
2354
+ const dirKeys = await withStore(
2355
+ DIRS_STORE,
2356
+ "readonly",
2357
+ (store) => store.getAllKeys()
2358
+ );
2359
+ for (const key of dirKeys) {
2360
+ if (key.startsWith(prefix)) {
2361
+ const rest = key.slice(prefix.length);
2362
+ const name = rest.split("/")[0];
2363
+ if (name && !rest.includes("/")) entries.set(name, true);
2364
+ }
2365
+ }
2366
+ return Array.from(entries).map(
2367
+ ([name, isDir]) => createDirEntry(name, isDir)
2368
+ );
1719
2369
  }
1720
- async list(prefix) {
1721
- const keyPrefix = prefix ? this.key(prefix) : this.key("");
1722
- const allKeys = await withStore("readonly", (store) => store.getAllKeys());
1723
- return allKeys.filter((k) => k.startsWith(keyPrefix)).map((k) => k.slice(this.prefix.length + 1));
2370
+ async rmdir(path, options) {
2371
+ const normalized = normalizePath2(path);
2372
+ if (!await this.dirExists(normalized)) {
2373
+ throw new Error(`ENOENT: ${path}`);
2374
+ }
2375
+ const prefix = `${this.key(normalized)}/`;
2376
+ if (options?.recursive) {
2377
+ const fileKeys = await withStore(
2378
+ FILES_STORE,
2379
+ "readonly",
2380
+ (store) => store.getAllKeys()
2381
+ );
2382
+ for (const key of fileKeys) {
2383
+ if (key.startsWith(prefix)) {
2384
+ await withStore(
2385
+ FILES_STORE,
2386
+ "readwrite",
2387
+ (store) => store.delete(key)
2388
+ );
2389
+ }
2390
+ }
2391
+ const dirKeys = await withStore(
2392
+ DIRS_STORE,
2393
+ "readonly",
2394
+ (store) => store.getAllKeys()
2395
+ );
2396
+ for (const key of dirKeys) {
2397
+ if (key.startsWith(prefix)) {
2398
+ await withStore(
2399
+ DIRS_STORE,
2400
+ "readwrite",
2401
+ (store) => store.delete(key)
2402
+ );
2403
+ }
2404
+ }
2405
+ }
2406
+ await withStore(
2407
+ DIRS_STORE,
2408
+ "readwrite",
2409
+ (store) => store.delete(this.key(normalized))
2410
+ );
1724
2411
  }
1725
2412
  async exists(path) {
1726
- return await this.get(path) !== null;
2413
+ const normalized = normalizePath2(path);
2414
+ const record = await withStore(
2415
+ FILES_STORE,
2416
+ "readonly",
2417
+ (store) => store.get(this.key(normalized))
2418
+ );
2419
+ if (record) return true;
2420
+ return this.dirExists(normalized);
2421
+ }
2422
+ async dirExists(path) {
2423
+ if (!path) return true;
2424
+ const result = await withStore(
2425
+ DIRS_STORE,
2426
+ "readonly",
2427
+ (store) => store.get(this.key(path))
2428
+ );
2429
+ return result !== void 0;
1727
2430
  }
1728
2431
  };
1729
2432
 
1730
- // src/vfs/backends/local-fs.ts
1731
- var LocalFSBackend = class {
2433
+ // src/vfs/backends/http.ts
2434
+ var HttpBackend = class {
1732
2435
  constructor(config) {
1733
2436
  this.config = config;
1734
2437
  }
1735
- async get(path) {
1736
- const res = await fetch(`${this.config.baseUrl}/${path}`);
1737
- if (!res.ok) return null;
2438
+ async readFile(path) {
2439
+ const res = await fetch(this.url(path));
2440
+ if (!res.ok) throw new Error(`ENOENT: ${path}`);
1738
2441
  return res.text();
1739
2442
  }
1740
- async put(path, content) {
1741
- await fetch(`${this.config.baseUrl}/${path}`, {
2443
+ async writeFile(path, content) {
2444
+ const res = await fetch(this.url(path), {
1742
2445
  method: "PUT",
1743
- body: content
2446
+ body: content,
2447
+ headers: { "Content-Type": "text/plain" }
1744
2448
  });
1745
- }
1746
- async delete(path) {
1747
- await fetch(`${this.config.baseUrl}/${path}`, { method: "DELETE" });
1748
- }
1749
- async list(prefix) {
1750
- const url = prefix ? `${this.config.baseUrl}?prefix=${encodeURIComponent(prefix)}` : this.config.baseUrl;
1751
- const res = await fetch(url);
1752
- return res.json();
2449
+ if (!res.ok) throw new Error(`Failed to write: ${path}`);
2450
+ }
2451
+ async unlink(path) {
2452
+ const res = await fetch(this.url(path), { method: "DELETE" });
2453
+ if (!res.ok) throw new Error(`Failed to delete: ${path}`);
2454
+ }
2455
+ async stat(path) {
2456
+ const res = await fetch(this.url(path, { stat: "true" }));
2457
+ if (!res.ok) throw new Error(`ENOENT: ${path}`);
2458
+ const data = await res.json();
2459
+ return createFileStats(data.size, new Date(data.mtime), data.isDirectory);
2460
+ }
2461
+ async mkdir(path, options) {
2462
+ const params = { mkdir: "true" };
2463
+ if (options?.recursive) params.recursive = "true";
2464
+ const res = await fetch(this.url(path, params), { method: "POST" });
2465
+ if (!res.ok) throw new Error(`Failed to mkdir: ${path}`);
2466
+ }
2467
+ async readdir(path) {
2468
+ const res = await fetch(this.url(path, { readdir: "true" }));
2469
+ if (!res.ok) throw new Error(`ENOENT: ${path}`);
2470
+ const entries = await res.json();
2471
+ return entries.map((e) => createDirEntry(e.name, e.isDirectory));
2472
+ }
2473
+ async rmdir(path, options) {
2474
+ const params = {};
2475
+ if (options?.recursive) params.recursive = "true";
2476
+ const res = await fetch(this.url(path, params), { method: "DELETE" });
2477
+ if (!res.ok) throw new Error(`Failed to rmdir: ${path}`);
1753
2478
  }
1754
2479
  async exists(path) {
1755
- const res = await fetch(`${this.config.baseUrl}/${path}`, {
1756
- method: "HEAD"
1757
- });
2480
+ const res = await fetch(this.url(path), { method: "HEAD" });
1758
2481
  return res.ok;
1759
2482
  }
2483
+ watch(path, callback) {
2484
+ const controller = new AbortController();
2485
+ this.startWatch(path, callback, controller.signal);
2486
+ return () => controller.abort();
2487
+ }
2488
+ async startWatch(path, callback, signal) {
2489
+ try {
2490
+ const res = await fetch(this.url("", { watch: path }), { signal });
2491
+ if (!res.ok) return;
2492
+ const reader = res.body?.getReader();
2493
+ if (!reader) return;
2494
+ const decoder = new TextDecoder();
2495
+ let buffer = "";
2496
+ while (!signal.aborted) {
2497
+ const { done, value } = await reader.read();
2498
+ if (done) break;
2499
+ buffer += decoder.decode(value, { stream: true });
2500
+ const lines = buffer.split("\n");
2501
+ buffer = lines.pop() ?? "";
2502
+ for (const line of lines) {
2503
+ if (line.startsWith("data: ")) {
2504
+ try {
2505
+ const event = JSON.parse(line.slice(6));
2506
+ callback(event.type, event.path);
2507
+ } catch {
2508
+ }
2509
+ }
2510
+ }
2511
+ }
2512
+ } catch {
2513
+ }
2514
+ }
2515
+ url(path, params) {
2516
+ const baseUrl = this.config.baseUrl.replace(/\/+$/, "");
2517
+ const cleanPath = path.replace(/^\/+/, "");
2518
+ const base = cleanPath ? `${baseUrl}/${cleanPath}` : baseUrl;
2519
+ if (!params) return base;
2520
+ const query = new URLSearchParams(params).toString();
2521
+ return `${base}?${query}`;
2522
+ }
1760
2523
  };
1761
2524
 
1762
- // src/vfs/backends/s3.ts
1763
- var S3Backend = class {
1764
- constructor(config) {
1765
- this.config = config;
2525
+ // src/vfs/store.ts
2526
+ var VFSStore = class {
2527
+ constructor(provider, options = {}) {
2528
+ this.provider = provider;
2529
+ this.root = options.root ?? "";
2530
+ if (options.sync) {
2531
+ this.local = new VirtualFS();
2532
+ this.syncEngine = new SyncEngineImpl(this.local, this.provider, {
2533
+ conflictStrategy: options.conflictStrategy,
2534
+ basePath: this.root
2535
+ });
2536
+ if (options.autoSyncIntervalMs) {
2537
+ this.syncEngine.startAutoSync(options.autoSyncIntervalMs);
2538
+ }
2539
+ }
1766
2540
  }
1767
- get baseUrl() {
1768
- return `https://${this.config.bucket}.s3.${this.config.region}.amazonaws.com`;
2541
+ local;
2542
+ syncEngine;
2543
+ root;
2544
+ async readFile(path, encoding) {
2545
+ if (this.local) {
2546
+ try {
2547
+ return await this.local.readFile(path, encoding);
2548
+ } catch {
2549
+ const content = await this.provider.readFile(
2550
+ this.remotePath(path),
2551
+ encoding
2552
+ );
2553
+ await this.local.applyRemoteFile(path, content);
2554
+ return content;
2555
+ }
2556
+ }
2557
+ return this.provider.readFile(this.remotePath(path), encoding);
1769
2558
  }
1770
- key(path) {
1771
- return this.config.prefix ? `${this.config.prefix}/${path}` : path;
2559
+ async writeFile(path, content) {
2560
+ if (this.local) {
2561
+ await this.local.writeFile(path, content);
2562
+ return;
2563
+ }
2564
+ await this.provider.writeFile(this.remotePath(path), content);
1772
2565
  }
1773
- async get(path) {
1774
- const res = await fetch(`${this.baseUrl}/${this.key(path)}`);
1775
- if (!res.ok) return null;
1776
- return res.text();
2566
+ async unlink(path) {
2567
+ if (this.local) {
2568
+ await this.local.unlink(path);
2569
+ return;
2570
+ }
2571
+ await this.provider.unlink(this.remotePath(path));
1777
2572
  }
1778
- async put(path, content) {
1779
- await fetch(`${this.baseUrl}/${this.key(path)}`, {
1780
- method: "PUT",
1781
- body: content,
1782
- headers: { "Content-Type": "text/plain" }
1783
- });
2573
+ async stat(path) {
2574
+ if (this.local) {
2575
+ try {
2576
+ return await this.local.stat(path);
2577
+ } catch {
2578
+ return this.provider.stat(this.remotePath(path));
2579
+ }
2580
+ }
2581
+ return this.provider.stat(this.remotePath(path));
1784
2582
  }
1785
- async delete(path) {
1786
- await fetch(`${this.baseUrl}/${this.key(path)}`, { method: "DELETE" });
2583
+ async mkdir(path, options) {
2584
+ if (this.local) {
2585
+ await this.local.mkdir(path, options);
2586
+ }
2587
+ await this.provider.mkdir(this.remotePath(path), options);
1787
2588
  }
1788
- async list(prefix) {
1789
- const listPrefix = prefix ? this.key(prefix) : this.config.prefix || "";
1790
- const res = await fetch(
1791
- `${this.baseUrl}?list-type=2&prefix=${encodeURIComponent(listPrefix)}`
1792
- );
1793
- const xml = await res.text();
1794
- return this.parseListResponse(xml);
2589
+ async readdir(path) {
2590
+ if (this.local) {
2591
+ try {
2592
+ return await this.local.readdir(path);
2593
+ } catch {
2594
+ return this.provider.readdir(this.remotePath(path));
2595
+ }
2596
+ }
2597
+ return this.provider.readdir(this.remotePath(path));
2598
+ }
2599
+ async rmdir(path, options) {
2600
+ if (this.local) {
2601
+ await this.local.rmdir(path, options);
2602
+ }
2603
+ await this.provider.rmdir(this.remotePath(path), options);
1795
2604
  }
1796
2605
  async exists(path) {
1797
- const res = await fetch(`${this.baseUrl}/${this.key(path)}`, {
1798
- method: "HEAD"
1799
- });
1800
- return res.ok;
2606
+ if (this.local) {
2607
+ if (await this.local.exists(path)) return true;
2608
+ return this.provider.exists(this.remotePath(path));
2609
+ }
2610
+ return this.provider.exists(this.remotePath(path));
2611
+ }
2612
+ async listFiles(prefix = "") {
2613
+ return this.walkFiles(prefix);
2614
+ }
2615
+ async loadProject(id) {
2616
+ const paths = await this.listFiles(id);
2617
+ if (paths.length === 0) return null;
2618
+ const files = /* @__PURE__ */ new Map();
2619
+ await Promise.all(
2620
+ paths.map(async (path) => {
2621
+ const content = await this.provider.readFile(this.remotePath(path));
2622
+ const relative = path.slice(id.length + 1);
2623
+ files.set(relative, { path: relative, content });
2624
+ if (this.local) {
2625
+ await this.local.applyRemoteFile(path, content);
2626
+ }
2627
+ })
2628
+ );
2629
+ return { id, entry: resolveEntry(files), files };
2630
+ }
2631
+ async saveProject(project) {
2632
+ if (this.local) {
2633
+ await Promise.all(
2634
+ Array.from(project.files.values()).map(
2635
+ (file) => this.local.writeFile(`${project.id}/${file.path}`, file.content)
2636
+ )
2637
+ );
2638
+ await this.sync();
2639
+ return;
2640
+ }
2641
+ await Promise.all(
2642
+ Array.from(project.files.values()).map(
2643
+ (file) => this.provider.writeFile(
2644
+ this.remotePath(`${project.id}/${file.path}`),
2645
+ file.content
2646
+ )
2647
+ )
2648
+ );
2649
+ }
2650
+ async sync() {
2651
+ if (!this.syncEngine) {
2652
+ return { pushed: 0, pulled: 0, conflicts: [] };
2653
+ }
2654
+ return this.syncEngine.sync();
1801
2655
  }
1802
- parseListResponse(xml) {
1803
- const matches = xml.matchAll(/<Key>([^<]+)<\/Key>/g);
1804
- const prefixLen = this.config.prefix ? this.config.prefix.length + 1 : 0;
1805
- return Array.from(matches, (m) => (m[1] ?? "").slice(prefixLen));
2656
+ on(event, callback) {
2657
+ if (!this.syncEngine) return () => {
2658
+ };
2659
+ return this.syncEngine.on(event, callback);
2660
+ }
2661
+ remotePath(path) {
2662
+ return this.root ? join(this.root, path) : path;
2663
+ }
2664
+ async walkFiles(prefix) {
2665
+ const results = [];
2666
+ const normalized = prefix ? prefix.replace(/^\/+/g, "") : "";
2667
+ let entries = [];
2668
+ try {
2669
+ entries = await this.provider.readdir(this.remotePath(normalized));
2670
+ } catch {
2671
+ return results;
2672
+ }
2673
+ for (const entry of entries) {
2674
+ const entryPath = normalized ? `${normalized}/${entry.name}` : entry.name;
2675
+ if (entry.isDirectory()) {
2676
+ results.push(...await this.walkFiles(entryPath));
2677
+ } else {
2678
+ results.push(entryPath);
2679
+ }
2680
+ }
2681
+ return results;
1806
2682
  }
1807
2683
  };
1808
2684
  export {
@@ -1811,17 +2687,16 @@ export {
1811
2687
  DEFAULT_IMAGE_CONFIG,
1812
2688
  DEV_SANDBOX,
1813
2689
  EsbuildConfigSchema,
2690
+ HttpBackend,
1814
2691
  ImageConfigSchema,
1815
2692
  ImageRegistry,
1816
2693
  IndexedDBBackend,
1817
2694
  InputSpecSchema,
1818
- LocalFSBackend,
1819
2695
  ManifestSchema,
1820
2696
  MountModeSchema,
1821
2697
  MountOptionsSchema,
1822
2698
  ParentBridge,
1823
2699
  PlatformSchema,
1824
- S3Backend,
1825
2700
  VFSStore,
1826
2701
  cdnTransformPlugin,
1827
2702
  createCompiler,