freestyle-sync 0.1.9 → 0.1.10
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/README.md +23 -1
- package/dist/src/main.js +98 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -79,4 +79,26 @@ export default defineConfig({
|
|
|
79
79
|
shellHistoryPlugin(),
|
|
80
80
|
],
|
|
81
81
|
});
|
|
82
|
-
```
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Project sync can exclude folders and include additional local paths. Include paths may point outside the project directory; they are copied into the remote project at the configured relative target.
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
export default defineConfig({
|
|
88
|
+
sync: {
|
|
89
|
+
exclude: ["dist", ".turbo"],
|
|
90
|
+
include: [
|
|
91
|
+
{ source: "../shared-config", target: "shared-config" },
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
plugins: [
|
|
95
|
+
nodeNpmPlugin(),
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`nodeNpmPlugin()` excludes `node_modules` from upload by default and installs dependencies on the remote runtime using the project lockfile: `pnpm install --frozen-lockfile`, Yarn `--frozen-lockfile`/`--immutable`, or `npm ci`. To keep the previous behavior of syncing `node_modules` and only repairing native/workspace packages remotely, use:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
nodeNpmPlugin({ syncNodeModules: true })
|
|
104
|
+
```
|
package/dist/src/main.js
CHANGED
|
@@ -10,7 +10,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
10
10
|
import "dotenv/config";
|
|
11
11
|
import { createHash } from "node:crypto";
|
|
12
12
|
import { createReadStream, realpathSync } from "node:fs";
|
|
13
|
-
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
13
|
+
import { chmod, copyFile, lstat, mkdir, mkdtemp, readFile, readlink, rm, stat, symlink, writeFile } from "node:fs/promises";
|
|
14
14
|
import { homedir, tmpdir } from "node:os";
|
|
15
15
|
import path from "node:path";
|
|
16
16
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -182,7 +182,8 @@ export async function sync(sdkOptions) {
|
|
|
182
182
|
progress.step("Scanning project files");
|
|
183
183
|
const cache = currentCache;
|
|
184
184
|
const base = cacheBaseForSync(options, cache);
|
|
185
|
-
const
|
|
185
|
+
const projectSyncConfig = await resolveProjectSyncConfig(options);
|
|
186
|
+
const projectEntries = await scanProject(options.projectRoot, options.includeGitDir, projectSyncConfig);
|
|
186
187
|
const projectCurrent = digestMap(projectEntries);
|
|
187
188
|
const projectChanges = diffEntries(projectEntries, base.projectFiles);
|
|
188
189
|
progress.step("Detecting auth and agent context");
|
|
@@ -719,23 +720,92 @@ async function writeCache(cachePath, cache) {
|
|
|
719
720
|
await mkdir(path.dirname(cachePath), { recursive: true });
|
|
720
721
|
await writeFile(cachePath, `${JSON.stringify(cache, null, 2)}\n`, "utf8");
|
|
721
722
|
}
|
|
722
|
-
async function
|
|
723
|
-
const
|
|
724
|
-
|
|
723
|
+
async function resolveProjectSyncConfig(options) {
|
|
724
|
+
const configs = [];
|
|
725
|
+
if (config.sync)
|
|
726
|
+
configs.push(config.sync);
|
|
727
|
+
for (const plugin of plugins) {
|
|
728
|
+
const pluginConfig = await plugin.configureProjectSync?.({ options, utils: pluginUtils });
|
|
729
|
+
if (pluginConfig)
|
|
730
|
+
configs.push(pluginConfig);
|
|
731
|
+
}
|
|
732
|
+
return {
|
|
733
|
+
exclude: configs.flatMap((projectConfig) => projectConfig.exclude ?? []).map(normalizeProjectPattern),
|
|
734
|
+
include: configs.flatMap((projectConfig) => normalizeProjectIncludes(options.projectRoot, projectConfig.include ?? [])),
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
function normalizeProjectIncludes(projectRoot, includes) {
|
|
738
|
+
return includes.map((include) => {
|
|
739
|
+
const source = typeof include === "string" ? include : include.source;
|
|
740
|
+
const target = typeof include === "string" ? undefined : include.target;
|
|
741
|
+
const resolvedSource = path.resolve(projectRoot, source);
|
|
742
|
+
return {
|
|
743
|
+
source: resolvedSource,
|
|
744
|
+
target: normalizeProjectPattern(target ?? path.basename(resolvedSource)),
|
|
745
|
+
};
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
function normalizeProjectPattern(value) {
|
|
749
|
+
const normalized = path.posix.normalize(value.replace(/\\/g, "/")).replace(/^\/+/, "").replace(/\/+$/, "");
|
|
750
|
+
if (!normalized || normalized === ".") {
|
|
751
|
+
throw new Error("project sync paths must not be empty");
|
|
752
|
+
}
|
|
753
|
+
if (normalized === ".." || normalized.startsWith("../")) {
|
|
754
|
+
throw new Error(`project sync paths must stay inside the remote project: ${value}`);
|
|
755
|
+
}
|
|
756
|
+
return normalized;
|
|
757
|
+
}
|
|
758
|
+
async function scanProject(projectRoot, includeGitDir, syncConfig) {
|
|
759
|
+
const entriesByPath = new Map();
|
|
760
|
+
const rootEntries = [];
|
|
761
|
+
await walk(projectRoot, "", rootEntries, {
|
|
725
762
|
skipDirectory(relativePath, name) {
|
|
726
763
|
if (!includeGitDir && relativePath === ".git")
|
|
727
764
|
return true;
|
|
728
|
-
return
|
|
765
|
+
return shouldSkipProjectDirectory(relativePath, name, syncConfig.exclude);
|
|
766
|
+
},
|
|
767
|
+
});
|
|
768
|
+
upsertProjectEntries(entriesByPath, rootEntries);
|
|
769
|
+
for (const include of syncConfig.include) {
|
|
770
|
+
upsertProjectEntries(entriesByPath, await scanProjectInclude(include, syncConfig.exclude));
|
|
771
|
+
}
|
|
772
|
+
return Array.from(entriesByPath.values()).sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
773
|
+
}
|
|
774
|
+
async function scanProjectInclude(include, exclude) {
|
|
775
|
+
const stats = await lstat(include.source).catch((error) => {
|
|
776
|
+
throw new Error(`project sync include does not exist: ${include.source} (${error instanceof Error ? error.message : String(error)})`);
|
|
777
|
+
});
|
|
778
|
+
if (stats.isFile() || stats.isSymbolicLink()) {
|
|
779
|
+
return [await digestEntry(include.source, include.target)];
|
|
780
|
+
}
|
|
781
|
+
if (!stats.isDirectory()) {
|
|
782
|
+
return [];
|
|
783
|
+
}
|
|
784
|
+
const entries = [];
|
|
785
|
+
await walk(include.source, "", entries, {
|
|
786
|
+
mapRelativePath(relativePath) {
|
|
787
|
+
return `${include.target}/${relativePath}`;
|
|
788
|
+
},
|
|
789
|
+
skipDirectory(relativePath, name) {
|
|
790
|
+
return shouldSkipProjectDirectory(relativePath, name, exclude);
|
|
729
791
|
},
|
|
730
792
|
});
|
|
731
|
-
return entries
|
|
793
|
+
return entries;
|
|
794
|
+
}
|
|
795
|
+
function upsertProjectEntries(entriesByPath, entries) {
|
|
796
|
+
for (const entry of entries)
|
|
797
|
+
entriesByPath.set(entry.relativePath, entry);
|
|
798
|
+
}
|
|
799
|
+
function shouldSkipProjectDirectory(relativePath, name, exclude) {
|
|
800
|
+
return exclude.some((excludedPath) => relativePath === excludedPath || name === excludedPath);
|
|
732
801
|
}
|
|
733
802
|
async function walk(root, relativePath, entries, options) {
|
|
734
803
|
const absolutePath = path.join(root, relativePath);
|
|
735
804
|
const dir = await import("node:fs/promises").then((fs) => fs.readdir(absolutePath, { withFileTypes: true }));
|
|
736
805
|
for (const dirent of dir) {
|
|
737
806
|
const childRelativePath = relativePath ? path.join(relativePath, dirent.name) : dirent.name;
|
|
738
|
-
const
|
|
807
|
+
const localRelativePath = toPosix(childRelativePath);
|
|
808
|
+
const normalizedRelativePath = options.mapRelativePath?.(localRelativePath) ?? localRelativePath;
|
|
739
809
|
const childAbsolutePath = path.join(root, childRelativePath);
|
|
740
810
|
if (dirent.isDirectory()) {
|
|
741
811
|
if (options.skipDirectory(normalizedRelativePath, dirent.name)) {
|
|
@@ -1387,7 +1457,7 @@ async function ensureRemoteBase(vm, remoteProjectDir) {
|
|
|
1387
1457
|
async function syncProject(vm, vmId, options, changes) {
|
|
1388
1458
|
if (changes.changed.length > 0) {
|
|
1389
1459
|
console.log(`VM ${vmId}: uploading ${changes.changed.length} changed project files...`);
|
|
1390
|
-
const archive = await createProjectArchive(
|
|
1460
|
+
const archive = await createProjectArchive(changes.changed);
|
|
1391
1461
|
try {
|
|
1392
1462
|
await uploadArchiveInChunks(vm, vmId, archive, "/tmp/freestyle-sync-project.tgz", "project");
|
|
1393
1463
|
await checkedExec(vm, `mkdir -p ${shellQuote(options.remoteProjectDir)} && tar --no-same-owner --no-same-permissions -xzf /tmp/freestyle-sync-project.tgz -C ${shellQuote(options.remoteProjectDir)} && rm -f /tmp/freestyle-sync-project.tgz`);
|
|
@@ -1512,11 +1582,11 @@ async function runInstall(vm, projectRoot, remoteProjectDir) {
|
|
|
1512
1582
|
}
|
|
1513
1583
|
async function detectInstallCommand(projectRoot) {
|
|
1514
1584
|
if (await exists(path.join(projectRoot, "pnpm-lock.yaml")))
|
|
1515
|
-
return "corepack enable && pnpm install";
|
|
1585
|
+
return "corepack enable && pnpm install --frozen-lockfile";
|
|
1516
1586
|
if (await exists(path.join(projectRoot, "yarn.lock")))
|
|
1517
|
-
return "corepack enable && yarn install";
|
|
1518
|
-
if (await exists(path.join(projectRoot, "package-lock.json")))
|
|
1519
|
-
return "npm
|
|
1587
|
+
return "corepack enable && yarn_version=$(yarn --version) && case \"$yarn_version\" in 1.*) yarn install --frozen-lockfile ;; *) yarn install --immutable ;; esac";
|
|
1588
|
+
if (await exists(path.join(projectRoot, "package-lock.json")) || await exists(path.join(projectRoot, "npm-shrinkwrap.json")))
|
|
1589
|
+
return "npm ci";
|
|
1520
1590
|
if (await exists(path.join(projectRoot, "requirements.txt")))
|
|
1521
1591
|
return "python3 -m pip install -r requirements.txt";
|
|
1522
1592
|
if (await exists(path.join(projectRoot, "pyproject.toml")))
|
|
@@ -1527,12 +1597,23 @@ async function detectInstallCommand(projectRoot) {
|
|
|
1527
1597
|
return "go mod download";
|
|
1528
1598
|
return undefined;
|
|
1529
1599
|
}
|
|
1530
|
-
async function createProjectArchive(
|
|
1600
|
+
async function createProjectArchive(entries) {
|
|
1531
1601
|
const tempDir = await mkdtemp(path.join(tmpdir(), "freestyle-sync-"));
|
|
1532
|
-
const
|
|
1602
|
+
const stagingDir = path.join(tempDir, "staging");
|
|
1533
1603
|
const archivePath = path.join(tempDir, "project.tgz");
|
|
1534
|
-
await
|
|
1535
|
-
|
|
1604
|
+
await mkdir(stagingDir, { recursive: true });
|
|
1605
|
+
for (const entry of entries) {
|
|
1606
|
+
const destination = path.join(stagingDir, ...entry.relativePath.split("/"));
|
|
1607
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
1608
|
+
if (entry.kind === "symlink") {
|
|
1609
|
+
await symlink(await readlink(entry.absolutePath), destination);
|
|
1610
|
+
}
|
|
1611
|
+
else {
|
|
1612
|
+
await copyFile(entry.absolutePath, destination);
|
|
1613
|
+
await chmod(destination, entry.mode);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
await createTar(["--no-xattrs", "-czf", archivePath, "-C", stagingDir, "."]);
|
|
1536
1617
|
return archivePath;
|
|
1537
1618
|
}
|
|
1538
1619
|
async function createContextArchive(entries) {
|