Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/test-standalone-install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,7 @@ jobs:
VP_LOCAL_TGZ="$FAKE_TGZ" VP_VERSION=local-test bash packages/cli/install.sh

test -f "$GRAND/.vite-plus/.keep"
test ! -e "$GRAND/.vite-plus/.vp-owned"
test -e "$GRAND/.vite-plus/current"
test -e "$GRAND/.vite-plus/bin/vp"
test ! -d "$GRAND/.local/share/vite-plus"
Expand Down Expand Up @@ -2168,6 +2169,9 @@ jobs:
if (-not (Test-Path (Join-Path $env:VP_HOME "current/bin/.vp-setup-complete"))) {
throw "vp-setup.exe did not complete the binary's self-setup"
}
if (Test-Path (Join-Path $env:VP_HOME ".vp-owned")) {
throw "Repair claimed ownership of an unmarked installation root"
}
if ([Environment]::GetEnvironmentVariable("Path", "User") -cne $pathBefore) {
throw "--no-modify-path changed User PATH"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ VITE+ - The Unified Toolchain for the Web
Installation
✓ Bin dir ~/.vite-plus/bin
✓ Data dir ~/.vite-plus
✓ Cache dir ~/.vite-plus/cache (not created yet)
✓ Cache dir ~/.vite-plus/cache
✓ Config dir ~/.vite-plus
✓ State dir ~/.vite-plus

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ skip-platforms = ["windows"]
env = { VP_NODE_MANAGER = "yes", VP_SELF_SETUP_NO_MODIFY_PATH = "1" }
steps = [
{ argv = ["vpt", "mkdir", "-p", "external/bin", "home"], comment = "Prepare isolated external install and VP_HOME", snapshot = false },
{ argv = ["vpt", "write-file", "home/.vp-owned", "vite-plus-owned-directory\n"], comment = "The fixture owns the preseeded runtime directory", snapshot = false },
{ argv = ["vpt", "cp", "$VP_HOME/bin/vp", "external/bin/vp"], comment = "Simulate a Homebrew-style vp outside VP_HOME", snapshot = false },
{ argv = ["vpt", "chmod", "+x", "external/bin/vp"], snapshot = false },
{ argv = ["vpt", "write-file", "external/node_modules/vite-plus/package.json", "{}"], snapshot = false },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
Prepare isolated external install and VP_HOME


## `vpt write-file home/.vp-owned 'vite-plus-owned-directory
'`

The fixture owns the preseeded runtime directory


## `vpt cp $VP_HOME/bin/vp external/bin/vp`

Simulate a Homebrew-style vp outside VP_HOME
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import {
copyFileSync,
existsSync,
lstatSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
symlinkSync,
writeFileSync,
} from 'node:fs';
import { dirname, join } from 'node:path';

// Model 1.4.0's records and its handoff to the new binary without registry downloads.
const windows = process.platform === 'win32';
const binaryName = windows ? 'vp.exe' : 'vp';
const sourceBin = join(process.env.VP_HOME, 'current/bin');
const split = process.argv.includes('--split');
assert.ok(!split || !windows, 'The split fixture requires XDG-isolated config and state roots.');
const user = mkdtempSync(join(process.cwd(), 'legacy-user-'));
const data = join(user, split ? 'data' : '.vite-plus');
const bin = split ? join(user, 'shared-bin') : join(data, 'bin');
const cache = split ? join(user, 'cache') : join(data, 'cache');
const config = split ? join(user, '.config/vite-plus') : data;
const state = split ? join(user, '.local/state/vite-plus') : data;
const env = {
...process.env,
HOME: user,
USERPROFILE: user,
ZDOTDIR: user,
XDG_CONFIG_HOME: join(user, '.config'),
XDG_DATA_HOME: join(user, '.local/share'),
XDG_STATE_HOME: join(user, '.local/state'),
APPDATA: join(user, 'AppData/Roaming'),
LOCALAPPDATA: join(user, 'AppData/Local'),
VP_SELF_SETUP_NO_MODIFY_PATH: '1',
VP_SKIP_DEPS_INSTALL: '1',
CI: '1',
NO_COLOR: '1',
};
for (const key of ['VP_HOME', 'VP_DATA_DIR', 'VP_CACHE_DIR', 'VP_BIN_DIR']) delete env[key];
Object.assign(
env,
split ? { VP_DATA_DIR: data, VP_CACHE_DIR: cache, VP_BIN_DIR: bin } : { VP_HOME: data },
);

function write(path, contents) {
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, contents);
}

function version(name, active = false) {
const directory = join(data, name);
const packageVersion = name.split('+')[0];
const lockfile = `lockfileVersion: '9.0'\nimporters:\n .:\n dependencies:\n vite-plus:\n specifier: ${packageVersion}\n version: ${packageVersion}\n`;
write(join(directory, 'bin/vp-install-lock.yaml'), lockfile);
write(
join(directory, 'node_modules/.vp-install-complete'),
createHash('sha256').update(lockfile).digest(),
);
write(
join(directory, 'package.json'),
JSON.stringify({
name: 'vp-global',
version: packageVersion,
private: true,
dependencies: { 'vite-plus': packageVersion },
}),
);
write(
join(directory, 'node_modules/vite-plus/package.json'),
JSON.stringify({ name: 'vite-plus', version: packageVersion }),
);
if (active) {
copyFileSync(join(sourceBin, binaryName), join(directory, 'bin', binaryName));
if (windows) copyFileSync(join(sourceBin, 'vp-shim.exe'), join(directory, 'bin/vp-shim.exe'));
} else {
write(join(directory, 'bin', binaryName), 'Historical binaries must not be executed.\n');
write(join(directory, 'bin/.vp-setup-complete'), '');
}
return directory;
}

function run(binary, args) {
const result = spawnSync(binary, args, { env, encoding: 'utf8', timeout: 30000 });
assert.equal(result.status, 0, `${result.error ?? ''}\n${result.stdout}\n${result.stderr}`);
return result.stdout + result.stderr;
}

try {
const old = version('1.4.0');
const forced = version('1.4.0+force.123.456');
const current = version('1.5.0', true);
const active = join(current, 'bin', binaryName);
symlinkSync(current, join(data, 'current'), windows ? 'junction' : 'dir');
const retained = [
[join(data, 'personal.txt'), 'personal data'],
[
join(data, 'js_runtime/node/22.0.0', windows ? 'node.exe' : 'bin/node'),
'legacy runtime without an ownership receipt',
],
[join(data, 'package_manager/pnpm/10.0.0/pnpm'), 'legacy package manager'],
[join(cache, 'downloads'), 'legacy cache'],
[join(config, 'config.json'), JSON.stringify({ nodeShimMode: 'system_first' })],
[join(state, 'legacy-state'), 'legacy state'],
[join(bin, 'unrelated-tool'), 'another tool'],
];
for (const [path, contents] of retained) write(path, contents);
const incomplete = version('1.3.0');
rmSync(join(incomplete, 'node_modules/.vp-install-complete'));
const unlocked = version('1.3.1');
write(join(unlocked, 'node_modules/.vp-install-complete'), 'unlocked');
const mismatched = version('1.3.2');
write(join(mismatched, 'bin/vp-install-lock.yaml'), 'different lockfile');

// This is the final command issued by the 1.4.0 upgrader after switching current.
run(active, ['env', 'setup', '--refresh']);
for (const root of new Set([data, cache, config, state])) {
assert.equal(existsSync(join(root, '.vp-owned')), false, `Upgrade claimed ${root}`);
}
assert.equal(existsSync(join(current, 'bin/.vp-setup-complete')), true);
console.log('Upgrade refresh completes without claiming existing roots.');
const preservedContents = retained.map(([path]) => [path, readFileSync(path)]);
const profile = join(env.XDG_CONFIG_HOME, 'nushell/config.nu');
write(
profile,
`# personal config\n# Vite+ bin\nsource "${config.replaceAll('\\', '/')}/env.nu"\n`,
);
const output = run(active, ['implode', '--yes']);
for (const directory of [old, forced, current])
assert.equal(existsSync(directory), false, directory);
assert.equal(lstatSync(join(data, 'current'), { throwIfNoEntry: false }), undefined);
assert.equal(lstatSync(join(bin, binaryName), { throwIfNoEntry: false }), undefined);
assert.equal(
lstatSync(join(data, 'fallback-bin', windows ? 'node.exe' : 'node'), { throwIfNoEntry: false }),
undefined,
);
assert.equal(readFileSync(profile, 'utf8'), '# personal config\n');
console.log('Verified current and historical CLI versions, links, and shims are removed.');
for (const [path, contents] of preservedContents)
assert.deepEqual(readFileSync(path), contents, path);
for (const directory of [incomplete, unlocked, mismatched])
assert.equal(existsSync(directory), true);
assert.match(output, /preserved to protect unverified data/);
for (const name of [
'personal.txt',
'js_runtime',
'package_manager',
'cache',
'config.json',
'legacy-state',
'1.3.0',
'1.3.1',
'1.3.2',
])
assert.ok(output.includes(name), output);
console.log(
'Unverified runtimes, package managers, cache, config, and incomplete versions remain and are reported.',
);
console.log(
'Personal files and unrelated tools are unchanged; only the matching shell entry is removed.',
);
} finally {
rmSync(user, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
name = "implode_confirmation"
vp = "global"
seed-runtime = false
env = { VP_SELF_SETUP_NO_MODIFY_PATH = "1" }
steps = [
{ argv = ["vp", "implode"], comment = "The old confirmation word leaves the installation intact", interactions = [
{ expect-milestone = "text:implode:ready" },
Expand All @@ -25,3 +26,51 @@ steps = [
] },
["vpt", "stat-file", "$VP_HOME", "--assert", "missing"],
]

[[case]]
name = "implode_unowned_directory"
vp = "global"
seed-runtime = false
skip-platforms = ["windows"]
env = { VP_SKIP_DEPS_INSTALL = "1", VP_VERSION = "0.0.0-test", VP_NODE_MANAGER = "no", VP_SELF_SETUP_NO_MODIFY_PATH = "1" }
steps = [
{ argv = ["vpt", "mkdir", "external"], snapshot = false },
{ argv = ["vpt", "cp", "$VP_HOME/bin/vp", "external/vp"], snapshot = false },
{ argv = ["vpt", "chmod", "+x", "external/vp"], snapshot = false },
{ argv = ["vpt", "write-file", "$HOME/Documents/personal.txt", "keep my documents\n"], snapshot = false },
{ argv = ["vpt", "write-file", "$HOME/.bashrc", "# personal config\n# Vite+ bin\n. \"$HOME/Documents/env\"\n"], snapshot = false },
{ argv = ["vp", "implode", "--yes"], envs = [["VP_HOME", "${HOME}/Documents"]], comment = "An unrelated override fails before touching files or profiles", continue-on-failure = true },
["vpt", "print-file", "$HOME/.bashrc"],
{ argv = ["./external/vp"], tty = false, envs = [["VP_HOME", "${HOME}/Documents"]], comment = "Setup succeeds without claiming a nonempty personal directory", snapshot = false },
["vpt", "stat-file", "$HOME/Documents/current/bin/.vp-setup-complete", "--assert", "file"],
{ argv = ["vp", "implode", "--yes"], envs = [["VP_HOME", "${HOME}/Documents"]], comment = "Only the newly owned cache is removed; the unmarked parent is preserved" },
["vpt", "stat-file", "$HOME/Documents/cache", "--assert", "missing"],
["vpt", "stat-file", "$HOME/Documents/.vp-owned", "--assert", "missing"],
["vpt", "print-file", "$HOME/Documents/personal.txt"],
["vpt", "print-file", "$HOME/.bashrc"],
]

[[case]]
name = "implode_nested_unowned_cache"
vp = "global"
seed-runtime = false
env = { VP_SELF_SETUP_NO_MODIFY_PATH = "1" }
steps = [
{ argv = ["vpt", "rm", "$VP_HOME/cache/.vp-owned"], snapshot = false },
{ argv = ["vpt", "write-file", "$VP_HOME/cache/personal.txt", "keep\n"], snapshot = false },
{ argv = ["vp", "implode", "--yes"], comment = "A marked ancestor cannot authorize removal of an unmarked category", snapshot = false },
["vpt", "stat-file", "$VP_HOME", "--assert", "dir"],
["vpt", "print-file", "$VP_HOME/cache/personal.txt"],
["vpt", "stat-file", "$VP_HOME/bin", "--assert", "dir"],
]

[[case]]
name = "implode_legacy_upgrade"
vp = "global"
steps = [["node", "legacy-upgrade.mjs"]]

[[case]]
name = "implode_legacy_upgrade_split"
vp = "global"
skip-platforms = ["windows"]
steps = [["node", "legacy-upgrade.mjs", "--split"]]
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ warn: This will completely remove vite-plus from your system!

Directories to remove:
- <home>/.vite-plus
Shim files to remove from: <home>/.vite-plus/bin
Verified shims to remove from: <home>/.vite-plus/bin
Verified shims to remove from: <home>/.vite-plus/fallback-bin

Type boom to confirm:
```
Expand All @@ -23,7 +24,8 @@ warn: This will completely remove vite-plus from your system!

Directories to remove:
- <home>/.vite-plus
Shim files to remove from: <home>/.vite-plus/bin
Verified shims to remove from: <home>/.vite-plus/bin
Verified shims to remove from: <home>/.vite-plus/fallback-bin

Type boom to confirm:
uninstall
Expand All @@ -47,7 +49,8 @@ warn: This will completely remove vite-plus from your system!

Directories to remove:
- <home>/.vite-plus
Shim files to remove from: <home>/.vite-plus/bin
Verified shims to remove from: <home>/.vite-plus/bin
Verified shims to remove from: <home>/.vite-plus/fallback-bin

Type boom to confirm:
```
Expand All @@ -59,7 +62,8 @@ warn: This will completely remove vite-plus from your system!

Directories to remove:
- <home>/.vite-plus
Shim files to remove from: <home>/.vite-plus/bin
Verified shims to remove from: <home>/.vite-plus/bin
Verified shims to remove from: <home>/.vite-plus/fallback-bin

Type boom to confirm:
implode
Expand All @@ -83,7 +87,8 @@ warn: This will completely remove vite-plus from your system!

Directories to remove:
- <home>/.vite-plus
Shim files to remove from: <home>/.vite-plus/bin
Verified shims to remove from: <home>/.vite-plus/bin
Verified shims to remove from: <home>/.vite-plus/fallback-bin

Type boom to confirm:
```
Expand All @@ -95,7 +100,8 @@ warn: This will completely remove vite-plus from your system!

Directories to remove:
- <home>/.vite-plus
Shim files to remove from: <home>/.vite-plus/bin
Verified shims to remove from: <home>/.vite-plus/bin
Verified shims to remove from: <home>/.vite-plus/fallback-bin

Type boom to confirm:

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# implode_legacy_upgrade

## `node legacy-upgrade.mjs`

```
Upgrade refresh completes without claiming existing roots.
Verified current and historical CLI versions, links, and shims are removed.
Unverified runtimes, package managers, cache, config, and incomplete versions remain and are reported.
Personal files and unrelated tools are unchanged; only the matching shell entry is removed.
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# implode_legacy_upgrade_split

## `node legacy-upgrade.mjs --split`

```
Upgrade refresh completes without claiming existing roots.
Verified current and historical CLI versions, links, and shims are removed.
Unverified runtimes, package managers, cache, config, and incomplete versions remain and are reported.
Personal files and unrelated tools are unchanged; only the matching shell entry is removed.
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# implode_nested_unowned_cache

## `vpt rm $VP_HOME/cache/.vp-owned`


## `vpt write-file $VP_HOME/cache/personal.txt 'keep
'`


## `vp implode --yes`

A marked ancestor cannot authorize removal of an unmarked category


## `vpt stat-file $VP_HOME --assert dir`

```
<home>/.vite-plus: dir
```

## `vpt print-file $VP_HOME/cache/personal.txt`

```
keep
```

## `vpt stat-file $VP_HOME/bin --assert dir`

```
<home>/.vite-plus/bin: dir
```
Loading
Loading