api-tuner 0.2.6 → 0.3.2

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/bin/index.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../index.js'
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../lib/merge-curl-output.js'
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env bash
2
+
3
+ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
4
+
5
+ # find JS entrypoint
6
+ script="$SCRIPT_DIR/merge-curl-output.js"
7
+
8
+ # if tsx exists in path
9
+ if command -v tsx > /dev/null 2>&1
10
+ then
11
+ # use tsx
12
+ node --import tsx --no-warnings "$script" "$@"
13
+ else
14
+ # use plain node
15
+ node "$script" "$@"
16
+ fi
package/bin/tuner.sh CHANGED
@@ -1,121 +1,21 @@
1
- #!/bin/bash
2
- SCRIPT_PATH=$(dirname "$(readlink -f "$0")")
1
+ #!/usr/bin/env bash
3
2
 
4
- eye="swipl -x ${SCRIPT_PATH}/../eye/lib/eye.pvm --"
3
+ PWD=$(pwd)
5
4
 
6
- # function prints version
7
- function version() {
8
- # read from ./package.json
9
- API_TUNER_VERSION=$(cat "${SCRIPT_PATH}"/../package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[",]//g' | tr -d '[[:space:]]')
10
- echo "API-TUNER v${API_TUNER_VERSION}"
11
- $eye --version
12
- }
5
+ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
6
+ cd "$SCRIPT_DIR" || exit
13
7
 
14
- function usage() {
15
- echo "Usage: api-tuner [options] <path>..."
16
- echo ""
17
- echo "Options:"
18
- echo " --lib <path> Specify rules to include in all tests. Can be used multiple times. Make sure to surround globs in quotes to prevent expansion."
19
- echo " --silent Less output"
20
- echo " --debug Enable debug output"
21
- echo " --raw Output raw results from eye"
22
- echo " --base-iri <iri> Specify the base IRI for parsing the test case files"
23
- echo " --version Show version information"
24
- echo " --help Show this help message"
25
- }
8
+ # find JS entrypoint
9
+ tuner=$(node -e "console.log(require.resolve('api-tuner/bin/index.js'))" 2> /dev/null)
26
10
 
27
- PARALLEL=true
28
- SILENT=false
29
- BASE_IRI=""
30
- DEBUG=false
31
- SUMMARY="node ${SCRIPT_PATH}/../lib/summarise-results.js --summary"
32
- PATHS=()
33
- LIBS=()
34
- # USAGE: ./tuner.sh --debug --version ...paths
35
- while [ $# -gt 0 ]; do
36
- case "$1" in
37
- --debug)
38
- DEBUG=true
39
- shift
40
- ;;
41
- --silent)
42
- SILENT=true
43
- shift
44
- ;;
45
- --raw)
46
- SUMMARY="node ${SCRIPT_PATH}/../lib/summarise-results.js"
47
- shift
48
- ;;
49
- --base-iri)
50
- BASE_IRI="$2"
51
- shift
52
- shift
53
- ;;
54
- --no-parallel)
55
- PARALLEL=false
56
- shift
57
- ;;
58
- --version)
59
- version
60
- exit 0
61
- ;;
62
- --help)
63
- usage
64
- exit 0
65
- ;;
66
- --lib)
67
- LIBS+=("$2")
68
- shift
69
- shift
70
- ;;
71
- *)
72
- PATHS+=("$1")
73
- shift
74
- ;;
75
- esac
76
- done
11
+ cd "$PWD" || exit
77
12
 
78
- # if no paths
79
- if [ ${#PATHS[@]} -eq 0 ]; then
80
- usage
81
- exit 1
82
- fi
83
-
84
- ARGS="--quiet --nope --pass"
85
-
86
- if [ "$DEBUG" = true ]; then
87
- ARGS="$ARGS ${SCRIPT_PATH}/../logging/debug.n3"
88
- fi
89
-
90
- if [ "$SILENT" != true ]; then
91
- ARGS="$ARGS ${SCRIPT_PATH}/../logging/info.n3"
92
- fi
93
-
94
- set -o pipefail
95
- process_path() {
96
- local path="$1"
97
- if [ "$SILENT" != true ] & [ $PARALLEL == false ]; then
98
- echo "" >&2
99
- echo "⚡️ RUNNING <file://$(realpath "$path")>" >&2
100
- fi
101
- node "${SCRIPT_PATH}/../lib/parse-test-case.js" --base-iri "$BASE_IRI" -- "${path}" \
102
- | $eye $ARGS "${SCRIPT_PATH}"/../rules/*.n3 ${LIBS[@]:+${LIBS[*]}} - \
103
- 2> >(while read -r line; do
104
- echo "$line" | sed -E 's/^"INFO" TRACE "(.*)"/ℹ️ \1/; s/^"DEBUG" TRACE "(.*)"/🐞 \1/' >&2
105
- done)
106
- }
107
-
108
-
109
- # if parallel
110
- if [ "$PARALLEL" = true ]; then
111
- # run in parallel
112
- for path in "${PATHS[@]}"; do
113
- process_path "$path" &
114
- done
115
- wait
13
+ # if tsx exists in path
14
+ if command -v tsx > /dev/null 2>&1
15
+ then
16
+ # use tsx
17
+ node --import tsx --no-warnings "$tuner" "$@"
116
18
  else
117
- # run sequentially
118
- for path in "${PATHS[@]}"; do
119
- process_path "$path"
120
- done
121
- fi | $SUMMARY
19
+ # use plain node
20
+ node "$tuner" "$@"
21
+ fi
package/index.js ADDED
@@ -0,0 +1,116 @@
1
+ import * as url from 'node:url';
2
+ import * as childProcess from 'node:child_process';
3
+ import { PassThrough } from 'node:stream';
4
+ import { resolve } from 'node:path';
5
+ import { program } from 'commander';
6
+ import getStream from 'get-stream';
7
+ import packageJson from './package.json' with { type: 'json' };
8
+ import parseTestCase from './lib/parse-test-case.js';
9
+ import summariseResults from './lib/summarise-results.js';
10
+ const eyePvmPath = url.fileURLToPath(new URL('eye/lib/eye.pvm', import.meta.url));
11
+ program
12
+ .name('api-tuner')
13
+ .option('--lib <lib>', 'Specify rules to include in all tests. Can be used multiple times. Make sure to surround globs in quotes to prevent expansion.', (lib, arr) => [...arr, lib], [])
14
+ .option('--silent', 'Less output', false)
15
+ .option('--debug', 'Enable debug output', false)
16
+ .option('--raw', 'Output raw results from eyes')
17
+ .requiredOption('--base-iri <baseIri>', 'Specify the base IRI for parsing the test case files')
18
+ .option('--version', 'Show version information')
19
+ .argument('[paths...]', 'Paths to test files')
20
+ .parse();
21
+ const options = program.opts();
22
+ if (options.version) {
23
+ process.stdout.write(`API-TUNER ${packageJson.version}\n`);
24
+ childProcess.execSync(`swipl -x ${eyePvmPath} -- --version`, { stdio: 'inherit' });
25
+ process.exit();
26
+ }
27
+ if (!program.args.length) {
28
+ program.help();
29
+ process.exit();
30
+ }
31
+ const eyeArgs = [
32
+ '--quiet',
33
+ '--nope',
34
+ '--pass',
35
+ ];
36
+ if (options.debug) {
37
+ const debugN3Path = url.fileURLToPath(new URL('logging/debug.n3', import.meta.url));
38
+ eyeArgs.push(debugN3Path);
39
+ }
40
+ if (!options.silent) {
41
+ const infoN3Path = url.fileURLToPath(new URL('logging/info.n3', import.meta.url));
42
+ eyeArgs.push(infoN3Path);
43
+ }
44
+ const rulesPath = url.fileURLToPath(new URL('rules/*.n3', import.meta.url));
45
+ const levelIcon = {
46
+ INFO: 'ℹ️',
47
+ DEBUG: '🐞',
48
+ 'Failed assertion': '❌ Failed assertion',
49
+ };
50
+ async function processPath(path) {
51
+ return new Promise(resolve => {
52
+ const testCaseStream = parseTestCase(path, options.baseIri);
53
+ const eyeProc = childProcess.spawn('swipl', [
54
+ '-x',
55
+ eyePvmPath,
56
+ '--',
57
+ ...eyeArgs,
58
+ rulesPath,
59
+ ...options.lib,
60
+ '-',
61
+ ], {
62
+ shell: true,
63
+ });
64
+ testCaseStream.pipe(eyeProc.stdin);
65
+ const stdout = new PassThrough();
66
+ const stderr = new PassThrough();
67
+ eyeProc.on('exit', (code) => {
68
+ resolve({
69
+ stdout,
70
+ stderr,
71
+ success: code === 0,
72
+ });
73
+ });
74
+ eyeProc.stdout.pipe(stdout);
75
+ eyeProc.stderr.pipe(stderr);
76
+ });
77
+ }
78
+ const testSuites = program.args.map(async (path) => {
79
+ const absolutePath = resolve(process.cwd(), path);
80
+ const result = await processPath(path);
81
+ const summaryPassThrough = new PassThrough();
82
+ const rawPassThrough = new PassThrough();
83
+ result.stdout.pipe(summaryPassThrough);
84
+ result.stdout.pipe(rawPassThrough);
85
+ const validationResult = await summariseResults(summaryPassThrough);
86
+ if (options.raw) {
87
+ const header = options.silent ? '' : `\n⚡️ SUITE <file://${absolutePath}>\n`;
88
+ process.stdout.write(header + await getStream(rawPassThrough));
89
+ }
90
+ if (!result.success) {
91
+ return {
92
+ summary: `\n🔎 SUITE <file://${absolutePath}>\n❌ FAIL Test script failed`,
93
+ success: false,
94
+ };
95
+ }
96
+ let summary = `\n🔎 SUITE <file://${absolutePath}>\n`;
97
+ if (!validationResult.success) {
98
+ const stderr = await getStream(result.stderr);
99
+ summary += stderr.replace(/"([^"]*)" TRACE ("([^"]*)")?/gm, (_, level, quoted, text) => {
100
+ return `${levelIcon[level]} ${text || ''}`;
101
+ });
102
+ }
103
+ summary += validationResult.summary;
104
+ return {
105
+ summary,
106
+ success: validationResult.success,
107
+ };
108
+ });
109
+ Promise.all(testSuites).then((results) => {
110
+ const summary = results.map(result => result.summary).join('\n');
111
+ if (!options.raw) {
112
+ process.stdout.write(summary + '\n');
113
+ }
114
+ // exit code equals number of failed tests
115
+ process.exit(results.filter(result => !result.success).length);
116
+ });
@@ -1,60 +1,54 @@
1
- import * as fs from 'node:fs/promises'
2
- import { createReadStream } from 'node:fs'
3
- import jsonld from 'jsonld'
4
- import rdf from '@zazuko/env-node'
5
- import { write } from '@jeswr/pretty-turtle'
6
-
1
+ import * as fs from 'node:fs/promises';
2
+ import { createReadStream } from 'node:fs';
3
+ // eslint-disable-next-line import/default
4
+ import jsonld from 'jsonld';
5
+ import rdf from '@zazuko/env-node';
6
+ import { write } from '@jeswr/pretty-turtle';
7
7
  const ns = rdf.namespace('https://api-tuner.described.at/');
8
-
9
8
  (async () => {
10
- const bodyPath = process.argv[2]
11
-
12
- const curlJsonPath = `${bodyPath}.curl.json`
13
- const { response: responseJson, headers: headersJson } = JSON.parse((await fs.readFile(curlJsonPath)).toString())
14
- /**
15
- * @type {Record<string, string | number | null>}
16
- */
17
- const curlJsonLd = Object.assign({
18
- '@context': {
19
- '@vocab': ns().value,
20
- },
21
- }, responseJson)
22
-
23
- /**
24
- * @type {import('@rdfjs/types').Quad[]}
25
- */
26
- const responseQuads = await jsonld.toRDF(curlJsonLd)
27
- const response = rdf.clownface({
28
- dataset: rdf.dataset(responseQuads),
29
- }).has(ns.exitcode).addOut(rdf.ns.rdf.type, ns.Response)
30
-
31
- let contentType
32
- if (typeof responseJson.content_type === 'string') {
33
- contentType = responseJson.content_type.substring(0, responseJson.content_type.indexOf(';')) || responseJson.content_type
34
- }
35
- let parser
36
- if (contentType) {
37
- parser = rdf.formats.parsers.get(contentType)
38
- }
39
- if (parser) {
40
- const bodyGraph = rdf.blankNode()
41
- const bodyStream = parser.import(createReadStream(bodyPath))
42
- for await (const quad of bodyStream) {
43
- response.dataset.add(rdf.quad(quad.subject, quad.predicate, quad.object, bodyGraph))
9
+ const bodyPath = process.argv[2];
10
+ const curlJsonPath = `${bodyPath}.curl.json`;
11
+ const { response: responseJson, headers: headersJson } = JSON.parse((await fs.readFile(curlJsonPath)).toString());
12
+ /**
13
+ * @type {Record<string, string | number | null>}
14
+ */
15
+ const curlJsonLd = Object.assign({
16
+ '@context': {
17
+ '@vocab': ns().value,
18
+ },
19
+ }, responseJson);
20
+ /**
21
+ * @type {import('@rdfjs/types').Quad[]}
22
+ */
23
+ const responseQuads = await jsonld.toRDF(curlJsonLd);
24
+ const response = rdf.clownface({
25
+ dataset: rdf.dataset(responseQuads),
26
+ }).has(ns.exitcode).addOut(rdf.ns.rdf.type, ns.Response);
27
+ let contentType;
28
+ if (typeof responseJson.content_type === 'string') {
29
+ contentType = responseJson.content_type.substring(0, responseJson.content_type.indexOf(';')) || responseJson.content_type;
44
30
  }
45
- response.addOut(ns.body, bodyGraph)
46
- } else {
47
- const body = await fs.readFile(bodyPath, 'utf-8')
48
- if (body) {
49
- response.addOut(ns.body, body)
31
+ let parser;
32
+ if (contentType) {
33
+ parser = rdf.formats.parsers.get(contentType);
50
34
  }
51
- }
52
-
53
- const headers = Object.entries(headersJson).flatMap(([header, values]) =>
54
- values.map(value => response.blankNode().addOut(ns.name, header).addOut(ns.value, value)))
55
- response.addOut(ns.headers, headers)
56
-
57
- process.stdout.write(await write([...response.dataset], {
58
- format: 'text/n3',
59
- }))
60
- })()
35
+ if (parser) {
36
+ const bodyGraph = rdf.blankNode();
37
+ const bodyStream = parser.import(createReadStream(bodyPath));
38
+ for await (const quad of bodyStream) {
39
+ response.dataset.add(rdf.quad(quad.subject, quad.predicate, quad.object, bodyGraph));
40
+ }
41
+ response.addOut(ns.body, bodyGraph);
42
+ }
43
+ else {
44
+ const body = await fs.readFile(bodyPath, 'utf-8');
45
+ if (body) {
46
+ response.addOut(ns.body, body);
47
+ }
48
+ }
49
+ const headers = Object.entries(headersJson).flatMap(([header, values]) => values.map(value => response.blankNode().addOut(ns('name'), header).addOut(ns.value, value)));
50
+ response.addOut(ns.headers, headers);
51
+ process.stdout.write(await write([...response.dataset], {
52
+ format: 'text/n3',
53
+ }));
54
+ })();
@@ -1,39 +1,20 @@
1
- import * as url from 'node:url'
2
- import { createReadStream } from 'node:fs'
3
- import yargs from 'yargs'
4
- import { hideBin } from 'yargs/helpers'
5
- import replaceStream from 'replacestream'
6
- import isAbsoluteUrl from 'is-absolute-url'
7
- import StreamConcat from 'stream-concat'
8
-
9
- const argv = yargs(hideBin(process.argv)).argv
10
-
11
- const baseIri = argv['base-iri'] || 'http://example.org/'
12
- const testCases = argv._
13
-
14
- function replacer(fileUrl) {
15
- return (_, match) => {
16
- if (match.startsWith('#')) {
17
- return `<${fileUrl}${match}>`
1
+ import * as url from 'node:url';
2
+ import { createReadStream } from 'node:fs';
3
+ import replaceStream from 'replacestream';
4
+ import isAbsoluteUrl from 'is-absolute-url';
5
+ export default function (testCase, baseIri) {
6
+ function replacer(fileUrl) {
7
+ return (_, match) => {
8
+ if (match.startsWith('#')) {
9
+ return `<${fileUrl}${match}>`;
10
+ }
11
+ if (isAbsoluteUrl(match)) {
12
+ return `<${match}>`;
13
+ }
14
+ return `<${baseIri}${match}>`;
15
+ };
18
16
  }
19
-
20
- if (isAbsoluteUrl(match)) {
21
- return `<${match}>`
22
- }
23
-
24
- return `<${baseIri}${match}>`
25
- }
26
- }
27
-
28
- let fileIndex = 0
29
- function nextStream() {
30
- if (fileIndex === testCases.length) {
31
- return null
32
- }
33
- const testCase = testCases[fileIndex++]
34
- const testCaseUrl = url.pathToFileURL(testCase).toString()
35
- return createReadStream(testCase)
36
- .pipe(replaceStream(/<([^>]*)>(?=([^"\\]*(\\.|"([^"\\]*\\.)*[^"\\]*"))*[^"]*$)/g, replacer(testCaseUrl)))
17
+ const testCaseUrl = url.pathToFileURL(testCase).toString();
18
+ return createReadStream(testCase)
19
+ .pipe(replaceStream(/<([^>]*)>(?=([^"\\]*(\\.|"([^"\\]*\\.)*[^"\\]*"))*[^"]*$)/g, replacer(testCaseUrl)));
37
20
  }
38
-
39
- new StreamConcat(nextStream).pipe(process.stdout)
@@ -1,65 +1,33 @@
1
- import { PassThrough } from 'node:stream'
2
- import SHACLValidator from 'rdf-validate-shacl'
3
- import rdf from '@zazuko/env-node'
4
- import yargs from 'yargs'
5
- import { hideBin } from 'yargs/helpers'
6
-
7
- const argv = yargs(hideBin(process.argv)).argv
8
-
9
- const shapesTtl = new URL('./shapes.ttl', import.meta.url)
10
-
11
- ;(async () => {
12
- const shapes = await rdf.dataset().import(rdf.fromFile(shapesTtl))
13
- const validator = new SHACLValidator(shapes, {
14
- factory: rdf,
15
- })
16
-
17
- const dataPassThrough = new PassThrough()
18
- process.stdin.pipe(dataPassThrough)
19
-
20
- if (!argv.summary) {
21
- process.stdin.pipe(process.stdout)
22
- }
23
-
24
- const data = await rdf.dataset().import(rdf.formats.parsers.import('text/n3', dataPassThrough, {
25
- format: 'n3',
26
- }))
27
-
28
- const validationReport = validator.validate(data)
29
-
30
- if (argv.summary) {
31
- const resultMap = rdf.clownface({ dataset: data })
32
- .has(rdf.ns.rdf.type, rdf.ns.earl.TestCase)
33
- .toArray()
34
- .reduce((map, testCase) => {
35
- const { pathname } = new URL(testCase)
36
-
37
- const testCases = map.get(pathname) || []
38
- testCases.push(testCase)
39
-
40
- return map.set(pathname, testCases)
41
- }, new Map())
42
-
43
- for (const [pathname, testCases] of resultMap.entries()) {
44
- const summary = ['']
45
- summary.push(`🔎 SUITE <file://${pathname}>`)
46
-
47
- for (const testCase of testCases) {
48
- const { hash } = new URL(testCase.value)
49
- const result = validationReport.results.find(result => result.focusNode.equals(testCase.term))
50
- const label = testCase.out(rdf.ns.rdfs.label).value
51
- const resultLine = label ? `${label} (<${hash}>)` : `<${hash}>`
52
-
53
- if (result?.severity.equals(rdf.ns.sh.Violation)) {
54
- summary.push(`❌ FAIL ${resultLine}`)
55
- } else {
56
- summary.push(`✅ PASS ${resultLine}`)
1
+ import SHACLValidator from 'rdf-validate-shacl';
2
+ import rdf from '@zazuko/env-node';
3
+ const shapesTtl = new URL('./shapes.ttl', import.meta.url);
4
+ export default async (resultStream) => {
5
+ const shapes = await rdf.dataset().import(rdf.fromFile(shapesTtl));
6
+ const validator = new SHACLValidator(shapes, {
7
+ factory: rdf,
8
+ });
9
+ const data = await rdf.dataset().import(rdf.formats.parsers.import('text/n3', resultStream, {
10
+ format: 'n3',
11
+ }));
12
+ const validationReport = validator.validate(data);
13
+ const testCases = rdf.clownface({ dataset: data })
14
+ .has(rdf.ns.rdf.type, rdf.ns.earl.TestCase)
15
+ .toArray();
16
+ const summary = [];
17
+ for (const testCase of testCases) {
18
+ const { hash } = new URL(testCase.value);
19
+ const result = validationReport.results.find(result => testCase.term.equals(result.focusNode));
20
+ const label = testCase.out(rdf.ns.rdfs.label).value;
21
+ const resultLine = label ? `${label} (<${hash}>)` : `<${hash}>`;
22
+ if (rdf.ns.sh.Violation.equals(result?.severity)) {
23
+ summary.push(`❌ FAIL ${resultLine}`);
24
+ }
25
+ else {
26
+ summary.push(`✅ PASS ${resultLine}`);
57
27
  }
58
- }
59
-
60
- process.stderr.write(summary.join('\n') + '\n')
61
28
  }
62
- }
63
-
64
- process.exit(validationReport.conforms ? 0 : 1)
65
- })()
29
+ return {
30
+ success: validationReport.conforms,
31
+ summary: summary.join('\n'),
32
+ };
33
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-tuner",
3
- "version": "0.2.6",
3
+ "version": "0.3.2",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -14,33 +14,45 @@
14
14
  "lint": "eslint . --quiet --ignore-path .gitignore",
15
15
  "pretest": "docker compose up -d",
16
16
  "test": "./bin/tuner.sh tests/*.n3 tests/**/*.n3 --base-iri http://localhost:1080/",
17
+ "prepack": "tsc",
17
18
  "release": "changeset publish"
18
19
  },
19
20
  "files": [
20
- "bin/tuner.sh",
21
+ "bin",
21
22
  "logging",
22
- "lib",
23
+ "lib/*.js",
24
+ "lib/*.txt",
25
+ "lib/*.ttl",
26
+ "lib/*.sh",
23
27
  "rules"
24
28
  ],
25
29
  "dependencies": {
26
30
  "@changesets/cli": "^2.27.12",
27
31
  "@jeswr/pretty-turtle": "^1.5.0",
28
32
  "@zazuko/env-node": "^2.1.4",
33
+ "commander": "^13.1.0",
34
+ "get-stream": "^9.0.1",
29
35
  "is-absolute-url": "^4.0.1",
30
36
  "jsonld": "^8.3.3",
31
37
  "rdf-validate-shacl": "^0.5.6",
32
- "replacestream": "^4.0.3",
33
- "stream-concat": "^2.0.0",
34
- "yargs": "^17.7.2"
38
+ "replacestream": "^4.0.3"
35
39
  },
36
40
  "devDependencies": {
37
41
  "@rdfjs/types": "^1",
38
42
  "@tpluscode/eslint-config": "^0.5.0",
39
43
  "@types/jsonld": "^1.5.15",
44
+ "@types/n3": "^1.24.2",
40
45
  "@types/rdf-validate-shacl": "^0.4.9",
46
+ "@types/replacestream": "^4.0.4",
41
47
  "@types/yargs": "^17.0.33",
48
+ "@typescript-eslint/eslint-plugin": "^7",
49
+ "@typescript-eslint/parser": "^7",
50
+ "eslint": "^8.57.1",
51
+ "eslint-import-resolver-typescript": "^4.3.4",
42
52
  "husky": "^9.1.7",
43
- "lint-staged": "^15.4.3"
53
+ "lint-staged": "^15.4.3",
54
+ "tsx": "^4.19.3",
55
+ "typescript": "^5.8.3"
44
56
  },
45
57
  "lint-staged": {
46
58
  "*.{js,ts}": [
package/rules/files.n3 CHANGED
@@ -13,9 +13,12 @@ prefix file: <http://www.w3.org/2000/10/swap/file#>
13
13
  ( ?suffix ) file:temp ?path .
14
14
  } <= {
15
15
  ?uri log:uri ( "urn:rand:" ( 1000 )!e:random )!string:concatenation .
16
+ # log:shell captures the traling newline
17
+ ( "mktemp -d"!log:shell "\n" "" ) string:replace ?tempDir .
16
18
 
17
19
  (
18
- #"/tmp/"
20
+ ?tempDir
21
+ "/"
19
22
  ?uri!log:uuid
20
23
  ?suffix
21
24
  ) string:concatenation ?path .
package/rules/requests.n3 CHANGED
@@ -70,7 +70,7 @@ prefix earl: <http://www.w3.org/ns/earl#>
70
70
  } .
71
71
 
72
72
  (
73
- "node " "lib/merge-curl-output.js"!file:libPath " "
73
+ "bin/merge-curl-output.sh"!file:libPath " "
74
74
  ?responseBodyFile
75
75
  " > " ?responseFile
76
76
  )!string:concatenation!e:exec .