javascript-solid-server 0.0.135 → 0.0.137

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.
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "servejss",
3
+ "version": "0.0.7",
4
+ "description": "Static file server with REST write support - npx serve alternative",
5
+ "keywords": [
6
+ "serve",
7
+ "static",
8
+ "server",
9
+ "http",
10
+ "rest",
11
+ "webdav",
12
+ "file-server",
13
+ "development",
14
+ "solid"
15
+ ],
16
+ "type": "module",
17
+ "bin": {
18
+ "servejss": "./bin/jsserve.js"
19
+ },
20
+ "files": [
21
+ "bin",
22
+ "src"
23
+ ],
24
+ "scripts": {
25
+ "test": "node --test test/*.test.js"
26
+ },
27
+ "dependencies": {
28
+ "chalk": "^5.3.0",
29
+ "commander": "^12.1.0",
30
+ "javascript-solid-server": ">=0.0.136"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/JavaScriptSolidServer/jsserve.git"
35
+ },
36
+ "homepage": "https://github.com/JavaScriptSolidServer/jsserve#readme",
37
+ "bugs": {
38
+ "url": "https://github.com/JavaScriptSolidServer/jsserve/issues"
39
+ },
40
+ "author": "JavaScriptSolidServer Contributors",
41
+ "license": "MIT",
42
+ "engines": {
43
+ "node": ">=18"
44
+ }
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.135",
3
+ "version": "0.0.137",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -204,11 +204,18 @@ export function broadcast(url) {
204
204
  // Notify direct subscribers
205
205
  notifySubscribers(url);
206
206
 
207
- // Also notify container subscribers (parent directory)
208
- // This allows subscribing to a container and getting notified of all child changes
209
- const containerUrl = getParentContainer(url);
210
- if (containerUrl && containerUrl !== url) {
207
+ // Walk up all ancestor containers so subscribing to a root
208
+ // catches changes in nested paths (e.g. /db/mydata/ catches /db/mydata/issues/1)
209
+ // Stop at the origin root to avoid climbing past the hostname
210
+ let originRoot;
211
+ try { originRoot = new URL(url).origin + '/'; } catch (e) { return; }
212
+
213
+ let currentUrl = url;
214
+ let containerUrl = getParentContainer(currentUrl);
215
+ while (containerUrl && containerUrl !== currentUrl && containerUrl.length >= originRoot.length) {
211
216
  notifySubscribers(containerUrl);
217
+ currentUrl = containerUrl;
218
+ containerUrl = getParentContainer(currentUrl);
212
219
  }
213
220
  }
214
221
 
package/src/server.js CHANGED
@@ -182,6 +182,8 @@ export function createServer(options = {}) {
182
182
  fastify.decorateRequest('defaultQuota', null);
183
183
  fastify.decorateRequest('config', null);
184
184
  fastify.decorateRequest('liveReloadEnabled', null);
185
+ fastify.decorateRequest('singleUser', null);
186
+ fastify.decorateRequest('singleUserName', null);
185
187
  fastify.addHook('onRequest', async (request) => {
186
188
  request.connegEnabled = connegEnabled;
187
189
  request.notificationsEnabled = notificationsEnabled || liveReloadEnabled;
@@ -195,6 +197,8 @@ export function createServer(options = {}) {
195
197
  request.defaultQuota = defaultQuota;
196
198
  request.config = { public: options.public, readOnly: options.readOnly };
197
199
  request.liveReloadEnabled = liveReloadEnabled;
200
+ request.singleUser = singleUser;
201
+ request.singleUserName = singleUserName;
198
202
 
199
203
  // Extract pod name from subdomain if enabled
200
204
  if (subdomainsEnabled && baseDomain) {
package/src/utils/url.js CHANGED
@@ -150,22 +150,45 @@ export function getResourceName(urlPath) {
150
150
 
151
151
  /**
152
152
  * Extract pod name from URL path or request
153
+ *
154
+ * Resolves to one of four shapes, by deployment mode:
155
+ *
156
+ * - Subdomain mode with a recognized subdomain → `request.podName` (from hostname).
157
+ * - Subdomain mode with no recognized subdomain → `null` (base-domain access;
158
+ * callers guard with `if (podName)` and skip pod-scoped side effects).
159
+ * - Single-user, root-pod (`singleUserName` empty or '/') → `'.'` so
160
+ * `path.join(dataRoot, '.', QUOTA_FILE)` collapses to `<dataRoot>/QUOTA_FILE`.
161
+ * - Single-user, named pod → `singleUserName` (all requests share the one pod,
162
+ * independent of URL — avoids mistaking a URL segment like `index.html`
163
+ * for a pod name).
164
+ * - Path-based multi-pod (default, no flags) → first URL segment, or `null`
165
+ * for requests at `/` that aren't inside any pod.
166
+ *
167
+ * Background: before this function knew about single-user mode, a
168
+ * `PUT /index.html` on a single-user root-pod deployment produced a pod name
169
+ * of `"index.html"`, and the quota sidecar landed at
170
+ * `<dataRoot>/index.html/.quota.json` → `ENOTDIR` (index.html is a file).
171
+ *
153
172
  * @param {string|object} pathOrRequest - URL path string or Fastify request object
154
- * @returns {string|null} - Pod name or null if not found
173
+ * @returns {string|null} - Pod name, `'.'` for root-pod, or `null` when no pod applies
155
174
  */
156
175
  export function getPodName(pathOrRequest) {
157
- // If it's a request object
158
- if (typeof pathOrRequest === 'object') {
159
- // Subdomain mode: pod name from hostname
160
- if (pathOrRequest.subdomainsEnabled && pathOrRequest.podName) {
161
- return pathOrRequest.podName;
176
+ if (typeof pathOrRequest === 'object' && pathOrRequest !== null) {
177
+ // Subdomain mode: hostname drives it. Unrecognized host → no pod.
178
+ if (pathOrRequest.subdomainsEnabled) {
179
+ return pathOrRequest.podName || null;
162
180
  }
163
- // Path mode: extract from URL
181
+ // Single-user mode: always the one pod, regardless of URL path.
182
+ if (pathOrRequest.singleUser) {
183
+ const name = pathOrRequest.singleUserName;
184
+ return (!name || name === '/') ? '.' : name;
185
+ }
186
+ // Path-based multi-pod: first URL segment.
164
187
  const urlPath = pathOrRequest.url?.split('?')[0] || '';
165
188
  return getPodNameFromPath(urlPath);
166
189
  }
167
190
 
168
- // If it's a string path
191
+ // String form: path-based pod extraction.
169
192
  return getPodNameFromPath(pathOrRequest);
170
193
  }
171
194
 
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Unit tests for src/utils/url.js
3
+ *
4
+ * Focus: getPodName() resolution across the four supported deployment modes.
5
+ * Regression guard for #278 (single-user root-pod PUT → ENOTDIR).
6
+ */
7
+
8
+ import { describe, it } from 'node:test';
9
+ import assert from 'node:assert';
10
+ import { getPodName } from '../src/utils/url.js';
11
+
12
+ describe('getPodName', () => {
13
+ describe('subdomain mode', () => {
14
+ it('returns request.podName when the subdomain is recognized', () => {
15
+ const req = { subdomainsEnabled: true, podName: 'alice', url: '/profile/card' };
16
+ assert.strictEqual(getPodName(req), 'alice');
17
+ });
18
+
19
+ it('returns null on base-domain access (no recognized subdomain)', () => {
20
+ const req = { subdomainsEnabled: true, podName: null, url: '/anything' };
21
+ assert.strictEqual(getPodName(req), null);
22
+ });
23
+ });
24
+
25
+ describe('single-user mode', () => {
26
+ it("returns '.' for a root pod (singleUserName empty)", () => {
27
+ const req = { singleUser: true, singleUserName: '', url: '/index.html' };
28
+ assert.strictEqual(getPodName(req), '.');
29
+ });
30
+
31
+ it("returns '.' for a root pod (singleUserName '/')", () => {
32
+ const req = { singleUser: true, singleUserName: '/', url: '/index.html' };
33
+ assert.strictEqual(getPodName(req), '.');
34
+ });
35
+
36
+ it('returns singleUserName for a named pod, regardless of URL', () => {
37
+ const req = { singleUser: true, singleUserName: 'me', url: '/index.html' };
38
+ assert.strictEqual(getPodName(req), 'me');
39
+ });
40
+
41
+ it('does not mistake a URL segment for a pod in single-user mode', () => {
42
+ // Regression for #278: PUT /index.html previously produced pod
43
+ // "index.html", making the quota sidecar path <dataRoot>/index.html/.quota.json.
44
+ const req = { singleUser: true, singleUserName: '', url: '/index.html' };
45
+ assert.notStrictEqual(getPodName(req), 'index.html');
46
+ });
47
+ });
48
+
49
+ describe('path-based multi-pod (default)', () => {
50
+ it('returns the first URL segment as the pod name', () => {
51
+ const req = { url: '/alice/profile/card' };
52
+ assert.strictEqual(getPodName(req), 'alice');
53
+ });
54
+
55
+ it('returns null for requests at /', () => {
56
+ const req = { url: '/' };
57
+ assert.strictEqual(getPodName(req), null);
58
+ });
59
+
60
+ it('skips system paths beginning with a dot', () => {
61
+ const req = { url: '/.well-known/openid-configuration' };
62
+ assert.strictEqual(getPodName(req), null);
63
+ });
64
+ });
65
+
66
+ describe('string-form input', () => {
67
+ it('extracts pod name from a URL path string', () => {
68
+ assert.strictEqual(getPodName('/alice/foo'), 'alice');
69
+ });
70
+
71
+ it('returns null for the root path', () => {
72
+ assert.strictEqual(getPodName('/'), null);
73
+ });
74
+ });
75
+ });