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