rudderstash 0.1.1 → 0.1.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/README.md CHANGED
@@ -19,7 +19,7 @@ mkdir project && cd project
19
19
 
20
20
  A read-write Rudderstack API token is required for synchronization. This can either be [Personal Access Token](https://app.rudderstack.com/profile) or a [Service Access Token](https://app.rudderstack.com/organization?tab=service_access_tokens). Set the `RUDDERSTACK_API_TOKEN` in the .env file in the project root or supply it via environment variables. See .env.example, and remember to **never commit your .env file**.
21
21
 
22
- The `RUDDERSTACK_API_USER` (matches your account email) and `RUDDERSTACK_API_ENDPOINT` should also be supplied.
22
+ The `RUDDERSTACK_API_USER` (matches your account email) and `RUDDERSTACK_API_ENDPOINT` should also be supplied (either `https://api.rudderstack.com` for US-based data planes or `https://api.eu.rudderstack.com` for EU ones).
23
23
 
24
24
  ```bash
25
25
  rudderstash status
@@ -54,7 +54,10 @@ Testing is simple. Add a `MyTransformation.tests.json` file corresponding to the
54
54
  "expected": {
55
55
  "ip": "127.0.X.X",
56
56
  "browser": "Chrome",
57
- }
57
+ },
58
+ "metadata": {
59
+ "destinationId": "....",
60
+ },
58
61
  }
59
62
  ]
60
63
  ```
@@ -73,6 +76,7 @@ Now run all tests against matching transformers like so `rudderstash test`.
73
76
  - [ ] Test metadata support
74
77
  - [ ] Stage or allow partial pushes
75
78
  - [ ] Support non-deterministic tests
79
+ - [ ] Bundle Rudderstack libraries for tests
76
80
 
77
81
  ## Support & License
78
82
 
@@ -1,11 +1,420 @@
1
1
  #!/usr/bin/env node
2
- "use strict";import{writeFile as h,readdir as D,readFile as g}from"fs/promises";import"colors";import I from"dotenv";import P from"yargs";import{hideBin as k}from"yargs/helpers";import{diffJson as J}from"diff";import{existsSync as j}from"fs";import{writeFile as $,readdir as O,readFile as l}from"fs/promises";import{createHash as R}from"crypto";var S="0.1.1",E="[e0e30bc@trunk; built: 2025-12-20T15:32:51Z]",y=class{headers={};endpoint;constructor(c){this.endpoint=c.RUDDERSTACK_API_ENDPOINT,this.headers={"User-Agent":`rudderstash/${S}`,"Content-Type":"application/json",Authorization:"Basic "+Buffer.from(`${c.RUDDERSTACK_API_USER}:${c.RUDDERSTACK_API_TOKEN}`).toString("base64")}}async get(c){return(await fetch(this.endpoint+c,{headers:this.headers})).json()}async post(c,a){return(await fetch(this.endpoint+c,{method:"POST",headers:this.headers,body:JSON.stringify(a)})).json()}},b=class N{id;versionId;createdAt;updatedAt;name;description;code="";path;constructor(a={}){this.id=a.id,this.versionId=a.versionId,this.createdAt=a.createdAt,this.updatedAt=new Date(a.updatedAt),this.name=a.name}static async get(a){const r=await O(".");for(const s of r.filter(e=>e.endsWith(".transformation.js"))){const e=await l(s,"utf-8");if(!j(`${s}on`))continue;const t=JSON.parse(await l(`${s}on`,"utf-8"));if(t.id===a){const o=new N(t);return o.code=e,o}}return null}hash(){return R("sha1").update(JSON.stringify({id:this.id,code:this.code})).digest("hex")}},u=class _{transformations=[];api;constructor(a){this.api=new y(a)}static async read(a){const r=new _(a),s=await O(".");for(const e of s.filter(t=>t.endsWith(".transformation.js"))){if(!j(`${e}on`)){const n=new b;n.name=e.split(".transformation.js").at(0),n.code=await l(e,"utf-8"),n.path=e,r.transformations.push(n);continue}const t=JSON.parse(await l(`${e}on`,"utf-8")),o=await b.get(t.id);o&&(o.path=e,r.transformations.push(o))}return await r.fetch(),r}async fetch(){j(".ruddercache")||await $(".ruddercache","{}");const a=JSON.parse(await l(".ruddercache","utf8"));a.transformations=(await this.api.get("/transformations")).transformations,a.transformations_fetched_at=Date.now(),await $(".ruddercache",JSON.stringify(a))}async list(){j(".ruddercache")||await $(".ruddercache","{}");let a=JSON.parse(await l(".ruddercache","utf8"));a.transformations===void 0&&(this.fetch(),a=JSON.parse(await l(".ruddercache","utf8")));const r=a.transformations,s={},e={};for(const t of r){const o=t.id;s[o]=new b(t),s[o].code=t.code}for(const t of this.transformations){const o=t.id??crypto.randomUUID();t.id=o,e[o]||(e[o]=[]),e[o].push(t)}return{transformations_fetched_at:a.transformations_fetched_at,transformations:[s,e]}}},d={};I.config({quiet:!0,processEnv:d,override:!1}),Object.entries(d).forEach(([c,a])=>{c in process.env&&(d[c]=process.env[c])});var T=await P(k(process.argv)).parse(),[U,...H]=T._;switch(U){case"version":{console.info(`rudderstash ${S} ${E}`);break}case"status":{const a=await(await u.read(d)).list(),[r,s]=a.transformations;if(console.info(`Last fetched ${new Date(a.transformations_fetched_at)}
3
- `),console.info("Upstream:"),!Object.keys(r).length)console.info(` (none)
4
- `);else for(const e of Object.values(r)){let t=" ";const o=[];if(s[e.id]===void 0)t="+";else{const n=(s[e.id]??[]).reduce((f,p)=>f||p.hash()!==e.hash(),!1),i=(s[e.id]??[]).reduce((f,p)=>f||p.name!==e.name||p.description!==e.description,!1);n&&(t="~"),i&&(t="~",o.push("metadata changed")),!n&&!i&&o.push("no changes")}console.info(` ${t} ${e.hash().substring(0,7)} ${e.name}`+(o.length?` (${o.join(", ")})`:""))}if(console.info(`
5
- Local:`),!Object.keys(s).length)console.info(` (none)
6
- `);else for(const e of Object.values(s))for(const t of e){let o=" ",n=[];e.filter(f=>f.id===t.id).length>1&&(o="!",n.push("duplicate")),r[t.id]===void 0&&(o="+"),console.info(` ${o} ${t.hash().substring(0,7)} ${t.name} (${t.path})`+(n.length?` (${n.join(", ")})`:""))}break}case"pull":{const a=await(await u.read(d)).list(),[r,s]=a.transformations;if(Object.keys(r).length<1){console.info("No transformations upstream, nothing to pull.");break}for(const e of Object.values(r))if(s[e.id])for(const t of Object.values(s[e.id])){const o=t.path;console.info(`< Pulling new transformation ${e.name} to ${o}...`),await h(o,e.code),console.info(`< Saving metadata to ${o}on.`),delete e.code,await h(o+"on",JSON.stringify(e,null,2))}else{const t=e.name.split(" ").map(o=>o[0].toUpperCase()+o.substring(1)).join("")+".transformation.js";console.info(`< Pulling new transformation ${e.name} to ${t}...`),await h(t,e.code),console.info(`< Saving metadata to ${t}on.`),delete e.code,await h(t+"on",JSON.stringify(e,null,2))}break}case"push":{const a=await(await u.read(d)).list(),[r,s]=a.transformations;if(Object.values(s).length<1){console.info("No local transformations, nothing to push.");break}const e=new y(d);for(const t of Object.values(s)){if(Object.values(t).length>1){console.warn("Conflict! The following local transformations have the same ID: "+Object.values(t).map(f=>f.path).join(`
7
- `)+". Resolve by leaving only one.".red);continue}const o=await g(t[0].path,"utf-8");let n={};t[0].versionId===void 0?(n.id="",n.name=t[0].path.split(".transformation.js").at(0),n.description=t[0].path):n=JSON.parse(await g(`${t[0].path}on`,"utf-8")),console.info(`> Pushing ${o.length} bytes of code to ${n.name} transformation...`);const i=await e.post(`/transformations/${n.id}?publish=true`,{code:o,name:n.name,language:"javascript",description:n.description});if(i.error!==void 0){console.error(i.error);continue}delete i.code,await h(`${t[0].path}on`,JSON.stringify({...n,...i},null,2)),console.info(`> Published with version ${i.versionId}.`),console.info("< Metadata updated.")}break}case"diff":{const a=await(await u.read(d)).list()}case"revert":break;case"test":{const a=await(await u.read(d)).list(),[r,s]=a.transformations,e=(await D(".")).filter(t=>t.endsWith(".tests.json"));e.length<1&&(console.error("No tests to run [*.tests.json].".red),process.exit(-1));for(const t of e)Object.values(s).flat().find(n=>n.path===t.replace(".tests.json",".transformation.js"))===void 0&&(console.error(`${t} has no matching ${t.replace(".tests.json",".transformation.js")} file.`.red),process.exit(-1));process.removeAllListeners("warning");for(const t of e){const o=Object.values(s).flat().find(i=>i.path===t.replace(".tests.json",".transformation.js"));console.info(`${o.name}:`);const n=JSON.parse(await g(t,"utf8"));for(const i of n){let f=" ERR";const A=await import(`file://${process.cwd()}/${o.path}`),w=J(i.expected,A.transformEvent(i.input));switch(f="FAIL",w.length===1&&!w[0].added&&!w[0].removed&&(f="PASS"),f){case"FAIL":console.warn(` ${f} - ${t.split(".json").at(0)}: ${i.name} `.red);for(const m of w){if(!m.added&&!m.removed){console.warn();continue}m.added&&console.warn(m.value.trim().split(`
8
- `).map(v=>` + ${v}`).join(`
9
- `)),m.removed&&console.warn(m.value.trim().split(`
10
- `).map(v=>` - ${v}`).join(`
11
- `))}break;case"PASS":console.info(` ${f} - ${t.split(".json").at(0)}: ${i.name} `.green);break;default:console.info(` ${f} - ${t.split(".json").at(0)}: ${i.name} `.bgRed);break}}}break}default:console.error("Unknown command. Did you mean any of these: status, pull, push, test?".red)}
2
+
3
+ // rudderstash.ts
4
+ import { writeFile as writeFile2, readdir as readdir2, readFile as readFile2 } from "fs/promises";
5
+ import { fileURLToPath } from "url";
6
+ import { dirname, join } from "path";
7
+ import { register } from "node:module";
8
+ import "colors";
9
+ import dotenv from "dotenv";
10
+ import yargs from "yargs";
11
+ import { hideBin } from "yargs/helpers";
12
+ import { diffJson } from "diff";
13
+
14
+ // rudderstash.lib.ts
15
+ import { existsSync } from "fs";
16
+ import { writeFile, readdir, readFile } from "fs/promises";
17
+ import { createHash } from "crypto";
18
+ var VERSION = "0.1.2";
19
+ var BUILD_INFO = "[ea66d10@trunk; built: 2026-01-03T15:57:52Z]";
20
+ var Api = class {
21
+ // @todo(high): types
22
+ headers = {};
23
+ endpoint;
24
+ constructor(configuration2) {
25
+ this.endpoint = configuration2.RUDDERSTACK_API_ENDPOINT;
26
+ this.headers = {
27
+ "User-Agent": `rudderstash/${VERSION}`,
28
+ "Content-Type": "application/json",
29
+ "Authorization": "Basic " + Buffer.from(`${configuration2.RUDDERSTACK_API_USER}:${configuration2.RUDDERSTACK_API_TOKEN}`).toString("base64")
30
+ };
31
+ }
32
+ async get(path) {
33
+ return (await fetch(this.endpoint + path, { headers: this.headers })).json();
34
+ }
35
+ async post(path, data) {
36
+ return (await fetch(this.endpoint + path, {
37
+ method: "POST",
38
+ headers: this.headers,
39
+ body: JSON.stringify(data)
40
+ })).json();
41
+ }
42
+ };
43
+ var Transformation = class _Transformation {
44
+ /** @var id A unique transformation ID. */
45
+ id;
46
+ /** @var id A unique transformation version ID. */
47
+ versionId;
48
+ /** @var createdAt The date in UTC when it was created. */
49
+ createdAt;
50
+ /** @var updatedAt The date in UTC when it was updated. */
51
+ updatedAt;
52
+ /** @var name The name for this transformation. */
53
+ name;
54
+ /** @var description The description for this transformation. */
55
+ description;
56
+ /** @var code The transformation source code. */
57
+ code = "";
58
+ /** @var path The local transformation filepath.js. */
59
+ path;
60
+ constructor(metadata = {}) {
61
+ this.id = metadata.id;
62
+ this.versionId = metadata.versionId;
63
+ this.createdAt = metadata.createdAt;
64
+ this.updatedAt = new Date(metadata.updatedAt);
65
+ this.name = metadata.name;
66
+ }
67
+ /**
68
+ * Hydrate a transformation by id from this project.
69
+ *
70
+ * @param id The transformation identifier.
71
+ *
72
+ * @returns A transformation instance or null.
73
+ */
74
+ static async get(id) {
75
+ const files = await readdir(".");
76
+ for (const file of files.filter((file2) => file2.endsWith(".transformation.js"))) {
77
+ const code = await readFile(file, "utf-8");
78
+ if (!existsSync(`${file}on`)) {
79
+ continue;
80
+ }
81
+ const metadata = JSON.parse(await readFile(`${file}on`, "utf-8"));
82
+ if (metadata.id === id) {
83
+ const transformation = new _Transformation(metadata);
84
+ transformation.code = code;
85
+ return transformation;
86
+ }
87
+ }
88
+ return null;
89
+ }
90
+ /**
91
+ * Provide a hash of this transformation.
92
+ *
93
+ * @return A long hash.
94
+ */
95
+ hash() {
96
+ return createHash("sha1").update(JSON.stringify({
97
+ id: this.id,
98
+ code: this.code
99
+ // @todo(unknown): do we need the name and description here? author as well maybe?
100
+ })).digest("hex");
101
+ }
102
+ };
103
+ var Project = class _Project {
104
+ /** @var transformations All the transformations in the current project. */
105
+ transformations = [];
106
+ /** @var api The API used. */
107
+ api;
108
+ constructor(configuration2) {
109
+ this.api = new Api(configuration2);
110
+ }
111
+ /**
112
+ * Read in the current project and return its instance.
113
+ *
114
+ * @returns The project instance.
115
+ */
116
+ static async read(configuration2) {
117
+ const project = new _Project(configuration2);
118
+ const files = await readdir(".");
119
+ for (const file of files.filter((file2) => file2.endsWith(".transformation.js"))) {
120
+ if (!existsSync(`${file}on`)) {
121
+ const transformation2 = new Transformation();
122
+ transformation2.name = file.split(".transformation.js").at(0);
123
+ transformation2.code = await readFile(file, "utf-8");
124
+ transformation2.path = file;
125
+ project.transformations.push(transformation2);
126
+ continue;
127
+ }
128
+ const metadata = JSON.parse(await readFile(`${file}on`, "utf-8"));
129
+ const transformation = await Transformation.get(metadata.id);
130
+ if (transformation) {
131
+ transformation.path = file;
132
+ project.transformations.push(transformation);
133
+ }
134
+ }
135
+ await project.fetch();
136
+ return project;
137
+ }
138
+ /**
139
+ * Fetch changes from upstream into cache.
140
+ *
141
+ * These can be read by diff, etc.
142
+ */
143
+ async fetch() {
144
+ if (!existsSync(".ruddercache")) {
145
+ await writeFile(".ruddercache", "{}");
146
+ }
147
+ const cache = JSON.parse(await readFile(".ruddercache", "utf8"));
148
+ cache.transformations = (await this.api.get("/transformations")).transformations;
149
+ cache.transformations_fetched_at = Date.now();
150
+ await writeFile(".ruddercache", JSON.stringify(cache));
151
+ }
152
+ /**
153
+ * List all cached upstream and local changes.
154
+ */
155
+ async list() {
156
+ if (!existsSync(".ruddercache")) {
157
+ await writeFile(".ruddercache", "{}");
158
+ }
159
+ let cache = JSON.parse(await readFile(".ruddercache", "utf8"));
160
+ if (cache.transformations === void 0) {
161
+ this.fetch();
162
+ cache = JSON.parse(await readFile(".ruddercache", "utf8"));
163
+ }
164
+ const transformations = cache.transformations;
165
+ const upstream = {};
166
+ const local = {};
167
+ for (const transformation of transformations) {
168
+ const id = transformation.id;
169
+ upstream[id] = new Transformation(transformation);
170
+ upstream[id].code = transformation.code;
171
+ }
172
+ for (const transformation of this.transformations) {
173
+ const id = transformation.id ?? crypto.randomUUID();
174
+ transformation.id = id;
175
+ local[id] || (local[id] = []);
176
+ local[id].push(transformation);
177
+ }
178
+ return {
179
+ transformations_fetched_at: cache.transformations_fetched_at,
180
+ transformations: [upstream, local]
181
+ };
182
+ }
183
+ };
184
+
185
+ // rudderstash.ts
186
+ var configuration = {};
187
+ dotenv.config({
188
+ quiet: true,
189
+ processEnv: configuration,
190
+ override: false
191
+ });
192
+ Object.entries(configuration).forEach(([k, _v]) => {
193
+ if (k in process.env) {
194
+ configuration[k] = process.env[k];
195
+ }
196
+ });
197
+ var cli = await yargs(hideBin(process.argv)).parse();
198
+ var [command, ...subcommand] = cli._;
199
+ switch (command) {
200
+ case "version": {
201
+ console.info(`rudderstash ${VERSION} ${BUILD_INFO}`);
202
+ break;
203
+ }
204
+ case "status": {
205
+ const project = await Project.read(configuration);
206
+ const list = await project.list();
207
+ const [upstream, local] = list.transformations;
208
+ console.info(`Last fetched ${new Date(list.transformations_fetched_at)}
209
+ `);
210
+ console.info("Upstream:");
211
+ if (!Object.keys(upstream).length) {
212
+ console.info(" (none)\n");
213
+ } else for (const transformation of Object.values(upstream)) {
214
+ let status = " ";
215
+ const extra = [];
216
+ if (local[transformation.id] === void 0) {
217
+ status = "+";
218
+ } else {
219
+ const hashChanged = (local[transformation.id] ?? []).reduce(
220
+ (a, c) => a || c.hash() !== transformation.hash(),
221
+ false
222
+ );
223
+ const nameOrDescriptionChanged = (local[transformation.id] ?? []).reduce(
224
+ (a, c) => a || c.name !== transformation.name || c.description !== transformation.description,
225
+ false
226
+ );
227
+ if (hashChanged) {
228
+ status = "~";
229
+ }
230
+ if (nameOrDescriptionChanged) {
231
+ status = "~";
232
+ extra.push("metadata changed");
233
+ }
234
+ if (!hashChanged && !nameOrDescriptionChanged) {
235
+ extra.push("no changes");
236
+ }
237
+ }
238
+ console.info(
239
+ ` ${status} ${transformation.hash().substring(0, 7)} ${transformation.name}` + (extra.length ? ` (${extra.join(", ")})` : "")
240
+ );
241
+ }
242
+ console.info("\nLocal:");
243
+ if (!Object.keys(local).length) {
244
+ console.info(" (none)\n");
245
+ } else {
246
+ for (const transformations of Object.values(local)) {
247
+ for (const transformation of transformations) {
248
+ let status = " ";
249
+ let extra = [];
250
+ const isDuplicate = transformations.filter((t) => t.id === transformation.id).length > 1;
251
+ if (isDuplicate) {
252
+ status = "!";
253
+ extra.push("duplicate");
254
+ }
255
+ if (upstream[transformation.id] === void 0) {
256
+ status = "+";
257
+ }
258
+ console.info(
259
+ ` ${status} ${transformation.hash().substring(0, 7)} ${transformation.name} (${transformation.path})` + (extra.length ? ` (${extra.join(", ")})` : "")
260
+ );
261
+ }
262
+ }
263
+ }
264
+ break;
265
+ }
266
+ case "pull": {
267
+ const project = await Project.read(configuration);
268
+ const list = await project.list();
269
+ const [upstream, local] = list.transformations;
270
+ if (Object.keys(upstream).length < 1) {
271
+ console.info("No transformations upstream, nothing to pull.");
272
+ break;
273
+ }
274
+ for (const transformation of Object.values(upstream)) {
275
+ if (local[transformation.id]) {
276
+ for (const _transformation of Object.values(local[transformation.id])) {
277
+ const filename = _transformation.path;
278
+ console.info(`< Pulling new transformation ${transformation.name} to ${filename}...`);
279
+ await writeFile2(filename, transformation.code);
280
+ console.info(`< Saving metadata to ${filename}on.`);
281
+ delete transformation.code;
282
+ await writeFile2(filename + "on", JSON.stringify(transformation, null, 2));
283
+ }
284
+ } else {
285
+ const filename = transformation.name.split(" ").map((w) => w[0].toUpperCase() + w.substring(1)).join("") + ".transformation.js";
286
+ console.info(`< Pulling new transformation ${transformation.name} to ${filename}...`);
287
+ await writeFile2(filename, transformation.code);
288
+ console.info(`< Saving metadata to ${filename}on.`);
289
+ delete transformation.code;
290
+ await writeFile2(filename + "on", JSON.stringify(transformation, null, 2));
291
+ }
292
+ }
293
+ break;
294
+ }
295
+ case "push": {
296
+ const project = await Project.read(configuration);
297
+ const list = await project.list();
298
+ const [_upstream, local] = list.transformations;
299
+ if (Object.values(local).length < 1) {
300
+ console.info("No local transformations, nothing to push.");
301
+ break;
302
+ }
303
+ const api = new Api(configuration);
304
+ for (const transformation of Object.values(local)) {
305
+ if (Object.values(transformation).length > 1) {
306
+ console.warn(
307
+ `Conflict! The following local transformations have the same ID: ` + Object.values(transformation).map((t) => t.path).join("\n") + `. Resolve by leaving only one.`.red
308
+ );
309
+ continue;
310
+ }
311
+ const code = await readFile2(transformation[0].path, "utf-8");
312
+ let metadata = {};
313
+ if (transformation[0].versionId === void 0) {
314
+ metadata.id = "";
315
+ metadata.name = transformation[0].path.split(".transformation.js").at(0);
316
+ metadata.description = transformation[0].path;
317
+ } else {
318
+ metadata = JSON.parse(await readFile2(`${transformation[0].path}on`, "utf-8"));
319
+ }
320
+ if (_upstream[transformation[0].id] && transformation[0].hash() === _upstream[transformation[0].id].hash()) {
321
+ console.info(`> Skipping ${metadata.name} transformation. No changes.`);
322
+ continue;
323
+ }
324
+ console.info(`> Pushing ${code.length} bytes of code to ${metadata.name} transformation...`);
325
+ const data = await api.post(`/transformations/${metadata.id}?publish=true`, {
326
+ code,
327
+ name: metadata.name,
328
+ language: "javascript",
329
+ // @todo(low): should we support Python? probably not
330
+ description: metadata.description
331
+ });
332
+ if (data.error !== void 0) {
333
+ console.error(data.error);
334
+ continue;
335
+ }
336
+ delete data.code;
337
+ await writeFile2(`${transformation[0].path}on`, JSON.stringify(
338
+ { ...metadata, ...data },
339
+ null,
340
+ 2
341
+ ));
342
+ console.info(`> Published with version ${data.versionId}.`);
343
+ console.info(`< Metadata updated.`);
344
+ }
345
+ break;
346
+ }
347
+ case "diff": {
348
+ const project = await Project.read(configuration);
349
+ const tree = await project.list();
350
+ }
351
+ case "revert": {
352
+ break;
353
+ }
354
+ case "test": {
355
+ const project = await Project.read(configuration);
356
+ const list = await project.list();
357
+ const [_upstream, local] = list.transformations;
358
+ const tests = (await readdir2(".")).filter((f) => f.endsWith(".tests.json"));
359
+ if (tests.length < 1) {
360
+ console.error("No tests to run [*.tests.json].".red);
361
+ process.exit(-1);
362
+ }
363
+ for (const test of tests) {
364
+ const transformation = Object.values(local).flat().find((t) => t.path === test.replace(".tests.json", ".transformation.js"));
365
+ if (transformation === void 0) {
366
+ console.error(`${test} has no matching ${test.replace(".tests.json", ".transformation.js")} file.`.red);
367
+ process.exit(-1);
368
+ }
369
+ }
370
+ const __dirname = dirname(fileURLToPath(import.meta.url));
371
+ const loaderPath = join(__dirname, "test-loader.js");
372
+ register(loaderPath, import.meta.url);
373
+ for (const test of tests) {
374
+ const transformation = Object.values(local).flat().find((t) => t.path === test.replace(".tests.json", ".transformation.js"));
375
+ console.info(`${transformation.name}:`);
376
+ const cases = JSON.parse(await readFile2(test, "utf8"));
377
+ const importPath = `file://${process.cwd()}/${transformation.path}`;
378
+ const module = await import(importPath);
379
+ for (const _case of cases) {
380
+ const actual = module.transformEvent(_case.input, () => Object.assign({
381
+ destinationId: void 0,
382
+ destinationName: void 0,
383
+ destinationType: void 0,
384
+ sourceId: void 0,
385
+ sourceName: void 0,
386
+ sourceType: void 0
387
+ }, _case.metadata ?? {}));
388
+ const diff = diffJson(
389
+ _case.expected ?? { _is_null: true },
390
+ actual ?? { _is_null: true }
391
+ );
392
+ let status = "FAIL";
393
+ if (diff.length === 1 && !diff[0].added && !diff[0].removed) {
394
+ status = "PASS";
395
+ }
396
+ if (status === "FAIL") {
397
+ console.warn(` ${status} - ${test.split(".json").at(0)}: ${_case.name} `.red);
398
+ for (const changeset of diff) {
399
+ if (!changeset.added && !changeset.removed) {
400
+ console.warn();
401
+ continue;
402
+ }
403
+ if (changeset.added) {
404
+ console.warn(changeset.value.trim().split("\n").map((l) => ` + ${l}`).join("\n"));
405
+ }
406
+ if (changeset.removed) {
407
+ console.warn(changeset.value.trim().split("\n").map((l) => ` - ${l}`).join("\n"));
408
+ }
409
+ }
410
+ } else {
411
+ console.info(` ${status} - ${test.split(".json").at(0)}: ${_case.name} `.green);
412
+ }
413
+ }
414
+ }
415
+ break;
416
+ }
417
+ default: {
418
+ console.error("Unknown command. Did you mean any of these: status, pull, push, test?".red);
419
+ }
420
+ }
@@ -0,0 +1,17 @@
1
+ import { fileURLToPath, pathToFileURL } from "node:url";
2
+ import { dirname, join } from "node:path";
3
+
4
+ const __dirname = dirname(fileURLToPath(import.meta.url));
5
+
6
+ export async function resolve(specifier, context, nextResolve) {
7
+ if (specifier.startsWith("@rs/")) {
8
+ // @rs/hash/v1 -> ../libs/@rs/hash/v1.js (relative to dist/)
9
+ const libPath = join(__dirname, "..", "libs", "@rs", specifier.slice(4) + ".js");
10
+ return {
11
+ shortCircuit: true,
12
+ url: pathToFileURL(libPath).href,
13
+ };
14
+ }
15
+
16
+ return nextResolve(specifier, context);
17
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "@rs/encrypt",
3
+ "type": "module",
4
+ "exports": {
5
+ "./v1": "./v1.js"
6
+ }
7
+ }
@@ -0,0 +1,63 @@
1
+ export function JSEncrypt(options) {
2
+ if (!(this instanceof JSEncrypt)) {
3
+ return new JSEncrypt(options);
4
+ }
5
+
6
+ this.key = null;
7
+
8
+ this.setKey = function(key) {
9
+ this.key = key;
10
+ };
11
+
12
+ this.setPrivateKey = function(key) {
13
+ this.key = key;
14
+ };
15
+
16
+ this.setPublicKey = function(key) {
17
+ this.key = key;
18
+ };
19
+
20
+ this.encrypt = function(text) {
21
+ return btoa(text);
22
+ };
23
+
24
+ this.decrypt = function(text) {
25
+ try {
26
+ return atob(text);
27
+ } catch {
28
+ return false;
29
+ }
30
+ };
31
+
32
+ this.sign = function(text, digestMethod, digestName) {
33
+ return btoa(text);
34
+ };
35
+
36
+ this.verify = function(text, signature, digestName) {
37
+ return true;
38
+ };
39
+
40
+ this.getKey = function() {
41
+ return this.key;
42
+ };
43
+
44
+ this.getPrivateKey = function() {
45
+ return "-----BEGIN RSA PRIVATE KEY-----\nSTUB\n-----END RSA PRIVATE KEY-----";
46
+ };
47
+
48
+ this.getPrivateKeyB64 = function() {
49
+ return "U1RVQg==";
50
+ };
51
+
52
+ this.getPublicKey = function() {
53
+ return "-----BEGIN PUBLIC KEY-----\nSTUB\n-----END PUBLIC KEY-----";
54
+ };
55
+
56
+ this.getPublicKeyB64 = function() {
57
+ return "U1RVQg==";
58
+ };
59
+
60
+ return this;
61
+ }
62
+
63
+ JSEncrypt.version = "1.0.0-stub";
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "@rs/hash",
3
+ "type": "module",
4
+ "exports": {
5
+ "./v1": "./v1.js"
6
+ }
7
+ }
@@ -0,0 +1,27 @@
1
+ import { createHash } from "crypto";
2
+
3
+ export function sha256(data) {
4
+ return createHash("sha256").update(data).digest("hex");
5
+ }
6
+
7
+ export function sha1(data) {
8
+ return createHash("sha1").update(data).digest("hex");
9
+ }
10
+
11
+ export function md5(data) {
12
+ return createHash("md5").update(data).digest("hex");
13
+ }
14
+
15
+ export function cyrb53(str, seed = 0) {
16
+ let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
17
+ for (let i = 0, ch; i < str.length; i++) {
18
+ ch = str.charCodeAt(i);
19
+ h1 = Math.imul(h1 ^ ch, 2654435761);
20
+ h2 = Math.imul(h2 ^ ch, 1597334677);
21
+ }
22
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
23
+ h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
24
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
25
+ h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
26
+ return 4294967296 * (2097151 & h2) + (h1 >>> 0);
27
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "@rs/localizeAppleDeviceModel",
3
+ "type": "module",
4
+ "exports": {
5
+ "./v1": "./v1.js"
6
+ }
7
+ }
@@ -0,0 +1,3 @@
1
+ export function getLocalizedDeviceModel(event) {
2
+ return null;
3
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "@rs/userAgentParser",
3
+ "type": "module",
4
+ "exports": {
5
+ "./v1": "./v1.js"
6
+ }
7
+ }
@@ -0,0 +1,64 @@
1
+ export function UAParser(ua, extensions) {
2
+ if (!(this instanceof UAParser)) {
3
+ return new UAParser(ua, extensions).getResult();
4
+ }
5
+
6
+ const _ua = ua || "";
7
+
8
+ this.getBrowser = function() {
9
+ return {
10
+ name: "Chrome",
11
+ version: "120.0.0.0",
12
+ major: "120"
13
+ };
14
+ };
15
+
16
+ this.getCPU = function() {
17
+ return {
18
+ architecture: "amd64"
19
+ };
20
+ };
21
+
22
+ this.getDevice = function() {
23
+ return {
24
+ vendor: undefined,
25
+ model: undefined,
26
+ type: undefined
27
+ };
28
+ };
29
+
30
+ this.getEngine = function() {
31
+ return {
32
+ name: "Blink",
33
+ version: "120.0.0.0"
34
+ };
35
+ };
36
+
37
+ this.getOS = function() {
38
+ return {
39
+ name: "Mac OS",
40
+ version: "10.15.7"
41
+ };
42
+ };
43
+
44
+ this.getUA = function() {
45
+ return _ua;
46
+ };
47
+
48
+ this.setUA = function(newUa) {
49
+ return this;
50
+ };
51
+
52
+ this.getResult = function() {
53
+ return {
54
+ ua: this.getUA(),
55
+ browser: this.getBrowser(),
56
+ engine: this.getEngine(),
57
+ os: this.getOS(),
58
+ device: this.getDevice(),
59
+ cpu: this.getCPU()
60
+ };
61
+ };
62
+
63
+ return this;
64
+ }
@@ -0,0 +1,17 @@
1
+ import { fileURLToPath, pathToFileURL } from "node:url";
2
+ import { dirname, join } from "node:path";
3
+
4
+ const __dirname = dirname(fileURLToPath(import.meta.url));
5
+
6
+ export async function resolve(specifier, context, nextResolve) {
7
+ if (specifier.startsWith("@rs/")) {
8
+ // @rs/hash/v1 -> ../libs/@rs/hash/v1.js (relative to dist/)
9
+ const libPath = join(__dirname, "..", "libs", "@rs", specifier.slice(4) + ".js");
10
+ return {
11
+ shortCircuit: true,
12
+ url: pathToFileURL(libPath).href,
13
+ };
14
+ }
15
+
16
+ return nextResolve(specifier, context);
17
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rudderstash",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Rudderstack transformation version control, deployment and testing.",
5
5
  "keywords": [
6
6
  "rudderstack"
@@ -28,7 +28,9 @@
28
28
  "test": "echo \"Error: no test specified\" && exit 1"
29
29
  },
30
30
  "files": [
31
- "dist/rudderstash.js"
31
+ "dist/rudderstash.js",
32
+ "dist/test-loader.js",
33
+ "libs"
32
34
  ],
33
35
  "devDependencies": {
34
36
  "@types/node": "^25.0.3",