storybooker 0.0.1

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.
@@ -0,0 +1,260 @@
1
+ // oxlint-disable new-cap
2
+ // oxlint-disable max-lines-per-function
3
+
4
+ import * as fs from "node:fs";
5
+ import * as path from "node:path";
6
+ import { styleText } from "node:util";
7
+ import createClient from "openapi-fetch";
8
+ import type { CommandModule } from "yargs";
9
+ import z from "zod";
10
+ import type { paths } from "../service-schema";
11
+ import { createAuthMiddleware } from "../utils/auth-utils";
12
+ import { buildStoryBook } from "../utils/sb-build";
13
+ import { testStoryBook } from "../utils/sb-test";
14
+ import {
15
+ sharedSchemas,
16
+ zodSchemaToCommandBuilder,
17
+ } from "../utils/schema-utils";
18
+ import { toReadableStream } from "../utils/stream-utils";
19
+ import type { ServiceClient } from "../utils/types";
20
+ import { zip } from "../utils/zip";
21
+
22
+ const CreateSchema = z.object({
23
+ project: sharedSchemas.project,
24
+ url: sharedSchemas.url,
25
+ cwd: sharedSchemas.cwd,
26
+ sha: z.string({ error: "URL is required to connect to the service." }).meta({
27
+ alias: ["id"],
28
+ description: "Unique ID of the build.",
29
+ }),
30
+ message: z
31
+ .string()
32
+ .optional()
33
+ .meta({ alias: ["m"], description: "Readable message for the build." }),
34
+ labels: z
35
+ .string()
36
+ .array()
37
+ .meta({
38
+ alias: ["l"],
39
+ description: "Labels associated with the build.",
40
+ }),
41
+ build: z
42
+ .union([z.string(), z.boolean()])
43
+ .optional()
44
+ .meta({
45
+ alias: ["b"],
46
+ description: "Name of the script in package.json to build the StoryBook.",
47
+ }),
48
+ test: z
49
+ .union([z.string(), z.boolean()])
50
+ .optional()
51
+ .meta({
52
+ alias: ["t"],
53
+ description: "Name of the script in package.json to test the StoryBook.",
54
+ }),
55
+ testCoverageDir: sharedSchemas.testCoverageDir,
56
+ testReportDir: sharedSchemas.testReportDir,
57
+ silent: z
58
+ .boolean()
59
+ .default(false)
60
+ .meta({
61
+ alias: ["s"],
62
+ description: "Silent the logs and only show final error/status.",
63
+ }),
64
+ authorName: z.string().optional().meta({
65
+ description: "Name of the author of the build.",
66
+ }),
67
+ authorEmail: z.email().optional().meta({
68
+ description: "Email of the author of the build.",
69
+ }),
70
+ ignoreError: z.boolean().default(false).meta({
71
+ hidden: true,
72
+ }),
73
+ authType: sharedSchemas.authType,
74
+ authValue: sharedSchemas.authValue,
75
+ });
76
+
77
+ export const createCommandModule: CommandModule = {
78
+ command: "create",
79
+ describe: "Create and upload StoryBook assets to the service.",
80
+ builder: zodSchemaToCommandBuilder(CreateSchema),
81
+ // oxlint-disable-next-line max-lines-per-function
82
+ handler: async (args): Promise<void> => {
83
+ const result = CreateSchema.safeParse(args);
84
+ if (!result.success) {
85
+ throw new Error(z.prettifyError(result.error));
86
+ }
87
+
88
+ const cwd = result.data.cwd ? path.resolve(result.data.cwd) : process.cwd();
89
+ if (cwd && !fs.existsSync(cwd)) {
90
+ throw new Error(`Path provided to CWD does not exists: '${cwd}'`);
91
+ }
92
+
93
+ const {
94
+ build,
95
+ silent,
96
+ url,
97
+ project,
98
+ sha,
99
+ ignoreError,
100
+ test,
101
+ testCoverageDir,
102
+ testReportDir,
103
+ authType,
104
+ authValue,
105
+ } = result.data;
106
+
107
+ const client = createClient<paths>({ baseUrl: url });
108
+ client.use(createAuthMiddleware({ authType, authValue }));
109
+
110
+ try {
111
+ console.group(styleText("bold", "\nCreate Build Entry"));
112
+ await createSBRBuild(client, result.data, ignoreError);
113
+ console.groupEnd();
114
+
115
+ console.group(styleText("bold", "\nBuild StoryBook"));
116
+ const buildDirpath = buildStoryBook({ build, cwd, silent });
117
+ if (buildDirpath) {
118
+ await uploadSBRBuild(client, {
119
+ project,
120
+ sha,
121
+ dirpath: buildDirpath,
122
+ variant: "storybook",
123
+ cwd,
124
+ });
125
+ console.groupEnd();
126
+ }
127
+
128
+ console.group(styleText("bold", "\nTest StoryBook"));
129
+ const { testCoverageDirpath, testReportDirpath } = testStoryBook({
130
+ cwd,
131
+ test,
132
+ silent,
133
+ testCoverageDir,
134
+ testReportDir,
135
+ });
136
+
137
+ if (testReportDirpath) {
138
+ await uploadSBRBuild(client, {
139
+ project,
140
+ sha,
141
+ dirpath: testReportDirpath,
142
+ cwd,
143
+ variant: "testReport",
144
+ });
145
+ }
146
+ if (testCoverageDirpath) {
147
+ await uploadSBRBuild(client, {
148
+ project,
149
+ sha,
150
+ dirpath: testCoverageDirpath,
151
+ cwd,
152
+ variant: "coverage",
153
+ });
154
+ }
155
+ console.groupEnd();
156
+
157
+ console.log(styleText("green", "Created build successfully."));
158
+ } catch (error) {
159
+ console.error(error);
160
+ process.exit(1);
161
+ }
162
+ },
163
+ };
164
+
165
+ async function createSBRBuild(
166
+ client: ServiceClient,
167
+ { project, sha, message, labels }: z.infer<typeof CreateSchema>,
168
+ ignorePrevious?: boolean,
169
+ ): Promise<void> {
170
+ const { error, response } = await client.POST(
171
+ "/projects/{projectId}/builds/create",
172
+ {
173
+ params: { path: { projectId: project } },
174
+ body: {
175
+ authorEmail: "Siddhant@asd.com",
176
+ authorName: "Siddhant",
177
+ labels,
178
+ sha,
179
+ message,
180
+ },
181
+ headers: {
182
+ Accept: "application/json",
183
+ "Content-Type": "application/x-www-form-urlencoded",
184
+ },
185
+ },
186
+ );
187
+
188
+ if (error) {
189
+ if (ignorePrevious) {
190
+ console.warn(
191
+ styleText("yellow", "> StoryBooker Build entry already exits."),
192
+ );
193
+ } else {
194
+ throw new Error(
195
+ error.errorMessage ||
196
+ `Request to service failed with status: ${response.status}.`,
197
+ );
198
+ }
199
+ } else {
200
+ console.log("New Build entry created '%s / %s'", project, sha);
201
+ }
202
+ }
203
+
204
+ async function uploadSBRBuild(
205
+ client: ServiceClient,
206
+ {
207
+ project,
208
+ sha,
209
+ dirpath,
210
+ variant,
211
+ cwd,
212
+ }: {
213
+ project: string;
214
+ sha: string;
215
+ dirpath: string;
216
+ variant: "storybook" | "testReport" | "coverage" | "screenshots";
217
+ cwd: string;
218
+ },
219
+ ): Promise<void> {
220
+ const zipFilepath = path.join(cwd, `${variant}.zip`);
221
+
222
+ console.log(
223
+ `> Compressing directory '%s' to file '%s'...`,
224
+ path.relative(cwd, dirpath),
225
+ path.relative(cwd, zipFilepath),
226
+ );
227
+ zip(path.join(dirpath, "*"), zipFilepath);
228
+
229
+ const fileSize = fs.statSync(zipFilepath).size;
230
+
231
+ console.log(`> Uploading file '%s'...`, path.relative(cwd, zipFilepath));
232
+ const { error, response } = await client.POST(
233
+ "/projects/{projectId}/builds/{buildSHA}/upload",
234
+ {
235
+ params: {
236
+ path: { projectId: project, buildSHA: sha },
237
+ query: { variant },
238
+ },
239
+ // @ts-expect-error assign stream to object
240
+ body: toReadableStream(fs.createReadStream(zipFilepath), fileSize),
241
+ bodySerializer: (body) => body,
242
+ headers: {
243
+ Accept: "application/json",
244
+ "Content-Type": "application/zip",
245
+ "Content-Length": fileSize.toString(),
246
+ },
247
+ duplex: "half",
248
+ },
249
+ );
250
+
251
+ if (error) {
252
+ throw new Error(
253
+ error.errorMessage ||
254
+ `Request to service failed with status: ${response.status}.`,
255
+ );
256
+ } else {
257
+ console.log("> Uploaded '%s / %s / %s'.", project, sha, variant);
258
+ fs.rmSync(zipFilepath);
259
+ }
260
+ }
@@ -0,0 +1,70 @@
1
+ // oxlint-disable new-cap
2
+
3
+ import { styleText } from "node:util";
4
+ import createClient from "openapi-fetch";
5
+ import type { CommandModule } from "yargs";
6
+ import z from "zod";
7
+ import type { paths } from "../service-schema";
8
+ import { createAuthMiddleware } from "../utils/auth-utils";
9
+ import {
10
+ sharedSchemas,
11
+ zodSchemaToCommandBuilder,
12
+ } from "../utils/schema-utils";
13
+
14
+ const PurgeSchema = z.object({
15
+ project: sharedSchemas.project,
16
+ url: sharedSchemas.url,
17
+ label: z.string().meta({
18
+ alias: ["l"],
19
+ description: "The label slug to purge associated builds.",
20
+ }),
21
+ authType: sharedSchemas.authType,
22
+ authValue: sharedSchemas.authValue,
23
+ });
24
+
25
+ export const purgeCommandModule: CommandModule = {
26
+ command: "purge",
27
+ describe: "Purge StoryBook assets from the service.",
28
+ builder: zodSchemaToCommandBuilder(PurgeSchema),
29
+ handler: async (args): Promise<void> => {
30
+ const result = PurgeSchema.safeParse(args);
31
+ if (!result.success) {
32
+ throw new Error(z.prettifyError(result.error));
33
+ }
34
+
35
+ const { label, project, url, authType, authValue } = result.data;
36
+ const client = createClient<paths>({ baseUrl: url });
37
+ client.use(createAuthMiddleware({ authType, authValue }));
38
+
39
+ try {
40
+ console.group(
41
+ styleText("bold", "\nPurge Label '%s' (project: %s)"),
42
+ label,
43
+ project,
44
+ );
45
+
46
+ console.log(`> Deleting label '%s' and associated builds...`, label);
47
+ const { error, response } = await client.DELETE(
48
+ "/projects/{projectId}/labels/{labelSlug}",
49
+ {
50
+ params: { path: { labelSlug: label, projectId: project } },
51
+ headers: { Accept: "application/json" },
52
+ },
53
+ );
54
+
55
+ if (error) {
56
+ throw new Error(
57
+ error.errorMessage ||
58
+ `Request to service failed with status: ${response.status}.`,
59
+ );
60
+ } else {
61
+ console.log("> Purged '%s / %s'.", project, label);
62
+ }
63
+
64
+ console.groupEnd();
65
+ } catch (error) {
66
+ console.error(error);
67
+ process.exit(1);
68
+ }
69
+ },
70
+ };
@@ -0,0 +1,42 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { styleText } from "node:util";
4
+ import type { CommandModule } from "yargs";
5
+ import z from "zod";
6
+ import { testStoryBook } from "../utils/sb-test";
7
+ import {
8
+ sharedSchemas,
9
+ zodSchemaToCommandBuilder,
10
+ } from "../utils/schema-utils";
11
+
12
+ const schema = z.object({
13
+ cwd: sharedSchemas.cwd,
14
+ testCoverageDir: sharedSchemas.testCoverageDir,
15
+ testReportDir: sharedSchemas.testReportDir,
16
+ });
17
+
18
+ export const testCommandModule: CommandModule = {
19
+ command: "test",
20
+ describe: "Run test on StoryBook with Vitest",
21
+ builder: zodSchemaToCommandBuilder(schema),
22
+ handler(args) {
23
+ const result = schema.safeParse(args);
24
+ if (!result.success) {
25
+ throw new Error(z.prettifyError(result.error));
26
+ }
27
+
28
+ const cwd = result.data.cwd ? path.resolve(result.data.cwd) : process.cwd();
29
+ if (cwd && !fs.existsSync(cwd)) {
30
+ throw new Error(`Path provided to CWD does not exists: '${cwd}'`);
31
+ }
32
+
33
+ console.group(styleText("bold", "\nTest StoryBook"));
34
+ testStoryBook({
35
+ cwd,
36
+ test: true,
37
+ testCoverageDir: result.data.testCoverageDir,
38
+ testReportDir: result.data.testReportDir,
39
+ });
40
+ console.groupEnd();
41
+ },
42
+ };
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ import yargs from "yargs";
2
+ import { hideBin } from "yargs/helpers";
3
+ import { name, version } from "../package.json" with { type: "json" };
4
+ import { createCommandModule } from "./commands/create";
5
+ import { purgeCommandModule } from "./commands/purge";
6
+ import { testCommandModule } from "./commands/test";
7
+
8
+ await yargs(hideBin(process.argv))
9
+ .scriptName(name)
10
+ .usage(`npx -y $0 [command] (options)`)
11
+ .version(version)
12
+ .command(createCommandModule)
13
+ .command(purgeCommandModule)
14
+ .command(testCommandModule)
15
+ .alias("h", "help")
16
+ .alias("v", "version")
17
+ .parse();