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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.161",
3
+ "version": "0.0.162",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -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
- // Phase 1 of #7: embed the resource's JSON-LD bytes as a data
334
- // island when it's already JSON-LD (the JSS-native format). Other
335
- // formats are out of Phase-1 scope; the wrapper still loads
336
- // correctly and mashlib XHR-fetches as before.
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
- // Cap-aware short-circuit: skip the read entirely when the file is
339
- // already over the embed cap. The island would be dropped anyway,
340
- // and large JSON-LD resources would otherwise load into memory on
341
- // every HTML navigation.
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 (storedContentType === 'application/ld+json' &&
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) embedJsonLd = 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 })
@@ -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
+ });