@teleporthq/teleport-shared 0.43.61 → 0.43.63
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/__tests__/utils/font-preconnect.ts +62 -0
- package/__tests__/utils/rich-text-embeds-runtime-module.ts +124 -0
- package/__tests__/utils/rich-text-embeds.ts +254 -0
- package/dist/cjs/index.d.ts +6 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +11 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/page-revalidate/api-route.d.ts +24 -0
- package/dist/cjs/page-revalidate/api-route.d.ts.map +1 -0
- package/dist/cjs/page-revalidate/api-route.js +30 -0
- package/dist/cjs/page-revalidate/api-route.js.map +1 -0
- package/dist/cjs/page-revalidate/constants.d.ts +22 -0
- package/dist/cjs/page-revalidate/constants.d.ts.map +1 -0
- package/dist/cjs/page-revalidate/constants.js +25 -0
- package/dist/cjs/page-revalidate/constants.js.map +1 -0
- package/dist/cjs/page-revalidate/index.d.ts +3 -0
- package/dist/cjs/page-revalidate/index.d.ts.map +1 -0
- package/dist/cjs/page-revalidate/index.js +19 -0
- package/dist/cjs/page-revalidate/index.js.map +1 -0
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/utils/font-preconnect.d.ts +40 -0
- package/dist/cjs/utils/font-preconnect.d.ts.map +1 -0
- package/dist/cjs/utils/font-preconnect.js +52 -0
- package/dist/cjs/utils/font-preconnect.js.map +1 -0
- package/dist/cjs/utils/product-ratings.d.ts +26 -0
- package/dist/cjs/utils/product-ratings.d.ts.map +1 -0
- package/dist/cjs/utils/product-ratings.js +30 -0
- package/dist/cjs/utils/product-ratings.js.map +1 -0
- package/dist/cjs/utils/rich-text-embeds-codegen.d.ts +9 -0
- package/dist/cjs/utils/rich-text-embeds-codegen.d.ts.map +1 -0
- package/dist/cjs/utils/rich-text-embeds-codegen.js +139 -0
- package/dist/cjs/utils/rich-text-embeds-codegen.js.map +1 -0
- package/dist/cjs/utils/rich-text-embeds.d.ts +251 -0
- package/dist/cjs/utils/rich-text-embeds.d.ts.map +1 -0
- package/dist/cjs/utils/rich-text-embeds.js +852 -0
- package/dist/cjs/utils/rich-text-embeds.js.map +1 -0
- package/dist/esm/index.d.ts +6 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +6 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/page-revalidate/api-route.d.ts +24 -0
- package/dist/esm/page-revalidate/api-route.d.ts.map +1 -0
- package/dist/esm/page-revalidate/api-route.js +26 -0
- package/dist/esm/page-revalidate/api-route.js.map +1 -0
- package/dist/esm/page-revalidate/constants.d.ts +22 -0
- package/dist/esm/page-revalidate/constants.d.ts.map +1 -0
- package/dist/esm/page-revalidate/constants.js +22 -0
- package/dist/esm/page-revalidate/constants.js.map +1 -0
- package/dist/esm/page-revalidate/index.d.ts +3 -0
- package/dist/esm/page-revalidate/index.d.ts.map +1 -0
- package/dist/esm/page-revalidate/index.js +3 -0
- package/dist/esm/page-revalidate/index.js.map +1 -0
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/utils/font-preconnect.d.ts +40 -0
- package/dist/esm/utils/font-preconnect.d.ts.map +1 -0
- package/dist/esm/utils/font-preconnect.js +48 -0
- package/dist/esm/utils/font-preconnect.js.map +1 -0
- package/dist/esm/utils/product-ratings.d.ts +26 -0
- package/dist/esm/utils/product-ratings.d.ts.map +1 -0
- package/dist/esm/utils/product-ratings.js +26 -0
- package/dist/esm/utils/product-ratings.js.map +1 -0
- package/dist/esm/utils/rich-text-embeds-codegen.d.ts +9 -0
- package/dist/esm/utils/rich-text-embeds-codegen.d.ts.map +1 -0
- package/dist/esm/utils/rich-text-embeds-codegen.js +134 -0
- package/dist/esm/utils/rich-text-embeds-codegen.js.map +1 -0
- package/dist/esm/utils/rich-text-embeds.d.ts +251 -0
- package/dist/esm/utils/rich-text-embeds.d.ts.map +1 -0
- package/dist/esm/utils/rich-text-embeds.js +822 -0
- package/dist/esm/utils/rich-text-embeds.js.map +1 -0
- package/package.json +3 -3
- package/src/index.ts +10 -0
- package/src/page-revalidate/api-route.ts +77 -0
- package/src/page-revalidate/constants.ts +24 -0
- package/src/page-revalidate/index.ts +2 -0
- package/src/utils/font-preconnect.ts +61 -0
- package/src/utils/product-ratings.ts +98 -0
- package/src/utils/rich-text-embeds-codegen.ts +216 -0
- package/src/utils/rich-text-embeds.ts +966 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { resolveFontPreconnectHints } from '../../src/utils/font-preconnect'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A Google Fonts stylesheet is a TWO-HOP dependency: the CSS comes from
|
|
5
|
+
* `fonts.googleapis.com`, and only once it has been parsed does the browser
|
|
6
|
+
* discover the font files on `fonts.gstatic.com` — a second origin needing its
|
|
7
|
+
* own DNS + TCP + TLS round trips, started after the CSS has already returned.
|
|
8
|
+
*
|
|
9
|
+
* Two things here are load-bearing:
|
|
10
|
+
* - the gstatic hint MUST be `crossorigin`, because fonts are fetched in CORS
|
|
11
|
+
* mode and a connection opened in the wrong mode cannot be reused (the hint
|
|
12
|
+
* then costs a connection instead of saving one);
|
|
13
|
+
* - a project that loads no Google font gets NO hints, because a preconnect to
|
|
14
|
+
* an origin the page never contacts is pure waste.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
describe('resolveFontPreconnectHints', () => {
|
|
18
|
+
const googleFontsHref = 'https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap'
|
|
19
|
+
|
|
20
|
+
it('returns both hints when the project loads a Google font', () => {
|
|
21
|
+
expect(resolveFontPreconnectHints([googleFontsHref])).toEqual([
|
|
22
|
+
{ href: 'https://fonts.googleapis.com', crossorigin: false },
|
|
23
|
+
{ href: 'https://fonts.gstatic.com', crossorigin: true },
|
|
24
|
+
])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// Without CORS mode the browser opens a second connection for the font files
|
|
28
|
+
// and the hint becomes a cost rather than a saving.
|
|
29
|
+
it('marks ONLY the font-file origin as crossorigin', () => {
|
|
30
|
+
const hints = resolveFontPreconnectHints([googleFontsHref])
|
|
31
|
+
const byHost = Object.fromEntries(hints.map((hint) => [hint.href, hint.crossorigin]))
|
|
32
|
+
|
|
33
|
+
expect(byHost['https://fonts.gstatic.com']).toBe(true)
|
|
34
|
+
expect(byHost['https://fonts.googleapis.com']).toBe(false)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('returns nothing for a project with no Google font', () => {
|
|
38
|
+
expect(resolveFontPreconnectHints(['/styles.css', '/fonts/Inter.woff2'])).toEqual([])
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('returns nothing for an empty or sparse asset list', () => {
|
|
42
|
+
expect(resolveFontPreconnectHints([])).toEqual([])
|
|
43
|
+
expect(resolveFontPreconnectHints([undefined, undefined])).toEqual([])
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// A page loading three families still only needs one pair of connections.
|
|
47
|
+
it('emits one pair however many font stylesheets there are', () => {
|
|
48
|
+
expect(
|
|
49
|
+
resolveFontPreconnectHints([
|
|
50
|
+
'https://fonts.googleapis.com/css2?family=Inter',
|
|
51
|
+
'https://fonts.googleapis.com/css2?family=Lora',
|
|
52
|
+
'/styles.css',
|
|
53
|
+
])
|
|
54
|
+
).toHaveLength(2)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// Self-hosted fonts need no hint: their files come from the page's own origin,
|
|
58
|
+
// which the browser has already connected to.
|
|
59
|
+
it('ignores a self-hosted font', () => {
|
|
60
|
+
expect(resolveFontPreconnectHints(['/assets/fonts/custom.css'])).toEqual([])
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { parse } from '@babel/parser'
|
|
2
|
+
import { RichTextEmbeds, RichTextEmbedsCodegen } from '../../src'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The generated project gets `components/embed-runtime.js`, emitted from the
|
|
6
|
+
* very functions in `src/utils/rich-text-embeds.ts` rather than hand-copied
|
|
7
|
+
* into a template. These tests are what makes that safe: the emitted module has
|
|
8
|
+
* to parse, evaluate with no module-scoped references left dangling, and answer
|
|
9
|
+
* exactly what the source module answers.
|
|
10
|
+
*/
|
|
11
|
+
const SOURCE = RichTextEmbedsCodegen.generateEmbedRuntimeModuleSource()
|
|
12
|
+
|
|
13
|
+
/** Evaluates the ES module source in-process and returns its exports. */
|
|
14
|
+
const loadRuntime = (): Record<string, (...args: never[]) => unknown> => {
|
|
15
|
+
const exportedNames = Array.from(
|
|
16
|
+
SOURCE.matchAll(/^export (?:var|function) ([A-Za-z_$][\w$]*)/gm)
|
|
17
|
+
).map((match) => match[1])
|
|
18
|
+
const body = `${SOURCE.replace(/^export /gm, '')}\nreturn { ${exportedNames.join(', ')} }`
|
|
19
|
+
// eslint-disable-next-line no-new-func
|
|
20
|
+
return new Function(body)() as Record<string, (...args: never[]) => unknown>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('the emitted embed runtime module', () => {
|
|
24
|
+
it('parses as an ES module', () => {
|
|
25
|
+
expect(() => parse(SOURCE, { sourceType: 'module' })).not.toThrow()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('carries no reference the module itself does not declare', () => {
|
|
29
|
+
expect(SOURCE).not.toContain('exports.')
|
|
30
|
+
expect(() => loadRuntime()).not.toThrow()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('exports every helper the generated editor and activator call', () => {
|
|
34
|
+
const runtime = loadRuntime()
|
|
35
|
+
const required = [
|
|
36
|
+
'isSafeEmbedUrl',
|
|
37
|
+
'resolveEmbedProvider',
|
|
38
|
+
'buildEmbedHtmlFromUrl',
|
|
39
|
+
'resolveEmbedMode',
|
|
40
|
+
'createEmbedValue',
|
|
41
|
+
'buildEmbedElementHtml',
|
|
42
|
+
'buildEmbedFrameChildren',
|
|
43
|
+
'readEmbedValueFromElement',
|
|
44
|
+
'readEncodedEmbedCode',
|
|
45
|
+
'findEmbedFrame',
|
|
46
|
+
'normalizeEmbedsForStorage',
|
|
47
|
+
'resolveEmbedSandboxFlags',
|
|
48
|
+
'buildEmbedSandboxDocument',
|
|
49
|
+
'parseEmbedHeightMessage',
|
|
50
|
+
'createEmbedToken',
|
|
51
|
+
'encodeEmbedCode',
|
|
52
|
+
'decodeEmbedCode',
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
required.forEach((name) => {
|
|
56
|
+
expect(typeof runtime[name]).toBe('function')
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('answers exactly what the source module answers', () => {
|
|
61
|
+
const runtime = loadRuntime() as unknown as typeof RichTextEmbeds
|
|
62
|
+
|
|
63
|
+
const urls = [
|
|
64
|
+
'https://youtu.be/dQw4w9WgXcQ',
|
|
65
|
+
'https://x.com/a/status/1',
|
|
66
|
+
'https://www.figma.com/design/abc/File?node-id=1-2',
|
|
67
|
+
'https://open.spotify.com/track/abc123',
|
|
68
|
+
'https://example.com/plain',
|
|
69
|
+
'javascript:alert(1)',
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
urls.forEach((url) => {
|
|
73
|
+
expect(runtime.isSafeEmbedUrl(url)).toBe(RichTextEmbeds.isSafeEmbedUrl(url))
|
|
74
|
+
|
|
75
|
+
const built = runtime.buildEmbedHtmlFromUrl(url)
|
|
76
|
+
const expected = RichTextEmbeds.buildEmbedHtmlFromUrl(url)
|
|
77
|
+
expect(built?.html).toBe(expected?.html)
|
|
78
|
+
expect(built?.provider.id).toBe(expected?.provider.id)
|
|
79
|
+
|
|
80
|
+
if (!built || !expected) {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const params = {
|
|
85
|
+
code: built.html,
|
|
86
|
+
provider: built.provider.id,
|
|
87
|
+
url,
|
|
88
|
+
ratio: built.provider.ratio,
|
|
89
|
+
caption: 'a & "b"',
|
|
90
|
+
}
|
|
91
|
+
expect(runtime.createEmbedValue(params)).toEqual(RichTextEmbeds.createEmbedValue(params))
|
|
92
|
+
expect(runtime.buildEmbedElementHtml(runtime.createEmbedValue(params))).toBe(
|
|
93
|
+
RichTextEmbeds.buildEmbedElementHtml(RichTextEmbeds.createEmbedValue(params))
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('produces the same sandbox payload as the source module', () => {
|
|
99
|
+
const runtime = loadRuntime() as unknown as typeof RichTextEmbeds
|
|
100
|
+
|
|
101
|
+
expect(runtime.resolveEmbedSandboxFlags('strict')).toBe(
|
|
102
|
+
RichTextEmbeds.resolveEmbedSandboxFlags('strict')
|
|
103
|
+
)
|
|
104
|
+
expect(runtime.resolveEmbedSandboxFlags('trusted')).toBe(
|
|
105
|
+
RichTextEmbeds.resolveEmbedSandboxFlags('trusted')
|
|
106
|
+
)
|
|
107
|
+
expect(runtime.buildEmbedSandboxDocument('<b>x</b>', 'tok')).toBe(
|
|
108
|
+
RichTextEmbeds.buildEmbedSandboxDocument('<b>x</b>', 'tok')
|
|
109
|
+
)
|
|
110
|
+
expect(
|
|
111
|
+
runtime.parseEmbedHeightMessage({ type: 'tq-embed-height', token: 'a', height: 9e9 })
|
|
112
|
+
).toEqual(
|
|
113
|
+
RichTextEmbeds.parseEmbedHeightMessage({ type: 'tq-embed-height', token: 'a', height: 9e9 })
|
|
114
|
+
)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('round-trips base64 the same way in both', () => {
|
|
118
|
+
const runtime = loadRuntime() as unknown as typeof RichTextEmbeds
|
|
119
|
+
const code = '<blockquote>café — 日本語 — 🎉</blockquote><script>x()</script>'
|
|
120
|
+
|
|
121
|
+
expect(runtime.encodeEmbedCode(code)).toBe(RichTextEmbeds.encodeEmbedCode(code))
|
|
122
|
+
expect(runtime.decodeEmbedCode(RichTextEmbeds.encodeEmbedCode(code))).toBe(code)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { parse } from '@babel/parser'
|
|
2
|
+
import { RichTextEmbeds, RichTextEmbedsCodegen } from '../../src'
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
EMBED_ATTR,
|
|
6
|
+
applyEmbedTemplate,
|
|
7
|
+
buildEmbedElementHtml,
|
|
8
|
+
buildEmbedHtmlFromUrl,
|
|
9
|
+
clampEmbedWidth,
|
|
10
|
+
createEmbedValue,
|
|
11
|
+
decodeEmbedCode,
|
|
12
|
+
embedBlockStyle,
|
|
13
|
+
embedFrameStyle,
|
|
14
|
+
encodeEmbedCode,
|
|
15
|
+
isSafeEmbedUrl,
|
|
16
|
+
normalizeEmbedAlign,
|
|
17
|
+
resolveEmbedMode,
|
|
18
|
+
resolveEmbedProvider,
|
|
19
|
+
} = RichTextEmbeds
|
|
20
|
+
|
|
21
|
+
// The DOM half of the contract (`readEmbedValueFromElement`, the layout and
|
|
22
|
+
// caption editors) is covered in teleport-gui, whose test runner has a DOM —
|
|
23
|
+
// this repo's jest runs in a node environment.
|
|
24
|
+
|
|
25
|
+
describe('provider resolution', () => {
|
|
26
|
+
it('matches every YouTube URL spelling to the same embed', () => {
|
|
27
|
+
const urls = [
|
|
28
|
+
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
|
29
|
+
'https://www.youtube.com/watch?list=PL123&v=dQw4w9WgXcQ',
|
|
30
|
+
'https://youtu.be/dQw4w9WgXcQ',
|
|
31
|
+
'https://www.youtube.com/shorts/dQw4w9WgXcQ',
|
|
32
|
+
'https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ',
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
urls.forEach((url) => {
|
|
36
|
+
const built = buildEmbedHtmlFromUrl(url)
|
|
37
|
+
expect(built).not.toBeNull()
|
|
38
|
+
expect(built?.provider.id).toBe('youtube')
|
|
39
|
+
expect(built?.html).toContain('https://www.youtube.com/embed/dQw4w9WgXcQ')
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('URL-encodes the whole URL where a provider takes it as a query parameter', () => {
|
|
44
|
+
const built = buildEmbedHtmlFromUrl('https://www.figma.com/design/abc123/My-File?node-id=1-2')
|
|
45
|
+
expect(built?.provider.id).toBe('figma')
|
|
46
|
+
expect(built?.html).toContain(
|
|
47
|
+
'url=https%3A%2F%2Fwww.figma.com%2Fdesign%2Fabc123%2FMy-File%3Fnode-id%3D1-2'
|
|
48
|
+
)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('falls back to a generic iframe only after every specific provider missed', () => {
|
|
52
|
+
expect(resolveEmbedProvider('https://example.com/some/page')?.provider.id).toBe('iframe-url')
|
|
53
|
+
expect(resolveEmbedProvider('https://vimeo.com/76979871')?.provider.id).toBe('vimeo')
|
|
54
|
+
expect(resolveEmbedProvider('https://codepen.io/team/pen/abcDEF')?.provider.id).toBe('codepen')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('refuses anything that is not an http(s) URL', () => {
|
|
58
|
+
expect(isSafeEmbedUrl('javascript:alert(1)')).toBe(false)
|
|
59
|
+
expect(isSafeEmbedUrl('data:text/html;base64,PHNjcmlwdD4=')).toBe(false)
|
|
60
|
+
expect(isSafeEmbedUrl('//evil.example.com')).toBe(false)
|
|
61
|
+
expect(isSafeEmbedUrl('')).toBe(false)
|
|
62
|
+
expect(isSafeEmbedUrl('https://ok.example.com/x')).toBe(true)
|
|
63
|
+
expect(resolveEmbedProvider('javascript:alert(1)')).toBeNull()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('keeps `$$` literal and never honours the replacement patterns String.replace reserves', () => {
|
|
67
|
+
const match = 'https://x.test/1'.match(/^https:\/\/x\.test\/(\d+)$/) as RegExpMatchArray
|
|
68
|
+
expect(applyEmbedTemplate("[$1][$$][$'][$`]", match, 'https://x.test/1')).toBe("[1][$][$'][$`]")
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('gives every provider patterns that agree on their capture groups', () => {
|
|
72
|
+
RichTextEmbeds.EMBED_PROVIDERS.forEach((provider) => {
|
|
73
|
+
const groupCounts = provider.patterns.map(
|
|
74
|
+
(pattern) => new RegExp(`${pattern.source}|`).exec('')?.length ?? 0
|
|
75
|
+
)
|
|
76
|
+
expect(new Set(groupCounts).size).toBe(1)
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('mode resolution', () => {
|
|
82
|
+
it('quarantines anything that can execute', () => {
|
|
83
|
+
expect(resolveEmbedMode('<script src="https://x.test/a.js"></script>')).toBe('sandbox')
|
|
84
|
+
expect(resolveEmbedMode('<img src=x onerror="alert(1)">')).toBe('sandbox')
|
|
85
|
+
expect(resolveEmbedMode('<a href="javascript:alert(1)">x</a>')).toBe('sandbox')
|
|
86
|
+
expect(resolveEmbedMode('<iframe src="https://x.test"></iframe>')).toBe('inline')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('does not mistake an attribute merely starting with "on" for a handler', () => {
|
|
90
|
+
expect(resolveEmbedMode('<iframe data-only="1" title="one"></iframe>')).toBe('inline')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('sends every script-based provider to the sandbox and the rest inline', () => {
|
|
94
|
+
expect(resolveEmbedMode(buildEmbedHtmlFromUrl('https://x.com/a/status/1')?.html ?? '')).toBe(
|
|
95
|
+
'sandbox'
|
|
96
|
+
)
|
|
97
|
+
expect(
|
|
98
|
+
resolveEmbedMode(buildEmbedHtmlFromUrl('https://youtu.be/dQw4w9WgXcQ')?.html ?? '')
|
|
99
|
+
).toBe('inline')
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe('base64 round-trip', () => {
|
|
104
|
+
it('survives non-ASCII markup', () => {
|
|
105
|
+
const code = '<blockquote>café — 日本語 — 🎉</blockquote>'
|
|
106
|
+
expect(decodeEmbedCode(encodeEmbedCode(code))).toBe(code)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe('stored markup', () => {
|
|
111
|
+
it('keeps an inline embed as real children so it renders without JavaScript', () => {
|
|
112
|
+
const value = createEmbedValue({
|
|
113
|
+
code: '<iframe src="https://www.youtube.com/embed/abc"></iframe>',
|
|
114
|
+
provider: 'youtube',
|
|
115
|
+
url: 'https://youtu.be/abc',
|
|
116
|
+
ratio: 56.25,
|
|
117
|
+
caption: 'A "quoted" caption & more',
|
|
118
|
+
})
|
|
119
|
+
const html = buildEmbedElementHtml(value)
|
|
120
|
+
|
|
121
|
+
expect(html).toContain(`${EMBED_ATTR.MODE}="inline"`)
|
|
122
|
+
expect(html).not.toContain(EMBED_ATTR.CODE)
|
|
123
|
+
expect(html).toContain('<iframe src="https://www.youtube.com/embed/abc"></iframe>')
|
|
124
|
+
expect(html).toContain('padding-top:56.25%')
|
|
125
|
+
expect(html).toContain('A "quoted" caption & more')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('encodes a script embed and leaves only a noscript fallback in the page', () => {
|
|
129
|
+
const code =
|
|
130
|
+
'<blockquote class="twitter-tweet"></blockquote><script src="https://x.test/w.js"></script>'
|
|
131
|
+
const value = createEmbedValue({ code, provider: 'twitter', url: 'https://x.com/a/status/1' })
|
|
132
|
+
const html = buildEmbedElementHtml(value)
|
|
133
|
+
|
|
134
|
+
expect(html).toContain(`${EMBED_ATTR.MODE}="sandbox"`)
|
|
135
|
+
expect(html).toContain(`${EMBED_ATTR.SANDBOX}="strict"`)
|
|
136
|
+
expect(html).toContain(`${EMBED_ATTR.CODE}="${encodeEmbedCode(code)}"`)
|
|
137
|
+
expect(html).not.toContain('<script')
|
|
138
|
+
expect(html).toContain('<noscript>')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('omits the noscript fallback for a pasted snippet that has no source URL', () => {
|
|
142
|
+
const html = buildEmbedElementHtml(createEmbedValue({ code: '<script>x()</script>' }))
|
|
143
|
+
expect(html).not.toContain('<noscript>')
|
|
144
|
+
expect(html).toContain(`${EMBED_ATTR.PROVIDER}="custom"`)
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe('layout vocabulary', () => {
|
|
149
|
+
it('clamps the width to a usable range', () => {
|
|
150
|
+
expect(clampEmbedWidth(0)).toBe(10)
|
|
151
|
+
expect(clampEmbedWidth(1000)).toBe(100)
|
|
152
|
+
expect(clampEmbedWidth(NaN)).toBe(100)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('falls back to center for an unknown alignment', () => {
|
|
156
|
+
expect(normalizeEmbedAlign('diagonal')).toBe('center')
|
|
157
|
+
expect(normalizeEmbedAlign('float-right')).toBe('float-right')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('only floats when the alignment asks for it', () => {
|
|
161
|
+
expect(embedBlockStyle({ width: 50, align: 'center' })).toContain('margin-left:auto')
|
|
162
|
+
expect(embedBlockStyle({ width: 50, align: 'center' })).not.toContain('float')
|
|
163
|
+
expect(embedBlockStyle({ width: 50, align: 'float-right' })).toContain('float:right')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('only builds a padding-top box for a usable ratio', () => {
|
|
167
|
+
expect(embedFrameStyle(56.25)).toContain('padding-top:56.25%')
|
|
168
|
+
expect(embedFrameStyle(null)).not.toContain('padding-top')
|
|
169
|
+
expect(embedFrameStyle(0)).not.toContain('padding-top')
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
describe('runtime serialization', () => {
|
|
174
|
+
it('emits a registry the generated module can evaluate', () => {
|
|
175
|
+
// tslint:disable-next-line:no-eval
|
|
176
|
+
const providers = eval(RichTextEmbedsCodegen.serializeEmbedProvidersForRuntime()) as Array<{
|
|
177
|
+
id: string
|
|
178
|
+
patterns: RegExp[]
|
|
179
|
+
}>
|
|
180
|
+
|
|
181
|
+
expect(providers).toHaveLength(RichTextEmbeds.EMBED_PROVIDERS.length)
|
|
182
|
+
expect(providers[0].patterns[0]).toBeInstanceOf(RegExp)
|
|
183
|
+
expect(providers.map((entry) => entry.id)).toEqual(
|
|
184
|
+
RichTextEmbeds.EMBED_PROVIDERS.map((entry) => entry.id)
|
|
185
|
+
)
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
describe('the sandbox payload', () => {
|
|
190
|
+
const {
|
|
191
|
+
EMBED_MAX_HEIGHT,
|
|
192
|
+
EMBED_MIN_HEIGHT,
|
|
193
|
+
buildEmbedSandboxDocument,
|
|
194
|
+
createEmbedToken,
|
|
195
|
+
parseEmbedHeightMessage,
|
|
196
|
+
resolveEmbedSandboxFlags,
|
|
197
|
+
} = RichTextEmbeds
|
|
198
|
+
|
|
199
|
+
it('withholds allow-same-origin unless the author opted in', () => {
|
|
200
|
+
expect(resolveEmbedSandboxFlags('strict')).not.toContain('allow-same-origin')
|
|
201
|
+
expect(resolveEmbedSandboxFlags('trusted')).toContain('allow-same-origin')
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('hands the frame a bootstrap script that is valid JavaScript', () => {
|
|
205
|
+
// The document is assembled from concatenated fragments, so a stray
|
|
206
|
+
// character would only surface as a silent parse failure inside a sandboxed
|
|
207
|
+
// frame — where nothing can report it. Parse it here instead.
|
|
208
|
+
const doc = buildEmbedSandboxDocument('', 'tok')
|
|
209
|
+
const script = /<script>([\s\S]*?)<\/script>/.exec(doc)
|
|
210
|
+
|
|
211
|
+
expect(script).not.toBeNull()
|
|
212
|
+
expect(() => parse((script as RegExpExecArray)[1], { sourceType: 'script' })).not.toThrow()
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('closes every tag it opens, so the embed lands inside the body', () => {
|
|
216
|
+
const doc = buildEmbedSandboxDocument('<b>x</b>', 'tok')
|
|
217
|
+
|
|
218
|
+
expect(doc.indexOf('<body>')).toBeLessThan(doc.indexOf('<b>x</b>'))
|
|
219
|
+
expect(doc.indexOf('<b>x</b>')).toBeLessThan(doc.indexOf('</body>'))
|
|
220
|
+
expect(doc.endsWith('</body></html>')).toBe(true)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('bakes the frame token into the document it hands the iframe', () => {
|
|
224
|
+
const token = createEmbedToken()
|
|
225
|
+
const doc = buildEmbedSandboxDocument('<blockquote>hi</blockquote>', token)
|
|
226
|
+
|
|
227
|
+
expect(doc).toContain(token)
|
|
228
|
+
expect(doc).not.toContain(RichTextEmbeds.EMBED_TOKEN_PLACEHOLDER)
|
|
229
|
+
expect(doc).toContain('<blockquote>hi</blockquote>')
|
|
230
|
+
expect(doc.indexOf('</head>')).toBeLessThan(doc.indexOf('<blockquote>'))
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('mints a distinct token per frame', () => {
|
|
234
|
+
expect(createEmbedToken()).not.toBe(createEmbedToken())
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('accepts only a well-formed height message, and clamps it', () => {
|
|
238
|
+
expect(parseEmbedHeightMessage(null)).toBeNull()
|
|
239
|
+
expect(parseEmbedHeightMessage('tq-embed-height')).toBeNull()
|
|
240
|
+
expect(parseEmbedHeightMessage({ type: 'other', token: 'a', height: 10 })).toBeNull()
|
|
241
|
+
expect(parseEmbedHeightMessage({ type: 'tq-embed-height', token: 1, height: 10 })).toBeNull()
|
|
242
|
+
expect(parseEmbedHeightMessage({ type: 'tq-embed-height', token: 'a', height: 'x' })).toBeNull()
|
|
243
|
+
expect(parseEmbedHeightMessage({ type: 'tq-embed-height', token: 'a', height: -5 })).toBeNull()
|
|
244
|
+
|
|
245
|
+
expect(parseEmbedHeightMessage({ type: 'tq-embed-height', token: 'a', height: 1 })).toEqual({
|
|
246
|
+
token: 'a',
|
|
247
|
+
height: EMBED_MIN_HEIGHT,
|
|
248
|
+
})
|
|
249
|
+
expect(parseEmbedHeightMessage({ type: 'tq-embed-height', token: 'a', height: 1e9 })).toEqual({
|
|
250
|
+
token: 'a',
|
|
251
|
+
height: EMBED_MAX_HEIGHT,
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
})
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -13,7 +13,12 @@ import * as HtmlContentModel from './utils/html-content-model';
|
|
|
13
13
|
import * as StyleDeclarations from './utils/style-declarations';
|
|
14
14
|
import * as StorefrontTax from './utils/storefront-tax';
|
|
15
15
|
import * as ProductDiscounts from './utils/product-discounts';
|
|
16
|
+
import * as ProductRatings from './utils/product-ratings';
|
|
17
|
+
import * as FontPreconnect from './utils/font-preconnect';
|
|
16
18
|
import * as EmailDate from './utils/email-date';
|
|
19
|
+
import * as RichTextEmbeds from './utils/rich-text-embeds';
|
|
20
|
+
import * as RichTextEmbedsCodegen from './utils/rich-text-embeds-codegen';
|
|
17
21
|
import * as DataCache from './cache';
|
|
18
|
-
|
|
22
|
+
import * as PageRevalidate from './page-revalidate';
|
|
23
|
+
export { Constants, StringUtils, UIDLUtils, GenericUtils, LocalImports, JSIdentifiers, ASTScope, ASTStatementOrder, ASTEffectCleanup, RoutePaths, LayoutTopology, HtmlContentModel, StyleDeclarations, StorefrontTax, ProductDiscounts, ProductRatings, FontPreconnect, EmailDate, RichTextEmbeds, RichTextEmbedsCodegen, DataCache, PageRevalidate, };
|
|
19
24
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/cjs/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,SAAS,MAAM,aAAa,CAAA;AACxC,OAAO,KAAK,WAAW,MAAM,sBAAsB,CAAA;AACnD,OAAO,KAAK,SAAS,MAAM,oBAAoB,CAAA;AAC/C,OAAO,KAAK,YAAY,MAAM,iBAAiB,CAAA;AAC/C,OAAO,KAAK,YAAY,MAAM,uBAAuB,CAAA;AACrD,OAAO,KAAK,aAAa,MAAM,wBAAwB,CAAA;AACvD,OAAO,KAAK,QAAQ,MAAM,mBAAmB,CAAA;AAC7C,OAAO,KAAK,iBAAiB,MAAM,6BAA6B,CAAA;AAChE,OAAO,KAAK,gBAAgB,MAAM,4BAA4B,CAAA;AAC9D,OAAO,KAAK,UAAU,MAAM,qBAAqB,CAAA;AACjD,OAAO,KAAK,cAAc,MAAM,yBAAyB,CAAA;AACzD,OAAO,KAAK,gBAAgB,MAAM,4BAA4B,CAAA;AAC9D,OAAO,KAAK,iBAAiB,MAAM,4BAA4B,CAAA;AAC/D,OAAO,KAAK,aAAa,MAAM,wBAAwB,CAAA;AACvD,OAAO,KAAK,gBAAgB,MAAM,2BAA2B,CAAA;AAC7D,OAAO,KAAK,SAAS,MAAM,oBAAoB,CAAA;AAC/C,OAAO,KAAK,SAAS,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,SAAS,MAAM,aAAa,CAAA;AACxC,OAAO,KAAK,WAAW,MAAM,sBAAsB,CAAA;AACnD,OAAO,KAAK,SAAS,MAAM,oBAAoB,CAAA;AAC/C,OAAO,KAAK,YAAY,MAAM,iBAAiB,CAAA;AAC/C,OAAO,KAAK,YAAY,MAAM,uBAAuB,CAAA;AACrD,OAAO,KAAK,aAAa,MAAM,wBAAwB,CAAA;AACvD,OAAO,KAAK,QAAQ,MAAM,mBAAmB,CAAA;AAC7C,OAAO,KAAK,iBAAiB,MAAM,6BAA6B,CAAA;AAChE,OAAO,KAAK,gBAAgB,MAAM,4BAA4B,CAAA;AAC9D,OAAO,KAAK,UAAU,MAAM,qBAAqB,CAAA;AACjD,OAAO,KAAK,cAAc,MAAM,yBAAyB,CAAA;AACzD,OAAO,KAAK,gBAAgB,MAAM,4BAA4B,CAAA;AAC9D,OAAO,KAAK,iBAAiB,MAAM,4BAA4B,CAAA;AAC/D,OAAO,KAAK,aAAa,MAAM,wBAAwB,CAAA;AACvD,OAAO,KAAK,gBAAgB,MAAM,2BAA2B,CAAA;AAC7D,OAAO,KAAK,cAAc,MAAM,yBAAyB,CAAA;AACzD,OAAO,KAAK,cAAc,MAAM,yBAAyB,CAAA;AACzD,OAAO,KAAK,SAAS,MAAM,oBAAoB,CAAA;AAC/C,OAAO,KAAK,cAAc,MAAM,0BAA0B,CAAA;AAC1D,OAAO,KAAK,qBAAqB,MAAM,kCAAkC,CAAA;AACzE,OAAO,KAAK,SAAS,MAAM,SAAS,CAAA;AACpC,OAAO,KAAK,cAAc,MAAM,mBAAmB,CAAA;AAEnD,OAAO,EACL,SAAS,EACT,WAAW,EACX,SAAS,EACT,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,QAAQ,EACR,iBAAiB,EACjB,gBAAgB,EAChB,UAAU,EACV,cAAc,EACd,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,SAAS,EACT,cAAc,EACd,qBAAqB,EACrB,SAAS,EACT,cAAc,GACf,CAAA"}
|
package/dist/cjs/index.js
CHANGED
|
@@ -23,7 +23,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
23
23
|
return result;
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.DataCache = exports.EmailDate = exports.ProductDiscounts = exports.StorefrontTax = exports.StyleDeclarations = exports.HtmlContentModel = exports.LayoutTopology = exports.RoutePaths = exports.ASTEffectCleanup = exports.ASTStatementOrder = exports.ASTScope = exports.JSIdentifiers = exports.LocalImports = exports.GenericUtils = exports.UIDLUtils = exports.StringUtils = exports.Constants = void 0;
|
|
26
|
+
exports.PageRevalidate = exports.DataCache = exports.RichTextEmbedsCodegen = exports.RichTextEmbeds = exports.EmailDate = exports.FontPreconnect = exports.ProductRatings = exports.ProductDiscounts = exports.StorefrontTax = exports.StyleDeclarations = exports.HtmlContentModel = exports.LayoutTopology = exports.RoutePaths = exports.ASTEffectCleanup = exports.ASTStatementOrder = exports.ASTScope = exports.JSIdentifiers = exports.LocalImports = exports.GenericUtils = exports.UIDLUtils = exports.StringUtils = exports.Constants = void 0;
|
|
27
27
|
var Constants = __importStar(require("./constants"));
|
|
28
28
|
exports.Constants = Constants;
|
|
29
29
|
var StringUtils = __importStar(require("./utils/string-utils"));
|
|
@@ -54,8 +54,18 @@ var StorefrontTax = __importStar(require("./utils/storefront-tax"));
|
|
|
54
54
|
exports.StorefrontTax = StorefrontTax;
|
|
55
55
|
var ProductDiscounts = __importStar(require("./utils/product-discounts"));
|
|
56
56
|
exports.ProductDiscounts = ProductDiscounts;
|
|
57
|
+
var ProductRatings = __importStar(require("./utils/product-ratings"));
|
|
58
|
+
exports.ProductRatings = ProductRatings;
|
|
59
|
+
var FontPreconnect = __importStar(require("./utils/font-preconnect"));
|
|
60
|
+
exports.FontPreconnect = FontPreconnect;
|
|
57
61
|
var EmailDate = __importStar(require("./utils/email-date"));
|
|
58
62
|
exports.EmailDate = EmailDate;
|
|
63
|
+
var RichTextEmbeds = __importStar(require("./utils/rich-text-embeds"));
|
|
64
|
+
exports.RichTextEmbeds = RichTextEmbeds;
|
|
65
|
+
var RichTextEmbedsCodegen = __importStar(require("./utils/rich-text-embeds-codegen"));
|
|
66
|
+
exports.RichTextEmbedsCodegen = RichTextEmbedsCodegen;
|
|
59
67
|
var DataCache = __importStar(require("./cache"));
|
|
60
68
|
exports.DataCache = DataCache;
|
|
69
|
+
var PageRevalidate = __importStar(require("./page-revalidate"));
|
|
70
|
+
exports.PageRevalidate = PageRevalidate;
|
|
61
71
|
//# sourceMappingURL=index.js.map
|
package/dist/cjs/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,qDAAwC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,qDAAwC;AAwBtC,8BAAS;AAvBX,gEAAmD;AAwBjD,kCAAW;AAvBb,4DAA+C;AAwB7C,8BAAS;AAvBX,4DAA+C;AAwB7C,oCAAY;AAvBd,kEAAqD;AAwBnD,oCAAY;AAvBd,oEAAuD;AAwBrD,sCAAa;AAvBf,0DAA6C;AAwB3C,4BAAQ;AAvBV,6EAAgE;AAwB9D,8CAAiB;AAvBnB,2EAA8D;AAwB5D,4CAAgB;AAvBlB,8DAAiD;AAwB/C,gCAAU;AAvBZ,sEAAyD;AAwBvD,wCAAc;AAvBhB,2EAA8D;AAwB5D,4CAAgB;AAvBlB,4EAA+D;AAwB7D,8CAAiB;AAvBnB,oEAAuD;AAwBrD,sCAAa;AAvBf,0EAA6D;AAwB3D,4CAAgB;AAvBlB,sEAAyD;AAwBvD,wCAAc;AAvBhB,sEAAyD;AAwBvD,wCAAc;AAvBhB,4DAA+C;AAwB7C,8BAAS;AAvBX,uEAA0D;AAwBxD,wCAAc;AAvBhB,sFAAyE;AAwBvE,sDAAqB;AAvBvB,iDAAoC;AAwBlC,8BAAS;AAvBX,gEAAmD;AAwBjD,wCAAc"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `POST /api/tq-revalidate`
|
|
3
|
+
*
|
|
4
|
+
* Re-runs `getStaticProps` for each given path, so a row an admin just created,
|
|
5
|
+
* edited or deleted reaches the public URL immediately instead of at the end of
|
|
6
|
+
* the page's ISR window. `res.revalidate` is a Pages-Router API-route method, so
|
|
7
|
+
* this route is the only place in a generated project that can do it — which is
|
|
8
|
+
* why the workflow node calls it over HTTP rather than doing the work itself.
|
|
9
|
+
*
|
|
10
|
+
* Reuses the cache secret rather than minting a second one: both endpoints
|
|
11
|
+
* authorise the same actor (the app's own server, mid-workflow) and a project
|
|
12
|
+
* already has this secret provisioned.
|
|
13
|
+
*
|
|
14
|
+
* Fails CLOSED when no secret is configured, for the same reason the cache
|
|
15
|
+
* route does: an unauthenticated rebuild endpoint on every published site is a
|
|
16
|
+
* free way to make a project re-render its pages on demand.
|
|
17
|
+
*
|
|
18
|
+
* Each path is rebuilt inside its OWN try/catch. `res.revalidate` throws for a
|
|
19
|
+
* path that was never generated (a deleted row's URL, a slug that no longer
|
|
20
|
+
* resolves), and one such path must not cost the caller the rebuilds it asked
|
|
21
|
+
* for alongside it — so failures are collected and reported, never thrown.
|
|
22
|
+
*/
|
|
23
|
+
export declare const generatePageRevalidateRoute: () => string;
|
|
24
|
+
//# sourceMappingURL=api-route.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-route.d.ts","sourceRoot":"","sources":["../../../src/page-revalidate/api-route.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,2BAA2B,QAAO,MAmD9C,CAAA"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generatePageRevalidateRoute = void 0;
|
|
4
|
+
var constants_1 = require("../cache/constants");
|
|
5
|
+
var constants_2 = require("./constants");
|
|
6
|
+
/**
|
|
7
|
+
* `POST /api/tq-revalidate`
|
|
8
|
+
*
|
|
9
|
+
* Re-runs `getStaticProps` for each given path, so a row an admin just created,
|
|
10
|
+
* edited or deleted reaches the public URL immediately instead of at the end of
|
|
11
|
+
* the page's ISR window. `res.revalidate` is a Pages-Router API-route method, so
|
|
12
|
+
* this route is the only place in a generated project that can do it — which is
|
|
13
|
+
* why the workflow node calls it over HTTP rather than doing the work itself.
|
|
14
|
+
*
|
|
15
|
+
* Reuses the cache secret rather than minting a second one: both endpoints
|
|
16
|
+
* authorise the same actor (the app's own server, mid-workflow) and a project
|
|
17
|
+
* already has this secret provisioned.
|
|
18
|
+
*
|
|
19
|
+
* Fails CLOSED when no secret is configured, for the same reason the cache
|
|
20
|
+
* route does: an unauthenticated rebuild endpoint on every published site is a
|
|
21
|
+
* free way to make a project re-render its pages on demand.
|
|
22
|
+
*
|
|
23
|
+
* Each path is rebuilt inside its OWN try/catch. `res.revalidate` throws for a
|
|
24
|
+
* path that was never generated (a deleted row's URL, a slug that no longer
|
|
25
|
+
* resolves), and one such path must not cost the caller the rebuilds it asked
|
|
26
|
+
* for alongside it — so failures are collected and reported, never thrown.
|
|
27
|
+
*/
|
|
28
|
+
var generatePageRevalidateRoute = function () { return "import crypto from 'crypto'\n\nconst MAX_PATHS = ".concat(constants_2.REVALIDATE_MAX_PATHS, "\n\nfunction isAuthorized(req) {\n const secret = process.env.").concat(constants_1.CACHE_SECRET_ENV, "\n const provided = req.headers['x-tq-cache-secret']\n if (!secret || typeof provided !== 'string') {\n return false\n }\n const a = Buffer.from(provided)\n const b = Buffer.from(secret)\n // timingSafeEqual throws on a length mismatch, so the length is compared first.\n if (a.length !== b.length) {\n return false\n }\n return crypto.timingSafeEqual(a, b)\n}\n\nexport default async function handler(req, res) {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n\n if (!isAuthorized(req)) {\n return res.status(401).json({ error: 'Unauthorized' })\n }\n\n const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body || {}\n const paths = (Array.isArray(body.paths) ? body.paths : [])\n .filter((path) => typeof path === 'string' && path.trim())\n .map((path) => (path.trim().charAt(0) === '/' ? path.trim() : '/' + path.trim()))\n .slice(0, MAX_PATHS)\n\n const revalidated = []\n const failed = []\n\n for (const path of paths) {\n try {\n await res.revalidate(path)\n revalidated.push(path)\n } catch (error) {\n // A path that was never statically generated throws here. That is the\n // normal case for a page whose row was just deleted, so it is reported\n // rather than raised.\n failed.push(path)\n }\n }\n\n return res.status(200).json({ ok: true, revalidated, failed })\n}\n"); };
|
|
29
|
+
exports.generatePageRevalidateRoute = generatePageRevalidateRoute;
|
|
30
|
+
//# sourceMappingURL=api-route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-route.js","sourceRoot":"","sources":["../../../src/page-revalidate/api-route.ts"],"names":[],"mappings":";;;AAAA,gDAAqD;AACrD,yCAAkD;AAElD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACI,IAAM,2BAA2B,GAAG,cAAc,OAAA,2DAErC,gCAAoB,4EAGT,4BAAgB,45CA8C9C,EAnDwD,CAmDxD,CAAA;AAnDY,QAAA,2BAA2B,+BAmDvC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared constants for the generated page-revalidation route.
|
|
3
|
+
*
|
|
4
|
+
* Lives in `teleport-shared` because two generators agree on them — the Next
|
|
5
|
+
* project plugin that emits the route and the `page-revalidate` workflow node
|
|
6
|
+
* that calls it — and a mismatch would be a 404 the node reports only as a
|
|
7
|
+
* warning, so nothing would ever fail loudly.
|
|
8
|
+
*/
|
|
9
|
+
/** Emitted API route, relative to the generated project root. */
|
|
10
|
+
export declare const REVALIDATE_API_DIR: string[];
|
|
11
|
+
export declare const REVALIDATE_ROUTE = "tq-revalidate";
|
|
12
|
+
/** The path the workflow node POSTs to. Kept beside the emitted route's name. */
|
|
13
|
+
export declare const REVALIDATE_ROUTE_PATH = "/api/tq-revalidate";
|
|
14
|
+
/**
|
|
15
|
+
* One request may not rebuild more than this many pages.
|
|
16
|
+
*
|
|
17
|
+
* Each path re-runs `getStaticProps`, so an unbounded list is an unbounded
|
|
18
|
+
* serverless invocation triggered by whatever the workflow put in the config.
|
|
19
|
+
* Every wiring the platform generates asks for one or two paths.
|
|
20
|
+
*/
|
|
21
|
+
export declare const REVALIDATE_MAX_PATHS = 20;
|
|
22
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/page-revalidate/constants.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,iEAAiE;AACjE,eAAO,MAAM,kBAAkB,UAAmB,CAAA;AAClD,eAAO,MAAM,gBAAgB,kBAAkB,CAAA;AAE/C,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,uBAAuB,CAAA;AAEzD;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,KAAK,CAAA"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared constants for the generated page-revalidation route.
|
|
4
|
+
*
|
|
5
|
+
* Lives in `teleport-shared` because two generators agree on them — the Next
|
|
6
|
+
* project plugin that emits the route and the `page-revalidate` workflow node
|
|
7
|
+
* that calls it — and a mismatch would be a 404 the node reports only as a
|
|
8
|
+
* warning, so nothing would ever fail loudly.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.REVALIDATE_MAX_PATHS = exports.REVALIDATE_ROUTE_PATH = exports.REVALIDATE_ROUTE = exports.REVALIDATE_API_DIR = void 0;
|
|
12
|
+
/** Emitted API route, relative to the generated project root. */
|
|
13
|
+
exports.REVALIDATE_API_DIR = ['pages', 'api'];
|
|
14
|
+
exports.REVALIDATE_ROUTE = 'tq-revalidate';
|
|
15
|
+
/** The path the workflow node POSTs to. Kept beside the emitted route's name. */
|
|
16
|
+
exports.REVALIDATE_ROUTE_PATH = '/api/tq-revalidate';
|
|
17
|
+
/**
|
|
18
|
+
* One request may not rebuild more than this many pages.
|
|
19
|
+
*
|
|
20
|
+
* Each path re-runs `getStaticProps`, so an unbounded list is an unbounded
|
|
21
|
+
* serverless invocation triggered by whatever the workflow put in the config.
|
|
22
|
+
* Every wiring the platform generates asks for one or two paths.
|
|
23
|
+
*/
|
|
24
|
+
exports.REVALIDATE_MAX_PATHS = 20;
|
|
25
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../src/page-revalidate/constants.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;AAEH,iEAAiE;AACpD,QAAA,kBAAkB,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;AACrC,QAAA,gBAAgB,GAAG,eAAe,CAAA;AAE/C,iFAAiF;AACpE,QAAA,qBAAqB,GAAG,oBAAoB,CAAA;AAEzD;;;;;;GAMG;AACU,QAAA,oBAAoB,GAAG,EAAE,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/page-revalidate/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./constants"), exports);
|
|
18
|
+
__exportStar(require("./api-route"), exports);
|
|
19
|
+
//# sourceMappingURL=index.js.map
|