javascript-solid-server 0.0.161 → 0.0.162
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/package.json +1 -1
- package/src/handlers/resource.js +50 -14
- package/test/data-island.test.js +84 -0
package/package.json
CHANGED
package/src/handlers/resource.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { emitChange } from '../notifications/events.js';
|
|
16
16
|
import { checkIfMatch, checkIfNoneMatchForGet, checkIfNoneMatchForWrite } from '../utils/conditional.js';
|
|
17
17
|
import { generateDatabrowserHtml, generateModuleDatabrowserHtml, shouldServeMashlib, DATA_ISLAND_MAX_BYTES } from '../mashlib/index.js';
|
|
18
|
+
import { turtleToJsonLd } from '../rdf/turtle.js';
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Live reload script - injected into HTML when --live-reload is enabled
|
|
@@ -330,23 +331,58 @@ export async function handleGet(request, reply) {
|
|
|
330
331
|
// Check if we should serve Mashlib data browser
|
|
331
332
|
// Only for RDF resources when Accept: text/html is requested
|
|
332
333
|
if (shouldServeMashlib(request, request.mashlibEnabled, storedContentType)) {
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
334
|
+
// #7 / #344: embed the resource as a JSON-LD data island so
|
|
335
|
+
// non-mashlib consumers (search-engine rich-results, archival
|
|
336
|
+
// crawlers) get the data without a second request, and so the
|
|
337
|
+
// shape is uniform regardless of the URL extension.
|
|
337
338
|
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
339
|
+
// JSS stores all RDF as JSON-LD on disk (PUT converts Turtle/N3
|
|
340
|
+
// before write — see the conneg branch in handlePut), so for
|
|
341
|
+
// `.ttl` / `.n3` URLs the bytes on disk are usually already
|
|
342
|
+
// JSON-LD. Try JSON parse first; only fall back to a Turtle parse
|
|
343
|
+
// when that fails (covers files placed on the filesystem
|
|
344
|
+
// out-of-band in their native format).
|
|
345
|
+
//
|
|
346
|
+
// Cap-aware short-circuit: skip the read entirely when the file
|
|
347
|
+
// is already over the embed cap. The island would be dropped
|
|
348
|
+
// anyway, and large RDF resources would otherwise load into
|
|
349
|
+
// memory on every HTML navigation. Other formats (rdf+xml, etc.)
|
|
350
|
+
// are not handled — the wrapper still loads and mashlib
|
|
351
|
+
// XHR-fetches them as before.
|
|
352
|
+
const islandConvertible =
|
|
353
|
+
storedContentType === RDF_TYPES.JSON_LD ||
|
|
354
|
+
storedContentType === RDF_TYPES.TURTLE ||
|
|
355
|
+
storedContentType === RDF_TYPES.N3;
|
|
342
356
|
let embedJsonLd;
|
|
343
|
-
if (
|
|
344
|
-
stats.size <= DATA_ISLAND_MAX_BYTES) {
|
|
345
|
-
// dataIsland() in mashlib/index.js coerces Buffer → string itself,
|
|
346
|
-
// so we hand it the Buffer directly instead of allocating a UTF-8
|
|
347
|
-
// string copy on every navigation.
|
|
357
|
+
if (islandConvertible && stats.size <= DATA_ISLAND_MAX_BYTES) {
|
|
348
358
|
const buf = await storage.read(storagePath);
|
|
349
|
-
if (buf)
|
|
359
|
+
if (buf) {
|
|
360
|
+
if (storedContentType === RDF_TYPES.JSON_LD) {
|
|
361
|
+
// Pass the Buffer through. dataIsland() decodes once when
|
|
362
|
+
// it needs to; we don't pre-validate or pre-decode here.
|
|
363
|
+
embedJsonLd = buf;
|
|
364
|
+
} else {
|
|
365
|
+
// Turtle / N3 URL. JSS stores everything as JSON-LD on
|
|
366
|
+
// disk (PUT converts), so try JSON parse first and pass
|
|
367
|
+
// the *decoded text* through (avoids a second decode
|
|
368
|
+
// inside dataIsland's String() coercion). Fall back to a
|
|
369
|
+
// Turtle parse for files placed on the filesystem
|
|
370
|
+
// out-of-band in their native format.
|
|
371
|
+
const text = buf.toString('utf8');
|
|
372
|
+
try {
|
|
373
|
+
JSON.parse(text);
|
|
374
|
+
embedJsonLd = text;
|
|
375
|
+
} catch {
|
|
376
|
+
try {
|
|
377
|
+
const jsonLd = await turtleToJsonLd(text, resourceUrl);
|
|
378
|
+
embedJsonLd = JSON.stringify(jsonLd);
|
|
379
|
+
} catch {
|
|
380
|
+
// Both parses failed → drop the island. The wrapper
|
|
381
|
+
// still renders and mashlib XHR-fetches the original.
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
350
386
|
}
|
|
351
387
|
const html = request.mashlibModule
|
|
352
388
|
? generateModuleDatabrowserHtml(request.mashlibModule, resourceUrl, { embedJsonLd })
|
package/test/data-island.test.js
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
import { describe, it, before, after } from 'node:test';
|
|
18
18
|
import assert from 'node:assert';
|
|
19
|
+
import fs from 'node:fs/promises';
|
|
20
|
+
import path from 'node:path';
|
|
19
21
|
import {
|
|
20
22
|
startTestServer,
|
|
21
23
|
stopTestServer,
|
|
@@ -207,3 +209,85 @@ describe('mashlib data island — integration (#7)', () => {
|
|
|
207
209
|
assert.doesNotMatch(body, /<!doctype html>/i);
|
|
208
210
|
});
|
|
209
211
|
});
|
|
212
|
+
|
|
213
|
+
// #344: data island also covers Turtle and N3 stored resources, by
|
|
214
|
+
// parsing them server-side and re-emitting the body as JSON-LD inside
|
|
215
|
+
// the script tag. Embedded shape is uniform across stored formats.
|
|
216
|
+
describe('mashlib data island — Turtle/N3 translation (#344)', () => {
|
|
217
|
+
before(async () => {
|
|
218
|
+
await startTestServer({ mashlibCdn: true, conneg: true });
|
|
219
|
+
await createTestPod('turtleisland');
|
|
220
|
+
await request('/turtleisland/public/note.ttl', {
|
|
221
|
+
method: 'PUT',
|
|
222
|
+
headers: { 'Content-Type': 'text/turtle' },
|
|
223
|
+
body: '@prefix foaf: <http://xmlns.com/foaf/0.1/> .\n' +
|
|
224
|
+
'<#note> foaf:name "turtle island" .\n',
|
|
225
|
+
auth: 'turtleisland'
|
|
226
|
+
});
|
|
227
|
+
await request('/turtleisland/public/note.n3', {
|
|
228
|
+
method: 'PUT',
|
|
229
|
+
headers: { 'Content-Type': 'text/n3' },
|
|
230
|
+
body: '@prefix foaf: <http://xmlns.com/foaf/0.1/> .\n' +
|
|
231
|
+
'<#note> foaf:name "n3 island" .\n',
|
|
232
|
+
auth: 'turtleisland'
|
|
233
|
+
});
|
|
234
|
+
// The both-parses-fail branch in the handler is unreachable via
|
|
235
|
+
// HTTP — handlePut validates Turtle/N3 input and rejects malformed
|
|
236
|
+
// bodies with 400 before they ever reach storage. To exercise the
|
|
237
|
+
// defensive guard we plant a file directly on disk in the test
|
|
238
|
+
// data dir, mimicking the "out-of-band placement" case the
|
|
239
|
+
// production code handles.
|
|
240
|
+
const brokenPath = path.resolve('./data/turtleisland/public/broken.ttl');
|
|
241
|
+
await fs.writeFile(
|
|
242
|
+
brokenPath,
|
|
243
|
+
'@prefix foaf: <http://xmlns.com/foaf/0.1/>\n' +
|
|
244
|
+
'<#note> foaf:name "broken — missing dot above" .\n'
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
after(async () => { await stopTestServer(); });
|
|
249
|
+
|
|
250
|
+
it('a browser GET to a Turtle resource embeds parsed JSON-LD', async () => {
|
|
251
|
+
const res = await request('/turtleisland/public/note.ttl', {
|
|
252
|
+
headers: { Accept: 'text/html,application/xhtml+xml,*/*;q=0.8' }
|
|
253
|
+
});
|
|
254
|
+
assertStatus(res, 200);
|
|
255
|
+
assertHeaderContains(res, 'Content-Type', 'text/html');
|
|
256
|
+
const body = await res.text();
|
|
257
|
+
assert.match(body, /id="dataisland"/);
|
|
258
|
+
assert.match(body, /<script type="application\/ld\+json"/);
|
|
259
|
+
// The Turtle name literal must round-trip into the embedded JSON-LD.
|
|
260
|
+
assert.match(body, /"turtle island"/);
|
|
261
|
+
// No raw Turtle prefix syntax should leak into the script body.
|
|
262
|
+
assert.doesNotMatch(body, /id="dataisland"[^>]*>[^<]*@prefix/);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('a browser GET to an N3 resource embeds parsed JSON-LD', async () => {
|
|
266
|
+
const res = await request('/turtleisland/public/note.n3', {
|
|
267
|
+
headers: { Accept: 'text/html,application/xhtml+xml,*/*;q=0.8' }
|
|
268
|
+
});
|
|
269
|
+
assertStatus(res, 200);
|
|
270
|
+
assertHeaderContains(res, 'Content-Type', 'text/html');
|
|
271
|
+
const body = await res.text();
|
|
272
|
+
assert.match(body, /id="dataisland"/);
|
|
273
|
+
assert.match(body, /"n3 island"/);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('out-of-band malformed Turtle drops the island, wrapper still renders', async () => {
|
|
277
|
+
// File was planted on disk directly (in `before`), bypassing the
|
|
278
|
+
// PUT validator. The handler's two-stage parse (JSON, then Turtle)
|
|
279
|
+
// both fail; the island is dropped silently and the mashlib
|
|
280
|
+
// wrapper is still served so the browser can XHR-fetch the
|
|
281
|
+
// resource and surface the parse problem to the user.
|
|
282
|
+
const res = await request('/turtleisland/public/broken.ttl', {
|
|
283
|
+
headers: { Accept: 'text/html,application/xhtml+xml,*/*;q=0.8' }
|
|
284
|
+
});
|
|
285
|
+
assertStatus(res, 200);
|
|
286
|
+
assertHeaderContains(res, 'Content-Type', 'text/html');
|
|
287
|
+
const body = await res.text();
|
|
288
|
+
assert.doesNotMatch(body, /id="dataisland"/,
|
|
289
|
+
'island must drop when both JSON and Turtle parses fail');
|
|
290
|
+
assert.match(body, /<!doctype html>/i);
|
|
291
|
+
assert.match(body, /mashlib/);
|
|
292
|
+
});
|
|
293
|
+
});
|