create-checkstack-plugin 0.1.0
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/package.json +33 -0
- package/src/cli-args.test.ts +72 -0
- package/src/cli-args.ts +130 -0
- package/src/cli.ts +177 -0
- package/src/external-plugin-lifecycle.it.test.ts +478 -0
- package/src/git.ts +39 -0
- package/src/local-registry.test.ts +143 -0
- package/src/local-registry.ts +313 -0
- package/src/npm-view-resolver.test.ts +140 -0
- package/src/npm-view-resolver.ts +140 -0
- package/src/scaffold-standalone.test.ts +211 -0
- package/src/scaffold-standalone.ts +104 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from "bun:test";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
scaffoldStandaloneWorkspace,
|
|
7
|
+
localSiblingNames,
|
|
8
|
+
} from "./scaffold-standalone";
|
|
9
|
+
import { createNpmViewResolver } from "./npm-view-resolver";
|
|
10
|
+
|
|
11
|
+
const tmpDirs: string[] = [];
|
|
12
|
+
|
|
13
|
+
function makeTmpDir(): string {
|
|
14
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ccp-standalone-"));
|
|
15
|
+
tmpDirs.push(dir);
|
|
16
|
+
return dir;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
for (const dir of tmpDirs.splice(0)) {
|
|
21
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
function readPkg(file: string): Record<string, unknown> {
|
|
26
|
+
return JSON.parse(fs.readFileSync(file, "utf8")) as Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A resolver that pins every published @checkstack/* dep to a fixed version. */
|
|
30
|
+
function fixedResolver(baseName: string, packageScope: string) {
|
|
31
|
+
return createNpmViewResolver({
|
|
32
|
+
runNpmView: ({ packageName }) =>
|
|
33
|
+
Promise.resolve({ version: `9.9.9-${packageName.length}` }),
|
|
34
|
+
localSiblings: localSiblingNames({ baseName, packageScope }),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("localSiblingNames", () => {
|
|
39
|
+
it("builds scoped sibling names", () => {
|
|
40
|
+
expect(localSiblingNames({ baseName: "widget", packageScope: "acme" })).toEqual(
|
|
41
|
+
new Set(["@acme/widget-common", "@acme/widget-backend", "@acme/widget-frontend"]),
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("builds unscoped sibling names", () => {
|
|
46
|
+
expect(localSiblingNames({ baseName: "widget", packageScope: "" })).toEqual(
|
|
47
|
+
new Set(["widget-common", "widget-backend", "widget-frontend"]),
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("scaffoldStandaloneWorkspace — layout", () => {
|
|
53
|
+
it("emits a root workspace + the trio under packages/", async () => {
|
|
54
|
+
const root = makeTmpDir();
|
|
55
|
+
await scaffoldStandaloneWorkspace({
|
|
56
|
+
rootDir: root,
|
|
57
|
+
baseName: "widget",
|
|
58
|
+
description: "Widget plugin",
|
|
59
|
+
packageScope: "acme",
|
|
60
|
+
resolveVersion: fixedResolver("widget", "acme"),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(fs.existsSync(path.join(root, "package.json"))).toBe(true);
|
|
64
|
+
expect(fs.existsSync(path.join(root, "tsconfig.json"))).toBe(true);
|
|
65
|
+
expect(fs.existsSync(path.join(root, "eslint.config.mjs"))).toBe(true);
|
|
66
|
+
expect(fs.existsSync(path.join(root, ".gitignore"))).toBe(true);
|
|
67
|
+
expect(fs.existsSync(path.join(root, "README.md"))).toBe(true);
|
|
68
|
+
expect(fs.existsSync(path.join(root, ".changeset", "config.json"))).toBe(true);
|
|
69
|
+
|
|
70
|
+
for (const type of ["common", "backend", "frontend"]) {
|
|
71
|
+
expect(
|
|
72
|
+
fs.existsSync(path.join(root, "packages", `widget-${type}`, "package.json")),
|
|
73
|
+
).toBe(true);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("makes the root a private Bun workspace with forwarding scripts", async () => {
|
|
78
|
+
const root = makeTmpDir();
|
|
79
|
+
await scaffoldStandaloneWorkspace({
|
|
80
|
+
rootDir: root,
|
|
81
|
+
baseName: "widget",
|
|
82
|
+
description: "Widget plugin",
|
|
83
|
+
packageScope: "acme",
|
|
84
|
+
resolveVersion: fixedResolver("widget", "acme"),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const pkg = readPkg(path.join(root, "package.json"));
|
|
88
|
+
expect(pkg.private).toBe(true);
|
|
89
|
+
expect(pkg.workspaces).toEqual(["packages/*"]);
|
|
90
|
+
const scripts = pkg.scripts as Record<string, string>;
|
|
91
|
+
expect(scripts.dev).toContain("@acme/widget-backend");
|
|
92
|
+
expect(scripts.pack).toContain("--bundle");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("scaffoldStandaloneWorkspace — versions", () => {
|
|
97
|
+
it("rewrites every published @checkstack/* dep to a concrete version", async () => {
|
|
98
|
+
const root = makeTmpDir();
|
|
99
|
+
await scaffoldStandaloneWorkspace({
|
|
100
|
+
rootDir: root,
|
|
101
|
+
baseName: "widget",
|
|
102
|
+
description: "Widget plugin",
|
|
103
|
+
packageScope: "acme",
|
|
104
|
+
resolveVersion: fixedResolver("widget", "acme"),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const backend = readPkg(
|
|
108
|
+
path.join(root, "packages", "widget-backend", "package.json"),
|
|
109
|
+
);
|
|
110
|
+
const deps = backend.dependencies as Record<string, string>;
|
|
111
|
+
const devDeps = backend.devDependencies as Record<string, string>;
|
|
112
|
+
// A published @checkstack/* dep is now a concrete caret.
|
|
113
|
+
expect(deps["@checkstack/common"]).toMatch(/^\^9\.9\.9/);
|
|
114
|
+
expect(devDeps["@checkstack/scripts"]).toMatch(/^\^9\.9\.9/);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("leaves the local trio siblings as workspace:* and no published workspace:* survives", async () => {
|
|
118
|
+
const root = makeTmpDir();
|
|
119
|
+
await scaffoldStandaloneWorkspace({
|
|
120
|
+
rootDir: root,
|
|
121
|
+
baseName: "widget",
|
|
122
|
+
description: "Widget plugin",
|
|
123
|
+
packageScope: "acme",
|
|
124
|
+
resolveVersion: fixedResolver("widget", "acme"),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const backend = readPkg(
|
|
128
|
+
path.join(root, "packages", "widget-backend", "package.json"),
|
|
129
|
+
);
|
|
130
|
+
const deps = backend.dependencies as Record<string, string>;
|
|
131
|
+
// The local common sibling stays a workspace range (installs locally).
|
|
132
|
+
expect(deps["@acme/widget-common"]).toBe("workspace:*");
|
|
133
|
+
|
|
134
|
+
// No @checkstack/* (published) range is left as workspace:* in any
|
|
135
|
+
// trio package.
|
|
136
|
+
for (const type of ["common", "backend", "frontend"]) {
|
|
137
|
+
const pkg = readPkg(
|
|
138
|
+
path.join(root, "packages", `widget-${type}`, "package.json"),
|
|
139
|
+
);
|
|
140
|
+
const sections = ["dependencies", "devDependencies", "peerDependencies"];
|
|
141
|
+
for (const section of sections) {
|
|
142
|
+
const block = pkg[section] as Record<string, string> | undefined;
|
|
143
|
+
if (!block) continue;
|
|
144
|
+
for (const [name, range] of Object.entries(block)) {
|
|
145
|
+
if (name.startsWith("@checkstack/")) {
|
|
146
|
+
expect(range.startsWith("workspace:")).toBe(false);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("scaffoldStandaloneWorkspace — bundle emission", () => {
|
|
155
|
+
it("sets checkstack.bundle on the backend listing the scoped siblings", async () => {
|
|
156
|
+
const root = makeTmpDir();
|
|
157
|
+
await scaffoldStandaloneWorkspace({
|
|
158
|
+
rootDir: root,
|
|
159
|
+
baseName: "widget",
|
|
160
|
+
description: "Widget plugin",
|
|
161
|
+
packageScope: "acme",
|
|
162
|
+
resolveVersion: fixedResolver("widget", "acme"),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const backend = readPkg(
|
|
166
|
+
path.join(root, "packages", "widget-backend", "package.json"),
|
|
167
|
+
);
|
|
168
|
+
const checkstack = backend.checkstack as { bundle?: string[] };
|
|
169
|
+
expect(checkstack.bundle).toEqual([
|
|
170
|
+
"@acme/widget-common",
|
|
171
|
+
"@acme/widget-frontend",
|
|
172
|
+
]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("does NOT set checkstack.bundle on the common/frontend siblings", async () => {
|
|
176
|
+
const root = makeTmpDir();
|
|
177
|
+
await scaffoldStandaloneWorkspace({
|
|
178
|
+
rootDir: root,
|
|
179
|
+
baseName: "widget",
|
|
180
|
+
description: "Widget plugin",
|
|
181
|
+
packageScope: "",
|
|
182
|
+
resolveVersion: fixedResolver("widget", ""),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
for (const type of ["common", "frontend"]) {
|
|
186
|
+
const pkg = readPkg(
|
|
187
|
+
path.join(root, "packages", `widget-${type}`, "package.json"),
|
|
188
|
+
);
|
|
189
|
+
const checkstack = pkg.checkstack as { bundle?: unknown };
|
|
190
|
+
expect(checkstack.bundle).toBeUndefined();
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("emits unscoped sibling bundle names when no scope is given", async () => {
|
|
195
|
+
const root = makeTmpDir();
|
|
196
|
+
await scaffoldStandaloneWorkspace({
|
|
197
|
+
rootDir: root,
|
|
198
|
+
baseName: "widget",
|
|
199
|
+
description: "Widget plugin",
|
|
200
|
+
packageScope: "",
|
|
201
|
+
resolveVersion: fixedResolver("widget", ""),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const backend = readPkg(
|
|
205
|
+
path.join(root, "packages", "widget-backend", "package.json"),
|
|
206
|
+
);
|
|
207
|
+
expect(backend.name).toBe("widget-backend");
|
|
208
|
+
const checkstack = backend.checkstack as { bundle?: string[] };
|
|
209
|
+
expect(checkstack.bundle).toEqual(["widget-common", "widget-frontend"]);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
scaffoldPlugin,
|
|
4
|
+
scaffoldStandaloneRoot,
|
|
5
|
+
type VersionResolver,
|
|
6
|
+
type ScaffoldIo,
|
|
7
|
+
} from "@checkstack/scripts/scaffold";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Compose the full standalone plugin workspace: the root workspace files
|
|
11
|
+
* plus the `common` + `backend` + `frontend` trio under `packages/`.
|
|
12
|
+
*
|
|
13
|
+
* This is the monorepo-decoupled orchestration the `create-checkstack-plugin`
|
|
14
|
+
* CLI runs (the CLI itself only adds prompts + `git init` on top). It is a
|
|
15
|
+
* thin composition over the `@checkstack/scripts` engine seams so it can be
|
|
16
|
+
* unit-tested against a tmpdir with an injected resolver and IO.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** The three package types the standalone default emits, in dependency order. */
|
|
20
|
+
export const STANDALONE_PLUGIN_TYPES = [
|
|
21
|
+
"common",
|
|
22
|
+
"backend",
|
|
23
|
+
"frontend",
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
export type StandalonePluginType = (typeof STANDALONE_PLUGIN_TYPES)[number];
|
|
27
|
+
|
|
28
|
+
export interface ScaffoldStandaloneWorkspaceOptions {
|
|
29
|
+
/** Repo root directory to create (the workspace root). */
|
|
30
|
+
rootDir: string;
|
|
31
|
+
/** Bare base name, e.g. `widget`. */
|
|
32
|
+
baseName: string;
|
|
33
|
+
description: string;
|
|
34
|
+
/** npm scope without `@` (e.g. `acme`); empty string for unscoped. */
|
|
35
|
+
packageScope: string;
|
|
36
|
+
/**
|
|
37
|
+
* Resolves concrete versions for the rendered `workspace:*` ranges.
|
|
38
|
+
* `@checkstack/*` deps resolve from the registry; local trio siblings are
|
|
39
|
+
* kept as `workspace:*` by the resolver.
|
|
40
|
+
*/
|
|
41
|
+
resolveVersion: VersionResolver;
|
|
42
|
+
io?: Partial<ScaffoldIo>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ScaffoldStandaloneWorkspaceResult {
|
|
46
|
+
rootDir: string;
|
|
47
|
+
packagesDir: string;
|
|
48
|
+
/** Absolute paths of every file written (root + trio). */
|
|
49
|
+
createdFiles: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build the set of local sibling package names (scope-aware) so the version
|
|
54
|
+
* resolver knows which `workspace:*` ranges to leave untouched.
|
|
55
|
+
*/
|
|
56
|
+
export function localSiblingNames({
|
|
57
|
+
baseName,
|
|
58
|
+
packageScope,
|
|
59
|
+
}: {
|
|
60
|
+
baseName: string;
|
|
61
|
+
packageScope: string;
|
|
62
|
+
}): Set<string> {
|
|
63
|
+
const prefix = packageScope ? `@${packageScope}/` : "";
|
|
64
|
+
return new Set(
|
|
65
|
+
STANDALONE_PLUGIN_TYPES.map((type) => `${prefix}${baseName}-${type}`),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function scaffoldStandaloneWorkspace({
|
|
70
|
+
rootDir,
|
|
71
|
+
baseName,
|
|
72
|
+
description,
|
|
73
|
+
packageScope,
|
|
74
|
+
resolveVersion,
|
|
75
|
+
io,
|
|
76
|
+
}: ScaffoldStandaloneWorkspaceOptions): Promise<ScaffoldStandaloneWorkspaceResult> {
|
|
77
|
+
const createdFiles: string[] = [];
|
|
78
|
+
|
|
79
|
+
const root = scaffoldStandaloneRoot({
|
|
80
|
+
rootDir,
|
|
81
|
+
baseName,
|
|
82
|
+
description,
|
|
83
|
+
packageScope,
|
|
84
|
+
io,
|
|
85
|
+
});
|
|
86
|
+
createdFiles.push(...root.createdFiles);
|
|
87
|
+
|
|
88
|
+
const packagesDir = path.join(rootDir, "packages");
|
|
89
|
+
|
|
90
|
+
for (const pluginType of STANDALONE_PLUGIN_TYPES) {
|
|
91
|
+
const result = await scaffoldPlugin({
|
|
92
|
+
mode: { kind: "standalone", targetDir: packagesDir },
|
|
93
|
+
baseName,
|
|
94
|
+
description,
|
|
95
|
+
pluginType,
|
|
96
|
+
packageScope,
|
|
97
|
+
resolveVersion,
|
|
98
|
+
io,
|
|
99
|
+
});
|
|
100
|
+
createdFiles.push(...result.createdFiles);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { rootDir, packagesDir, createdFiles };
|
|
104
|
+
}
|