From 274defbf031ee71100a3e4acf7de4df96e2e6a37 Mon Sep 17 00:00:00 2001 From: ecrum19 Date: Thu, 4 Jun 2026 10:45:51 +0200 Subject: [PATCH 1/2] added file viewer, size display, and pod registration --- package-lock.json | 241 ++++++-- package.json | 3 + src/components/Guides/LandingGuide.vue | 26 +- src/components/Guides/PodBrowserGuide.vue | 6 + src/components/PodBrowser.vue | 331 ++++++++++- src/components/PodRegistration.vue | 147 +++-- src/components/PodResourceInspector.vue | 523 ++++++++++++++++++ src/composables/usePodResourceInspector.ts | 226 ++++++++ src/services/solid/fileUpload.ts | 30 + src/services/solid/resourceInspector.ts | 471 ++++++++++++++++ src/stores/containerSize.ts | 38 ++ tests/components/AllComponentsSmoke.test.ts | 1 + tests/components/PodBrowserFeatures.test.ts | 91 ++- tests/components/PodRegistration.test.ts | 188 +++++++ tests/components/PodResourceInspector.test.ts | 213 +++++++ tests/unit/containerSizeStore.test.ts | 15 + tests/unit/resourceInspector.test.ts | 141 +++++ 17 files changed, 2575 insertions(+), 116 deletions(-) create mode 100644 src/components/PodResourceInspector.vue create mode 100644 src/composables/usePodResourceInspector.ts create mode 100644 src/services/solid/resourceInspector.ts create mode 100644 src/stores/containerSize.ts create mode 100644 tests/components/PodRegistration.test.ts create mode 100644 tests/components/PodResourceInspector.test.ts create mode 100644 tests/unit/containerSizeStore.test.ts create mode 100644 tests/unit/resourceInspector.test.ts diff --git a/package-lock.json b/package-lock.json index e0850d9..84a25cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "solid-cockpit", - "version": "1.2.0", + "version": "1.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "solid-cockpit", - "version": "1.2.0", + "version": "1.2.1", "license": "MIT", "dependencies": { "@comunica/context-entries": "^5.2.0", @@ -25,7 +25,10 @@ "actor-query-process-remote-cache": "^0.1.0", "core-js": "^3.8.3", "fs": "^0.0.1-security", + "jsonld": "^9.0.0", "material-icons": "^1.13.14", + "n3": "^2.0.3", + "papaparse": "^5.5.3", "pinia": "^2.3.1", "query-sparql-remote-cache": "^0.0.9", "sparqljs": "^3.7.3", @@ -16881,6 +16884,18 @@ "jsonld-context-parse": "bin/jsonld-context-parse.js" } }, + "node_modules/@comunica/actor-rdf-parse-n3/node_modules/n3": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/n3/-/n3-1.26.0.tgz", + "integrity": "sha512-SQknS0ua90rN+3RHuk8BeIqeYyqIH/+ecViZxX08jR4j6MugqWRjtONl3uANG/crWXnOM2WIqBJtjIhVYFha+w==", + "dependencies": { + "buffer": "^6.0.3", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">=12.0" + } + }, "node_modules/@comunica/actor-rdf-parse-n3/node_modules/rdf-data-factory": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/rdf-data-factory/-/rdf-data-factory-2.0.2.tgz", @@ -17316,6 +17331,18 @@ "n3": "^1.17.0" } }, + "node_modules/@comunica/actor-rdf-serialize-n3/node_modules/n3": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/n3/-/n3-1.26.0.tgz", + "integrity": "sha512-SQknS0ua90rN+3RHuk8BeIqeYyqIH/+ecViZxX08jR4j6MugqWRjtONl3uANG/crWXnOM2WIqBJtjIhVYFha+w==", + "dependencies": { + "buffer": "^6.0.3", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">=12.0" + } + }, "node_modules/@comunica/actor-rdf-serialize-shaclc": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@comunica/actor-rdf-serialize-shaclc/-/actor-rdf-serialize-shaclc-4.2.0.tgz", @@ -24693,18 +24720,6 @@ "entities": "^4.5.0" } }, - "node_modules/@comunica/query-sparql-link-traversal-solid/node_modules/n3": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/n3/-/n3-2.0.3.tgz", - "integrity": "sha512-um/toGVENTarHBYIK2TdH6ByBhW75WpdKpv8iTYt9wF2QfBk8s8a16iaWZFUAAC1BKfGdb99kfgx6pltdDwfKA==", - "dependencies": { - "buffer": "^6.0.3", - "readable-stream": "^4.0.0" - }, - "engines": { - "node": ">=12.0" - } - }, "node_modules/@comunica/query-sparql-link-traversal-solid/node_modules/rdf-data-factory": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/rdf-data-factory/-/rdf-data-factory-2.0.2.tgz", @@ -29402,18 +29417,6 @@ "entities": "^4.5.0" } }, - "node_modules/@comunica/query-sparql-solid/node_modules/n3": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/n3/-/n3-2.0.3.tgz", - "integrity": "sha512-um/toGVENTarHBYIK2TdH6ByBhW75WpdKpv8iTYt9wF2QfBk8s8a16iaWZFUAAC1BKfGdb99kfgx6pltdDwfKA==", - "dependencies": { - "buffer": "^6.0.3", - "readable-stream": "^4.0.0" - }, - "engines": { - "node": ">=12.0" - } - }, "node_modules/@comunica/query-sparql-solid/node_modules/rdf-data-factory": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/rdf-data-factory/-/rdf-data-factory-2.0.2.tgz", @@ -33828,18 +33831,6 @@ "entities": "^4.5.0" } }, - "node_modules/@comunica/query-sparql/node_modules/n3": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/n3/-/n3-2.0.3.tgz", - "integrity": "sha512-um/toGVENTarHBYIK2TdH6ByBhW75WpdKpv8iTYt9wF2QfBk8s8a16iaWZFUAAC1BKfGdb99kfgx6pltdDwfKA==", - "dependencies": { - "buffer": "^6.0.3", - "readable-stream": "^4.0.0" - }, - "engines": { - "node": ">=12.0" - } - }, "node_modules/@comunica/query-sparql/node_modules/rdf-data-factory": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/rdf-data-factory/-/rdf-data-factory-2.0.2.tgz", @@ -34671,6 +34662,26 @@ "kuler": "^2.0.0" } }, + "node_modules/@digitalbazaar/http-client": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@digitalbazaar/http-client/-/http-client-4.3.0.tgz", + "integrity": "sha512-6lMpxpt9BOmqHKGs9Xm6DP4LlZTBFer/ZjHvP3FcW3IaUWYIWC7dw5RFZnvw4fP57kAVcm1dp3IF+Y50qhBvAw==", + "dependencies": { + "ky": "^1.14.2", + "undici": "^6.23.0" + }, + "engines": { + "node": ">=18.0" + } + }, + "node_modules/@digitalbazaar/http-client/node_modules/undici": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.26.0.tgz", + "integrity": "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==", + "engines": { + "node": ">=18.17" + } + }, "node_modules/@esbuild/darwin-arm64": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", @@ -34973,6 +34984,18 @@ "url": "https://github.com/sponsors/rubensworks/" } }, + "node_modules/@inrupt/solid-client/node_modules/n3": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/n3/-/n3-1.26.0.tgz", + "integrity": "sha512-SQknS0ua90rN+3RHuk8BeIqeYyqIH/+ecViZxX08jR4j6MugqWRjtONl3uANG/crWXnOM2WIqBJtjIhVYFha+w==", + "dependencies": { + "buffer": "^6.0.3", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">=12.0" + } + }, "node_modules/@inrupt/solid-client/node_modules/rdf-data-factory": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/rdf-data-factory/-/rdf-data-factory-2.0.2.tgz", @@ -35695,6 +35718,18 @@ "@triply/yasgui": "4.x" } }, + "node_modules/@triply/yasr/node_modules/n3": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/n3/-/n3-1.26.0.tgz", + "integrity": "sha512-SQknS0ua90rN+3RHuk8BeIqeYyqIH/+ecViZxX08jR4j6MugqWRjtONl3uANG/crWXnOM2WIqBJtjIhVYFha+w==", + "dependencies": { + "buffer": "^6.0.3", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">=12.0" + } + }, "node_modules/@tsconfig/node22": { "version": "22.0.5", "resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.5.tgz", @@ -38537,6 +38572,18 @@ "fetch-sparql-endpoint": "bin/fetch-sparql-endpoint.js" } }, + "node_modules/fetch-sparql-endpoint/node_modules/n3": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/n3/-/n3-1.26.0.tgz", + "integrity": "sha512-SQknS0ua90rN+3RHuk8BeIqeYyqIH/+ecViZxX08jR4j6MugqWRjtONl3uANG/crWXnOM2WIqBJtjIhVYFha+w==", + "dependencies": { + "buffer": "^6.0.3", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">=12.0" + } + }, "node_modules/fetch-sparql-endpoint/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -39705,6 +39752,20 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonld": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonld/-/jsonld-9.0.0.tgz", + "integrity": "sha512-pjMIdkXfC1T2wrX9B9i2uXhGdyCmgec3qgMht+TDj+S0qX3bjWMQUfL7NeqEhuRTi8G5ESzmL9uGlST7nzSEWg==", + "dependencies": { + "@digitalbazaar/http-client": "^4.2.0", + "canonicalize": "^2.1.0", + "lru-cache": "^6.0.0", + "rdf-canonize": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/jsonld-context-parser": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsonld-context-parser/-/jsonld-context-parser-3.1.0.tgz", @@ -39777,6 +39838,25 @@ "url": "https://github.com/sponsors/rubensworks/" } }, + "node_modules/jsonld/node_modules/canonicalize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/canonicalize/-/canonicalize-2.1.0.tgz", + "integrity": "sha512-F705O3xrsUtgt98j7leetNhTWPe+5S72rlL5O4jA1pKqBVQ/dT1O1D6PFxmSXvc0SUOinWS57DKx0I3CHrXJHQ==", + "bin": { + "canonicalize": "bin/canonicalize.js" + } + }, + "node_modules/jsonld/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -39818,6 +39898,17 @@ "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", "license": "MIT" }, + "node_modules/ky": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/ky/-/ky-1.14.3.tgz", + "integrity": "sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky?sponsor=1" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -40115,10 +40206,9 @@ "license": "MIT" }, "node_modules/n3": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/n3/-/n3-1.26.0.tgz", - "integrity": "sha512-SQknS0ua90rN+3RHuk8BeIqeYyqIH/+ecViZxX08jR4j6MugqWRjtONl3uANG/crWXnOM2WIqBJtjIhVYFha+w==", - "license": "MIT", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/n3/-/n3-2.0.3.tgz", + "integrity": "sha512-um/toGVENTarHBYIK2TdH6ByBhW75WpdKpv8iTYt9wF2QfBk8s8a16iaWZFUAAC1BKfGdb99kfgx6pltdDwfKA==", "dependencies": { "buffer": "^6.0.3", "readable-stream": "^4.0.0" @@ -40440,8 +40530,7 @@ "node_modules/papaparse": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", - "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==", - "license": "MIT" + "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==" }, "node_modules/parent-module": { "version": "1.0.1", @@ -41289,6 +41378,17 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/rdf-canonize": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/rdf-canonize/-/rdf-canonize-5.0.0.tgz", + "integrity": "sha512-g8OUrgMXAR9ys/ZuJVfBr05sPPoMA7nHIVs8VEvg9QwM5W4GR2qSFEEHjsyHF1eWlBaf8Ev40WNjQFQ+nJTO3w==", + "dependencies": { + "setimmediate": "^1.0.5" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/rdf-data-factory": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/rdf-data-factory/-/rdf-data-factory-1.1.3.tgz", @@ -41997,6 +42097,18 @@ "readable-stream": "^4.0.0" } }, + "node_modules/rdf-parse/node_modules/n3": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/n3/-/n3-1.26.0.tgz", + "integrity": "sha512-SQknS0ua90rN+3RHuk8BeIqeYyqIH/+ecViZxX08jR4j6MugqWRjtONl3uANG/crWXnOM2WIqBJtjIhVYFha+w==", + "dependencies": { + "buffer": "^6.0.3", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">=12.0" + } + }, "node_modules/rdf-parse/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -42103,6 +42215,18 @@ "readable-stream": "^4.3.0" } }, + "node_modules/rdf-streaming-store/node_modules/n3": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/n3/-/n3-1.26.0.tgz", + "integrity": "sha512-SQknS0ua90rN+3RHuk8BeIqeYyqIH/+ecViZxX08jR4j6MugqWRjtONl3uANG/crWXnOM2WIqBJtjIhVYFha+w==", + "dependencies": { + "buffer": "^6.0.3", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">=12.0" + } + }, "node_modules/rdf-streaming-store/node_modules/rdf-data-factory": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/rdf-data-factory/-/rdf-data-factory-2.0.2.tgz", @@ -42511,6 +42635,11 @@ "randombytes": "^2.1.0" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "node_modules/shaclc-parse": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/shaclc-parse/-/shaclc-parse-1.4.3.tgz", @@ -42521,6 +42650,18 @@ "n3": "^1.16.3" } }, + "node_modules/shaclc-parse/node_modules/n3": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/n3/-/n3-1.26.0.tgz", + "integrity": "sha512-SQknS0ua90rN+3RHuk8BeIqeYyqIH/+ecViZxX08jR4j6MugqWRjtONl3uANG/crWXnOM2WIqBJtjIhVYFha+w==", + "dependencies": { + "buffer": "^6.0.3", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">=12.0" + } + }, "node_modules/shaclc-write": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/shaclc-write/-/shaclc-write-1.6.3.tgz", @@ -42531,18 +42672,6 @@ "rdf-string-ttl": "^2.0.1" } }, - "node_modules/shaclc-write/node_modules/n3": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/n3/-/n3-2.0.3.tgz", - "integrity": "sha512-um/toGVENTarHBYIK2TdH6ByBhW75WpdKpv8iTYt9wF2QfBk8s8a16iaWZFUAAC1BKfGdb99kfgx6pltdDwfKA==", - "dependencies": { - "buffer": "^6.0.3", - "readable-stream": "^4.0.0" - }, - "engines": { - "node": ">=12.0" - } - }, "node_modules/shaclc-write/node_modules/rdf-data-factory": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/rdf-data-factory/-/rdf-data-factory-2.0.2.tgz", diff --git a/package.json b/package.json index 5d5216d..0567cac 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,10 @@ "actor-query-process-remote-cache": "^0.1.0", "core-js": "^3.8.3", "fs": "^0.0.1-security", + "jsonld": "^9.0.0", "material-icons": "^1.13.14", + "n3": "^2.0.3", + "papaparse": "^5.5.3", "pinia": "^2.3.1", "query-sparql-remote-cache": "^0.0.9", "sparqljs": "^3.7.3", diff --git a/src/components/Guides/LandingGuide.vue b/src/components/Guides/LandingGuide.vue index 605a3ec..039e45e 100644 --- a/src/components/Guides/LandingGuide.vue +++ b/src/components/Guides/LandingGuide.vue @@ -97,7 +97,8 @@
  • Home authenticates with a Solid identity provider, shows session state, lets you choose a registered - pod, and provides copy controls for current identifiers. + pod, provides copy controls for current identifiers, and lets you + manually register a pod URL when a provider did not add it to your WebID automatically.
  • Data Upload accepts a typed @@ -122,6 +123,29 @@ +
    +

    When you need to register a pod manually

    +
      +
    • + Some Solid providers create a new pod without updating the + storage link on an already existing WebID. +
    • +
    • + In that case, the new pod will not appear automatically in the pod selector on + Home. +
    • +
    • + Use Register new pod or + Add Pod URL, paste the new pod container URL, and + click Register Pod. +
    • +
    • + After registration, switch to that pod from the selector if you want to start working + in it immediately. +
    • +
    +
    + diff --git a/src/components/Guides/PodBrowserGuide.vue b/src/components/Guides/PodBrowserGuide.vue index 3500bc0..6f943dd 100644 --- a/src/components/Guides/PodBrowserGuide.vue +++ b/src/components/Guides/PodBrowserGuide.vue @@ -86,6 +86,12 @@
  • Containers include direct child-item counts.
  • +
  • + Container Direct size is calculated only + when you open that container’s details. It sums the files directly inside + the container, caches the result for the session, and recomputes after + browser actions like move, rename, delete, or create container. +
  • diff --git a/src/components/PodBrowser.vue b/src/components/PodBrowser.vue index 4b1911d..e437ada 100644 --- a/src/components/PodBrowser.vue +++ b/src/components/PodBrowser.vue @@ -18,6 +18,13 @@ +
    + {{ createContainerSuccessMessage }} + +
    +
    @@ -59,6 +66,10 @@

    +
    +
    +
    + Create empty container + + Add a new child container inside the currently selected container. + +
    +
    + + + Create Container + +
    +

    + {{ createContainerFeedback }} +

    +
    +
    @@ -126,7 +166,7 @@ containerCheck(url) ? "folder" : "description" }}
    - {{ url }} + {{ getItemName(url) }}
    @@ -204,17 +244,13 @@
    {{ - itemDetails.itemType === "Container" ? "folder_copy" : "save" + itemDetails.itemType === "Container" ? "save" : "save" }}
    {{ - itemDetails.itemType === "Container" ? "Direct items" : "File size" - }} - {{ - itemDetails.itemType === "Container" - ? (itemDetails.directChildren ?? "Not available") - : (itemDetails.sizeLabel || "Not available") + itemDetails.itemType === "Container" ? "Direct size" : "Size" }} + {{ itemDetails.sizeLabel || "Not available" }}
    @@ -269,6 +305,18 @@ {{ itemDetails.contentType || "Unknown" }} +
    + + folder_copy + Direct items + + {{ + itemDetails.directChildren ?? "Not available" + }} +
    link @@ -303,6 +351,13 @@ {{ downloadFeedback }}

    + +
    ', + }, + "v-icon": { + template: '', + }, + "v-select": { + props: ["modelValue", "items"], + emits: ["update:modelValue"], + template: ` + + `, + }, + "v-text-field": { + props: ["modelValue", "label", "placeholder"], + emits: ["update:modelValue"], + template: ` + + `, + }, + }; +} + +function mountPodRegistration(selectedPodUrl = "https://pod.example/") { + const pinia = createPinia(); + const authStore = useAuthStore(pinia); + authStore.setAuth(true, "https://user.example/profile/card#me"); + authStore.setSelectedPodUrl(selectedPodUrl); + + return mount(PodRegistration, { + global: { + plugins: [pinia], + stubs: makeVuetifyStubs(), + }, + }); +} + +describe("PodRegistration manual registration flow", () => { + beforeEach(() => { + vi.clearAllMocks(); + getPodURLsMock.mockResolvedValue(["https://pod.example/"]); + checkUrlMock.mockReturnValue(false); + getSolidDatasetMock.mockResolvedValue({}); + }); + + it("shows manual registration controls even when a pod is already selected", async () => { + const wrapper = mountPodRegistration(); + await flushPromises(); + + expect(wrapper.text()).toContain("Register new pod"); + + await wrapper + .findAll(".v-btn-stub") + .find((button) => button.text().includes("Register new pod")) + ?.trigger("click"); + await flushPromises(); + + expect(wrapper.text()).toContain("Register a pod URL"); + expect(wrapper.text()).toContain("your WebID already existed"); + }); + + it("registers a manually entered pod URL for users who already have a pod", async () => { + let callCount = 0; + getPodURLsMock.mockImplementation(async () => { + callCount += 1; + return callCount > 1 + ? ["https://pod.example/", "https://pod.example/new/"] + : ["https://pod.example/"]; + }); + + const wrapper = mountPodRegistration(); + await flushPromises(); + + await wrapper + .findAll(".v-btn-stub") + .find((button) => button.text().includes("Register new pod")) + ?.trigger("click"); + await flushPromises(); + + await wrapper.get(".v-text-field-stub").setValue("https://pod.example/new/"); + await wrapper.get("form").trigger("submit"); + await flushPromises(); + + expect(webIdDatasetMock).toHaveBeenCalledWith( + "https://user.example/profile/card#me", + "https://pod.example/new/" + ); + expect(wrapper.text()).toContain("Pod URL registered. Click Change Pod if you want to switch to it now."); + }); + + it("does not write a pod URL to the WebID when the target is not a readable Solid container", async () => { + getSolidDatasetMock.mockRejectedValueOnce(new Error("403 Forbidden")); + + const wrapper = mountPodRegistration(); + await flushPromises(); + + await wrapper + .findAll(".v-btn-stub") + .find((button) => button.text().includes("Register new pod")) + ?.trigger("click"); + await flushPromises(); + + await wrapper.get(".v-text-field-stub").setValue("https://pod.example/not-readable/"); + await wrapper.get("form").trigger("submit"); + await flushPromises(); + + expect(webIdDatasetMock).not.toHaveBeenCalled(); + expect(wrapper.text()).toContain("could not be confirmed as a readable Solid pod container"); + }); +}); + +describe("Landing guide pod registration note", () => { + it("documents the manual registration workflow for newly created pods", async () => { + const wrapper = mount(LandingGuide); + + await wrapper.get(".guide-toggle").trigger("click"); + await flushPromises(); + + expect(wrapper.text()).toContain("When you need to register a pod manually"); + expect(wrapper.text()).toContain("provider did not add it to your WebID automatically"); + expect(wrapper.text()).toContain("Register new pod"); + }); +}); diff --git a/tests/components/PodResourceInspector.test.ts b/tests/components/PodResourceInspector.test.ts new file mode 100644 index 0000000..134370d --- /dev/null +++ b/tests/components/PodResourceInspector.test.ts @@ -0,0 +1,213 @@ +import { mount } from "@vue/test-utils"; +import { nextTick } from "vue"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import PodResourceInspector from "../../src/components/PodResourceInspector.vue"; + +const { + fetchAclAgentsMock, + fetchPublicAccessMock, + fetchResourcePreviewMock, + fetchResourceFullTextMock, + validateResourceContentMock, + buildCsvPreviewTableMock, + analyzeResourceStructureMock, + saveResourceContentMock, +} = vi.hoisted(() => ({ + fetchAclAgentsMock: vi.fn(async () => ({ + "https://user.example/profile/card#me": { + read: true, + write: true, + append: true, + control: false, + }, + })), + fetchPublicAccessMock: vi.fn(async () => ({ + read: true, + append: false, + write: false, + control: false, + })), + fetchResourcePreviewMock: vi.fn(async () => ({ + text: '{\n "hello": "world"\n}', + formatInfo: { + format: "json", + label: "JSON", + contentType: "application/json", + editable: true, + supported: true, + }, + truncated: false, + byteLength: 22, + })), + fetchResourceFullTextMock: vi.fn(async () => ({ + text: '{\n "hello": "world"\n}', + formatInfo: { + format: "json", + label: "JSON", + contentType: "application/json", + editable: true, + supported: true, + }, + truncated: false, + byteLength: 22, + })), + validateResourceContentMock: vi.fn(async () => ({ + valid: true, + summary: "JSON syntax is valid.", + details: [], + })), + buildCsvPreviewTableMock: vi.fn(() => null), + analyzeResourceStructureMock: vi.fn(() => ({ + title: "JSON object count", + value: "1 object", + })), + saveResourceContentMock: vi.fn(async () => {}), +})); + +vi.mock("../../src/services/solid/getData.ts", () => ({ + fetchAclAgents: fetchAclAgentsMock, + fetchPublicAccess: fetchPublicAccessMock, +})); + +vi.mock("../../src/services/solid/resourceInspector.ts", () => ({ + fetchResourcePreview: fetchResourcePreviewMock, + fetchResourceFullText: fetchResourceFullTextMock, + validateResourceContent: validateResourceContentMock, + buildCsvPreviewTable: buildCsvPreviewTableMock, + analyzeResourceStructure: analyzeResourceStructureMock, + saveResourceContent: saveResourceContentMock, +})); + +const flushPromises = async () => { + await Promise.resolve(); + await Promise.resolve(); + await nextTick(); +}; + +type InspectorVm = { + permissionState: "unknown" | "confirmed" | "blocked"; +}; + +function mountInspector() { + return mount(PodResourceInspector, { + props: { + resourceUrl: "https://pod.example/data.json", + contentType: "application/json", + webId: "https://user.example/profile/card#me", + }, + global: { + stubs: { + "v-btn": { + props: ["disabled", "loading"], + emits: ["click"], + template: + '', + }, + }, + }, + }); +} + +describe("PodResourceInspector", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("loads the preview lazily when the panel is opened", async () => { + const wrapper = mountInspector(); + + expect(fetchResourcePreviewMock).not.toHaveBeenCalled(); + expect(wrapper.text()).not.toContain("JSON syntax is valid."); + + await wrapper.get(".inspector-toggle").trigger("click"); + await flushPromises(); + + expect(fetchResourcePreviewMock).toHaveBeenCalledOnce(); + expect(fetchAclAgentsMock).toHaveBeenCalledOnce(); + expect(wrapper.text()).toContain("JSON syntax is valid."); + expect(wrapper.text()).toContain("1 object"); + expect(wrapper.text()).toContain('"hello": "world"'); + }); + + it("collapses longer previews and lets the user expand them on demand", async () => { + fetchResourcePreviewMock.mockResolvedValueOnce({ + text: Array.from({ length: 24 }, (_, index) => `line ${index + 1}`).join("\n"), + formatInfo: { + format: "txt", + label: "Plain text", + contentType: "text/plain", + editable: true, + supported: true, + }, + truncated: false, + byteLength: 180, + }); + analyzeResourceStructureMock.mockReturnValueOnce({ + title: "Text dimensions", + value: "24 rows / 7 columns", + }); + + const wrapper = mountInspector(); + await wrapper.get(".inspector-toggle").trigger("click"); + await flushPromises(); + + expect(wrapper.find(".preview-expand-toggle").exists()).toBe(true); + expect(wrapper.find(".inspector-text-surface").classes()).toContain("collapsed"); + + await wrapper.get(".preview-expand-toggle").trigger("click"); + await flushPromises(); + + expect(wrapper.find(".inspector-text-surface").classes()).not.toContain("collapsed"); + expect(wrapper.text()).toContain("Show less"); + }); + + it("supports entering edit mode and saving validated changes", async () => { + const wrapper = mountInspector(); + + await wrapper.get(".inspector-toggle").trigger("click"); + await flushPromises(); + await wrapper.get(".v-btn-stub").trigger("click"); + await flushPromises(); + + const editor = wrapper.get(".inspector-editor"); + await editor.setValue('{\n "hello": "updated"\n}'); + await flushPromises(); + + const buttons = wrapper.findAll(".v-btn-stub"); + await buttons[0].trigger("click"); + await flushPromises(); + + expect(fetchResourceFullTextMock).toHaveBeenCalledOnce(); + expect(saveResourceContentMock).toHaveBeenCalledWith( + "https://pod.example/data.json", + '{\n "hello": "updated"\n}', + expect.objectContaining({ + format: "json", + }) + ); + expect(wrapper.text()).toContain("Saved changes to the pod resource."); + }); + + it("keeps editing unavailable when ACL data indicates read-only access", async () => { + fetchAclAgentsMock.mockResolvedValueOnce({ + "https://user.example/profile/card#me": { + read: true, + write: false, + append: false, + control: false, + }, + }); + fetchPublicAccessMock.mockResolvedValueOnce({ + read: true, + write: false, + append: false, + control: false, + }); + + const wrapper = mountInspector(); + await wrapper.get(".inspector-toggle").trigger("click"); + await flushPromises(); + + expect((wrapper.vm as unknown as InspectorVm).permissionState).toBe("blocked"); + }); +}); diff --git a/tests/unit/containerSizeStore.test.ts b/tests/unit/containerSizeStore.test.ts new file mode 100644 index 0000000..8890503 --- /dev/null +++ b/tests/unit/containerSizeStore.test.ts @@ -0,0 +1,15 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createPinia, setActivePinia } from "pinia"; +import { useContainerSizeStore } from "../../src/stores/containerSize.ts"; + +test("container size store returns cached values until invalidated", () => { + setActivePinia(createPinia()); + const store = useContainerSizeStore(); + + store.setDirectSize("https://pod.example/docs/", 2048); + assert.equal(store.getDirectSize("https://pod.example/docs/"), 2048); + + store.markStale("https://pod.example/docs/"); + assert.equal(store.getDirectSize("https://pod.example/docs/"), undefined); +}); diff --git a/tests/unit/resourceInspector.test.ts b/tests/unit/resourceInspector.test.ts new file mode 100644 index 0000000..a06831d --- /dev/null +++ b/tests/unit/resourceInspector.test.ts @@ -0,0 +1,141 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { + analyzeResourceStructure, + buildCsvPreviewTable, + detectResourceFormat, + fetchResourcePreview, + saveResourceContent, + validateResourceContent, +} from "../../src/services/solid/resourceInspector.ts"; + +test("detectResourceFormat resolves supported formats from extension and content type", () => { + const turtleInfo = detectResourceFormat( + "https://pod.example/data/report.ttl", + "text/turtle; charset=utf-8" + ); + assert.equal(turtleInfo.format, "ttl"); + assert.equal(turtleInfo.supported, true); + + const csvInfo = detectResourceFormat( + "https://pod.example/data/report.unknown", + "text/csv" + ); + assert.equal(csvInfo.format, "csv"); +}); + +test("fetchResourcePreview truncates large responses without requiring a full file load", async () => { + const fetchFn = async () => + new Response("abcdefghi", { + status: 200, + headers: { "content-type": "text/plain" }, + }); + + const preview = await fetchResourcePreview("https://pod.example/note.txt", { + fetchFn: fetchFn as typeof fetch, + maxBytes: 5, + }); + + assert.equal(preview.text, "abcde"); + assert.equal(preview.truncated, true); + assert.equal(preview.formatInfo.format, "txt"); +}); + +test("validateResourceContent reports JSON and RDF syntax errors", async () => { + const jsonValidation = await validateResourceContent( + detectResourceFormat("https://pod.example/data.json", "application/json"), + '{"broken": }' + ); + assert.equal(jsonValidation.valid, false); + + const rdfValidation = await validateResourceContent( + detectResourceFormat("https://pod.example/data.ttl", "text/turtle"), + '@prefix ex: . ex:s ex:p "missing-dot"' + ); + assert.equal(rdfValidation.valid, false); +}); + +test("buildCsvPreviewTable returns headers, rows, and parse warnings", () => { + const preview = buildCsvPreviewTable("name,age\nAlice,42\nBob"); + assert.ok(preview); + assert.deepEqual(preview?.headers, ["name", "age"]); + assert.equal(preview?.rows.length, 2); +}); + +test("analyzeResourceStructure summarizes csv, json, text, and rdf resources", () => { + assert.deepEqual( + analyzeResourceStructure( + detectResourceFormat("https://pod.example/data.csv", "text/csv"), + "name,age\nAlice,42\nBob,20" + ), + { + title: "CSV dimensions", + value: "3 rows / 2 columns", + } + ); + + assert.deepEqual( + analyzeResourceStructure( + detectResourceFormat("https://pod.example/data.txt", "text/plain"), + "alpha\nbeta" + ), + { + title: "Text dimensions", + value: "2 rows / 5 columns", + } + ); + + assert.deepEqual( + analyzeResourceStructure( + detectResourceFormat("https://pod.example/data.json", "application/json"), + '{"root":{"child":1},"list":[{"leaf":true}]}' + ), + { + title: "JSON object count", + value: "3 objects", + } + ); + + assert.deepEqual( + analyzeResourceStructure( + detectResourceFormat("https://pod.example/data.ttl", "text/turtle"), + '@prefix ex: . ex:s ex:p ex:o .' + ), + { + title: "RDF triple count", + value: "1 triple", + } + ); +}); + +test("saveResourceContent writes edited content with the inferred content type", async () => { + const calls: Array<{ + url: string; + contentType: string; + payload: string; + }> = []; + + await saveResourceContent( + "https://pod.example/data.json", + '{"ok":true}', + detectResourceFormat("https://pod.example/data.json", "application/json"), + (async () => new Response(null, { status: 200 })) as typeof fetch, + (async (url, data, options) => { + calls.push({ + url: String(url), + contentType: String(options?.contentType), + payload: await (data as Blob).text(), + }); + return { + internal_resourceInfo: { + sourceIri: String(url), + }, + } as never; + }) as never + ); + + assert.equal(calls.length, 1); + assert.equal(calls[0].url, "https://pod.example/data.json"); + assert.equal(calls[0].contentType, "application/json"); + assert.equal(calls[0].payload, '{"ok":true}'); +}); From 79662fc96b4b5bef6bb914b3c5f8e5735f5a1a4c Mon Sep 17 00:00:00 2001 From: ecrum19 Date: Thu, 4 Jun 2026 11:43:10 +0200 Subject: [PATCH 2/2] added version update script --- CITATION.bib | 2 +- CITATION.cff | 4 +- README.md | 21 +++-- package-lock.json | 4 +- package.json | 3 +- scripts/bump-version.mjs | 150 +++++++++++++++++++++++++++++++++ tests/unit/bumpVersion.test.ts | 55 ++++++++++++ vite.config.js | 2 +- vitest.config.ts | 2 +- 9 files changed, 224 insertions(+), 19 deletions(-) create mode 100644 scripts/bump-version.mjs create mode 100644 tests/unit/bumpVersion.test.ts diff --git a/CITATION.bib b/CITATION.bib index f6033e2..7eaf9ba 100644 --- a/CITATION.bib +++ b/CITATION.bib @@ -2,7 +2,7 @@ @misc{solidcockpit_2026 author = {Crum, Elias}, title = {{Solid Cockpit}}, year = {2026}, - version = {1.0.0}, + version = {1.3.0}, publisher = {GitHub}, howpublished = {\\url{https://github.com/KNowledgeOnWebScale/solid-cockpit}}, note = {Software. Web app: \\url{https://knowledgeonwebscale.github.io/solid-cockpit}.} diff --git a/CITATION.cff b/CITATION.cff index eda3b99..b52c533 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -2,8 +2,8 @@ cff-version: 1.2.0 message: "If you use Solid Cockpit in academic work, please cite it using the metadata below." title: "Solid Cockpit" type: software -version: "1.0.0" -date-released: 2026-03-04 +version: "1.3.0" +date-released: 2026-06-04 license: "MIT" authors: - family-names: "Crum" diff --git a/README.md b/README.md index 7418c9d..cd1173c 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ # Solid Cockpit -![Version](https://img.shields.io/badge/version-1.0.0-blue) -![Web App Tag](https://img.shields.io/badge/web--app--tag-web--app--v1.0.0-0a7ea4) +![Version](https://img.shields.io/badge/version-1.3.0-blue) +![Web App Tag](https://img.shields.io/badge/web--app--tag-v1.3.0-0a7ea4) ![Vue](https://img.shields.io/badge/vue-3.2.13-42b883) ![Vite](https://img.shields.io/badge/vite-6.2.3-646cff) ![License](https://img.shields.io/badge/license-MIT-green) @@ -75,7 +75,7 @@ Then upload `void.ttl` to the pod root using the app's `Data Upload` page. If you use this tool in an academic publication, you can cite: -`Crum, E. (2026). Solid Cockpit (Version 1.0.0) [Software]. GitHub. https://github.com/KNowledgeOnWebScale/solid-cockpit` +`Crum, E. (2026). Solid Cockpit (Version 1.3.0) [Software]. GitHub. https://github.com/KNowledgeOnWebScale/solid-cockpit` BibTeX: @@ -84,7 +84,7 @@ BibTeX: author = {Crum, Elias}, title = {{Solid Cockpit}}, year = {2026}, - version = {1.0.0}, + version = {1.3.0}, publisher = {GitHub}, howpublished = {\url{https://github.com/KNowledgeOnWebScale/solid-cockpit}}, note = {Software. Web app: \url{https://knowledgeonwebscale.github.io/solid-cockpit}. Accessed: 2026-03-04} @@ -265,13 +265,13 @@ CI compliance check: Current app version: -- `package.json` version: `1.0.0` -- web-app release tag convention: `web-app-v` -- current computed web-app tag: `web-app-v1.0.0` +- `package.json` version: `1.3.0` +- release tag convention: `v` +- current computed release tag: `v1.3.0` In-app visibility: -- Footer displays semantic version (`vX.Y.Z`) and computed release tag (`web-app-vX.Y.Z`) +- Footer displays semantic version (`vX.Y.Z`). - Values are injected at build time from `package.json` via Vite defines Recommended release workflow: @@ -279,7 +279,7 @@ Recommended release workflow: 1. Update version: ```bash -npm version X.Y.Z +npm run version:bump -- X.Y.Z ``` 2. Build and validate: @@ -293,8 +293,7 @@ npm run build:highmem ```bash git tag vX.Y.Z -git tag web-app-vX.Y.Z -git push origin vX.Y.Z web-app-vX.Y.Z +git push origin vX.Y.Z ``` ### Deployment diff --git a/package-lock.json b/package-lock.json index 84a25cb..557a2ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "solid-cockpit", - "version": "1.2.1", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "solid-cockpit", - "version": "1.2.1", + "version": "1.3.0", "license": "MIT", "dependencies": { "@comunica/context-entries": "^5.2.0", diff --git a/package.json b/package.json index 0567cac..bfd95dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "solid-cockpit", - "version": "1.2.1", + "version": "1.3.0", "description": "The Solid Cockpit application for Solid Pod utilization", "main": "~/src/App.vue", "homepage": "https://knowledgeonwebscale.github.io/solid-cockpit", @@ -15,6 +15,7 @@ }, "scripts": { "dev": "vite", + "version:bump": "node ./scripts/bump-version.mjs", "build": "vite build", "build:highmem": "NODE_OPTIONS=--max-old-space-size=8192 vite build", "serve": "vite preview", diff --git a/scripts/bump-version.mjs b/scripts/bump-version.mjs new file mode 100644 index 0000000..f14326c --- /dev/null +++ b/scripts/bump-version.mjs @@ -0,0 +1,150 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; + +const VERSION_RE = /^\d+\.\d+\.\d+$/; + +export function resolveNextVersion(currentVersion, requestedVersion) { + if (!requestedVersion) { + throw new Error("Provide a version like 1.2.3 or a bump keyword: patch, minor, major."); + } + + if (VERSION_RE.test(requestedVersion)) { + return requestedVersion; + } + + const parts = currentVersion.split(".").map((part) => Number(part)); + if (parts.length !== 3 || parts.some((part) => !Number.isInteger(part))) { + throw new Error(`Current package version is not valid semver: ${currentVersion}`); + } + + const [major, minor, patch] = parts; + switch (requestedVersion) { + case "patch": + return `${major}.${minor}.${patch + 1}`; + case "minor": + return `${major}.${minor + 1}.0`; + case "major": + return `${major + 1}.0.0`; + default: + throw new Error( + `Unsupported bump target "${requestedVersion}". Use major, minor, patch, or an explicit X.Y.Z version.` + ); + } +} + +export function updateReadmeVersionReferences(content, version) { + const releaseTag = `v${version}`; + return content + .replace( + /!\[Version\]\(https:\/\/img\.shields\.io\/badge\/version-[^)]+-blue\)/, + `![Version](https://img.shields.io/badge/version-${version}-blue)` + ) + .replace( + /!\[Web App Tag\]\(https:\/\/img\.shields\.io\/badge\/web--app--tag-[^)]+-0a7ea4\)/, + `![Web App Tag](https://img.shields.io/badge/web--app--tag-${releaseTag}-0a7ea4)` + ) + .replace( + /`Crum, E\. \(2026\)\. Solid Cockpit \(Version [^)]+\) \[Software\]\. GitHub\. https:\/\/github\.com\/KNowledgeOnWebScale\/solid-cockpit`/, + `\`Crum, E. (2026). Solid Cockpit (Version ${version}) [Software]. GitHub. https://github.com/KNowledgeOnWebScale/solid-cockpit\`` + ) + .replace(/version\s+=\s+\{[^}]+\},/, `version = {${version}},`) + .replace(/- `package\.json` version: `[^`]+`/, `- \`package.json\` version: \`${version}\``) + .replace(/- web-app release tag convention: `[^`]+`/, `- release tag convention: \`v\``) + .replace(/- current computed web-app tag: `[^`]+`/, `- current computed release tag: \`${releaseTag}\``) + .replace(/- Footer displays semantic version \(`vX\.Y\.Z`\) and computed release tag \(`[^`]+`\)/, "- Footer displays semantic version (`vX.Y.Z`).") + .replace(/npm version X\.Y\.Z/, "npm run version:bump -- X.Y.Z") + .replace(/git tag web-app-vX\.Y\.Z\ngit push origin vX\.Y\.Z web-app-vX\.Y\.Z/, "git push origin vX.Y.Z"); +} + +export function updateCitationCff(content, version, releaseDate) { + return content + .replace(/version:\s+"[^"]+"/, `version: "${version}"`) + .replace(/date-released:\s+\d{4}-\d{2}-\d{2}/, `date-released: ${releaseDate}`); +} + +export function updateCitationBib(content, version) { + return content.replace(/version\s+=\s+\{[^}]+\},/, `version = {${version}},`); +} + +function runVersionCommand(rootDir, nextVersion) { + const result = spawnSync( + "npm", + ["version", nextVersion, "--no-git-tag-version", "--allow-same-version"], + { + cwd: rootDir, + stdio: "inherit", + shell: process.platform === "win32", + } + ); + + if (result.status !== 0) { + throw new Error(`npm version failed with exit code ${result.status ?? "unknown"}`); + } +} + +function syncStaticVersionReferences(rootDir, version, releaseDate) { + const readmePath = resolve(rootDir, "README.md"); + const citationCffPath = resolve(rootDir, "CITATION.cff"); + const citationBibPath = resolve(rootDir, "CITATION.bib"); + + writeFileSync( + readmePath, + updateReadmeVersionReferences(readFileSync(readmePath, "utf8"), version), + "utf8" + ); + writeFileSync( + citationCffPath, + updateCitationCff(readFileSync(citationCffPath, "utf8"), version, releaseDate), + "utf8" + ); + writeFileSync( + citationBibPath, + updateCitationBib(readFileSync(citationBibPath, "utf8"), version), + "utf8" + ); +} + +export function runBumpVersion(rootDir, requestedVersion, releaseDate = new Date().toISOString().slice(0, 10)) { + const packageJsonPath = resolve(rootDir, "package.json"); + const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")); + const currentVersion = packageJson.version ?? "0.0.0"; + const nextVersion = resolveNextVersion(currentVersion, requestedVersion); + + runVersionCommand(rootDir, nextVersion); + syncStaticVersionReferences(rootDir, nextVersion, releaseDate); + + return { + currentVersion, + nextVersion, + releaseTag: `v${nextVersion}`, + releaseDate, + }; +} + +const isMainModule = + process.argv[1] && + fileURLToPath(import.meta.url) === resolve(process.argv[1]); + +if (isMainModule) { + const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + + try { + const { currentVersion, nextVersion, releaseTag, releaseDate } = runBumpVersion( + rootDir, + process.argv[2] + ); + + console.log( + [ + `Updated version ${currentVersion} -> ${nextVersion}`, + `Release tag: ${releaseTag}`, + `Release date: ${releaseDate}`, + ].join("\n") + ); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; + } +} diff --git a/tests/unit/bumpVersion.test.ts b/tests/unit/bumpVersion.test.ts new file mode 100644 index 0000000..21ef697 --- /dev/null +++ b/tests/unit/bumpVersion.test.ts @@ -0,0 +1,55 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + resolveNextVersion, + updateCitationBib, + updateCitationCff, + updateReadmeVersionReferences, +} from "../../scripts/bump-version.mjs"; + +test("resolveNextVersion supports semver bump keywords and explicit versions", () => { + assert.equal(resolveNextVersion("1.2.3", "patch"), "1.2.4"); + assert.equal(resolveNextVersion("1.2.3", "minor"), "1.3.0"); + assert.equal(resolveNextVersion("1.2.3", "major"), "2.0.0"); + assert.equal(resolveNextVersion("1.2.3", "4.5.6"), "4.5.6"); +}); + +test("updateReadmeVersionReferences updates badges, citation text, and release workflow text", () => { + const input = [ + "![Version](https://img.shields.io/badge/version-1.0.0-blue)", + "![Web App Tag](https://img.shields.io/badge/web--app--tag-web--app--v1.0.0-0a7ea4)", + "`Crum, E. (2026). Solid Cockpit (Version 1.0.0) [Software]. GitHub. https://github.com/KNowledgeOnWebScale/solid-cockpit`", + " version = {1.0.0},", + "- `package.json` version: `1.0.0`", + "- web-app release tag convention: `web-app-v`", + "- current computed web-app tag: `web-app-v1.0.0`", + "- Footer displays semantic version (`vX.Y.Z`) and computed release tag (`web-app-vX.Y.Z`)", + "npm version X.Y.Z", + "git tag web-app-vX.Y.Z\ngit push origin vX.Y.Z web-app-vX.Y.Z", + ].join("\n"); + + const updated = updateReadmeVersionReferences(input, "1.2.1"); + + assert.match(updated, /badge\/version-1\.2\.1-blue/); + assert.match(updated, /web--app--tag-v1\.2\.1-0a7ea4/); + assert.match(updated, /Version 1\.2\.1/); + assert.match(updated, /version\s+=\s+\{1\.2\.1\},/); + assert.match(updated, /`package\.json` version: `1\.2\.1`/); + assert.match(updated, /release tag convention: `v`/); + assert.match(updated, /`v1\.2\.1`/); + assert.match(updated, /Footer displays semantic version \(`vX\.Y\.Z`\)\./); + assert.match(updated, /npm run version:bump -- X\.Y\.Z/); + assert.match(updated, /git push origin vX\.Y\.Z/); +}); + +test("citation helpers update version and release date fields", () => { + assert.equal( + updateCitationCff('version: "1.0.0"\ndate-released: 2026-03-04\n', "1.2.1", "2026-06-04"), + 'version: "1.2.1"\ndate-released: 2026-06-04\n' + ); + + assert.equal( + updateCitationBib(" version = {1.0.0},\n", "1.2.1"), + " version = {1.2.1},\n" + ); +}); diff --git a/vite.config.js b/vite.config.js index 5aa7da1..d53e688 100644 --- a/vite.config.js +++ b/vite.config.js @@ -7,7 +7,7 @@ const packageJson = JSON.parse( readFileSync(new URL("./package.json", import.meta.url), "utf-8") ); const appVersion = packageJson.version ?? "0.0.0"; -const appReleaseTag = `web-app-v${appVersion}`; +const appReleaseTag = `v${appVersion}`; // https://vitejs.dev/config/ export default defineConfig(({ command }) => { diff --git a/vitest.config.ts b/vitest.config.ts index ea79f1b..fd2537f 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -7,7 +7,7 @@ const packageJson = JSON.parse( readFileSync(new URL("./package.json", import.meta.url), "utf-8") ); const appVersion = packageJson.version ?? "0.0.0"; -const appReleaseTag = `web-app-v${appVersion}`; +const appReleaseTag = `v${appVersion}`; export default defineConfig({ plugins: [vue()],