framer-code-link 0.4.5 → 0.4.6
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.mjs +72 -59
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -81,11 +81,11 @@ const firstCharacterRegex = /^[a-zA-Z$_]/;
|
|
|
81
81
|
const remainingCharactersRegex = /[^a-zA-Z0-9$_]/g;
|
|
82
82
|
const onlyDotsRegex = /^\.+$/;
|
|
83
83
|
const tsxExtension = ".tsx";
|
|
84
|
-
var NameType = /* @__PURE__ */ function(NameType
|
|
85
|
-
NameType
|
|
86
|
-
NameType
|
|
87
|
-
NameType
|
|
88
|
-
return NameType
|
|
84
|
+
var NameType = /* @__PURE__ */ function(NameType) {
|
|
85
|
+
NameType["Variable"] = "Variable";
|
|
86
|
+
NameType["Selector"] = "Selector";
|
|
87
|
+
NameType["Directory"] = "Directory";
|
|
88
|
+
return NameType;
|
|
89
89
|
}(NameType || {});
|
|
90
90
|
function sanitizedName(type, name) {
|
|
91
91
|
if (!name) return null;
|
|
@@ -139,7 +139,7 @@ function pathJoin(...parts) {
|
|
|
139
139
|
});
|
|
140
140
|
return res;
|
|
141
141
|
}
|
|
142
|
-
function normalizePath(filePath) {
|
|
142
|
+
function normalizePath$1(filePath) {
|
|
143
143
|
if (!filePath) return "";
|
|
144
144
|
const isAbsolute = filePath.startsWith("/");
|
|
145
145
|
const segments = filePath.replace(/\\/g, "/").split("/");
|
|
@@ -170,7 +170,7 @@ function sanitizeFilePath(input, capitalizeReactComponent = true) {
|
|
|
170
170
|
extension
|
|
171
171
|
};
|
|
172
172
|
}
|
|
173
|
-
function isSupportedExtension(filePath) {
|
|
173
|
+
function isSupportedExtension$1(filePath) {
|
|
174
174
|
return /\.(tsx?|jsx?|json)$/i.test(filePath);
|
|
175
175
|
}
|
|
176
176
|
/**
|
|
@@ -275,12 +275,12 @@ var require_picocolors = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
275
275
|
//#endregion
|
|
276
276
|
//#region src/utils/logging.ts
|
|
277
277
|
var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
|
|
278
|
-
let LogLevel = /* @__PURE__ */ function(LogLevel
|
|
279
|
-
LogLevel
|
|
280
|
-
LogLevel
|
|
281
|
-
LogLevel
|
|
282
|
-
LogLevel
|
|
283
|
-
return LogLevel
|
|
278
|
+
let LogLevel = /* @__PURE__ */ function(LogLevel) {
|
|
279
|
+
LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
|
|
280
|
+
LogLevel[LogLevel["INFO"] = 1] = "INFO";
|
|
281
|
+
LogLevel[LogLevel["WARN"] = 2] = "WARN";
|
|
282
|
+
LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
|
|
283
|
+
return LogLevel;
|
|
284
284
|
}({});
|
|
285
285
|
let currentLevel = LogLevel.INFO;
|
|
286
286
|
let lastMessage = "";
|
|
@@ -325,9 +325,9 @@ function logWithDedupe(message, writer) {
|
|
|
325
325
|
/**
|
|
326
326
|
* Print the startup banner - one colored line
|
|
327
327
|
*/
|
|
328
|
-
function banner(version
|
|
328
|
+
function banner(version, port) {
|
|
329
329
|
console.log();
|
|
330
|
-
let message = ` ${import_picocolors.default.cyan(import_picocolors.default.bold("⚡ Code Link"))} ${import_picocolors.default.dim(`v${version
|
|
330
|
+
let message = ` ${import_picocolors.default.cyan(import_picocolors.default.bold("⚡ Code Link"))} ${import_picocolors.default.dim(`v${version}`)}`;
|
|
331
331
|
if (currentLevel <= LogLevel.DEBUG) message += ` ${import_picocolors.default.dim("Port")} ${import_picocolors.default.yellow(port)}`;
|
|
332
332
|
console.log(message);
|
|
333
333
|
console.log();
|
|
@@ -597,7 +597,7 @@ function getRelativePath(projectDir, absolutePath) {
|
|
|
597
597
|
* - Resolving . and .. segments
|
|
598
598
|
* - Removing duplicate slashes
|
|
599
599
|
*/
|
|
600
|
-
function normalizePath
|
|
600
|
+
function normalizePath(filePath) {
|
|
601
601
|
if (!filePath) return "";
|
|
602
602
|
const isAbsolute = filePath.startsWith("/");
|
|
603
603
|
const segments = filePath.replace(/\\/g, "/").split("/");
|
|
@@ -635,7 +635,7 @@ const SUPPORTED_EXTENSIONS$1 = [
|
|
|
635
635
|
];
|
|
636
636
|
const DEFAULT_EXTENSION$1 = ".tsx";
|
|
637
637
|
function normalizePersistedFileName(fileName) {
|
|
638
|
-
let normalized = normalizePath
|
|
638
|
+
let normalized = normalizePath(fileName.trim());
|
|
639
639
|
if (!SUPPORTED_EXTENSIONS$1.some((ext) => normalized.toLowerCase().endsWith(ext))) normalized = `${normalized}${DEFAULT_EXTENSION$1}`;
|
|
640
640
|
return normalized;
|
|
641
641
|
}
|
|
@@ -712,7 +712,7 @@ const SUPPORTED_EXTENSIONS = [
|
|
|
712
712
|
".json"
|
|
713
713
|
];
|
|
714
714
|
const DEFAULT_EXTENSION = ".tsx";
|
|
715
|
-
const DEFAULT_REMOTE_DRIFT_MS =
|
|
715
|
+
const DEFAULT_REMOTE_DRIFT_MS = 2500;
|
|
716
716
|
/** Normalize file name for case-insensitive comparison (macOS/Windows compat) */
|
|
717
717
|
function normalizeForComparison(fileName) {
|
|
718
718
|
return fileName.toLowerCase();
|
|
@@ -730,8 +730,8 @@ async function listFiles(filesDir) {
|
|
|
730
730
|
await walk(entryPath);
|
|
731
731
|
continue;
|
|
732
732
|
}
|
|
733
|
-
if (!isSupportedExtension
|
|
734
|
-
const sanitizedPath = sanitizeFilePath(normalizePath(path.relative(filesDir, entryPath)), false).path;
|
|
733
|
+
if (!isSupportedExtension(entry.name)) continue;
|
|
734
|
+
const sanitizedPath = sanitizeFilePath(normalizePath$1(path.relative(filesDir, entryPath)), false).path;
|
|
735
735
|
try {
|
|
736
736
|
const [content, stats] = await Promise.all([fs.readFile(entryPath, "utf-8"), fs.stat(entryPath)]);
|
|
737
737
|
files.push({
|
|
@@ -759,7 +759,7 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
|
|
|
759
759
|
const detect = options.detectConflicts ?? true;
|
|
760
760
|
const preferRemote = options.preferRemote ?? false;
|
|
761
761
|
const persistedState = options.persistedState;
|
|
762
|
-
const getPersistedState = (fileName) => persistedState?.get(normalizeForComparison(fileName))
|
|
762
|
+
const getPersistedState = (fileName) => persistedState?.get(normalizeForComparison(fileName));
|
|
763
763
|
debug(`Detecting conflicts for ${String(remoteFiles.length)} remote files`);
|
|
764
764
|
const localFiles = await listFiles(filesDir);
|
|
765
765
|
const localFileMap = new Map(localFiles.map((f) => [normalizeForComparison(f.name), f]));
|
|
@@ -855,7 +855,7 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
|
|
|
855
855
|
};
|
|
856
856
|
}
|
|
857
857
|
function autoResolveConflicts(conflicts, versions, options = {}) {
|
|
858
|
-
const versionMap = new Map(versions.map((version
|
|
858
|
+
const versionMap = new Map(versions.map((version) => [version.fileName, version.latestRemoteVersionMs]));
|
|
859
859
|
const remoteDriftMs = options.remoteDriftMs ?? DEFAULT_REMOTE_DRIFT_MS;
|
|
860
860
|
const autoResolvedLocal = [];
|
|
861
861
|
const autoResolvedRemote = [];
|
|
@@ -875,28 +875,31 @@ function autoResolveConflicts(conflicts, versions, options = {}) {
|
|
|
875
875
|
}
|
|
876
876
|
continue;
|
|
877
877
|
}
|
|
878
|
+
if (localClean) {
|
|
879
|
+
debug(` Local clean -> REMOTE (safe to overwrite)`);
|
|
880
|
+
autoResolvedRemote.push(conflict);
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
878
883
|
if (!latestRemoteVersionMs) {
|
|
879
|
-
debug(`
|
|
884
|
+
debug(` Local modified, no remote version data -> conflict`);
|
|
880
885
|
remainingConflicts.push(conflict);
|
|
881
886
|
continue;
|
|
882
887
|
}
|
|
883
888
|
if (!lastSyncedAt) {
|
|
884
|
-
debug(`
|
|
889
|
+
debug(` Local modified, no sync timestamp -> conflict`);
|
|
885
890
|
remainingConflicts.push(conflict);
|
|
886
891
|
continue;
|
|
887
892
|
}
|
|
888
893
|
debug(` Remote: ${new Date(latestRemoteVersionMs).toISOString()}`);
|
|
889
894
|
debug(` Synced: ${new Date(lastSyncedAt).toISOString()}`);
|
|
890
895
|
const remoteUnchanged = latestRemoteVersionMs <= lastSyncedAt + remoteDriftMs;
|
|
891
|
-
|
|
896
|
+
const driftMargin = latestRemoteVersionMs - lastSyncedAt;
|
|
897
|
+
if (remoteUnchanged) {
|
|
892
898
|
debug(` Remote unchanged, local changed -> LOCAL`);
|
|
899
|
+
if (driftMargin > 0) debug(` (within drift tolerance: ${driftMargin}ms < ${remoteDriftMs}ms threshold)`);
|
|
893
900
|
autoResolvedLocal.push(conflict);
|
|
894
|
-
} else
|
|
895
|
-
debug(`
|
|
896
|
-
autoResolvedRemote.push(conflict);
|
|
897
|
-
} else if (remoteUnchanged && localClean) debug(` Both unchanged, skipping`);
|
|
898
|
-
else {
|
|
899
|
-
debug(` Both changed, real conflict`);
|
|
901
|
+
} else {
|
|
902
|
+
debug(` Both changed -> conflict (remote ahead by ${driftMargin}ms, threshold: ${remoteDriftMs}ms)`);
|
|
900
903
|
remainingConflicts.push(conflict);
|
|
901
904
|
}
|
|
902
905
|
}
|
|
@@ -973,15 +976,15 @@ function resolveRemoteReference(filesDir, rawName) {
|
|
|
973
976
|
};
|
|
974
977
|
}
|
|
975
978
|
function sanitizeRelativePath(relativePath) {
|
|
976
|
-
const trimmed = normalizePath(relativePath.trim());
|
|
979
|
+
const trimmed = normalizePath$1(relativePath.trim());
|
|
977
980
|
const sanitized = sanitizeFilePath(SUPPORTED_EXTENSIONS.some((ext) => trimmed.toLowerCase().endsWith(ext)) ? trimmed : `${trimmed}${DEFAULT_EXTENSION}`, false);
|
|
978
|
-
const normalized = normalizePath(sanitized.path);
|
|
981
|
+
const normalized = normalizePath$1(sanitized.path);
|
|
979
982
|
return {
|
|
980
983
|
relativePath: normalized,
|
|
981
984
|
extension: sanitized.extension || path.extname(normalized) || DEFAULT_EXTENSION
|
|
982
985
|
};
|
|
983
986
|
}
|
|
984
|
-
function isSupportedExtension
|
|
987
|
+
function isSupportedExtension(fileName) {
|
|
985
988
|
const lower = fileName.toLowerCase();
|
|
986
989
|
return SUPPORTED_EXTENSIONS.some((ext) => lower.endsWith(ext));
|
|
987
990
|
}
|
|
@@ -1082,8 +1085,8 @@ var Installer = class {
|
|
|
1082
1085
|
finished: (files) => {
|
|
1083
1086
|
if (files.size > 0) debug("ATA: type acquisition complete");
|
|
1084
1087
|
},
|
|
1085
|
-
errorMessage: (message, error
|
|
1086
|
-
warn(`ATA warning: ${message}`, error
|
|
1088
|
+
errorMessage: (message, error) => {
|
|
1089
|
+
warn(`ATA warning: ${message}`, error);
|
|
1087
1090
|
},
|
|
1088
1091
|
receivedFile: (code, receivedPath) => {
|
|
1089
1092
|
(async () => {
|
|
@@ -1204,9 +1207,9 @@ var Installer = class {
|
|
|
1204
1207
|
const response = await fetch(`https://registry.npmjs.org/${pkgName}`);
|
|
1205
1208
|
if (!response.ok) return;
|
|
1206
1209
|
const npmData = await response.json();
|
|
1207
|
-
const version
|
|
1208
|
-
if (!version
|
|
1209
|
-
const pkg = npmData.versions[version
|
|
1210
|
+
const version = npmData["dist-tags"]?.latest;
|
|
1211
|
+
if (!version || !npmData.versions?.[version]) return;
|
|
1212
|
+
const pkg = npmData.versions[version];
|
|
1210
1213
|
if (pkg.exports) for (const key of Object.keys(pkg.exports)) pkg.exports[key] = fixExportTypes(pkg.exports[key]);
|
|
1211
1214
|
await fs.mkdir(pkgDir, { recursive: true });
|
|
1212
1215
|
await fs.writeFile(pkgJsonPath, JSON.stringify(pkg, null, 2));
|
|
@@ -1314,11 +1317,11 @@ declare module "*.json"
|
|
|
1314
1317
|
if (await this.hasTypePackage(reactDomDir, REACT_DOM_TYPES_VERSION, reactDomFiles)) debug("📦 React DOM types (from cache)");
|
|
1315
1318
|
else await this.downloadTypePackage("@types/react-dom", REACT_DOM_TYPES_VERSION, reactDomDir, reactDomFiles);
|
|
1316
1319
|
}
|
|
1317
|
-
async hasTypePackage(destinationDir, version
|
|
1320
|
+
async hasTypePackage(destinationDir, version, files) {
|
|
1318
1321
|
try {
|
|
1319
1322
|
const pkgJsonPath = path.join(destinationDir, "package.json");
|
|
1320
1323
|
const pkgJson = await fs.readFile(pkgJsonPath, "utf-8");
|
|
1321
|
-
if (JSON.parse(pkgJson).version !== version
|
|
1324
|
+
if (JSON.parse(pkgJson).version !== version) return false;
|
|
1322
1325
|
for (const file of files) {
|
|
1323
1326
|
if (file === "package.json") continue;
|
|
1324
1327
|
await fs.access(path.join(destinationDir, file));
|
|
@@ -1328,8 +1331,8 @@ declare module "*.json"
|
|
|
1328
1331
|
return false;
|
|
1329
1332
|
}
|
|
1330
1333
|
}
|
|
1331
|
-
async downloadTypePackage(pkgName, version
|
|
1332
|
-
const baseUrl = `https://unpkg.com/${pkgName}@${version
|
|
1334
|
+
async downloadTypePackage(pkgName, version, destinationDir, files) {
|
|
1335
|
+
const baseUrl = `https://unpkg.com/${pkgName}@${version}`;
|
|
1333
1336
|
await fs.mkdir(destinationDir, { recursive: true });
|
|
1334
1337
|
await Promise.all(files.map(async (file) => {
|
|
1335
1338
|
const destination = path.join(destinationDir, file);
|
|
@@ -1388,17 +1391,17 @@ async function fetchWithRetry(url, init, retries = MAX_FETCH_RETRIES) {
|
|
|
1388
1391
|
return response;
|
|
1389
1392
|
} catch (err) {
|
|
1390
1393
|
clearTimeout(timeout);
|
|
1391
|
-
const error
|
|
1392
|
-
const isRetryable = error
|
|
1394
|
+
const error = err;
|
|
1395
|
+
const isRetryable = error.cause?.code === "ECONNRESET" || error.cause?.code === "ETIMEDOUT" || error.cause?.code === "UND_ERR_CONNECT_TIMEOUT" || error.message.includes("timeout");
|
|
1393
1396
|
if (isRetryable) checkFatalFailure(urlString);
|
|
1394
1397
|
if (attempt < retries && isRetryable) {
|
|
1395
1398
|
const delay = attempt * 1e3;
|
|
1396
|
-
warn(`Fetch failed (${error
|
|
1399
|
+
warn(`Fetch failed (${error.cause?.code ?? error.message}) for ${urlString}, retrying in ${delay}ms...`);
|
|
1397
1400
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1398
1401
|
continue;
|
|
1399
1402
|
}
|
|
1400
|
-
warn(`Fetch failed for ${urlString}`, error
|
|
1401
|
-
throw error
|
|
1403
|
+
warn(`Fetch failed for ${urlString}`, error);
|
|
1404
|
+
throw error;
|
|
1402
1405
|
}
|
|
1403
1406
|
}
|
|
1404
1407
|
throw new Error(`Max retries exceeded for ${urlString}`);
|
|
@@ -1561,8 +1564,8 @@ function initWatcher(filesDir) {
|
|
|
1561
1564
|
});
|
|
1562
1565
|
debug(`Watching directory: ${filesDir}`);
|
|
1563
1566
|
const emitEvent = async (kind, absolutePath) => {
|
|
1564
|
-
if (!isSupportedExtension(absolutePath)) return;
|
|
1565
|
-
const rawRelativePath = normalizePath(getRelativePath(filesDir, absolutePath));
|
|
1567
|
+
if (!isSupportedExtension$1(absolutePath)) return;
|
|
1568
|
+
const rawRelativePath = normalizePath$1(getRelativePath(filesDir, absolutePath));
|
|
1566
1569
|
const relativePath = sanitizeFilePath(rawRelativePath, false).path;
|
|
1567
1570
|
let effectiveAbsolutePath = absolutePath;
|
|
1568
1571
|
if (relativePath !== rawRelativePath && kind === "add") {
|
|
@@ -1612,6 +1615,13 @@ function initWatcher(filesDir) {
|
|
|
1612
1615
|
|
|
1613
1616
|
//#endregion
|
|
1614
1617
|
//#region src/utils/file-metadata-cache.ts
|
|
1618
|
+
/**
|
|
1619
|
+
* In-memory cache on top of state-persistence.
|
|
1620
|
+
*/
|
|
1621
|
+
/** Normalize file name for case-insensitive lookup (macOS/Windows compat) */
|
|
1622
|
+
function normalizeKey(fileName) {
|
|
1623
|
+
return fileName.toLowerCase();
|
|
1624
|
+
}
|
|
1615
1625
|
var FileMetadataCache = class {
|
|
1616
1626
|
metadata = /* @__PURE__ */ new Map();
|
|
1617
1627
|
persisted = /* @__PURE__ */ new Map();
|
|
@@ -1624,7 +1634,7 @@ var FileMetadataCache = class {
|
|
|
1624
1634
|
const loaded = await loadPersistedState(projectDir);
|
|
1625
1635
|
this.persisted = loaded;
|
|
1626
1636
|
this.metadata = /* @__PURE__ */ new Map();
|
|
1627
|
-
for (const [fileName, state] of loaded.entries()) this.metadata.set(fileName, {
|
|
1637
|
+
for (const [fileName, state] of loaded.entries()) this.metadata.set(normalizeKey(fileName), {
|
|
1628
1638
|
localHash: state.contentHash,
|
|
1629
1639
|
lastSyncedHash: state.contentHash,
|
|
1630
1640
|
lastRemoteTimestamp: state.timestamp
|
|
@@ -1632,10 +1642,10 @@ var FileMetadataCache = class {
|
|
|
1632
1642
|
this.initialized = true;
|
|
1633
1643
|
}
|
|
1634
1644
|
get(fileName) {
|
|
1635
|
-
return this.metadata.get(fileName);
|
|
1645
|
+
return this.metadata.get(normalizeKey(fileName));
|
|
1636
1646
|
}
|
|
1637
1647
|
has(fileName) {
|
|
1638
|
-
return this.metadata.has(fileName);
|
|
1648
|
+
return this.metadata.has(normalizeKey(fileName));
|
|
1639
1649
|
}
|
|
1640
1650
|
size() {
|
|
1641
1651
|
return this.metadata.size;
|
|
@@ -1644,33 +1654,36 @@ var FileMetadataCache = class {
|
|
|
1644
1654
|
return this.persisted;
|
|
1645
1655
|
}
|
|
1646
1656
|
recordRemoteWrite(fileName, content, remoteModifiedAt) {
|
|
1657
|
+
const key = normalizeKey(fileName);
|
|
1647
1658
|
const contentHash = hashFileContent(content);
|
|
1648
|
-
this.metadata.set(
|
|
1659
|
+
this.metadata.set(key, {
|
|
1649
1660
|
localHash: contentHash,
|
|
1650
1661
|
lastSyncedHash: contentHash,
|
|
1651
1662
|
lastRemoteTimestamp: remoteModifiedAt
|
|
1652
1663
|
});
|
|
1653
|
-
this.persisted.set(
|
|
1664
|
+
this.persisted.set(key, {
|
|
1654
1665
|
contentHash,
|
|
1655
1666
|
timestamp: remoteModifiedAt
|
|
1656
1667
|
});
|
|
1657
1668
|
this.schedulePersist();
|
|
1658
1669
|
}
|
|
1659
1670
|
recordSyncedSnapshot(fileName, contentHash, remoteModifiedAt) {
|
|
1660
|
-
|
|
1671
|
+
const key = normalizeKey(fileName);
|
|
1672
|
+
this.metadata.set(key, {
|
|
1661
1673
|
localHash: contentHash,
|
|
1662
1674
|
lastSyncedHash: contentHash,
|
|
1663
1675
|
lastRemoteTimestamp: remoteModifiedAt
|
|
1664
1676
|
});
|
|
1665
|
-
this.persisted.set(
|
|
1677
|
+
this.persisted.set(key, {
|
|
1666
1678
|
contentHash,
|
|
1667
1679
|
timestamp: remoteModifiedAt
|
|
1668
1680
|
});
|
|
1669
1681
|
this.schedulePersist();
|
|
1670
1682
|
}
|
|
1671
1683
|
recordDelete(fileName) {
|
|
1672
|
-
|
|
1673
|
-
this.
|
|
1684
|
+
const key = normalizeKey(fileName);
|
|
1685
|
+
this.metadata.delete(key);
|
|
1686
|
+
this.persisted.delete(key);
|
|
1674
1687
|
this.schedulePersist();
|
|
1675
1688
|
}
|
|
1676
1689
|
async flush() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "framer-code-link",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"description": "CLI tool for syncing Framer code components - controller-centric architecture",
|
|
5
5
|
"main": "dist/index.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@typescript/ata": "^0.9.8",
|
|
26
26
|
"chokidar": "^5.0.0",
|
|
27
|
-
"commander": "^14.0.
|
|
27
|
+
"commander": "^14.0.3",
|
|
28
28
|
"prettier": "^3.7.4",
|
|
29
29
|
"typescript": "^5.9.3",
|
|
30
30
|
"ws": "^8.18.3"
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"@code-link/shared": "1.0.0",
|
|
34
34
|
"@types/node": "^22.19.2",
|
|
35
35
|
"@types/ws": "^8.18.1",
|
|
36
|
-
"tsdown": "^0.
|
|
36
|
+
"tsdown": "^0.20.1",
|
|
37
37
|
"tsx": "^4.21.0",
|
|
38
38
|
"vitest": "^4.0.15"
|
|
39
39
|
}
|