@stubborn-sh/stubs-packager 0.0.3

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/dist/index.cjs ADDED
@@ -0,0 +1,225 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ contractToWireMock: () => contractToWireMock,
34
+ deployStubsJar: () => deployStubsJar,
35
+ packageStubsJar: () => packageStubsJar
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/contract-to-wiremock.ts
40
+ var import_js_yaml = __toESM(require("js-yaml"), 1);
41
+ function contractToWireMock(contract) {
42
+ let raw;
43
+ try {
44
+ raw = import_js_yaml.default.load(contract.content, { schema: import_js_yaml.default.JSON_SCHEMA });
45
+ } catch (err) {
46
+ throw new Error(
47
+ `Failed to parse YAML contract "${contract.contractName}": ${err instanceof Error ? err.message : String(err)}`
48
+ );
49
+ }
50
+ if (typeof raw !== "object" || raw === null) {
51
+ throw new Error(`Contract "${contract.contractName}" is not a valid object`);
52
+ }
53
+ const parsed = raw;
54
+ if (parsed.request === void 0) {
55
+ throw new Error(`Contract "${contract.contractName}" is missing required "request" field`);
56
+ }
57
+ if (parsed.response === void 0) {
58
+ throw new Error(`Contract "${contract.contractName}" is missing required "response" field`);
59
+ }
60
+ if (parsed.request.method === void 0 || parsed.request.method === "") {
61
+ throw new Error(
62
+ `Contract "${contract.contractName}" request is missing required "method" field`
63
+ );
64
+ }
65
+ const mapping = buildMapping(parsed);
66
+ return JSON.stringify(mapping, null, 2);
67
+ }
68
+ function buildMapping(parsed) {
69
+ const req = parsed.request;
70
+ const res = parsed.response;
71
+ const request = buildWireMockRequest(req);
72
+ const response = buildWireMockResponse(res);
73
+ return {
74
+ request,
75
+ response,
76
+ ...parsed.priority !== void 0 ? { priority: parsed.priority } : {}
77
+ };
78
+ }
79
+ function buildWireMockRequest(req) {
80
+ const result = {
81
+ method: (req.method ?? "").toUpperCase()
82
+ };
83
+ if (req.urlPath !== void 0) {
84
+ result["urlPath"] = req.urlPath;
85
+ } else if (req.url !== void 0) {
86
+ result["url"] = req.url;
87
+ }
88
+ if (req.headers !== void 0 && Object.keys(req.headers).length > 0) {
89
+ const wireMockHeaders = {};
90
+ for (const [key, value] of Object.entries(req.headers)) {
91
+ wireMockHeaders[key] = { equalTo: value };
92
+ }
93
+ result["headers"] = wireMockHeaders;
94
+ }
95
+ if (req.queryParameters !== void 0 && Object.keys(req.queryParameters).length > 0) {
96
+ const wireMockParams = {};
97
+ for (const [key, value] of Object.entries(req.queryParameters)) {
98
+ wireMockParams[key] = { equalTo: value };
99
+ }
100
+ result["queryParameters"] = wireMockParams;
101
+ }
102
+ if (req.body !== void 0) {
103
+ const bodyStr = typeof req.body === "string" ? req.body : JSON.stringify(req.body);
104
+ result["bodyPatterns"] = [{ equalToJson: bodyStr }];
105
+ }
106
+ return result;
107
+ }
108
+ function buildWireMockResponse(res) {
109
+ const result = {
110
+ status: res.status ?? 200
111
+ };
112
+ if (res.headers !== void 0 && Object.keys(res.headers).length > 0) {
113
+ result["headers"] = res.headers;
114
+ }
115
+ if (res.body !== void 0) {
116
+ if (typeof res.body === "object" && res.body !== null) {
117
+ result["jsonBody"] = res.body;
118
+ } else {
119
+ result["body"] = typeof res.body === "string" ? res.body : JSON.stringify(res.body);
120
+ }
121
+ }
122
+ return result;
123
+ }
124
+
125
+ // src/packager.ts
126
+ var import_node_fs = require("fs");
127
+ var import_node_path = require("path");
128
+ var import_archiver = __toESM(require("archiver"), 1);
129
+ var import_publisher = require("@stubborn-sh/publisher");
130
+ async function packageStubsJar(options) {
131
+ const contracts = await (0, import_publisher.scanContracts)(options.contractsDir);
132
+ if (contracts.length === 0) {
133
+ throw new Error(`No contract files found in ${options.contractsDir}`);
134
+ }
135
+ const { groupId, artifactId, version } = options.coordinates;
136
+ const basePath = `META-INF/${groupId}/${artifactId}/${version}`;
137
+ return new Promise((resolve, reject) => {
138
+ const output = (0, import_node_fs.createWriteStream)(options.outputPath);
139
+ const archive = (0, import_archiver.default)("zip", { zlib: { level: 9 } });
140
+ let sizeBytes = 0;
141
+ output.on("close", () => {
142
+ sizeBytes = archive.pointer();
143
+ resolve({
144
+ outputPath: options.outputPath,
145
+ contractCount: contracts.length,
146
+ mappingCount: contracts.length,
147
+ sizeBytes
148
+ });
149
+ });
150
+ archive.on("error", reject);
151
+ archive.pipe(output);
152
+ for (const contract of contracts) {
153
+ const safeName = sanitizeEntryName(contract.contractName);
154
+ archive.append(contract.content, {
155
+ name: `${basePath}/contracts/${safeName}`
156
+ });
157
+ }
158
+ for (const contract of contracts) {
159
+ const wireMockJson = contractToWireMock(contract);
160
+ const mappingName = toMappingFileName(sanitizeEntryName(contract.contractName));
161
+ archive.append(wireMockJson, {
162
+ name: `${basePath}/mappings/${mappingName}`
163
+ });
164
+ }
165
+ archive.append("Manifest-Version: 1.0\n", {
166
+ name: "META-INF/MANIFEST.MF"
167
+ });
168
+ archive.finalize().catch(reject);
169
+ });
170
+ }
171
+ function sanitizeEntryName(name) {
172
+ return name.replace(/\\/g, "/").replace(/^\/+/, "").split("/").filter((segment) => segment !== ".." && segment !== ".").join("/");
173
+ }
174
+ function toMappingFileName(contractName) {
175
+ const withoutExt = contractName.slice(0, contractName.length - (0, import_node_path.extname)(contractName).length);
176
+ return withoutExt.replace(/[/\\]/g, "_") + ".json";
177
+ }
178
+
179
+ // src/deployer.ts
180
+ var import_promises = require("fs/promises");
181
+ async function deployStubsJar(options) {
182
+ const url = buildDeployUrl(options);
183
+ await (0, import_promises.stat)(options.jarPath).catch(() => {
184
+ throw new Error(`JAR file not found: ${options.jarPath}`);
185
+ });
186
+ const jarContent = await (0, import_promises.readFile)(options.jarPath);
187
+ const headers = {
188
+ "Content-Type": "application/java-archive"
189
+ };
190
+ if (options.token !== void 0) {
191
+ headers["Authorization"] = `Bearer ${options.token}`;
192
+ } else if (options.username !== void 0 && options.password !== void 0) {
193
+ headers["Authorization"] = `Basic ${Buffer.from(`${options.username}:${options.password}`).toString("base64")}`;
194
+ }
195
+ const response = await fetch(url, {
196
+ method: "PUT",
197
+ headers,
198
+ body: jarContent,
199
+ signal: AbortSignal.timeout(3e4)
200
+ });
201
+ if (!response.ok) {
202
+ const body = await response.text().catch(() => "");
203
+ throw new Error(
204
+ `Failed to deploy stubs JAR to ${url}: ${response.status} ${response.statusText}${body ? ` \u2014 ${body}` : ""}`
205
+ );
206
+ }
207
+ return { url, statusCode: response.status };
208
+ }
209
+ function buildDeployUrl(options) {
210
+ const classifier = options.classifier ?? "stubs";
211
+ const groupPath = options.groupId.replace(/\./g, "/");
212
+ const baseUrl = options.repositoryUrl.replace(/\/+$/, "");
213
+ const parsed = new URL(baseUrl);
214
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
215
+ throw new Error(`Invalid repository URL scheme: ${parsed.protocol} (only http/https allowed)`);
216
+ }
217
+ return `${baseUrl}/${groupPath}/${options.artifactId}/${options.version}/${options.artifactId}-${options.version}-${classifier}.jar`;
218
+ }
219
+ // Annotate the CommonJS export names for ESM import in node:
220
+ 0 && (module.exports = {
221
+ contractToWireMock,
222
+ deployStubsJar,
223
+ packageStubsJar
224
+ });
225
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/contract-to-wiremock.ts","../src/packager.ts","../src/deployer.ts"],"sourcesContent":["export { contractToWireMock } from \"./contract-to-wiremock.js\";\r\nexport { packageStubsJar } from \"./packager.js\";\r\nexport type { MavenCoordinates, PackageOptions, PackageResult } from \"./packager.js\";\r\nexport { deployStubsJar } from \"./deployer.js\";\r\nexport type { DeployOptions, DeployResult } from \"./deployer.js\";\r\n","import type { ScannedContract } from \"@stubborn-sh/publisher\";\r\nimport yaml from \"js-yaml\";\r\n\r\n/** Raw WireMock mapping JSON structure for serialization. */\r\ninterface WireMockMapping {\r\n readonly request: WireMockRequest;\r\n readonly response: WireMockResponse;\r\n readonly priority?: number;\r\n}\r\n\r\ninterface WireMockRequest {\r\n readonly method: string;\r\n readonly url?: string;\r\n readonly urlPath?: string;\r\n readonly headers?: Record<string, WireMockMatcher>;\r\n readonly queryParameters?: Record<string, WireMockMatcher>;\r\n readonly bodyPatterns?: readonly WireMockBodyPattern[];\r\n}\r\n\r\ninterface WireMockMatcher {\r\n readonly equalTo: string;\r\n}\r\n\r\ninterface WireMockBodyPattern {\r\n readonly equalToJson?: string;\r\n readonly matchesJsonPath?: string;\r\n readonly matches?: string;\r\n}\r\n\r\ninterface WireMockResponse {\r\n readonly status: number;\r\n readonly headers?: Record<string, string>;\r\n readonly body?: string;\r\n readonly jsonBody?: unknown;\r\n}\r\n\r\ninterface ParsedYamlContract {\r\n request?: {\r\n method?: string;\r\n url?: string;\r\n urlPath?: string;\r\n headers?: Record<string, string>;\r\n body?: unknown;\r\n queryParameters?: Record<string, string>;\r\n };\r\n response?: {\r\n status?: number;\r\n headers?: Record<string, string>;\r\n body?: unknown;\r\n };\r\n priority?: number;\r\n}\r\n\r\n/**\r\n * Convert a scanned YAML contract to a WireMock JSON mapping string.\r\n *\r\n * This is the reverse of `parseWireMockMapping` from the stub-server package.\r\n * The output is compatible with Java Stub Runner (WireMock format).\r\n */\r\nexport function contractToWireMock(contract: ScannedContract): string {\r\n let raw: unknown;\r\n try {\r\n raw = yaml.load(contract.content, { schema: yaml.JSON_SCHEMA });\r\n } catch (err: unknown) {\r\n throw new Error(\r\n `Failed to parse YAML contract \"${contract.contractName}\": ${err instanceof Error ? err.message : String(err)}`,\r\n );\r\n }\r\n\r\n if (typeof raw !== \"object\" || raw === null) {\r\n throw new Error(`Contract \"${contract.contractName}\" is not a valid object`);\r\n }\r\n\r\n const parsed = raw as ParsedYamlContract;\r\n\r\n if (parsed.request === undefined) {\r\n throw new Error(`Contract \"${contract.contractName}\" is missing required \"request\" field`);\r\n }\r\n\r\n if (parsed.response === undefined) {\r\n throw new Error(`Contract \"${contract.contractName}\" is missing required \"response\" field`);\r\n }\r\n\r\n if (parsed.request.method === undefined || parsed.request.method === \"\") {\r\n throw new Error(\r\n `Contract \"${contract.contractName}\" request is missing required \"method\" field`,\r\n );\r\n }\r\n\r\n const mapping = buildMapping(parsed);\r\n return JSON.stringify(mapping, null, 2);\r\n}\r\n\r\nfunction buildMapping(parsed: ParsedYamlContract): WireMockMapping {\r\n // Safe: validated in contractToWireMock before calling buildMapping\r\n const req = parsed.request as NonNullable<ParsedYamlContract[\"request\"]>;\r\n const res = parsed.response as NonNullable<ParsedYamlContract[\"response\"]>;\r\n\r\n const request = buildWireMockRequest(req);\r\n const response = buildWireMockResponse(res);\r\n\r\n return {\r\n request,\r\n response,\r\n ...(parsed.priority !== undefined ? { priority: parsed.priority } : {}),\r\n };\r\n}\r\n\r\nfunction buildWireMockRequest(req: NonNullable<ParsedYamlContract[\"request\"]>): WireMockRequest {\r\n const result: Record<string, unknown> = {\r\n method: (req.method ?? \"\").toUpperCase(),\r\n };\r\n\r\n if (req.urlPath !== undefined) {\r\n result[\"urlPath\"] = req.urlPath;\r\n } else if (req.url !== undefined) {\r\n result[\"url\"] = req.url;\r\n }\r\n\r\n if (req.headers !== undefined && Object.keys(req.headers).length > 0) {\r\n const wireMockHeaders: Record<string, WireMockMatcher> = {};\r\n for (const [key, value] of Object.entries(req.headers)) {\r\n wireMockHeaders[key] = { equalTo: value };\r\n }\r\n result[\"headers\"] = wireMockHeaders;\r\n }\r\n\r\n if (req.queryParameters !== undefined && Object.keys(req.queryParameters).length > 0) {\r\n const wireMockParams: Record<string, WireMockMatcher> = {};\r\n for (const [key, value] of Object.entries(req.queryParameters)) {\r\n wireMockParams[key] = { equalTo: value };\r\n }\r\n result[\"queryParameters\"] = wireMockParams;\r\n }\r\n\r\n if (req.body !== undefined) {\r\n const bodyStr = typeof req.body === \"string\" ? req.body : JSON.stringify(req.body);\r\n result[\"bodyPatterns\"] = [{ equalToJson: bodyStr }];\r\n }\r\n\r\n return result as unknown as WireMockRequest;\r\n}\r\n\r\nfunction buildWireMockResponse(res: NonNullable<ParsedYamlContract[\"response\"]>): WireMockResponse {\r\n const result: Record<string, unknown> = {\r\n status: res.status ?? 200,\r\n };\r\n\r\n if (res.headers !== undefined && Object.keys(res.headers).length > 0) {\r\n result[\"headers\"] = res.headers;\r\n }\r\n\r\n if (res.body !== undefined) {\r\n if (typeof res.body === \"object\" && res.body !== null) {\r\n result[\"jsonBody\"] = res.body;\r\n } else {\r\n result[\"body\"] = typeof res.body === \"string\" ? res.body : JSON.stringify(res.body);\r\n }\r\n }\r\n\r\n return result as unknown as WireMockResponse;\r\n}\r\n","import { createWriteStream } from \"node:fs\";\r\nimport { extname } from \"node:path\";\r\nimport archiver from \"archiver\";\r\nimport { scanContracts } from \"@stubborn-sh/publisher\";\r\nimport { contractToWireMock } from \"./contract-to-wiremock.js\";\r\n\r\n/** Maven coordinates for the stubs JAR. */\r\nexport interface MavenCoordinates {\r\n /** Group ID (e.g., \"com.example\"). */\r\n readonly groupId: string;\r\n /** Artifact ID (e.g., \"order-service\"). */\r\n readonly artifactId: string;\r\n /** Version (e.g., \"1.0.0\"). */\r\n readonly version: string;\r\n /** Classifier (default: \"stubs\"). */\r\n readonly classifier?: string;\r\n}\r\n\r\n/** Options for packaging contracts into a stubs JAR. */\r\nexport interface PackageOptions {\r\n /** Directory containing YAML contract files. */\r\n readonly contractsDir: string;\r\n /** Maven coordinates for the JAR. */\r\n readonly coordinates: MavenCoordinates;\r\n /** Output path for the generated JAR (ZIP) file. */\r\n readonly outputPath: string;\r\n}\r\n\r\n/** Result of a packaging operation. */\r\nexport interface PackageResult {\r\n /** Path to the generated JAR file. */\r\n readonly outputPath: string;\r\n /** Number of contracts packaged. */\r\n readonly contractCount: number;\r\n /** Number of WireMock mappings generated. */\r\n readonly mappingCount: number;\r\n /** Total file size in bytes. */\r\n readonly sizeBytes: number;\r\n}\r\n\r\n/**\r\n * Package YAML contracts into a Maven stubs JAR (ZIP format).\r\n *\r\n * The JAR follows the standard SCC Maven plugin layout:\r\n * ```\r\n * META-INF/{groupId}/{artifactId}/{version}/\r\n * mappings/*.json (WireMock mappings)\r\n * contracts/*.yaml (original YAML contracts)\r\n * ```\r\n *\r\n * Java consumers can use this JAR with `@AutoConfigureStubRunner` or\r\n * the `fetchStubsJar()` / `loadLocalJar()` functions from the jest package.\r\n */\r\nexport async function packageStubsJar(options: PackageOptions): Promise<PackageResult> {\r\n const contracts = await scanContracts(options.contractsDir);\r\n\r\n if (contracts.length === 0) {\r\n throw new Error(`No contract files found in ${options.contractsDir}`);\r\n }\r\n\r\n const { groupId, artifactId, version } = options.coordinates;\r\n const basePath = `META-INF/${groupId}/${artifactId}/${version}`;\r\n\r\n return new Promise((resolve, reject) => {\r\n const output = createWriteStream(options.outputPath);\r\n const archive = archiver(\"zip\", { zlib: { level: 9 } });\r\n\r\n let sizeBytes = 0;\r\n\r\n output.on(\"close\", () => {\r\n sizeBytes = archive.pointer();\r\n resolve({\r\n outputPath: options.outputPath,\r\n contractCount: contracts.length,\r\n mappingCount: contracts.length,\r\n sizeBytes,\r\n });\r\n });\r\n\r\n archive.on(\"error\", reject);\r\n archive.pipe(output);\r\n\r\n // Add original YAML contracts\r\n for (const contract of contracts) {\r\n const safeName = sanitizeEntryName(contract.contractName);\r\n archive.append(contract.content, {\r\n name: `${basePath}/contracts/${safeName}`,\r\n });\r\n }\r\n\r\n // Convert each contract to WireMock JSON and add as mapping\r\n for (const contract of contracts) {\r\n const wireMockJson = contractToWireMock(contract);\r\n const mappingName = toMappingFileName(sanitizeEntryName(contract.contractName));\r\n archive.append(wireMockJson, {\r\n name: `${basePath}/mappings/${mappingName}`,\r\n });\r\n }\r\n\r\n // Add minimal MANIFEST.MF\r\n archive.append(\"Manifest-Version: 1.0\\n\", {\r\n name: \"META-INF/MANIFEST.MF\",\r\n });\r\n\r\n archive.finalize().catch(reject);\r\n });\r\n}\r\n\r\n/** Strip path traversal sequences and absolute path prefixes from a ZIP entry name. */\r\nfunction sanitizeEntryName(name: string): string {\r\n return name\r\n .replace(/\\\\/g, \"/\")\r\n .replace(/^\\/+/, \"\")\r\n .split(\"/\")\r\n .filter((segment) => segment !== \"..\" && segment !== \".\")\r\n .join(\"/\");\r\n}\r\n\r\n/** Convert a contract file name (e.g., \"order/get.yaml\") to a WireMock mapping name (\"order_get.json\"). */\r\nfunction toMappingFileName(contractName: string): string {\r\n const withoutExt = contractName.slice(0, contractName.length - extname(contractName).length);\r\n return withoutExt.replace(/[/\\\\]/g, \"_\") + \".json\";\r\n}\r\n","import { readFile, stat } from \"node:fs/promises\";\r\n\r\n/** Configuration for deploying a stubs JAR to a Maven repository. */\r\nexport interface DeployOptions {\r\n /** Path to the local stubs JAR file. */\r\n readonly jarPath: string;\r\n /** Maven repository URL (e.g., \"https://nexus.example.com/repository/releases\"). */\r\n readonly repositoryUrl: string;\r\n /** Group ID (e.g., \"com.example\"). */\r\n readonly groupId: string;\r\n /** Artifact ID (e.g., \"order-service\"). */\r\n readonly artifactId: string;\r\n /** Version (e.g., \"1.0.0\"). */\r\n readonly version: string;\r\n /** Classifier (default: \"stubs\"). */\r\n readonly classifier?: string;\r\n /** Repository authentication. */\r\n readonly username?: string;\r\n readonly password?: string;\r\n readonly token?: string;\r\n}\r\n\r\n/** Result of a deploy operation. */\r\nexport interface DeployResult {\r\n /** The full URL where the JAR was uploaded. */\r\n readonly url: string;\r\n /** HTTP status code from the upload. */\r\n readonly statusCode: number;\r\n}\r\n\r\n/**\r\n * Deploy a stubs JAR to a Maven repository via HTTP PUT.\r\n *\r\n * Supports Basic Auth and Bearer token authentication.\r\n * The JAR is uploaded to the standard Maven path:\r\n * `{repoUrl}/{groupPath}/{artifactId}/{version}/{artifactId}-{version}-{classifier}.jar`\r\n */\r\nexport async function deployStubsJar(options: DeployOptions): Promise<DeployResult> {\r\n const url = buildDeployUrl(options);\r\n\r\n await stat(options.jarPath).catch(() => {\r\n throw new Error(`JAR file not found: ${options.jarPath}`);\r\n });\r\n\r\n const jarContent = await readFile(options.jarPath);\r\n\r\n const headers: Record<string, string> = {\r\n \"Content-Type\": \"application/java-archive\",\r\n };\r\n\r\n if (options.token !== undefined) {\r\n headers[\"Authorization\"] = `Bearer ${options.token}`;\r\n } else if (options.username !== undefined && options.password !== undefined) {\r\n headers[\"Authorization\"] =\r\n `Basic ${Buffer.from(`${options.username}:${options.password}`).toString(\"base64\")}`;\r\n }\r\n\r\n const response = await fetch(url, {\r\n method: \"PUT\",\r\n headers,\r\n body: jarContent,\r\n signal: AbortSignal.timeout(30_000),\r\n });\r\n\r\n if (!response.ok) {\r\n const body = await response.text().catch(() => \"\");\r\n throw new Error(\r\n `Failed to deploy stubs JAR to ${url}: ${response.status} ${response.statusText}${body ? ` — ${body}` : \"\"}`,\r\n );\r\n }\r\n\r\n return { url, statusCode: response.status };\r\n}\r\n\r\n/** Build the Maven repository URL for a stubs JAR. */\r\nfunction buildDeployUrl(options: DeployOptions): string {\r\n const classifier = options.classifier ?? \"stubs\";\r\n const groupPath = options.groupId.replace(/\\./g, \"/\");\r\n const baseUrl = options.repositoryUrl.replace(/\\/+$/, \"\");\r\n\r\n // Validate URL scheme\r\n const parsed = new URL(baseUrl);\r\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\r\n throw new Error(`Invalid repository URL scheme: ${parsed.protocol} (only http/https allowed)`);\r\n }\r\n\r\n return `${baseUrl}/${groupPath}/${options.artifactId}/${options.version}/${options.artifactId}-${options.version}-${classifier}.jar`;\r\n}\r\n"],"mappings":"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","names":["yaml","archiver"]}
package/dist/index.js ADDED
@@ -0,0 +1,186 @@
1
+ // src/contract-to-wiremock.ts
2
+ import yaml from "js-yaml";
3
+ function contractToWireMock(contract) {
4
+ let raw;
5
+ try {
6
+ raw = yaml.load(contract.content, { schema: yaml.JSON_SCHEMA });
7
+ } catch (err) {
8
+ throw new Error(
9
+ `Failed to parse YAML contract "${contract.contractName}": ${err instanceof Error ? err.message : String(err)}`
10
+ );
11
+ }
12
+ if (typeof raw !== "object" || raw === null) {
13
+ throw new Error(`Contract "${contract.contractName}" is not a valid object`);
14
+ }
15
+ const parsed = raw;
16
+ if (parsed.request === void 0) {
17
+ throw new Error(`Contract "${contract.contractName}" is missing required "request" field`);
18
+ }
19
+ if (parsed.response === void 0) {
20
+ throw new Error(`Contract "${contract.contractName}" is missing required "response" field`);
21
+ }
22
+ if (parsed.request.method === void 0 || parsed.request.method === "") {
23
+ throw new Error(
24
+ `Contract "${contract.contractName}" request is missing required "method" field`
25
+ );
26
+ }
27
+ const mapping = buildMapping(parsed);
28
+ return JSON.stringify(mapping, null, 2);
29
+ }
30
+ function buildMapping(parsed) {
31
+ const req = parsed.request;
32
+ const res = parsed.response;
33
+ const request = buildWireMockRequest(req);
34
+ const response = buildWireMockResponse(res);
35
+ return {
36
+ request,
37
+ response,
38
+ ...parsed.priority !== void 0 ? { priority: parsed.priority } : {}
39
+ };
40
+ }
41
+ function buildWireMockRequest(req) {
42
+ const result = {
43
+ method: (req.method ?? "").toUpperCase()
44
+ };
45
+ if (req.urlPath !== void 0) {
46
+ result["urlPath"] = req.urlPath;
47
+ } else if (req.url !== void 0) {
48
+ result["url"] = req.url;
49
+ }
50
+ if (req.headers !== void 0 && Object.keys(req.headers).length > 0) {
51
+ const wireMockHeaders = {};
52
+ for (const [key, value] of Object.entries(req.headers)) {
53
+ wireMockHeaders[key] = { equalTo: value };
54
+ }
55
+ result["headers"] = wireMockHeaders;
56
+ }
57
+ if (req.queryParameters !== void 0 && Object.keys(req.queryParameters).length > 0) {
58
+ const wireMockParams = {};
59
+ for (const [key, value] of Object.entries(req.queryParameters)) {
60
+ wireMockParams[key] = { equalTo: value };
61
+ }
62
+ result["queryParameters"] = wireMockParams;
63
+ }
64
+ if (req.body !== void 0) {
65
+ const bodyStr = typeof req.body === "string" ? req.body : JSON.stringify(req.body);
66
+ result["bodyPatterns"] = [{ equalToJson: bodyStr }];
67
+ }
68
+ return result;
69
+ }
70
+ function buildWireMockResponse(res) {
71
+ const result = {
72
+ status: res.status ?? 200
73
+ };
74
+ if (res.headers !== void 0 && Object.keys(res.headers).length > 0) {
75
+ result["headers"] = res.headers;
76
+ }
77
+ if (res.body !== void 0) {
78
+ if (typeof res.body === "object" && res.body !== null) {
79
+ result["jsonBody"] = res.body;
80
+ } else {
81
+ result["body"] = typeof res.body === "string" ? res.body : JSON.stringify(res.body);
82
+ }
83
+ }
84
+ return result;
85
+ }
86
+
87
+ // src/packager.ts
88
+ import { createWriteStream } from "fs";
89
+ import { extname } from "path";
90
+ import archiver from "archiver";
91
+ import { scanContracts } from "@stubborn-sh/publisher";
92
+ async function packageStubsJar(options) {
93
+ const contracts = await scanContracts(options.contractsDir);
94
+ if (contracts.length === 0) {
95
+ throw new Error(`No contract files found in ${options.contractsDir}`);
96
+ }
97
+ const { groupId, artifactId, version } = options.coordinates;
98
+ const basePath = `META-INF/${groupId}/${artifactId}/${version}`;
99
+ return new Promise((resolve, reject) => {
100
+ const output = createWriteStream(options.outputPath);
101
+ const archive = archiver("zip", { zlib: { level: 9 } });
102
+ let sizeBytes = 0;
103
+ output.on("close", () => {
104
+ sizeBytes = archive.pointer();
105
+ resolve({
106
+ outputPath: options.outputPath,
107
+ contractCount: contracts.length,
108
+ mappingCount: contracts.length,
109
+ sizeBytes
110
+ });
111
+ });
112
+ archive.on("error", reject);
113
+ archive.pipe(output);
114
+ for (const contract of contracts) {
115
+ const safeName = sanitizeEntryName(contract.contractName);
116
+ archive.append(contract.content, {
117
+ name: `${basePath}/contracts/${safeName}`
118
+ });
119
+ }
120
+ for (const contract of contracts) {
121
+ const wireMockJson = contractToWireMock(contract);
122
+ const mappingName = toMappingFileName(sanitizeEntryName(contract.contractName));
123
+ archive.append(wireMockJson, {
124
+ name: `${basePath}/mappings/${mappingName}`
125
+ });
126
+ }
127
+ archive.append("Manifest-Version: 1.0\n", {
128
+ name: "META-INF/MANIFEST.MF"
129
+ });
130
+ archive.finalize().catch(reject);
131
+ });
132
+ }
133
+ function sanitizeEntryName(name) {
134
+ return name.replace(/\\/g, "/").replace(/^\/+/, "").split("/").filter((segment) => segment !== ".." && segment !== ".").join("/");
135
+ }
136
+ function toMappingFileName(contractName) {
137
+ const withoutExt = contractName.slice(0, contractName.length - extname(contractName).length);
138
+ return withoutExt.replace(/[/\\]/g, "_") + ".json";
139
+ }
140
+
141
+ // src/deployer.ts
142
+ import { readFile, stat } from "fs/promises";
143
+ async function deployStubsJar(options) {
144
+ const url = buildDeployUrl(options);
145
+ await stat(options.jarPath).catch(() => {
146
+ throw new Error(`JAR file not found: ${options.jarPath}`);
147
+ });
148
+ const jarContent = await readFile(options.jarPath);
149
+ const headers = {
150
+ "Content-Type": "application/java-archive"
151
+ };
152
+ if (options.token !== void 0) {
153
+ headers["Authorization"] = `Bearer ${options.token}`;
154
+ } else if (options.username !== void 0 && options.password !== void 0) {
155
+ headers["Authorization"] = `Basic ${Buffer.from(`${options.username}:${options.password}`).toString("base64")}`;
156
+ }
157
+ const response = await fetch(url, {
158
+ method: "PUT",
159
+ headers,
160
+ body: jarContent,
161
+ signal: AbortSignal.timeout(3e4)
162
+ });
163
+ if (!response.ok) {
164
+ const body = await response.text().catch(() => "");
165
+ throw new Error(
166
+ `Failed to deploy stubs JAR to ${url}: ${response.status} ${response.statusText}${body ? ` \u2014 ${body}` : ""}`
167
+ );
168
+ }
169
+ return { url, statusCode: response.status };
170
+ }
171
+ function buildDeployUrl(options) {
172
+ const classifier = options.classifier ?? "stubs";
173
+ const groupPath = options.groupId.replace(/\./g, "/");
174
+ const baseUrl = options.repositoryUrl.replace(/\/+$/, "");
175
+ const parsed = new URL(baseUrl);
176
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
177
+ throw new Error(`Invalid repository URL scheme: ${parsed.protocol} (only http/https allowed)`);
178
+ }
179
+ return `${baseUrl}/${groupPath}/${options.artifactId}/${options.version}/${options.artifactId}-${options.version}-${classifier}.jar`;
180
+ }
181
+ export {
182
+ contractToWireMock,
183
+ deployStubsJar,
184
+ packageStubsJar
185
+ };
186
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/contract-to-wiremock.ts","../src/packager.ts","../src/deployer.ts"],"sourcesContent":["import type { ScannedContract } from \"@stubborn-sh/publisher\";\r\nimport yaml from \"js-yaml\";\r\n\r\n/** Raw WireMock mapping JSON structure for serialization. */\r\ninterface WireMockMapping {\r\n readonly request: WireMockRequest;\r\n readonly response: WireMockResponse;\r\n readonly priority?: number;\r\n}\r\n\r\ninterface WireMockRequest {\r\n readonly method: string;\r\n readonly url?: string;\r\n readonly urlPath?: string;\r\n readonly headers?: Record<string, WireMockMatcher>;\r\n readonly queryParameters?: Record<string, WireMockMatcher>;\r\n readonly bodyPatterns?: readonly WireMockBodyPattern[];\r\n}\r\n\r\ninterface WireMockMatcher {\r\n readonly equalTo: string;\r\n}\r\n\r\ninterface WireMockBodyPattern {\r\n readonly equalToJson?: string;\r\n readonly matchesJsonPath?: string;\r\n readonly matches?: string;\r\n}\r\n\r\ninterface WireMockResponse {\r\n readonly status: number;\r\n readonly headers?: Record<string, string>;\r\n readonly body?: string;\r\n readonly jsonBody?: unknown;\r\n}\r\n\r\ninterface ParsedYamlContract {\r\n request?: {\r\n method?: string;\r\n url?: string;\r\n urlPath?: string;\r\n headers?: Record<string, string>;\r\n body?: unknown;\r\n queryParameters?: Record<string, string>;\r\n };\r\n response?: {\r\n status?: number;\r\n headers?: Record<string, string>;\r\n body?: unknown;\r\n };\r\n priority?: number;\r\n}\r\n\r\n/**\r\n * Convert a scanned YAML contract to a WireMock JSON mapping string.\r\n *\r\n * This is the reverse of `parseWireMockMapping` from the stub-server package.\r\n * The output is compatible with Java Stub Runner (WireMock format).\r\n */\r\nexport function contractToWireMock(contract: ScannedContract): string {\r\n let raw: unknown;\r\n try {\r\n raw = yaml.load(contract.content, { schema: yaml.JSON_SCHEMA });\r\n } catch (err: unknown) {\r\n throw new Error(\r\n `Failed to parse YAML contract \"${contract.contractName}\": ${err instanceof Error ? err.message : String(err)}`,\r\n );\r\n }\r\n\r\n if (typeof raw !== \"object\" || raw === null) {\r\n throw new Error(`Contract \"${contract.contractName}\" is not a valid object`);\r\n }\r\n\r\n const parsed = raw as ParsedYamlContract;\r\n\r\n if (parsed.request === undefined) {\r\n throw new Error(`Contract \"${contract.contractName}\" is missing required \"request\" field`);\r\n }\r\n\r\n if (parsed.response === undefined) {\r\n throw new Error(`Contract \"${contract.contractName}\" is missing required \"response\" field`);\r\n }\r\n\r\n if (parsed.request.method === undefined || parsed.request.method === \"\") {\r\n throw new Error(\r\n `Contract \"${contract.contractName}\" request is missing required \"method\" field`,\r\n );\r\n }\r\n\r\n const mapping = buildMapping(parsed);\r\n return JSON.stringify(mapping, null, 2);\r\n}\r\n\r\nfunction buildMapping(parsed: ParsedYamlContract): WireMockMapping {\r\n // Safe: validated in contractToWireMock before calling buildMapping\r\n const req = parsed.request as NonNullable<ParsedYamlContract[\"request\"]>;\r\n const res = parsed.response as NonNullable<ParsedYamlContract[\"response\"]>;\r\n\r\n const request = buildWireMockRequest(req);\r\n const response = buildWireMockResponse(res);\r\n\r\n return {\r\n request,\r\n response,\r\n ...(parsed.priority !== undefined ? { priority: parsed.priority } : {}),\r\n };\r\n}\r\n\r\nfunction buildWireMockRequest(req: NonNullable<ParsedYamlContract[\"request\"]>): WireMockRequest {\r\n const result: Record<string, unknown> = {\r\n method: (req.method ?? \"\").toUpperCase(),\r\n };\r\n\r\n if (req.urlPath !== undefined) {\r\n result[\"urlPath\"] = req.urlPath;\r\n } else if (req.url !== undefined) {\r\n result[\"url\"] = req.url;\r\n }\r\n\r\n if (req.headers !== undefined && Object.keys(req.headers).length > 0) {\r\n const wireMockHeaders: Record<string, WireMockMatcher> = {};\r\n for (const [key, value] of Object.entries(req.headers)) {\r\n wireMockHeaders[key] = { equalTo: value };\r\n }\r\n result[\"headers\"] = wireMockHeaders;\r\n }\r\n\r\n if (req.queryParameters !== undefined && Object.keys(req.queryParameters).length > 0) {\r\n const wireMockParams: Record<string, WireMockMatcher> = {};\r\n for (const [key, value] of Object.entries(req.queryParameters)) {\r\n wireMockParams[key] = { equalTo: value };\r\n }\r\n result[\"queryParameters\"] = wireMockParams;\r\n }\r\n\r\n if (req.body !== undefined) {\r\n const bodyStr = typeof req.body === \"string\" ? req.body : JSON.stringify(req.body);\r\n result[\"bodyPatterns\"] = [{ equalToJson: bodyStr }];\r\n }\r\n\r\n return result as unknown as WireMockRequest;\r\n}\r\n\r\nfunction buildWireMockResponse(res: NonNullable<ParsedYamlContract[\"response\"]>): WireMockResponse {\r\n const result: Record<string, unknown> = {\r\n status: res.status ?? 200,\r\n };\r\n\r\n if (res.headers !== undefined && Object.keys(res.headers).length > 0) {\r\n result[\"headers\"] = res.headers;\r\n }\r\n\r\n if (res.body !== undefined) {\r\n if (typeof res.body === \"object\" && res.body !== null) {\r\n result[\"jsonBody\"] = res.body;\r\n } else {\r\n result[\"body\"] = typeof res.body === \"string\" ? res.body : JSON.stringify(res.body);\r\n }\r\n }\r\n\r\n return result as unknown as WireMockResponse;\r\n}\r\n","import { createWriteStream } from \"node:fs\";\r\nimport { extname } from \"node:path\";\r\nimport archiver from \"archiver\";\r\nimport { scanContracts } from \"@stubborn-sh/publisher\";\r\nimport { contractToWireMock } from \"./contract-to-wiremock.js\";\r\n\r\n/** Maven coordinates for the stubs JAR. */\r\nexport interface MavenCoordinates {\r\n /** Group ID (e.g., \"com.example\"). */\r\n readonly groupId: string;\r\n /** Artifact ID (e.g., \"order-service\"). */\r\n readonly artifactId: string;\r\n /** Version (e.g., \"1.0.0\"). */\r\n readonly version: string;\r\n /** Classifier (default: \"stubs\"). */\r\n readonly classifier?: string;\r\n}\r\n\r\n/** Options for packaging contracts into a stubs JAR. */\r\nexport interface PackageOptions {\r\n /** Directory containing YAML contract files. */\r\n readonly contractsDir: string;\r\n /** Maven coordinates for the JAR. */\r\n readonly coordinates: MavenCoordinates;\r\n /** Output path for the generated JAR (ZIP) file. */\r\n readonly outputPath: string;\r\n}\r\n\r\n/** Result of a packaging operation. */\r\nexport interface PackageResult {\r\n /** Path to the generated JAR file. */\r\n readonly outputPath: string;\r\n /** Number of contracts packaged. */\r\n readonly contractCount: number;\r\n /** Number of WireMock mappings generated. */\r\n readonly mappingCount: number;\r\n /** Total file size in bytes. */\r\n readonly sizeBytes: number;\r\n}\r\n\r\n/**\r\n * Package YAML contracts into a Maven stubs JAR (ZIP format).\r\n *\r\n * The JAR follows the standard SCC Maven plugin layout:\r\n * ```\r\n * META-INF/{groupId}/{artifactId}/{version}/\r\n * mappings/*.json (WireMock mappings)\r\n * contracts/*.yaml (original YAML contracts)\r\n * ```\r\n *\r\n * Java consumers can use this JAR with `@AutoConfigureStubRunner` or\r\n * the `fetchStubsJar()` / `loadLocalJar()` functions from the jest package.\r\n */\r\nexport async function packageStubsJar(options: PackageOptions): Promise<PackageResult> {\r\n const contracts = await scanContracts(options.contractsDir);\r\n\r\n if (contracts.length === 0) {\r\n throw new Error(`No contract files found in ${options.contractsDir}`);\r\n }\r\n\r\n const { groupId, artifactId, version } = options.coordinates;\r\n const basePath = `META-INF/${groupId}/${artifactId}/${version}`;\r\n\r\n return new Promise((resolve, reject) => {\r\n const output = createWriteStream(options.outputPath);\r\n const archive = archiver(\"zip\", { zlib: { level: 9 } });\r\n\r\n let sizeBytes = 0;\r\n\r\n output.on(\"close\", () => {\r\n sizeBytes = archive.pointer();\r\n resolve({\r\n outputPath: options.outputPath,\r\n contractCount: contracts.length,\r\n mappingCount: contracts.length,\r\n sizeBytes,\r\n });\r\n });\r\n\r\n archive.on(\"error\", reject);\r\n archive.pipe(output);\r\n\r\n // Add original YAML contracts\r\n for (const contract of contracts) {\r\n const safeName = sanitizeEntryName(contract.contractName);\r\n archive.append(contract.content, {\r\n name: `${basePath}/contracts/${safeName}`,\r\n });\r\n }\r\n\r\n // Convert each contract to WireMock JSON and add as mapping\r\n for (const contract of contracts) {\r\n const wireMockJson = contractToWireMock(contract);\r\n const mappingName = toMappingFileName(sanitizeEntryName(contract.contractName));\r\n archive.append(wireMockJson, {\r\n name: `${basePath}/mappings/${mappingName}`,\r\n });\r\n }\r\n\r\n // Add minimal MANIFEST.MF\r\n archive.append(\"Manifest-Version: 1.0\\n\", {\r\n name: \"META-INF/MANIFEST.MF\",\r\n });\r\n\r\n archive.finalize().catch(reject);\r\n });\r\n}\r\n\r\n/** Strip path traversal sequences and absolute path prefixes from a ZIP entry name. */\r\nfunction sanitizeEntryName(name: string): string {\r\n return name\r\n .replace(/\\\\/g, \"/\")\r\n .replace(/^\\/+/, \"\")\r\n .split(\"/\")\r\n .filter((segment) => segment !== \"..\" && segment !== \".\")\r\n .join(\"/\");\r\n}\r\n\r\n/** Convert a contract file name (e.g., \"order/get.yaml\") to a WireMock mapping name (\"order_get.json\"). */\r\nfunction toMappingFileName(contractName: string): string {\r\n const withoutExt = contractName.slice(0, contractName.length - extname(contractName).length);\r\n return withoutExt.replace(/[/\\\\]/g, \"_\") + \".json\";\r\n}\r\n","import { readFile, stat } from \"node:fs/promises\";\r\n\r\n/** Configuration for deploying a stubs JAR to a Maven repository. */\r\nexport interface DeployOptions {\r\n /** Path to the local stubs JAR file. */\r\n readonly jarPath: string;\r\n /** Maven repository URL (e.g., \"https://nexus.example.com/repository/releases\"). */\r\n readonly repositoryUrl: string;\r\n /** Group ID (e.g., \"com.example\"). */\r\n readonly groupId: string;\r\n /** Artifact ID (e.g., \"order-service\"). */\r\n readonly artifactId: string;\r\n /** Version (e.g., \"1.0.0\"). */\r\n readonly version: string;\r\n /** Classifier (default: \"stubs\"). */\r\n readonly classifier?: string;\r\n /** Repository authentication. */\r\n readonly username?: string;\r\n readonly password?: string;\r\n readonly token?: string;\r\n}\r\n\r\n/** Result of a deploy operation. */\r\nexport interface DeployResult {\r\n /** The full URL where the JAR was uploaded. */\r\n readonly url: string;\r\n /** HTTP status code from the upload. */\r\n readonly statusCode: number;\r\n}\r\n\r\n/**\r\n * Deploy a stubs JAR to a Maven repository via HTTP PUT.\r\n *\r\n * Supports Basic Auth and Bearer token authentication.\r\n * The JAR is uploaded to the standard Maven path:\r\n * `{repoUrl}/{groupPath}/{artifactId}/{version}/{artifactId}-{version}-{classifier}.jar`\r\n */\r\nexport async function deployStubsJar(options: DeployOptions): Promise<DeployResult> {\r\n const url = buildDeployUrl(options);\r\n\r\n await stat(options.jarPath).catch(() => {\r\n throw new Error(`JAR file not found: ${options.jarPath}`);\r\n });\r\n\r\n const jarContent = await readFile(options.jarPath);\r\n\r\n const headers: Record<string, string> = {\r\n \"Content-Type\": \"application/java-archive\",\r\n };\r\n\r\n if (options.token !== undefined) {\r\n headers[\"Authorization\"] = `Bearer ${options.token}`;\r\n } else if (options.username !== undefined && options.password !== undefined) {\r\n headers[\"Authorization\"] =\r\n `Basic ${Buffer.from(`${options.username}:${options.password}`).toString(\"base64\")}`;\r\n }\r\n\r\n const response = await fetch(url, {\r\n method: \"PUT\",\r\n headers,\r\n body: jarContent,\r\n signal: AbortSignal.timeout(30_000),\r\n });\r\n\r\n if (!response.ok) {\r\n const body = await response.text().catch(() => \"\");\r\n throw new Error(\r\n `Failed to deploy stubs JAR to ${url}: ${response.status} ${response.statusText}${body ? ` — ${body}` : \"\"}`,\r\n );\r\n }\r\n\r\n return { url, statusCode: response.status };\r\n}\r\n\r\n/** Build the Maven repository URL for a stubs JAR. */\r\nfunction buildDeployUrl(options: DeployOptions): string {\r\n const classifier = options.classifier ?? \"stubs\";\r\n const groupPath = options.groupId.replace(/\\./g, \"/\");\r\n const baseUrl = options.repositoryUrl.replace(/\\/+$/, \"\");\r\n\r\n // Validate URL scheme\r\n const parsed = new URL(baseUrl);\r\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\r\n throw new Error(`Invalid repository URL scheme: ${parsed.protocol} (only http/https allowed)`);\r\n }\r\n\r\n return `${baseUrl}/${groupPath}/${options.artifactId}/${options.version}/${options.artifactId}-${options.version}-${classifier}.jar`;\r\n}\r\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@stubborn-sh/stubs-packager",
3
+ "version": "0.0.3",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "require": "./dist/index.cjs"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsup",
19
+ "typecheck": "tsc --noEmit"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "dependencies": {
25
+ "@stubborn-sh/publisher": "0.1.0",
26
+ "archiver": "^7.0.0",
27
+ "js-yaml": "^4.1.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/archiver": "^6.0.0",
31
+ "tsup": "^8.4.0"
32
+ }
33
+ }