qunitx-cli 0.23.2 → 0.23.4

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
@@ -306,10 +306,11 @@ Don't forget to add the plugin's file extension(s) to `qunitx.extensions` so dir
306
306
 
307
307
  ### Environment variables
308
308
 
309
- | Variable | Description |
310
- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
311
- | `CHROME_BIN` | Path to the Chrome/Chromium executable. Required on systems where Chrome is not on `PATH` (e.g. many CI environments). Set automatically when using `browser-actions/setup-chrome` in GitHub Actions. |
312
- | `QUNITX_BROWSER` | Browser engine to use (`chromium`, `firefox`, `webkit`). Equivalent to `--browser` on the CLI. Useful in CI matrix jobs. |
309
+ | Variable | Description |
310
+ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
311
+ | `CHROME_BIN` | Path to the Chrome/Chromium executable. Required on systems where Chrome is not on `PATH` (e.g. many CI environments). Set automatically when using `browser-actions/setup-chrome` in GitHub Actions. |
312
+ | `QUNITX_BROWSER` | Browser engine to use (`chromium`, `firefox`, `webkit`). Equivalent to `--browser` on the CLI. Useful in CI matrix jobs. |
313
+ | `NODE_COMPILE_CACHE` | Standard Node env, auto-enabled by qunitx. Stores V8 bytecode for the CLI + its dep graph on disk so the second and subsequent `qunitx` runs skip the parser pass — measured ~8% faster end-to-end (more on slow CI disks). Defaults to `${TMPDIR}/node-compile-cache`; set to a path to relocate (handy for a CI cache key) or `""` to disable. |
313
314
 
314
315
  If you do not provide any HTML template, qunitx falls back to its built-in `test/tests.html` boilerplate internally, so `qunitx init` is optional.
315
316
 
package/bin/qunitx.js CHANGED
@@ -3,12 +3,22 @@
3
3
  // Prefers a pre-built SEA binary from the matching optional platform package
4
4
  // (qunitx-cli-linux-x64, qunitx-cli-darwin-arm64, etc.) when available.
5
5
  // Falls back to the bundled JS CLI (dist/cli.js) which requires Node.js + node_modules.
6
+ import nodeModule, { createRequire } from 'node:module';
6
7
  import { spawn } from 'node:child_process';
7
8
  import { access, constants, readFile } from 'node:fs/promises';
8
- import { createRequire } from 'node:module';
9
9
  import { fileURLToPath } from 'node:url';
10
10
  import { dirname, join } from 'node:path';
11
11
 
12
+ // Turn on V8's on-disk compile cache before importing dist/cli.js — caches
13
+ // the bundle + its external deps (esbuild, playwright-core, ws) across cold
14
+ // invocations. The active dir is written back to process.env so any child
15
+ // spawn (SEA binary below, daemon auto-spawn from cli.ts) inherits and also
16
+ // auto-enables from boot. `in` check preserves a user-set empty string.
17
+ const cacheResult = nodeModule.enableCompileCache?.();
18
+ if (cacheResult?.directory && !('NODE_COMPILE_CACHE' in process.env)) {
19
+ process.env.NODE_COMPILE_CACHE = cacheResult.directory;
20
+ }
21
+
12
22
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
23
  const require = createRequire(import.meta.url);
14
24
 
package/dist/cli.js CHANGED
@@ -374,7 +374,7 @@ var init_package = __esm({
374
374
  package_default = {
375
375
  name: "qunitx-cli",
376
376
  type: "module",
377
- version: "0.23.2",
377
+ version: "0.23.4",
378
378
  description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
379
379
  author: "Izel Nakri",
380
380
  license: "MIT",
@@ -877,7 +877,7 @@ function awaitClose(socket) {
877
877
  async function pingDaemon() {
878
878
  const socket = await tryConnect();
879
879
  if (!socket) return null;
880
- const result = new Promise((resolve) => {
880
+ const result2 = new Promise((resolve) => {
881
881
  attachLineParser(socket, (chunk) => {
882
882
  if (chunk.type === "pong") resolve(chunk);
883
883
  });
@@ -885,7 +885,7 @@ async function pingDaemon() {
885
885
  socket.once("error", () => resolve(null));
886
886
  });
887
887
  send(socket, { type: "ping" });
888
- const pong = await result;
888
+ const pong = await result2;
889
889
  socket.end();
890
890
  return pong;
891
891
  }
@@ -1085,32 +1085,32 @@ var init_test_file_paths = __esm({
1085
1085
  import path5 from "node:path";
1086
1086
  function parseCliFlags(projectRoot) {
1087
1087
  const providedFlags = process.argv.slice(2).reduce(
1088
- (result, arg) => {
1088
+ (result2, arg) => {
1089
1089
  if (arg.startsWith("--debug")) {
1090
- return Object.assign(result, { debug: parseBoolean(arg.split("=")[1]) });
1090
+ return Object.assign(result2, { debug: parseBoolean(arg.split("=")[1]) });
1091
1091
  } else if (arg.startsWith("--watch")) {
1092
- return Object.assign(result, { watch: parseBoolean(arg.split("=")[1]) });
1092
+ return Object.assign(result2, { watch: parseBoolean(arg.split("=")[1]) });
1093
1093
  } else if (arg === "-o" || arg.startsWith("-o=") || arg.startsWith("--open")) {
1094
1094
  const value = arg.split("=")[1];
1095
1095
  const open = value === void 0 || value === "true" ? true : value === "false" ? false : value;
1096
- return Object.assign(result, { open });
1096
+ return Object.assign(result2, { open });
1097
1097
  } else if (arg.startsWith("--failfast") || arg.startsWith("--failFast")) {
1098
- return Object.assign(result, { failFast: parseBoolean(arg.split("=")[1]) });
1098
+ return Object.assign(result2, { failFast: parseBoolean(arg.split("=")[1]) });
1099
1099
  } else if (arg.startsWith("--timeout")) {
1100
- return Object.assign(result, { timeout: Number(arg.split("=")[1]) || FALLBACK_TIMEOUT_MS });
1100
+ return Object.assign(result2, { timeout: Number(arg.split("=")[1]) || FALLBACK_TIMEOUT_MS });
1101
1101
  } else if (arg.startsWith("--output")) {
1102
- return Object.assign(result, { output: arg.split("=")[1] });
1102
+ return Object.assign(result2, { output: arg.split("=")[1] });
1103
1103
  } else if (arg.endsWith(".html")) {
1104
- if (result.htmlPaths) {
1105
- result.htmlPaths.push(arg);
1104
+ if (result2.htmlPaths) {
1105
+ result2.htmlPaths.push(arg);
1106
1106
  } else {
1107
- result.htmlPaths = [arg];
1107
+ result2.htmlPaths = [arg];
1108
1108
  }
1109
- return result;
1109
+ return result2;
1110
1110
  } else if (arg.startsWith("--port")) {
1111
- return Object.assign(result, { port: Number(arg.split("=")[1]), portExplicit: true });
1111
+ return Object.assign(result2, { port: Number(arg.split("=")[1]), portExplicit: true });
1112
1112
  } else if (arg.startsWith("--extensions")) {
1113
- return Object.assign(result, {
1113
+ return Object.assign(result2, {
1114
1114
  extensions: arg.split("=")[1].split(",").map((extension) => extension.trim())
1115
1115
  });
1116
1116
  } else if (arg.startsWith("--browser")) {
@@ -1121,22 +1121,22 @@ function parseCliFlags(projectRoot) {
1121
1121
  );
1122
1122
  process.exit(1);
1123
1123
  }
1124
- return Object.assign(result, { browser: value });
1124
+ return Object.assign(result2, { browser: value });
1125
1125
  } else if (arg.startsWith("--before")) {
1126
- return Object.assign(result, { before: parseModule(arg.split("=")[1]) });
1126
+ return Object.assign(result2, { before: parseModule(arg.split("=")[1]) });
1127
1127
  } else if (arg.startsWith("--after")) {
1128
- return Object.assign(result, { after: parseModule(arg.split("=")[1]) });
1128
+ return Object.assign(result2, { after: parseModule(arg.split("=")[1]) });
1129
1129
  } else if (arg === "--trace-perf") {
1130
- return result;
1130
+ return result2;
1131
1131
  }
1132
1132
  if (arg.startsWith("-")) {
1133
1133
  console.warn(`# Warning: Unknown flag "${arg}" \u2014 ignored`);
1134
- return result;
1134
+ return result2;
1135
1135
  }
1136
- result.inputs.add(
1136
+ result2.inputs.add(
1137
1137
  arg.startsWith(projectRoot) || arg.startsWith("/") ? arg : path5.join(process.cwd(), arg)
1138
1138
  );
1139
- return result;
1139
+ return result2;
1140
1140
  },
1141
1141
  { inputs: /* @__PURE__ */ new Set([]) }
1142
1142
  );
@@ -1152,10 +1152,10 @@ function parseCliFlags(projectRoot) {
1152
1152
  }
1153
1153
  return { ...providedFlags, inputs: Array.from(providedFlags.inputs) };
1154
1154
  }
1155
- function parseBoolean(result, defaultValue = true) {
1156
- if (result === "true") {
1155
+ function parseBoolean(result2, defaultValue = true) {
1156
+ if (result2 === "true") {
1157
1157
  return true;
1158
- } else if (result === "false") {
1158
+ } else if (result2 === "false") {
1159
1159
  return false;
1160
1160
  }
1161
1161
  return defaultValue;
@@ -1375,7 +1375,7 @@ var init_indent_string = __esm({
1375
1375
 
1376
1376
  // lib/utils/source-map-decoder.ts
1377
1377
  function decodeMappings(mappings) {
1378
- const result = [];
1378
+ const result2 = [];
1379
1379
  const cursor = { position: 0 };
1380
1380
  const mappingsLength = mappings.length;
1381
1381
  let segments = [];
@@ -1395,14 +1395,14 @@ function decodeMappings(mappings) {
1395
1395
  } else if (charCode === COMMA) {
1396
1396
  cursor.position++;
1397
1397
  } else {
1398
- result.push(segments);
1398
+ result2.push(segments);
1399
1399
  segments = [];
1400
1400
  generatedCol = 0;
1401
1401
  cursor.position++;
1402
1402
  }
1403
1403
  }
1404
- result.push(segments);
1405
- return result;
1404
+ result2.push(segments);
1405
+ return result2;
1406
1406
  }
1407
1407
  function parseSourceMap(json, outDir) {
1408
1408
  const map = JSON.parse(json);
@@ -2517,9 +2517,9 @@ function registerSharedStaticHandler(server, groupConfigs) {
2517
2517
  function replaceAssetPaths(html, htmlPath, projectRoot) {
2518
2518
  const assetPaths = findInternalAssetsFromHTML(html);
2519
2519
  const htmlDirectory = htmlPath.split("/").slice(0, -1).join("/");
2520
- return assetPaths.reduce((result, assetPath) => {
2520
+ return assetPaths.reduce((result2, assetPath) => {
2521
2521
  const normalizedFullAbsolutePath = path6.normalize(`${htmlDirectory}/${assetPath}`);
2522
- return result.replace(assetPath, normalizedFullAbsolutePath.replace(projectRoot, "."));
2522
+ return result2.replace(assetPath, normalizedFullAbsolutePath.replace(projectRoot, "."));
2523
2523
  }, html);
2524
2524
  }
2525
2525
  function testRuntimeToInject(config, groupId) {
@@ -3197,19 +3197,19 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
3197
3197
  jsx: "automatic",
3198
3198
  footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
3199
3199
  };
3200
- const hasSmallOutput = (result) => (result.outputFiles ?? []).some(
3200
+ const hasSmallOutput = (result2) => (result2.outputFiles ?? []).some(
3201
3201
  (outputFile) => GROUP_OUTPUT_REGEX.test(outputFile.path) && !outputFile.path.endsWith(".map") && outputFile.contents.length < EMPTY_BUNDLE_THRESHOLD
3202
3202
  );
3203
3203
  const buildWithRetry = async (retriesLeft) => {
3204
- const result = await esbuild.build(buildOptions);
3205
- if (!hasSmallOutput(result) || retriesLeft === 0) return result;
3204
+ const result2 = await esbuild.build(buildOptions);
3205
+ if (!hasSmallOutput(result2) || retriesLeft === 0) return result2;
3206
3206
  await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
3207
3207
  return buildWithRetry(retriesLeft - 1);
3208
3208
  };
3209
3209
  try {
3210
- const result = await buildWithRetry(MAX_RETRIES);
3210
+ const result2 = await buildWithRetry(MAX_RETRIES);
3211
3211
  await Promise.all(
3212
- (result.outputFiles ?? []).map((outputFile) => {
3212
+ (result2.outputFiles ?? []).map((outputFile) => {
3213
3213
  const match = GROUP_OUTPUT_REGEX.exec(outputFile.path);
3214
3214
  if (!match) return Promise.resolve();
3215
3215
  const slotIndex = parseInt(match[1]);
@@ -3271,12 +3271,12 @@ function buildFilteredTests(filteredTests, outputPath, config) {
3271
3271
  );
3272
3272
  }
3273
3273
  async function runWithOverlayfsRetry(getContents, needsDisk) {
3274
- let { result, js } = await getContents();
3274
+ let { result: result2, js } = await getContents();
3275
3275
  const initialSize = js.length;
3276
3276
  for (let retry = 1; retry <= MAX_RETRIES; retry++) {
3277
3277
  if (js.length >= EMPTY_BUNDLE_THRESHOLD) break;
3278
3278
  await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
3279
- ({ result, js } = await getContents());
3279
+ ({ result: result2, js } = await getContents());
3280
3280
  }
3281
3281
  if (js.length < EMPTY_BUNDLE_THRESHOLD && js.length !== initialSize) {
3282
3282
  console.log(
@@ -3285,7 +3285,7 @@ async function runWithOverlayfsRetry(getContents, needsDisk) {
3285
3285
  }
3286
3286
  if (needsDisk) {
3287
3287
  await Promise.all(
3288
- result.outputFiles.map((outputFile) => fs10.writeFile(outputFile.path, outputFile.contents))
3288
+ result2.outputFiles.map((outputFile) => fs10.writeFile(outputFile.path, outputFile.contents))
3289
3289
  );
3290
3290
  }
3291
3291
  return js;
@@ -3293,9 +3293,9 @@ async function runWithOverlayfsRetry(getContents, needsDisk) {
3293
3293
  function buildWithOverlayfsRetry(options, needsDisk) {
3294
3294
  const buildOpts = { ...options, write: false };
3295
3295
  return runWithOverlayfsRetry(async () => {
3296
- const result = await esbuild.build(buildOpts);
3297
- const jsFile = result.outputFiles.find((f) => !f.path.endsWith(".map"));
3298
- return { result, js: Buffer.from(jsFile.contents) };
3296
+ const result2 = await esbuild.build(buildOpts);
3297
+ const jsFile = result2.outputFiles.find((f) => !f.path.endsWith(".map"));
3298
+ return { result: result2, js: Buffer.from(jsFile.contents) };
3299
3299
  }, needsDisk);
3300
3300
  }
3301
3301
  async function buildIncrementally(options, fileKey, cache, needsDisk) {
@@ -3308,9 +3308,9 @@ async function buildIncrementally(options, fileKey, cache, needsDisk) {
3308
3308
  }
3309
3309
  const ctx = cache._esbuildContext;
3310
3310
  return runWithOverlayfsRetry(async () => {
3311
- const result = await ctx.rebuild();
3312
- const jsFile = result.outputFiles.find((f) => !f.path.endsWith(".map"));
3313
- return { result, js: Buffer.from(jsFile.contents) };
3311
+ const result2 = await ctx.rebuild();
3312
+ const jsFile = result2.outputFiles.find((f) => !f.path.endsWith(".map"));
3313
+ return { result: result2, js: Buffer.from(jsFile.contents) };
3314
3314
  }, needsDisk);
3315
3315
  }
3316
3316
  function esbuildTarget(browser) {
@@ -3697,12 +3697,12 @@ function handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFi
3697
3697
  }
3698
3698
  config._building = true;
3699
3699
  config._justAddedFiles = event === "add" ? /* @__PURE__ */ new Set([filePath]) : /* @__PURE__ */ new Set();
3700
- const result = onEventFunc(event, filePath);
3701
- if (!(result instanceof Promise)) {
3700
+ const result2 = onEventFunc(event, filePath);
3701
+ if (!(result2 instanceof Promise)) {
3702
3702
  config._building = false;
3703
3703
  return Promise.resolve();
3704
3704
  }
3705
- return result.then(() => onFinishFunc?.(filePath, event)).catch((error) => console.error("#", red("Build error:"), error.message || error)).finally(() => {
3705
+ return result2.then(() => onFinishFunc?.(filePath, event)).catch((error) => console.error("#", red("Build error:"), error.message || error)).finally(() => {
3706
3706
  config._building = false;
3707
3707
  config._lastBuildEndMs = Date.now();
3708
3708
  if (config._pendingBuildTrigger) {
@@ -4211,14 +4211,14 @@ async function buildCachedContent(config, htmlPaths) {
4211
4211
  config.htmlPaths.map((htmlPath) => fs14.readFile(htmlPath).catch(() => null))
4212
4212
  );
4213
4213
  const cachedContent = htmlPaths.reduce(
4214
- (result, _htmlPath, index) => {
4214
+ (result2, _htmlPath, index) => {
4215
4215
  const buffer = htmlBuffers[index];
4216
- if (buffer === null) return result;
4216
+ if (buffer === null) return result2;
4217
4217
  const filePath = config.htmlPaths[index];
4218
4218
  const html = buffer.toString();
4219
4219
  if (isCustomTemplate(html)) {
4220
- result.dynamicContentHTMLs[filePath] = html;
4221
- result.htmlPathsToRunTests.push(filePath.replace(config.projectRoot, ""));
4220
+ result2.dynamicContentHTMLs[filePath] = html;
4221
+ result2.htmlPathsToRunTests.push(filePath.replace(config.projectRoot, ""));
4222
4222
  } else {
4223
4223
  console.log(
4224
4224
  "#",
@@ -4226,12 +4226,12 @@ async function buildCachedContent(config, htmlPaths) {
4226
4226
  `WARNING: Static html file with no {{qunitxScript}} or handlebars-style tokens detected. Therefore ignoring ${filePath}`
4227
4227
  )
4228
4228
  );
4229
- result.staticHTMLs[filePath] = html;
4229
+ result2.staticHTMLs[filePath] = html;
4230
4230
  }
4231
4231
  findInternalAssetsFromHTML(html).forEach((key) => {
4232
- result.assets.add(normalizeInternalAssetPathFromHTML(config.projectRoot, key, filePath));
4232
+ result2.assets.add(normalizeInternalAssetPathFromHTML(config.projectRoot, key, filePath));
4233
4233
  });
4234
- return result;
4234
+ return result2;
4235
4235
  },
4236
4236
  {
4237
4237
  allTestCode: null,
@@ -4270,16 +4270,16 @@ async function readTimingCache(projectRoot) {
4270
4270
  }
4271
4271
  }
4272
4272
  function computeFileTimes(groups, weights, wallTimes) {
4273
- const result = /* @__PURE__ */ new Map();
4273
+ const result2 = /* @__PURE__ */ new Map();
4274
4274
  groups.forEach((group, i) => {
4275
4275
  const wallMs = wallTimes.get(i);
4276
4276
  if (wallMs === void 0) return;
4277
4277
  const total = group.reduce((sum, f) => sum + (weights.get(f) ?? 0), 0);
4278
4278
  group.forEach(
4279
- (f) => result.set(f, total > 0 ? wallMs * ((weights.get(f) ?? 0) / total) : wallMs / group.length)
4279
+ (f) => result2.set(f, total > 0 ? wallMs * ((weights.get(f) ?? 0) / total) : wallMs / group.length)
4280
4280
  );
4281
4281
  });
4282
- return result;
4282
+ return result2;
4283
4283
  }
4284
4284
  async function persistTimings(fileTimes, projectRoot) {
4285
4285
  await fs14.writeFile(
@@ -4681,7 +4681,11 @@ __export(daemon_exports, {
4681
4681
  import { spawn as spawn3 } from "node:child_process";
4682
4682
  import fs16, { existsSync as existsSync3 } from "node:fs";
4683
4683
  import path13 from "node:path";
4684
- import { fileURLToPath as fileURLToPath2 } from "node:url";
4684
+ async function buildDaemonSpawn() {
4685
+ const sea = await import("node:sea").catch(() => null);
4686
+ if (sea?.isSea()) return { bin: process.execPath, args: ["daemon", "_serve"] };
4687
+ return { bin: process.execPath, args: [process.argv[1], "daemon", "_serve"] };
4688
+ }
4685
4689
  function runDaemonCommand() {
4686
4690
  const sub = process.argv[3];
4687
4691
  if (sub === "_serve") return runServeMode();
@@ -4719,7 +4723,8 @@ function waitForFile(filePath, timeoutMs) {
4719
4723
  async function spawnAndWaitForDaemon() {
4720
4724
  const parsed = parseDaemonIdleTimeout(process.env.QUNITX_DAEMON_IDLE_TIMEOUT);
4721
4725
  if (parsed.warning) process.stderr.write(parsed.warning + "\n");
4722
- spawn3(process.execPath, [CLI_ENTRY, "daemon", "_serve"], {
4726
+ const { bin, args } = await buildDaemonSpawn();
4727
+ spawn3(bin, args, {
4723
4728
  detached: true,
4724
4729
  stdio: "ignore",
4725
4730
  env: { ...process.env, QUNITX_DAEMON_CWD: process.cwd() }
@@ -4739,9 +4744,9 @@ async function startDaemon() {
4739
4744
  `);
4740
4745
  return 0;
4741
4746
  }
4742
- const result = await spawnAndWaitForDaemon();
4743
- if (result) {
4744
- process.stdout.write(`Daemon started (pid ${result.pid})
4747
+ const result2 = await spawnAndWaitForDaemon();
4748
+ if (result2) {
4749
+ process.stdout.write(`Daemon started (pid ${result2.pid})
4745
4750
  `);
4746
4751
  return 0;
4747
4752
  }
@@ -4772,7 +4777,7 @@ async function statusDaemon() {
4772
4777
  );
4773
4778
  return 0;
4774
4779
  }
4775
- var SPAWN_TIMEOUT_MS, highlight2, color2, USAGE, __filename, CLI_ENTRY;
4780
+ var SPAWN_TIMEOUT_MS, highlight2, color2, USAGE;
4776
4781
  var init_daemon = __esm({
4777
4782
  "lib/commands/daemon/index.ts"() {
4778
4783
  init_color();
@@ -4798,11 +4803,16 @@ ${color2("QUNITX_DAEMON_LOG=<path>")} : redirect the daemon's stdout + stder
4798
4803
 
4799
4804
  ${highlight2("Tip:")} set ${color2("QUNITX_DAEMON=1")} to auto-spawn the daemon on the first qunitx run; ${color2("$ qunitx --help")} for top-level options.
4800
4805
  `;
4801
- __filename = fileURLToPath2(import.meta.url);
4802
- CLI_ENTRY = path13.resolve(path13.dirname(__filename), "..", "..", "..", "cli.ts");
4803
4806
  }
4804
4807
  });
4805
4808
 
4809
+ // lib/utils/enable-compile-cache.ts
4810
+ import module from "node:module";
4811
+ var result = module.enableCompileCache?.();
4812
+ if (result?.directory && !("NODE_COMPILE_CACHE" in process.env)) {
4813
+ process.env.NODE_COMPILE_CACHE = result.directory;
4814
+ }
4815
+
4806
4816
  // cli.ts
4807
4817
  init_chrome_prelaunch();
4808
4818
  init_package();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "qunitx-cli",
3
3
  "type": "module",
4
- "version": "0.23.2",
4
+ "version": "0.23.4",
5
5
  "description": "Browser runner for QUnitx: run your qunitx tests in google-chrome",
6
6
  "author": "Izel Nakri",
7
7
  "license": "MIT",