@nordcraft/ssr 1.0.97 → 2.0.1

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.
@@ -8,7 +8,7 @@ import { CUSTOM_PROPERTIES_STYLESHEET_ID } from '@nordcraft/core/dist/styling/th
8
8
  import { easySort } from '@nordcraft/core/dist/utils/collections'
9
9
  import { VOID_HTML_ELEMENTS } from '@nordcraft/core/dist/utils/html'
10
10
  import { validateUrl } from '@nordcraft/core/dist/utils/url'
11
- import { isDefined } from '@nordcraft/core/dist/utils/util'
11
+ import { isDefined, toBoolean } from '@nordcraft/core/dist/utils/util'
12
12
  import { escapeAttrValue } from '../rendering/attributes'
13
13
  import type { ProjectFiles, ToddleProject } from '../ssr.types'
14
14
  import { isCloudflareImagePath } from '../utils/media'
@@ -277,44 +277,51 @@ export const getHeadItems = ({
277
277
  Object.entries(pageInfo.meta),
278
278
  // Sort by index if it exists
279
279
  ([_, meta]) => meta.index ?? Infinity,
280
- ).forEach(([id, metaEntry]) => {
281
- if (Object.values(HeadTagTypes).includes(metaEntry.tag)) {
282
- // If the tag has a name or property attribute, we use that as the key
283
- // to avoid duplicates and to ensure sorting of tags later
284
- const key = Object.entries(metaEntry.attrs ?? {}).find(
285
- ([key]) => key === 'name' || key === 'property',
286
- )
287
- const headItemKey: HeadItemType = `${metaEntry.tag}:${
288
- isDefined(key) ? applyFormula(key[1], context) : (id ?? nanoid())
289
- }`
290
- headItems.set(
291
- headItemKey,
292
- // Add the id to the tag so it's easier to dynamically update it later
293
- // from our runtime (main.ts)
294
- `<${metaEntry.tag} data-toddle-id="${id}" ${Object.entries(
295
- metaEntry.attrs ?? {},
280
+ )
281
+ .filter(
282
+ ([_, meta]) =>
283
+ // Only include enabled meta tags
284
+ !isDefined(meta.enabled) ||
285
+ toBoolean(applyFormula(meta.enabled, context)),
286
+ )
287
+ .forEach(([id, metaEntry]) => {
288
+ if (Object.values(HeadTagTypes).includes(metaEntry.tag)) {
289
+ // If the tag has a name or property attribute, we use that as the key
290
+ // to avoid duplicates and to ensure sorting of tags later
291
+ const key = Object.entries(metaEntry.attrs ?? {}).find(
292
+ ([key]) => key === 'name' || key === 'property',
296
293
  )
297
- .map(([key, formula]) => {
298
- const value = applyFormula(formula, context)
299
- if (value === true) {
300
- // If the value is true, we just return the key - this is useful
301
- // for tags like <script async> where async doesn't have a value
302
- return key
303
- }
304
- return `${key}="${escapeAttrValue(value)}"`
305
- })
306
- .join(' ')} ${
307
- VOID_HTML_ELEMENTS.includes(metaEntry.tag)
308
- ? `/>`
309
- : `>${
310
- metaEntry.content
311
- ? applyFormula(metaEntry.content, context)
312
- : ''
313
- }</${metaEntry.tag}>`
314
- }`,
315
- )
316
- }
317
- })
294
+ const headItemKey: HeadItemType = `${metaEntry.tag}:${
295
+ isDefined(key) ? applyFormula(key[1], context) : (id ?? nanoid())
296
+ }`
297
+ headItems.set(
298
+ headItemKey,
299
+ // Add the id to the tag so it's easier to dynamically update it later
300
+ // from our runtime (main.ts)
301
+ `<${metaEntry.tag} data-toddle-id="${id}" ${Object.entries(
302
+ metaEntry.attrs ?? {},
303
+ )
304
+ .map(([key, formula]) => {
305
+ const value = applyFormula(formula, context)
306
+ if (value === true) {
307
+ // If the value is true, we just return the key - this is useful
308
+ // for tags like <script async> where async doesn't have a value
309
+ return key
310
+ }
311
+ return `${key}="${escapeAttrValue(value)}"`
312
+ })
313
+ .join(' ')} ${
314
+ VOID_HTML_ELEMENTS.includes(metaEntry.tag)
315
+ ? `/>`
316
+ : `>${
317
+ metaEntry.content
318
+ ? applyFormula(metaEntry.content, context)
319
+ : ''
320
+ }</${metaEntry.tag}>`
321
+ }`,
322
+ )
323
+ }
324
+ })
318
325
  }
319
326
  return headItems
320
327
  }
@@ -241,7 +241,7 @@ describe('removeTestData', () => {
241
241
  inputs: {},
242
242
  },
243
243
  },
244
- }).apis['a'],
244
+ }).apis?.['a'],
245
245
  ).toEqual({
246
246
  name: 'foo',
247
247
  url: valueFormula('https://example.com'),
@@ -1,9 +1,13 @@
1
1
  import type { PageComponent } from '@nordcraft/core/dist/component/component.types'
2
2
  import { valueFormula } from '@nordcraft/core/dist/formula/formulaUtils'
3
3
  import { describe, expect, test } from 'bun:test'
4
- import { serverEnv } from '../rendering/formulaContext'
4
+ import { getServerToddleObject, serverEnv } from '../rendering/formulaContext'
5
5
  import type { Route } from '../ssr.types'
6
- import { matchPageForUrl, matchRouteForUrl } from './routing'
6
+ import {
7
+ getRouteDestination,
8
+ matchPageForUrl,
9
+ matchRouteForUrl,
10
+ } from './routing'
7
11
 
8
12
  describe('matchPageForUrl', () => {
9
13
  test('it finds the correct page for a url', () => {
@@ -206,7 +210,7 @@ describe('matchRouteForUrl', () => {
206
210
  getFormula: () => undefined,
207
211
  getCustomFormula: () => undefined,
208
212
  },
209
- }),
213
+ }) as any,
210
214
  ).toEqual({ name: 'docsRedirect', route: routes['docsRedirect'] })
211
215
  })
212
216
  test('it ignores disabled routes', () => {
@@ -269,7 +273,164 @@ describe('matchRouteForUrl', () => {
269
273
  getFormula: () => undefined,
270
274
  getCustomFormula: () => undefined,
271
275
  },
272
- }),
276
+ }) as any,
273
277
  ).toEqual({ name: 'docsRedirect', route: routes['docsRedirect'] })
274
278
  })
275
279
  })
280
+ describe('getRouteDestination', () => {
281
+ const getEnv = (req: Request) =>
282
+ serverEnv({
283
+ branchName: 'main',
284
+ req,
285
+ logErrors: false,
286
+ })
287
+
288
+ test('it returns the destination for a rewrite route', () => {
289
+ const route: Route = {
290
+ type: 'rewrite',
291
+ source: { path: [{ type: 'static', name: 'search' }], query: {} },
292
+ destination: {
293
+ url: valueFormula('results'),
294
+ },
295
+ }
296
+ const req = new Request('http://localhost:3000/search')
297
+ const url = getRouteDestination({
298
+ serverContext: getServerToddleObject({}),
299
+ req,
300
+ route,
301
+ env: getEnv(req),
302
+ })
303
+ expect(url?.toString()).toEqual('http://localhost:3000/results')
304
+ })
305
+
306
+ test('it returns the destination for a redirect route', () => {
307
+ const route: Route = {
308
+ type: 'redirect',
309
+ source: { path: [{ type: 'static', name: 'old' }], query: {} },
310
+ destination: {
311
+ url: valueFormula('new'),
312
+ },
313
+ }
314
+ const req = new Request('http://localhost:3000/old')
315
+ const url = getRouteDestination({
316
+ serverContext: getServerToddleObject({}),
317
+ req,
318
+ route,
319
+ env: getEnv(req),
320
+ })
321
+ expect(url?.toString()).toEqual('http://localhost:3000/new')
322
+ })
323
+
324
+ test('it returns undefined if a redirect points to the same URL', () => {
325
+ const route: Route = {
326
+ type: 'redirect',
327
+ source: { path: [{ type: 'static', name: 'same' }], query: {} },
328
+ destination: {
329
+ url: valueFormula('same'),
330
+ },
331
+ }
332
+ const req = new Request('http://localhost:3000/same')
333
+ const url = getRouteDestination({
334
+ serverContext: getServerToddleObject({}),
335
+ req,
336
+ route,
337
+ env: getEnv(req),
338
+ })
339
+ expect(url).toBeUndefined()
340
+ })
341
+
342
+ test('it allows a rewrite to point to the same URL', () => {
343
+ const route: Route = {
344
+ type: 'rewrite',
345
+ source: { path: [{ type: 'static', name: 'same' }], query: {} },
346
+ destination: {
347
+ url: valueFormula('same'),
348
+ },
349
+ }
350
+ const req = new Request('http://localhost:3000/same')
351
+ const url = getRouteDestination({
352
+ serverContext: getServerToddleObject({}),
353
+ req,
354
+ route,
355
+ env: getEnv(req),
356
+ })
357
+ expect(url?.toString()).toEqual('http://localhost:3000/same')
358
+ })
359
+
360
+ test('it handles dynamic parameters in the destination', () => {
361
+ const route: Route = {
362
+ type: 'rewrite',
363
+ source: {
364
+ path: [
365
+ { type: 'static', name: 'user' },
366
+ { type: 'param', name: 'id', testValue: '123' },
367
+ ],
368
+ query: {},
369
+ },
370
+ destination: {
371
+ path: {
372
+ profile: {
373
+ index: 0,
374
+ formula: valueFormula('profile'),
375
+ },
376
+ id: {
377
+ index: 1,
378
+ formula: {
379
+ path: ['Route parameters', 'path', 'id'],
380
+ type: 'path',
381
+ },
382
+ },
383
+ },
384
+ },
385
+ }
386
+ const req = new Request('http://localhost:3000/user/456')
387
+ const url = getRouteDestination({
388
+ serverContext: getServerToddleObject({}),
389
+ req,
390
+ route,
391
+ env: getEnv(req),
392
+ })
393
+ expect(url?.toString()).toEqual('http://localhost:3000/profile/456')
394
+ })
395
+
396
+ test('it handles relative URLs in the destination', () => {
397
+ const route: Route = {
398
+ type: 'redirect',
399
+ source: { path: [{ type: 'static', name: 'old' }], query: {} },
400
+ destination: {
401
+ url: valueFormula('/new-relative'),
402
+ },
403
+ }
404
+ const req = new Request('http://localhost:3000/old')
405
+ const url = getRouteDestination({
406
+ serverContext: getServerToddleObject({}),
407
+ req,
408
+ route,
409
+ env: getEnv(req),
410
+ })
411
+ expect(url?.toString()).toEqual('http://localhost:3000/new-relative')
412
+ })
413
+
414
+ test('it handles query parameters in the destination', () => {
415
+ const route: Route = {
416
+ type: 'rewrite',
417
+ source: { path: [{ type: 'static', name: 'search' }], query: {} },
418
+ destination: {
419
+ url: valueFormula('results'),
420
+ queryParams: {
421
+ q: {
422
+ formula: valueFormula('toddle'),
423
+ },
424
+ },
425
+ },
426
+ }
427
+ const req = new Request('http://localhost:3000/search')
428
+ const url = getRouteDestination({
429
+ serverContext: getServerToddleObject({}),
430
+ req,
431
+ route,
432
+ env: getEnv(req),
433
+ })
434
+ expect(url?.toString()).toEqual('http://localhost:3000/results?q=toddle')
435
+ })
436
+ })
@@ -127,12 +127,7 @@ export const getRouteDestination = ({
127
127
  serverContext,
128
128
  })
129
129
 
130
- const url = getUrl(
131
- route.destination,
132
- formulaContext,
133
- // Redirects can redirect to relative URLs - rewrites can't
134
- route.type === 'redirect' ? requestUrl.origin : undefined,
135
- )
130
+ const url = getUrl(route.destination, formulaContext, requestUrl.origin)
136
131
  if (
137
132
  route.type === 'redirect' &&
138
133
  requestUrl.origin === url.origin &&
@@ -141,11 +136,6 @@ export const getRouteDestination = ({
141
136
  // Redirects are not allowed to redirect to the same URL as their source
142
137
  return
143
138
  }
144
- if (route.type === 'rewrite' && requestUrl.origin === url.origin) {
145
- // Rewrites are not allowed from the same origin as the source
146
- // This prevents potential recursive fetch calls from the server to itself
147
- return
148
- }
149
139
  return url
150
140
  // eslint-disable-next-line no-empty
151
141
  } catch {}
@@ -67,7 +67,7 @@ describe('transformRelativePaths()', () => {
67
67
  },
68
68
  },
69
69
  },
70
- })
70
+ } as any)
71
71
  })
72
72
  test('it keeps absolute urls', () => {
73
73
  expect(
@@ -96,7 +96,7 @@ describe('transformRelativePaths()', () => {
96
96
  },
97
97
  },
98
98
  },
99
- })
99
+ } as any)
100
100
  })
101
101
  test('it does not transform non-src attributes', () => {
102
102
  expect(
@@ -125,6 +125,6 @@ describe('transformRelativePaths()', () => {
125
125
  },
126
126
  },
127
127
  },
128
- })
128
+ } as any)
129
129
  })
130
130
  })