fimo 0.2.3-staging.8 → 0.2.3-staging.9
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/assets/skills/fimo/references/setup-plain-vite.md +4 -1
- package/assets/skills/fimo/references/setup-react-router.md +3 -2
- package/assets/skills/fimo-cli/references/agent-setup.md +1 -1
- package/assets/skills/fimo-cli/references/branches.md +2 -0
- package/dist/cli/bundle.json +2 -2
- package/dist/cli/index.js +758 -631
- package/dist/runtime/templates.d.ts +5 -5
- package/dist/runtime/templates.d.ts.map +1 -1
- package/dist/runtime/templates.js +14 -13
- package/package.json +1 -2
- package/scripts/lib/dev-release-state.mjs +44 -0
- package/scripts/lib/dev-release-state.test.ts +36 -0
- package/scripts/postinstall.mjs +6 -4
- package/scripts/publish-npm.mjs +12 -1
- package/templates/react-router/package.json +1 -8
- package/templates/react-router/pnpm-workspace.yaml +6 -3
- package/scripts/lib/cleanup-release.mjs +0 -64
- package/scripts/lib/cleanup-release.test.ts +0 -142
- package/scripts/publish-tarball.mjs +0 -245
|
@@ -30,8 +30,7 @@ export declare function getTemplatePath(framework?: TemplateFramework): string;
|
|
|
30
30
|
*/
|
|
31
31
|
export declare function getLegacyTemplateDir(): string;
|
|
32
32
|
export interface ReleaseMeta {
|
|
33
|
-
|
|
34
|
-
source: 'npm' | 'url' | 'dev' | null;
|
|
33
|
+
source: 'npm' | 'dev' | null;
|
|
35
34
|
}
|
|
36
35
|
export declare function readReleaseMeta(): ReleaseMeta;
|
|
37
36
|
export declare function readCliSemver(): string | null;
|
|
@@ -51,9 +50,10 @@ interface PkgWithDeps {
|
|
|
51
50
|
devDependencies?: Record<string, string>;
|
|
52
51
|
}
|
|
53
52
|
/**
|
|
54
|
-
* Rewrite `fimo` (in dependencies + devDependencies) to the current pin.
|
|
55
|
-
*
|
|
56
|
-
*
|
|
53
|
+
* Rewrite `fimo` (in dependencies + devDependencies) to the current pin. Only
|
|
54
|
+
* touches entries that look like a value we own: the template's dist-tag, an old
|
|
55
|
+
* tarball URL, an exact/ranged semver, or a local `file:` dependency. Bespoke
|
|
56
|
+
* values are left alone.
|
|
57
57
|
*
|
|
58
58
|
* Returns true if anything changed.
|
|
59
59
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/runtime/templates.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/runtime/templates.ts"],"names":[],"mappings":"AAgCA,eAAO,MAAM,mBAAmB,oDAAqD,CAAC;AACtF,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AACrE,eAAO,MAAM,gBAAgB,EAAE,iBAAkC,CAAC;AAElE;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,SAAS,GAAE,iBAAoC,GAAG,MAAM,CAEvF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAM7C;AAmBD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,KAAK,GAAG,KAAK,GAAG,IAAI,CAAC;CAC9B;AAED,wBAAgB,eAAe,IAAI,WAAW,CAS7C;AAED,wBAAgB,aAAa,IAAI,MAAM,GAAG,IAAI,CAW7C;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,OAAO,GAAE;IAAE,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAO,GAC/C,MAAM,GAAG,IAAI,CAUf;AAED,uDAAuD;AACvD,wBAAgB,cAAc,CAAC,OAAO,GAAE;IAAE,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAO,GAAG,MAAM,GAAG,IAAI,CAE9F;AAED,UAAU,WAAW;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC1C;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,GAAE,MAAM,GAAG,IAAuB,GAAG,OAAO,CA2B/F"}
|
|
@@ -27,7 +27,6 @@ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
|
27
27
|
const PACKAGE_ROOT = path.resolve(MODULE_DIR, '..', '..');
|
|
28
28
|
const RELEASE_FILE = path.join(PACKAGE_ROOT, 'release.json');
|
|
29
29
|
const PACKAGE_JSON_FILE = path.join(PACKAGE_ROOT, 'package.json');
|
|
30
|
-
const TARBALL_URL_BASE = 'https://pub-41cdea46386f4b238d8c528c4327dfc1.r2.dev/cli';
|
|
31
30
|
export const TEMPLATE_FRAMEWORKS = ['react-router', 'astro', 'next', 'vite'];
|
|
32
31
|
export const DEFAULT_TEMPLATE = 'react-router';
|
|
33
32
|
/**
|
|
@@ -70,12 +69,11 @@ export function readReleaseMeta() {
|
|
|
70
69
|
try {
|
|
71
70
|
const raw = fs.readFileSync(RELEASE_FILE, 'utf8');
|
|
72
71
|
const parsed = JSON.parse(raw);
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
return { id, source };
|
|
72
|
+
const source = parsed.source === 'npm' || parsed.source === 'dev' ? parsed.source : null;
|
|
73
|
+
return { source };
|
|
76
74
|
}
|
|
77
75
|
catch {
|
|
78
|
-
return {
|
|
76
|
+
return { source: null };
|
|
79
77
|
}
|
|
80
78
|
}
|
|
81
79
|
export function readCliSemver() {
|
|
@@ -101,9 +99,6 @@ export function computeFimoPin(meta, semver, options = {}) {
|
|
|
101
99
|
return semver;
|
|
102
100
|
}
|
|
103
101
|
}
|
|
104
|
-
if (meta.id) {
|
|
105
|
-
return `${TARBALL_URL_BASE}/fimo-${meta.id}.tgz`;
|
|
106
|
-
}
|
|
107
102
|
if ((meta.source === 'dev' || meta.source === null) && options.devPackageRoot) {
|
|
108
103
|
return `file:${options.devPackageRoot}`;
|
|
109
104
|
}
|
|
@@ -114,9 +109,10 @@ export function resolveFimoPin(options = {}) {
|
|
|
114
109
|
return computeFimoPin(readReleaseMeta(), readCliSemver(), options);
|
|
115
110
|
}
|
|
116
111
|
/**
|
|
117
|
-
* Rewrite `fimo` (in dependencies + devDependencies) to the current pin.
|
|
118
|
-
*
|
|
119
|
-
*
|
|
112
|
+
* Rewrite `fimo` (in dependencies + devDependencies) to the current pin. Only
|
|
113
|
+
* touches entries that look like a value we own: the template's dist-tag, an old
|
|
114
|
+
* tarball URL, an exact/ranged semver, or a local `file:` dependency. Bespoke
|
|
115
|
+
* values are left alone.
|
|
120
116
|
*
|
|
121
117
|
* Returns true if anything changed.
|
|
122
118
|
*/
|
|
@@ -134,9 +130,14 @@ export function rewriteFimoDep(pkg, pin = resolveFimoPin()) {
|
|
|
134
130
|
if (typeof current !== 'string') {
|
|
135
131
|
continue;
|
|
136
132
|
}
|
|
137
|
-
|
|
133
|
+
// Migrate projects still pinned to the retired R2 tarball, repin a semver,
|
|
134
|
+
// rewrite the committed template's dist-tag, or (dev flow) swap a local
|
|
135
|
+
// `file:` build for a published version on deploy.
|
|
136
|
+
const looksLikeLegacyR2 = current.includes('r2.dev/cli/fimo-');
|
|
137
|
+
const looksLikeLocalFile = current.startsWith('file:');
|
|
138
138
|
const looksLikeSemver = /^[\^~]?\d+\.\d+\.\d+/.test(current);
|
|
139
|
-
|
|
139
|
+
const looksLikeOwnedDistTag = current === 'latest' || current === 'staging' || current === 'experimental';
|
|
140
|
+
if ((looksLikeLegacyR2 || looksLikeLocalFile || looksLikeSemver || looksLikeOwnedDistTag) && current !== pin) {
|
|
140
141
|
deps['fimo'] = pin;
|
|
141
142
|
changed = true;
|
|
142
143
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fimo",
|
|
3
|
-
"version": "0.2.3-staging.
|
|
3
|
+
"version": "0.2.3-staging.9",
|
|
4
4
|
"description": "Fimo CLI - create, deploy, and manage Fimo projects",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fimo": "dist/cli/index.js"
|
|
@@ -45,7 +45,6 @@
|
|
|
45
45
|
"check:types": "tsc -b tsconfig.json",
|
|
46
46
|
"start": "tsx src/cli/index.ts",
|
|
47
47
|
"install-cli": "pnpm build && node scripts/install-cli.mjs",
|
|
48
|
-
"publish-cli": "node scripts/publish-tarball.mjs",
|
|
49
48
|
"publish:npm": "node scripts/publish-npm.mjs",
|
|
50
49
|
"prepublishOnly": "pnpm run build && pnpm run build:bundle",
|
|
51
50
|
"release:bump": "node scripts/bump-version.mjs",
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { relative, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Record the latest locally-published experimental CLI version for this
|
|
7
|
+
* checkout. `fimo deploy` uses this to swap a project-local `file:` dependency
|
|
8
|
+
* to an immutable npm version before pushing to a remote sandbox.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} cliRoot - apps/cli root
|
|
11
|
+
* @param {{ version: string, channel: string }} release
|
|
12
|
+
* @returns {string} the file written
|
|
13
|
+
*/
|
|
14
|
+
export function writeDevReleaseState(cliRoot, release) {
|
|
15
|
+
const repoRoot = resolve(cliRoot, '..', '..');
|
|
16
|
+
const dir = resolve(repoRoot, '.fimo');
|
|
17
|
+
mkdirSync(dir, { recursive: true });
|
|
18
|
+
|
|
19
|
+
const payload = {
|
|
20
|
+
version: release.version,
|
|
21
|
+
channel: release.channel,
|
|
22
|
+
packageRoot: relative(repoRoot, cliRoot),
|
|
23
|
+
gitSha: readGitSha(repoRoot),
|
|
24
|
+
publishedAt: new Date().toISOString(),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const file = resolve(dir, 'dev.json');
|
|
28
|
+
writeFileSync(file, JSON.stringify(payload, null, 2) + '\n');
|
|
29
|
+
return file;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readGitSha(repoRoot) {
|
|
33
|
+
const result = spawnSync('git', ['rev-parse', 'HEAD'], {
|
|
34
|
+
cwd: repoRoot,
|
|
35
|
+
encoding: 'utf8',
|
|
36
|
+
});
|
|
37
|
+
if (result.status === 0) {
|
|
38
|
+
const sha = result.stdout.trim();
|
|
39
|
+
if (sha.length > 0) {
|
|
40
|
+
return sha;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { writeDevReleaseState } from './dev-release-state.mjs';
|
|
8
|
+
|
|
9
|
+
const tmpRoots: string[] = [];
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
for (const root of tmpRoots.splice(0)) {
|
|
13
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('writeDevReleaseState', () => {
|
|
18
|
+
it('writes repo-root .fimo/dev.json for the checkout', () => {
|
|
19
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fimo-dev-state-'));
|
|
20
|
+
tmpRoots.push(root);
|
|
21
|
+
const cliRoot = path.join(root, 'apps', 'cli');
|
|
22
|
+
fs.mkdirSync(cliRoot, { recursive: true });
|
|
23
|
+
|
|
24
|
+
const file = writeDevReleaseState(cliRoot, {
|
|
25
|
+
version: '0.2.3-experimental.42',
|
|
26
|
+
channel: 'experimental',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(file).toBe(path.join(root, '.fimo', 'dev.json'));
|
|
30
|
+
expect(JSON.parse(fs.readFileSync(file, 'utf8'))).toMatchObject({
|
|
31
|
+
version: '0.2.3-experimental.42',
|
|
32
|
+
channel: 'experimental',
|
|
33
|
+
packageRoot: 'apps/cli',
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
});
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -17,10 +17,12 @@
|
|
|
17
17
|
// never break the user's install).
|
|
18
18
|
//
|
|
19
19
|
// pnpm caveat: by default, pnpm strips build scripts from non-approved deps.
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
20
|
+
// Fimo-controlled installs (`fimo create --install`, sandbox `fimopm install`,
|
|
21
|
+
// and E2E helper installs) pass a scoped allow-all-builds flag for pnpm so the
|
|
22
|
+
// fimo postinstall hook can refresh project skills. The template intentionally
|
|
23
|
+
// does not persist allowlists in package.json / pnpm-workspace.yaml because
|
|
24
|
+
// those conflict with sandbox install flags and drift when deps change. npm /
|
|
25
|
+
// yarn run lifecycle scripts by default and need no extra config.
|
|
24
26
|
|
|
25
27
|
import fs from 'node:fs';
|
|
26
28
|
import path from 'node:path';
|
package/scripts/publish-npm.mjs
CHANGED
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
// staging URLs for experimental/staging, prod for latest. Never a tunnel.
|
|
15
15
|
// 4. For prerelease channels, synthesizes a disposable X.Y.Z-<tag>.<n> version.
|
|
16
16
|
// 5. Runs `npm publish --tag <distTag>` (no --provenance — strapi/fimo is private).
|
|
17
|
-
// 6.
|
|
17
|
+
// 6. On real experimental publishes, records the exact version in repo-root
|
|
18
|
+
// .fimo/dev.json for local source-checkout deploys.
|
|
19
|
+
// 7. Restores package.json + removes the transient release.json.
|
|
18
20
|
//
|
|
19
21
|
// `npm publish` triggers prepublishOnly (clean → build → build:bundle), so the
|
|
20
22
|
// bundled tarball is always fresh.
|
|
@@ -25,6 +27,7 @@ import { dirname, resolve } from 'node:path';
|
|
|
25
27
|
import { fileURLToPath } from 'node:url';
|
|
26
28
|
|
|
27
29
|
import { resolveChannel } from './lib/channels.mjs';
|
|
30
|
+
import { writeDevReleaseState } from './lib/dev-release-state.mjs';
|
|
28
31
|
import {
|
|
29
32
|
computePublishVersion,
|
|
30
33
|
pinTemplateFimo,
|
|
@@ -120,6 +123,14 @@ try {
|
|
|
120
123
|
console.error(`[publish-npm] npm publish failed (exit ${result.status}).`);
|
|
121
124
|
process.exit(result.status ?? 1);
|
|
122
125
|
}
|
|
126
|
+
|
|
127
|
+
if (!dryRun && channel.distTag === 'experimental') {
|
|
128
|
+
const devStatePath = writeDevReleaseState(CLI_ROOT, {
|
|
129
|
+
version: publishVersion,
|
|
130
|
+
channel: channel.distTag,
|
|
131
|
+
});
|
|
132
|
+
console.log(`[publish-npm] wrote ${devStatePath}`);
|
|
133
|
+
}
|
|
123
134
|
} finally {
|
|
124
135
|
// Restore the workspace: package.json version, template fimo pin, and remove
|
|
125
136
|
// the transient release.json so local `fimo` runs fall back to localhost.
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "react-router dev --port 5173",
|
|
8
|
-
"prebuild": "fimo validate",
|
|
9
8
|
"build": "react-router build && fimo export-static",
|
|
10
9
|
"lint": "oxlint --deny-warnings",
|
|
11
10
|
"typecheck": "tsc --noEmit",
|
|
@@ -22,7 +21,7 @@
|
|
|
22
21
|
"cmdk": "^1.1.1",
|
|
23
22
|
"date-fns": "^4.1.0",
|
|
24
23
|
"embla-carousel-react": "^8.6.0",
|
|
25
|
-
"fimo": "0.2.3-staging.
|
|
24
|
+
"fimo": "0.2.3-staging.9",
|
|
26
25
|
"input-otp": "^1.4.2",
|
|
27
26
|
"isbot": "^5",
|
|
28
27
|
"lucide-react": "^0.577.0",
|
|
@@ -53,11 +52,5 @@
|
|
|
53
52
|
"tw-animate-css": "^1.4.0",
|
|
54
53
|
"typescript": "5.9.3",
|
|
55
54
|
"vite": "^8.0.13"
|
|
56
|
-
},
|
|
57
|
-
"pnpm": {
|
|
58
|
-
"onlyBuiltDependencies": [
|
|
59
|
-
"esbuild",
|
|
60
|
-
"fimo"
|
|
61
|
-
]
|
|
62
55
|
}
|
|
63
56
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# Build approval is handled by Fimo-controlled install commands (`fimo create
|
|
2
|
+
# --install`, sandbox `fimopm install`, and E2E helpers) via scoped pnpm
|
|
3
|
+
# allow-all-builds flags. Do NOT add `allowBuilds` / `onlyBuiltDependencies`
|
|
4
|
+
# here: pnpm refuses to have both an allowlist and allow-all-builds
|
|
5
|
+
# (ERR_PNPM_CONFIG_CONFLICT_BUILT_DEPENDENCIES), which makes sandbox installs
|
|
6
|
+
# fail before the project's `fimo` dep is available.
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { rmSync } from 'node:fs';
|
|
2
|
-
|
|
3
|
-
// Helpers around the workspace-`release.json` cleanup behaviour used by
|
|
4
|
-
// `publish-tarball.mjs`. Extracted so the cleanup logic can be unit
|
|
5
|
-
// tested without spinning up the full clean → build → bundle → pack →
|
|
6
|
-
// upload chain.
|
|
7
|
-
//
|
|
8
|
-
// The contract is small but worth pinning:
|
|
9
|
-
// 1. `cleanupReleaseFile` is idempotent and safe when the file is
|
|
10
|
-
// already absent.
|
|
11
|
-
// 2. `registerReleaseCleanup` wires cleanup to process.on('exit'),
|
|
12
|
-
// SIGINT, and SIGTERM so the file is removed on any termination
|
|
13
|
-
// path (including process.exit calls inside the publish script).
|
|
14
|
-
// 3. The `skip` option short-circuits both surfaces - `cleanupReleaseFile`
|
|
15
|
-
// checks it, and `registerReleaseCleanup` closes over it.
|
|
16
|
-
|
|
17
|
-
export function cleanupReleaseFile(releaseFile, options = {}) {
|
|
18
|
-
if (options.skip) {
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
try {
|
|
22
|
-
rmSync(releaseFile, { force: true });
|
|
23
|
-
return true;
|
|
24
|
-
} catch {
|
|
25
|
-
// Best effort. A stale release.json is annoying but not
|
|
26
|
-
// worth bailing the publish for.
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Registers cleanup against the Node lifecycle and returns a callable
|
|
32
|
-
// that does the cleanup once-only. Useful in two ways:
|
|
33
|
-
// a) The bottom of the publish script calls the returned function
|
|
34
|
-
// explicitly so the success path can log a confirmation line.
|
|
35
|
-
// b) Failure paths (process.exit / SIGINT / SIGTERM) hit the
|
|
36
|
-
// registered handlers and still run the cleanup.
|
|
37
|
-
//
|
|
38
|
-
// Idempotency is enforced via a closure flag - calling the returned
|
|
39
|
-
// function twice (e.g., once explicitly + once on exit) only removes
|
|
40
|
-
// the file the first time.
|
|
41
|
-
export function registerReleaseCleanup(releaseFile, options = {}) {
|
|
42
|
-
let alreadyCleaned = false;
|
|
43
|
-
const skip = options.skip === true;
|
|
44
|
-
|
|
45
|
-
const cleanupOnce = () => {
|
|
46
|
-
if (alreadyCleaned || skip) {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
alreadyCleaned = true;
|
|
50
|
-
return cleanupReleaseFile(releaseFile);
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
process.on('exit', cleanupOnce);
|
|
54
|
-
process.on('SIGINT', () => {
|
|
55
|
-
cleanupOnce();
|
|
56
|
-
process.exit(130);
|
|
57
|
-
});
|
|
58
|
-
process.on('SIGTERM', () => {
|
|
59
|
-
cleanupOnce();
|
|
60
|
-
process.exit(143);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
return cleanupOnce;
|
|
64
|
-
}
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { execFileSync, spawnSync } from 'node:child_process';
|
|
2
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
|
|
7
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
8
|
-
|
|
9
|
-
import { cleanupReleaseFile, registerReleaseCleanup } from './cleanup-release.mjs';
|
|
10
|
-
|
|
11
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
const CLEANUP_MJS = path.resolve(__dirname, 'cleanup-release.mjs');
|
|
13
|
-
|
|
14
|
-
let dir: string;
|
|
15
|
-
let releaseFile: string;
|
|
16
|
-
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
dir = mkdtempSync(path.join(tmpdir(), 'fimo-cli-cleanup-'));
|
|
19
|
-
releaseFile = path.join(dir, 'release.json');
|
|
20
|
-
writeFileSync(releaseFile, JSON.stringify({ id: 'fixture', source: 'url', apiUrl: 'https://api.staging.fimo.team' }));
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
afterEach(() => {
|
|
24
|
-
rmSync(dir, { recursive: true, force: true });
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
describe('cleanupReleaseFile', () => {
|
|
28
|
-
it('removes the file', () => {
|
|
29
|
-
expect(existsSync(releaseFile)).toBe(true);
|
|
30
|
-
expect(cleanupReleaseFile(releaseFile)).toBe(true);
|
|
31
|
-
expect(existsSync(releaseFile)).toBe(false);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('respects the skip option (file survives)', () => {
|
|
35
|
-
cleanupReleaseFile(releaseFile, { skip: true });
|
|
36
|
-
expect(existsSync(releaseFile)).toBe(true);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('is a no-op when the file is absent (no throw)', () => {
|
|
40
|
-
rmSync(releaseFile);
|
|
41
|
-
expect(() => cleanupReleaseFile(releaseFile)).not.toThrow();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('is idempotent (re-running on an already-removed file is fine)', () => {
|
|
45
|
-
cleanupReleaseFile(releaseFile);
|
|
46
|
-
expect(() => cleanupReleaseFile(releaseFile)).not.toThrow();
|
|
47
|
-
expect(existsSync(releaseFile)).toBe(false);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('returns false when skipped', () => {
|
|
51
|
-
expect(cleanupReleaseFile(releaseFile, { skip: true })).toBe(false);
|
|
52
|
-
expect(existsSync(releaseFile)).toBe(true);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe('registerReleaseCleanup - direct (synchronous) call', () => {
|
|
57
|
-
// These tests exercise the returned closure directly. The
|
|
58
|
-
// process.on('exit') wiring is verified by the spawn-based test
|
|
59
|
-
// below (running the registration in a child whose lifetime we
|
|
60
|
-
// can observe).
|
|
61
|
-
//
|
|
62
|
-
// We do NOT call process.on('exit') in the test process itself,
|
|
63
|
-
// since that would pollute the vitest runner and other test files.
|
|
64
|
-
// Instead, the closure returned by registerReleaseCleanup is
|
|
65
|
-
// invoked manually as it would be from `cleanupReleaseJson()` at
|
|
66
|
-
// the bottom of publish-tarball.mjs.
|
|
67
|
-
|
|
68
|
-
it('first explicit call cleans; subsequent calls are no-ops', () => {
|
|
69
|
-
const cleanup = registerReleaseCleanup(releaseFile);
|
|
70
|
-
expect(existsSync(releaseFile)).toBe(true);
|
|
71
|
-
cleanup();
|
|
72
|
-
expect(existsSync(releaseFile)).toBe(false);
|
|
73
|
-
// Re-instate the file and verify the closure doesn't clean again.
|
|
74
|
-
writeFileSync(releaseFile, 'second');
|
|
75
|
-
cleanup();
|
|
76
|
-
expect(readFileSync(releaseFile, 'utf8')).toBe('second');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('skip:true short-circuits the closure', () => {
|
|
80
|
-
const cleanup = registerReleaseCleanup(releaseFile, { skip: true });
|
|
81
|
-
cleanup();
|
|
82
|
-
expect(existsSync(releaseFile)).toBe(true);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe('registerReleaseCleanup - process lifecycle (subprocess)', () => {
|
|
87
|
-
// The process.on('exit') wiring only fires when the host process
|
|
88
|
-
// actually exits. To verify it without exiting the test runner,
|
|
89
|
-
// we spawn a small child that registers cleanup and then returns
|
|
90
|
-
// - observing the file state from the parent after the child
|
|
91
|
-
// terminates.
|
|
92
|
-
|
|
93
|
-
function runChild(opts: { skip?: boolean; signal?: 'SIGINT' | 'SIGTERM' | null } = {}) {
|
|
94
|
-
const childScript = path.join(dir, 'child.mjs');
|
|
95
|
-
const skipFlag = opts.skip ? 'true' : 'false';
|
|
96
|
-
if (opts.signal) {
|
|
97
|
-
// Long-running child that we send a signal to. Loops to keep
|
|
98
|
-
// the process alive until the signal arrives.
|
|
99
|
-
writeFileSync(
|
|
100
|
-
childScript,
|
|
101
|
-
`import { registerReleaseCleanup } from '${CLEANUP_MJS}';
|
|
102
|
-
registerReleaseCleanup(${JSON.stringify(releaseFile)}, { skip: ${skipFlag} });
|
|
103
|
-
setInterval(() => {}, 1000);
|
|
104
|
-
`
|
|
105
|
-
);
|
|
106
|
-
const child = spawnSync(process.execPath, [childScript], {
|
|
107
|
-
timeout: 4000,
|
|
108
|
-
killSignal: opts.signal,
|
|
109
|
-
});
|
|
110
|
-
// spawnSync's timeout sends killSignal; we still get .status / .signal.
|
|
111
|
-
return child;
|
|
112
|
-
}
|
|
113
|
-
writeFileSync(
|
|
114
|
-
childScript,
|
|
115
|
-
`import { registerReleaseCleanup } from '${CLEANUP_MJS}';
|
|
116
|
-
registerReleaseCleanup(${JSON.stringify(releaseFile)}, { skip: ${skipFlag} });
|
|
117
|
-
// Exit naturally after registering - process.on('exit') should fire.
|
|
118
|
-
`
|
|
119
|
-
);
|
|
120
|
-
return execFileSync(process.execPath, [childScript], { encoding: 'utf8' });
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
it('removes the file when the child exits naturally', () => {
|
|
124
|
-
runChild();
|
|
125
|
-
expect(existsSync(releaseFile)).toBe(false);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('preserves the file when the child registered with skip:true', () => {
|
|
129
|
-
runChild({ skip: true });
|
|
130
|
-
expect(existsSync(releaseFile)).toBe(true);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('removes the file when the child is killed with SIGTERM', () => {
|
|
134
|
-
runChild({ signal: 'SIGTERM' });
|
|
135
|
-
expect(existsSync(releaseFile)).toBe(false);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('removes the file when the child is killed with SIGINT', () => {
|
|
139
|
-
runChild({ signal: 'SIGINT' });
|
|
140
|
-
expect(existsSync(releaseFile)).toBe(false);
|
|
141
|
-
});
|
|
142
|
-
});
|