@teleporthq/teleport-project-generator-next 0.43.44 → 0.43.46
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__/_helpers/rules-of-hooks.ts +206 -0
- package/__tests__/emitted-components-rules-of-hooks.test.ts +133 -0
- package/__tests__/state-data-source-runtime-user-fetch.test.ts +268 -0
- package/dist/cjs/countdown/component-generator.d.ts.map +1 -1
- package/dist/cjs/countdown/component-generator.js +1 -1
- package/dist/cjs/countdown/component-generator.js.map +1 -1
- package/dist/cjs/state-data-source-plugin.d.ts.map +1 -1
- package/dist/cjs/state-data-source-plugin.js +351 -23
- package/dist/cjs/state-data-source-plugin.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/widgets/motion-component.d.ts.map +1 -1
- package/dist/cjs/widgets/motion-component.js +1 -1
- package/dist/cjs/widgets/motion-component.js.map +1 -1
- package/dist/esm/countdown/component-generator.d.ts.map +1 -1
- package/dist/esm/countdown/component-generator.js +1 -1
- package/dist/esm/countdown/component-generator.js.map +1 -1
- package/dist/esm/state-data-source-plugin.d.ts.map +1 -1
- package/dist/esm/state-data-source-plugin.js +352 -24
- package/dist/esm/state-data-source-plugin.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/widgets/motion-component.d.ts.map +1 -1
- package/dist/esm/widgets/motion-component.js +1 -1
- package/dist/esm/widgets/motion-component.js.map +1 -1
- package/package.json +19 -19
- package/src/countdown/component-generator.ts +7 -5
- package/src/state-data-source-plugin.ts +526 -31
- package/src/widgets/motion-component.ts +7 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal Rules-of-Hooks analyser for the React source the Next project
|
|
3
|
+
* generator emits as plain strings.
|
|
4
|
+
*
|
|
5
|
+
* Those component files are template literals, so TypeScript, ESLint and the
|
|
6
|
+
* react-hooks plugin never see them — a hook placed after an early `return`
|
|
7
|
+
* type-checks, lints and builds cleanly and then throws
|
|
8
|
+
* "Rendered more hooks than during the previous render" in the browser on the
|
|
9
|
+
* second render. `tq-countdown` shipped exactly that defect: its `targetDate`
|
|
10
|
+
* useMemo sat below the mounted-guard return, so the component rendered 7 hooks
|
|
11
|
+
* on the first pass and 8 on the next, crashing every page that embedded it.
|
|
12
|
+
*
|
|
13
|
+
* The two conditions checked here are the two that React enforces at runtime:
|
|
14
|
+
* 1. a hook may not run after a statement that can return early
|
|
15
|
+
* 2. a hook may not run inside a condition, loop or try block
|
|
16
|
+
*
|
|
17
|
+
* Hook calls inside NESTED functions (a `useEffect` callback, an event handler,
|
|
18
|
+
* a `.map()` body) belong to that inner function, not the component, so the walk
|
|
19
|
+
* deliberately stops at every function boundary.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { parse } from '@babel/parser'
|
|
23
|
+
|
|
24
|
+
type Node = Record<string, unknown> & { type: string; loc?: { start: { line: number } } }
|
|
25
|
+
|
|
26
|
+
const FUNCTION_TYPES = new Set([
|
|
27
|
+
'FunctionDeclaration',
|
|
28
|
+
'FunctionExpression',
|
|
29
|
+
'ArrowFunctionExpression',
|
|
30
|
+
'ObjectMethod',
|
|
31
|
+
'ClassMethod',
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
const CONTROL_FLOW_TYPES = new Set([
|
|
35
|
+
'IfStatement',
|
|
36
|
+
'ForStatement',
|
|
37
|
+
'ForInStatement',
|
|
38
|
+
'ForOfStatement',
|
|
39
|
+
'WhileStatement',
|
|
40
|
+
'DoWhileStatement',
|
|
41
|
+
'SwitchStatement',
|
|
42
|
+
'TryStatement',
|
|
43
|
+
'ConditionalExpression',
|
|
44
|
+
'LogicalExpression',
|
|
45
|
+
])
|
|
46
|
+
|
|
47
|
+
const HOOK_NAME = /^use[A-Z]/
|
|
48
|
+
|
|
49
|
+
export type HookViolationKind = 'after-early-return' | 'conditional'
|
|
50
|
+
|
|
51
|
+
export interface HookViolation {
|
|
52
|
+
kind: HookViolationKind
|
|
53
|
+
hook: string
|
|
54
|
+
/** Enclosing function name, or `<anonymous>`. */
|
|
55
|
+
fn: string
|
|
56
|
+
line: number
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isNode(value: unknown): value is Node {
|
|
60
|
+
return !!value && typeof value === 'object' && typeof (value as Node).type === 'string'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Every child node of `node`, in source order. */
|
|
64
|
+
function childNodes(node: Node): Node[] {
|
|
65
|
+
const out: Node[] = []
|
|
66
|
+
for (const key of Object.keys(node)) {
|
|
67
|
+
if (key === 'loc' || key === 'leadingComments' || key === 'trailingComments') {
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
const value = (node as Record<string, unknown>)[key]
|
|
71
|
+
if (Array.isArray(value)) {
|
|
72
|
+
for (const entry of value) {
|
|
73
|
+
if (isNode(entry)) {
|
|
74
|
+
out.push(entry)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
if (isNode(value)) {
|
|
80
|
+
out.push(value)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function callName(node: Node): string | null {
|
|
87
|
+
if (node.type !== 'CallExpression') {
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
const callee = node.callee as Node | undefined
|
|
91
|
+
if (!callee) {
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
if (callee.type === 'Identifier') {
|
|
95
|
+
return String((callee as unknown as { name: string }).name)
|
|
96
|
+
}
|
|
97
|
+
// React.useMemo(...) — the hook is the property, not the object.
|
|
98
|
+
if (callee.type === 'MemberExpression') {
|
|
99
|
+
const property = (callee as unknown as { property?: Node }).property
|
|
100
|
+
if (property && property.type === 'Identifier') {
|
|
101
|
+
return String((property as unknown as { name: string }).name)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Walks `node` and its descendants, stopping at nested function boundaries. */
|
|
108
|
+
function walkOwnScope(
|
|
109
|
+
node: Node,
|
|
110
|
+
visit: (current: Node, insideControlFlow: boolean) => void,
|
|
111
|
+
insideControlFlow = false
|
|
112
|
+
): void {
|
|
113
|
+
for (const child of childNodes(node)) {
|
|
114
|
+
if (FUNCTION_TYPES.has(child.type)) {
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
const nested = insideControlFlow || CONTROL_FLOW_TYPES.has(child.type)
|
|
118
|
+
visit(child, insideControlFlow)
|
|
119
|
+
walkOwnScope(child, visit, nested)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** True when `statement` can hand control back to the caller before the next statement runs. */
|
|
124
|
+
function canReturnEarly(statement: Node): boolean {
|
|
125
|
+
if (statement.type === 'ReturnStatement' || statement.type === 'ThrowStatement') {
|
|
126
|
+
return true
|
|
127
|
+
}
|
|
128
|
+
let found = false
|
|
129
|
+
walkOwnScope(statement, (current) => {
|
|
130
|
+
if (current.type === 'ReturnStatement' || current.type === 'ThrowStatement') {
|
|
131
|
+
found = true
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
return found
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function functionName(node: Node, parentName: string | null): string {
|
|
138
|
+
const id = (node as unknown as { id?: { name?: string } }).id
|
|
139
|
+
if (id && id.name) {
|
|
140
|
+
return id.name
|
|
141
|
+
}
|
|
142
|
+
return parentName ?? '<anonymous>'
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function analyseFunction(node: Node, name: string, violations: HookViolation[]): void {
|
|
146
|
+
const body = (node as unknown as { body?: Node }).body
|
|
147
|
+
if (!body || body.type !== 'BlockStatement') {
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
const statements = ((body as unknown as { body?: Node[] }).body ?? []).filter(isNode)
|
|
151
|
+
|
|
152
|
+
let earlyReturnIndex = -1
|
|
153
|
+
for (let index = 0; index < statements.length; index++) {
|
|
154
|
+
if (canReturnEarly(statements[index])) {
|
|
155
|
+
earlyReturnIndex = index
|
|
156
|
+
break
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
statements.forEach((statement, index) => {
|
|
161
|
+
const inspect = (current: Node, insideControlFlow: boolean): void => {
|
|
162
|
+
const called = callName(current)
|
|
163
|
+
if (!called || !HOOK_NAME.test(called)) {
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
const line = current.loc ? current.loc.start.line : 0
|
|
167
|
+
if (insideControlFlow || CONTROL_FLOW_TYPES.has(statement.type)) {
|
|
168
|
+
violations.push({ kind: 'conditional', hook: called, fn: name, line })
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
if (earlyReturnIndex !== -1 && index > earlyReturnIndex) {
|
|
172
|
+
violations.push({ kind: 'after-early-return', hook: called, fn: name, line })
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
inspect(statement, false)
|
|
176
|
+
walkOwnScope(statement, inspect)
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Returns every Rules-of-Hooks violation in `source`.
|
|
182
|
+
*
|
|
183
|
+
* `source` must be a complete ES module — the generators emit exactly that.
|
|
184
|
+
*/
|
|
185
|
+
export function findRulesOfHooksViolations(source: string): HookViolation[] {
|
|
186
|
+
const ast = parse(source, { sourceType: 'module', plugins: ['jsx'] }) as unknown as Node
|
|
187
|
+
const violations: HookViolation[] = []
|
|
188
|
+
|
|
189
|
+
const visit = (node: Node, inferredName: string | null): void => {
|
|
190
|
+
if (FUNCTION_TYPES.has(node.type)) {
|
|
191
|
+
analyseFunction(node, functionName(node, inferredName), violations)
|
|
192
|
+
}
|
|
193
|
+
for (const child of childNodes(node)) {
|
|
194
|
+
// `const TqCountdown = (props) => {...}` — carry the binding name onto the arrow.
|
|
195
|
+
let nextName: string | null = null
|
|
196
|
+
if (node.type === 'VariableDeclarator') {
|
|
197
|
+
const id = (node as unknown as { id?: { name?: string } }).id
|
|
198
|
+
nextName = id && id.name ? id.name : null
|
|
199
|
+
}
|
|
200
|
+
visit(child, nextName)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
visit(ast, null)
|
|
205
|
+
return violations
|
|
206
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every React component the Next project generator emits as a source STRING is
|
|
3
|
+
* invisible to TypeScript, ESLint and eslint-plugin-react-hooks. This suite is
|
|
4
|
+
* the only thing standing between a Rules-of-Hooks mistake in one of those
|
|
5
|
+
* templates and a runtime crash on every published page that uses it.
|
|
6
|
+
*
|
|
7
|
+
* Regression origin: `tq-countdown` computed its `targetDate` with `useMemo`
|
|
8
|
+
* BELOW the `if (!mounted) return …` guard. React counted 7 hooks on the first
|
|
9
|
+
* render and 8 on the second and threw "Rendered more hooks than during the
|
|
10
|
+
* previous render". The countdown lives in the Navigation component, so the
|
|
11
|
+
* whole site fell through to the error boundary — on localhost and on Vercel.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { findRulesOfHooksViolations } from './_helpers/rules-of-hooks'
|
|
15
|
+
|
|
16
|
+
import { generateCountdownComponentCode } from '../src/countdown/component-generator'
|
|
17
|
+
import { generateKanbanComponentCode } from '../src/kanban/component-generator'
|
|
18
|
+
import { generateDragDropComponentCode } from '../src/drag-drop/component-generator'
|
|
19
|
+
import { generateRichTextEditorComponentCode } from '../src/rich-text-editor/component-generator'
|
|
20
|
+
import { generateMotionComponentCode } from '../src/widgets/motion-component'
|
|
21
|
+
import { generateSignatureComponentCode } from '../src/widgets/signature-component'
|
|
22
|
+
import { generateCategoriesFilterComponentCode } from '../src/widgets/categories-filter-component'
|
|
23
|
+
import { generateCategoriesMegamenuComponentCode } from '../src/widgets/categories-megamenu-component'
|
|
24
|
+
import { generateBarcodeComponentCode } from '../src/widgets/barcode-component'
|
|
25
|
+
import { generateQrCodeComponentCode } from '../src/widgets/qrcode-component'
|
|
26
|
+
import { generateEmojiPickerComponentCode } from '../src/widgets/emoji-picker-component'
|
|
27
|
+
import { generateColorPickerComponentCode } from '../src/widgets/color-picker-component'
|
|
28
|
+
import { generateFormFileInputComponentCode } from '../src/widgets/form-file-input-component'
|
|
29
|
+
import { COLLAPSIBLE_TEXT_OVERFLOW_COMPONENT_SOURCE } from '../src/collapsible-text/collapsible-text-overflow-component'
|
|
30
|
+
import { NAV_ACTIVE_LINK_COMPONENT_SOURCE } from '../src/nav-active-link/nav-active-link-component'
|
|
31
|
+
import { TRACKER_COMPONENT_SOURCE } from '../src/analytics/tracker-component'
|
|
32
|
+
|
|
33
|
+
const EMITTED_COMPONENTS: Array<[string, string]> = [
|
|
34
|
+
['tq-countdown', generateCountdownComponentCode()],
|
|
35
|
+
['tq-kanban', generateKanbanComponentCode()],
|
|
36
|
+
['tq-drag-drop', generateDragDropComponentCode()],
|
|
37
|
+
['rich-text-editor', generateRichTextEditorComponentCode()],
|
|
38
|
+
['tq-motion', generateMotionComponentCode()],
|
|
39
|
+
['tq-signature', generateSignatureComponentCode()],
|
|
40
|
+
['tq-categories-filter', generateCategoriesFilterComponentCode()],
|
|
41
|
+
['tq-categories-megamenu', generateCategoriesMegamenuComponentCode()],
|
|
42
|
+
['tq-barcode', generateBarcodeComponentCode()],
|
|
43
|
+
['tq-qrcode', generateQrCodeComponentCode()],
|
|
44
|
+
['tq-emoji-picker', generateEmojiPickerComponentCode()],
|
|
45
|
+
['tq-color-picker', generateColorPickerComponentCode()],
|
|
46
|
+
['tq-form-file-input', generateFormFileInputComponentCode()],
|
|
47
|
+
['tq-collapsible-text-overflow', COLLAPSIBLE_TEXT_OVERFLOW_COMPONENT_SOURCE],
|
|
48
|
+
['nav-active-links', NAV_ACTIVE_LINK_COMPONENT_SOURCE],
|
|
49
|
+
['analytics-tracker', TRACKER_COMPONENT_SOURCE],
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
describe('emitted React components obey the Rules of Hooks', () => {
|
|
53
|
+
it.each(EMITTED_COMPONENTS)(
|
|
54
|
+
'%s calls no hook conditionally or after an early return',
|
|
55
|
+
(_name, source) => {
|
|
56
|
+
const violations = findRulesOfHooksViolations(source)
|
|
57
|
+
expect(violations).toEqual([])
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
it('covers every component source the generator can emit', () => {
|
|
62
|
+
// A new widget added without a row here would ship unguarded.
|
|
63
|
+
expect(EMITTED_COMPONENTS).toHaveLength(16)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('the Rules-of-Hooks analyser itself', () => {
|
|
68
|
+
it('flags a hook placed after a conditional early return', () => {
|
|
69
|
+
const violations = findRulesOfHooksViolations(`
|
|
70
|
+
import React, { useState, useMemo } from 'react'
|
|
71
|
+
const Broken = ({ value }) => {
|
|
72
|
+
const [mounted, setMounted] = useState(false)
|
|
73
|
+
if (!mounted) {
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
const doubled = useMemo(() => value * 2, [value])
|
|
77
|
+
return <span>{doubled}</span>
|
|
78
|
+
}
|
|
79
|
+
export default Broken
|
|
80
|
+
`)
|
|
81
|
+
expect(violations).toEqual([
|
|
82
|
+
{ kind: 'after-early-return', hook: 'useMemo', fn: 'Broken', line: 8 },
|
|
83
|
+
])
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('flags a hook called inside a condition', () => {
|
|
87
|
+
const violations = findRulesOfHooksViolations(`
|
|
88
|
+
import React, { useEffect } from 'react'
|
|
89
|
+
const Broken = ({ enabled }) => {
|
|
90
|
+
if (enabled) {
|
|
91
|
+
useEffect(() => {}, [])
|
|
92
|
+
}
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
export default Broken
|
|
96
|
+
`)
|
|
97
|
+
expect(violations).toEqual([{ kind: 'conditional', hook: 'useEffect', fn: 'Broken', line: 5 }])
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('does not flag an early return INSIDE a hook callback', () => {
|
|
101
|
+
const violations = findRulesOfHooksViolations(`
|
|
102
|
+
import React, { useEffect, useMemo } from 'react'
|
|
103
|
+
const Fine = ({ mode, value }) => {
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (mode !== 'numeric') {
|
|
106
|
+
return undefined
|
|
107
|
+
}
|
|
108
|
+
const id = setInterval(() => {}, 1000)
|
|
109
|
+
return () => clearInterval(id)
|
|
110
|
+
}, [mode])
|
|
111
|
+
const doubled = useMemo(() => value * 2, [value])
|
|
112
|
+
return <span>{doubled}</span>
|
|
113
|
+
}
|
|
114
|
+
export default Fine
|
|
115
|
+
`)
|
|
116
|
+
expect(violations).toEqual([])
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('does not flag hooks in a sibling component declared after one that returns early', () => {
|
|
120
|
+
const violations = findRulesOfHooksViolations(`
|
|
121
|
+
import React, { useState } from 'react'
|
|
122
|
+
const First = () => {
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
125
|
+
const Second = () => {
|
|
126
|
+
const [value] = useState(0)
|
|
127
|
+
return <span>{value}</span>
|
|
128
|
+
}
|
|
129
|
+
export default Second
|
|
130
|
+
`)
|
|
131
|
+
expect(violations).toEqual([])
|
|
132
|
+
})
|
|
133
|
+
})
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import generator from '@babel/generator'
|
|
2
|
+
import * as types from '@babel/types'
|
|
3
|
+
import { createStateDataSourcePlugin } from '../src/state-data-source-plugin'
|
|
4
|
+
|
|
5
|
+
// Regression guard for the "Characters page renders empty lists" defect
|
|
6
|
+
// (GuildForge run, examples/uidl-samples/project.json): page states bound to a
|
|
7
|
+
// data source with a `{{Current User.*}}` query were silently dropped — no
|
|
8
|
+
// getStaticProps fetch is possible (the placeholder only resolves at runtime)
|
|
9
|
+
// and nothing else populated them, so every list bound to such a state stayed
|
|
10
|
+
// empty forever. The plugin must instead emit an API route + a client-side
|
|
11
|
+
// useEffect fetch keyed on the signed-in user.
|
|
12
|
+
|
|
13
|
+
const DS_ID = '8e4bd2e9-1ceb-41ee-91ea-6f2aca94076e'
|
|
14
|
+
|
|
15
|
+
const USER_QUERY =
|
|
16
|
+
"SELECT id, user_id, name FROM characters WHERE (user_id = '{{Current User.id}}' OR user_id IS NULL) ORDER BY name ASC LIMIT 20"
|
|
17
|
+
|
|
18
|
+
const makeState = (query?: string) => ({
|
|
19
|
+
type: 'array',
|
|
20
|
+
defaultValue: [] as unknown[],
|
|
21
|
+
dataSourceBinding: {
|
|
22
|
+
dataSourceId: DS_ID,
|
|
23
|
+
refPath: ['characters'],
|
|
24
|
+
},
|
|
25
|
+
...(query ? { query } : {}),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const makeJsxComponentChunk = () => ({
|
|
29
|
+
type: 'AST',
|
|
30
|
+
fileType: 'js',
|
|
31
|
+
name: 'jsx-component',
|
|
32
|
+
linkAfter: [] as string[],
|
|
33
|
+
content: types.variableDeclaration('const', [
|
|
34
|
+
types.variableDeclarator(
|
|
35
|
+
types.identifier('Characters'),
|
|
36
|
+
types.arrowFunctionExpression(
|
|
37
|
+
[],
|
|
38
|
+
types.blockStatement([types.returnStatement(types.nullLiteral())])
|
|
39
|
+
)
|
|
40
|
+
),
|
|
41
|
+
]),
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const makeStructure = (
|
|
45
|
+
stateDefinitions: Record<string, unknown>,
|
|
46
|
+
{ auth = true, dynamicRouteAttribute }: { auth?: boolean; dynamicRouteAttribute?: string } = {}
|
|
47
|
+
) => ({
|
|
48
|
+
uidl: {
|
|
49
|
+
name: 'Characters',
|
|
50
|
+
stateDefinitions,
|
|
51
|
+
node: { type: 'element', content: { elementType: 'container' } },
|
|
52
|
+
outputOptions: {
|
|
53
|
+
folderPath: [] as string[],
|
|
54
|
+
...(dynamicRouteAttribute && { dynamicRouteAttribute }),
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
chunks: [makeJsxComponentChunk()] as any[],
|
|
58
|
+
dependencies: {} as Record<string, unknown>,
|
|
59
|
+
options: {
|
|
60
|
+
dataSources: {
|
|
61
|
+
[DS_ID]: {
|
|
62
|
+
id: DS_ID,
|
|
63
|
+
type: 'teleport',
|
|
64
|
+
config: {},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
extractedResources: {} as Record<string, unknown>,
|
|
68
|
+
...(auth ? { auth: { provider: 'teleport' } } : {}),
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const componentCode = (structure: { chunks: any[] }): string => {
|
|
73
|
+
const chunk = structure.chunks.find((c) => c.name === 'jsx-component')
|
|
74
|
+
return generator(chunk.content as types.Node).code
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe('state-data-source-plugin runtime {{Current User.*}} fetch', () => {
|
|
78
|
+
const plugin = createStateDataSourcePlugin()
|
|
79
|
+
|
|
80
|
+
it('emits an API route + useEffect fetch for a Current-User-bound state', async () => {
|
|
81
|
+
const structure = makeStructure({ charactersResults: makeState(USER_QUERY) })
|
|
82
|
+
|
|
83
|
+
const result = await plugin(structure as any)
|
|
84
|
+
|
|
85
|
+
// API route extracted under pages/api/page-state with the parameterized query
|
|
86
|
+
const resources = result.options.extractedResources as Record<
|
|
87
|
+
string,
|
|
88
|
+
{ path: string[]; content: string }
|
|
89
|
+
>
|
|
90
|
+
const routeKeys = Object.keys(resources).filter((k) => k.startsWith('pages/api/page-state/'))
|
|
91
|
+
expect(routeKeys).toHaveLength(1)
|
|
92
|
+
const route = resources[routeKeys[0]]
|
|
93
|
+
expect(route.path).toEqual(['pages', 'api', 'page-state'])
|
|
94
|
+
expect(route.content).toContain('user_id = $1')
|
|
95
|
+
expect(route.content).toContain('const { currentUserId } = req.query')
|
|
96
|
+
expect(route.content).not.toContain('{{Current User.id}}')
|
|
97
|
+
|
|
98
|
+
// Client-side effect wired to the signed-in user
|
|
99
|
+
const code = componentCode(result as any)
|
|
100
|
+
expect(code).toContain('const __pageStateCtx = useGlobalContext()')
|
|
101
|
+
expect(code).toContain('useEffect(')
|
|
102
|
+
expect(code).toContain('__pageStateCtx?.currentUser')
|
|
103
|
+
expect(code).toContain('encodeURIComponent(__user?.id ?? "")')
|
|
104
|
+
expect(code).toContain('setCharactersResults(Array.isArray(__data) ? __data : [])')
|
|
105
|
+
expect(code).toContain('/api/page-state/')
|
|
106
|
+
|
|
107
|
+
// Hook + context imports registered
|
|
108
|
+
expect(result.dependencies.useEffect).toBeDefined()
|
|
109
|
+
expect(result.dependencies.useGlobalContext).toBeDefined()
|
|
110
|
+
|
|
111
|
+
// No getStaticProps fetch was emitted for the runtime state
|
|
112
|
+
const gsp = result.chunks.find((c: any) => c.name === 'getStaticProps')
|
|
113
|
+
if (gsp) {
|
|
114
|
+
expect(generator(gsp.content as types.Node).code).not.toContain('charactersResults')
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('shares one fetch effect between states with identical queries and keeps distinct queries apart', async () => {
|
|
119
|
+
const structure = makeStructure({
|
|
120
|
+
charactersResults: makeState(USER_QUERY),
|
|
121
|
+
mirror: makeState(USER_QUERY),
|
|
122
|
+
mainSpotlight: makeState(
|
|
123
|
+
"SELECT id FROM characters WHERE user_id = '{{Current User.id}}' AND is_main = TRUE LIMIT 1"
|
|
124
|
+
),
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const result = await plugin(structure as any)
|
|
128
|
+
|
|
129
|
+
const routeKeys = Object.keys(result.options.extractedResources as object).filter((k) =>
|
|
130
|
+
k.startsWith('pages/api/page-state/')
|
|
131
|
+
)
|
|
132
|
+
expect(routeKeys).toHaveLength(2)
|
|
133
|
+
|
|
134
|
+
const code = componentCode(result as any)
|
|
135
|
+
// Shared group: both setters inside the same effect
|
|
136
|
+
expect(code).toContain('setCharactersResults(')
|
|
137
|
+
expect(code).toContain('setMirror(')
|
|
138
|
+
expect(code).toContain('setMainSpotlight(')
|
|
139
|
+
expect(code.match(/useEffect\(/g)).toHaveLength(2)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('does nothing for such states when the project has no authentication', async () => {
|
|
143
|
+
const structure = makeStructure({ charactersResults: makeState(USER_QUERY) }, { auth: false })
|
|
144
|
+
|
|
145
|
+
const result = await plugin(structure as any)
|
|
146
|
+
|
|
147
|
+
expect(Object.keys(result.options.extractedResources as object)).toHaveLength(0)
|
|
148
|
+
const code = componentCode(result as any)
|
|
149
|
+
expect(code).not.toContain('useEffect(')
|
|
150
|
+
expect(result.dependencies.useEffect).toBeUndefined()
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('still skips queries with placeholders other than {{Current User.*}}', async () => {
|
|
154
|
+
const structure = makeStructure({
|
|
155
|
+
detailRows: makeState("SELECT * FROM events WHERE id = '{{Current Page Entity.id}}'"),
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
const result = await plugin(structure as any)
|
|
159
|
+
|
|
160
|
+
expect(Object.keys(result.options.extractedResources as object)).toHaveLength(0)
|
|
161
|
+
expect(componentCode(result as any)).not.toContain('useEffect(')
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('emits a route-param-driven fetch for a {{Current Page Entity.id}} query on a details page', async () => {
|
|
165
|
+
// GuildForge guild-details regression: guildMemberships was bound to an
|
|
166
|
+
// entity-scoped query; the binding was silently dropped so the members
|
|
167
|
+
// list stayed empty in the generated app while the GUI editor (which
|
|
168
|
+
// resolves the binding itself) showed it populated.
|
|
169
|
+
const entityQuery =
|
|
170
|
+
'SELECT m.id, u.name FROM memberships m LEFT JOIN users u ON m.user_id = u.id ' +
|
|
171
|
+
"WHERE m.guild_id = '{{Current Page Entity.id}}' ORDER BY u.name ASC"
|
|
172
|
+
const structure = makeStructure(
|
|
173
|
+
{
|
|
174
|
+
guildMemberships: {
|
|
175
|
+
...makeState(entityQuery),
|
|
176
|
+
dataSourceBinding: { dataSourceId: DS_ID, refPath: [] },
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
{ dynamicRouteAttribute: 'id' }
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
const result = await plugin(structure as any)
|
|
183
|
+
|
|
184
|
+
const resources = result.options.extractedResources as Record<string, { content: string }>
|
|
185
|
+
const routeKeys = Object.keys(resources).filter((k) => k.startsWith('pages/api/page-state/'))
|
|
186
|
+
expect(routeKeys).toHaveLength(1)
|
|
187
|
+
expect(resources[routeKeys[0]].content).toContain('m.guild_id = $1')
|
|
188
|
+
expect(resources[routeKeys[0]].content).toContain('const { currentPageEntityId } = req.query')
|
|
189
|
+
|
|
190
|
+
const code = componentCode(result as any)
|
|
191
|
+
expect(code).toContain('const __pageStateRouter = useRouter()')
|
|
192
|
+
expect(code).toContain('__pageStateRouter.query?.["id"]')
|
|
193
|
+
expect(code).toContain('encodeURIComponent(__entityId)')
|
|
194
|
+
expect(code).toContain('setGuildMemberships(Array.isArray(__data) ? __data : [])')
|
|
195
|
+
// Entity-only query must not require auth context
|
|
196
|
+
expect(code).not.toContain('__pageStateCtx')
|
|
197
|
+
expect(result.dependencies.useRouter).toBeDefined()
|
|
198
|
+
expect(result.dependencies.useGlobalContext).toBeUndefined()
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('works without authentication when the query only uses {{Current Page Entity.id}}', async () => {
|
|
202
|
+
const entityQuery = "SELECT * FROM memberships WHERE guild_id = '{{Current Page Entity.id}}'"
|
|
203
|
+
const structure = makeStructure(
|
|
204
|
+
{ members: makeState(entityQuery) },
|
|
205
|
+
{ auth: false, dynamicRouteAttribute: 'id' }
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
const result = await plugin(structure as any)
|
|
209
|
+
expect(
|
|
210
|
+
Object.keys(result.options.extractedResources as object).filter((k) =>
|
|
211
|
+
k.startsWith('pages/api/page-state/')
|
|
212
|
+
)
|
|
213
|
+
).toHaveLength(1)
|
|
214
|
+
expect(componentCode(result as any)).toContain('useEffect(')
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('skips a {{Current Page Entity.id}} query on a non-details page (no route param)', async () => {
|
|
218
|
+
const entityQuery = "SELECT * FROM memberships WHERE guild_id = '{{Current Page Entity.id}}'"
|
|
219
|
+
const structure = makeStructure({ members: makeState(entityQuery) })
|
|
220
|
+
|
|
221
|
+
const result = await plugin(structure as any)
|
|
222
|
+
expect(Object.keys(result.options.extractedResources as object)).toHaveLength(0)
|
|
223
|
+
expect(componentCode(result as any)).not.toContain('useEffect(')
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('combines user and entity tokens in one fetch with both guards', async () => {
|
|
227
|
+
const mixedQuery =
|
|
228
|
+
"SELECT * FROM memberships WHERE guild_id = '{{Current Page Entity.id}}' " +
|
|
229
|
+
"AND user_id = '{{Current User.id}}'"
|
|
230
|
+
const structure = makeStructure(
|
|
231
|
+
{ myMembership: makeState(mixedQuery) },
|
|
232
|
+
{ dynamicRouteAttribute: 'id' }
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
const result = await plugin(structure as any)
|
|
236
|
+
|
|
237
|
+
const resources = result.options.extractedResources as Record<string, { content: string }>
|
|
238
|
+
const routeKey = Object.keys(resources).find((k) => k.startsWith('pages/api/page-state/'))!
|
|
239
|
+
expect(resources[routeKey].content).toContain('guild_id = $1')
|
|
240
|
+
expect(resources[routeKey].content).toContain('user_id = $2')
|
|
241
|
+
expect(resources[routeKey].content).toContain('currentPageEntityId, currentUserId')
|
|
242
|
+
|
|
243
|
+
const code = componentCode(result as any)
|
|
244
|
+
expect(code).toContain('const __pageStateCtx = useGlobalContext()')
|
|
245
|
+
expect(code).toContain('const __pageStateRouter = useRouter()')
|
|
246
|
+
expect(code).toContain('if (!__user) return')
|
|
247
|
+
expect(code).toContain('if (!__entityId) return')
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('keeps build-time states in getStaticProps while runtime states get effects', async () => {
|
|
251
|
+
const structure = makeStructure({
|
|
252
|
+
allGuilds: makeState('SELECT * FROM guilds ORDER BY name ASC'),
|
|
253
|
+
myCharacters: makeState(USER_QUERY),
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
const result = await plugin(structure as any)
|
|
257
|
+
|
|
258
|
+
const gsp = result.chunks.find((c: any) => c.name === 'getStaticProps')
|
|
259
|
+
expect(gsp).toBeDefined()
|
|
260
|
+
const gspCode = generator(gsp.content as types.Node).code
|
|
261
|
+
expect(gspCode).toContain('allGuilds')
|
|
262
|
+
expect(gspCode).not.toContain('myCharacters')
|
|
263
|
+
|
|
264
|
+
const code = componentCode(result as any)
|
|
265
|
+
expect(code).toContain('setMyCharacters(')
|
|
266
|
+
expect(code).not.toContain('setAllGuilds(')
|
|
267
|
+
})
|
|
268
|
+
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"component-generator.d.ts","sourceRoot":"","sources":["../../../src/countdown/component-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,8BAA8B,QAAO,
|
|
1
|
+
{"version":3,"file":"component-generator.d.ts","sourceRoot":"","sources":["../../../src/countdown/component-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,8BAA8B,QAAO,MA4JjD,CAAA"}
|
|
@@ -15,7 +15,7 @@ exports.generateCountdownComponentCode = void 0;
|
|
|
15
15
|
* to the live countdown — so hydration matches and ticking starts client-side.
|
|
16
16
|
*/
|
|
17
17
|
var generateCountdownComponentCode = function () {
|
|
18
|
-
return "import React, { useState, useEffect, useMemo } from 'react'\nimport Countdown, { zeroPad } from 'react-countdown'\n\nconst ALL_UNITS = ['days', 'hours', 'minutes', 'seconds']\nconst DEFAULT_LABELS = { days: 'Days', hours: 'Hours', minutes: 'Minutes', seconds: 'Seconds' }\n\nconst parseCsv = (value) =>\n typeof value === 'string' ? value.split(',').map((s) => s.trim()).filter(Boolean) : []\n\nconst resolveUnits = (units) => {\n const parsed = parseCsv(units).filter((u) => ALL_UNITS.indexOf(u) !== -1)\n return parsed.length > 0 ? parsed : ALL_UNITS\n}\n\nconst resolveLabels = (units, labels) => {\n const raw = parseCsv(labels)\n return units.map((u, i) => raw[i] || DEFAULT_LABELS[u] || u)\n}\n\nconst wrapperStyle = (format) => ({\n display: 'flex',\n alignItems: 'center',\n gap: format === 'inline' ? 0 : 16,\n fontVariantNumeric: 'tabular-nums',\n})\n\nconst renderSegments = (segments, format) => {\n if (format === 'inline') {\n return (\n <div className=\"tq-countdown tq-countdown-inline\" style={wrapperStyle('inline')}>\n <span className=\"tq-countdown-value\">{segments.map((s) => s.value).join(':')}</span>\n </div>\n )\n }\n return (\n <div className=\"tq-countdown tq-countdown-blocks\" style={wrapperStyle('blocks')}>\n {segments.map((s, i) => (\n <div\n key={i}\n className=\"tq-countdown-unit\"\n style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}\n >\n <span className=\"tq-countdown-value\" style={{ fontSize: '2em', fontWeight: 700, lineHeight: 1 }}>\n {s.value}\n </span>\n {s.label ? (\n <span className=\"tq-countdown-label\" style={{ fontSize: '0.75em', opacity: 0.7 }}>\n {s.label}\n </span>\n ) : null}\n </div>\n ))}\n </div>\n )\n}\n\nconst dateSegments = (timeDelta, units, labels) => {\n const showDays = units.indexOf('days') !== -1\n const hours = showDays ? timeDelta.hours : timeDelta.days * 24 + timeDelta.hours\n const byUnit = {\n days: timeDelta.days,\n hours: hours,\n minutes: timeDelta.minutes,\n seconds: timeDelta.seconds,\n }\n return units.map((unit, i) => ({ value: zeroPad(byUnit[unit] != null ? byUnit[unit] : 0), label: labels[i] || '' }))\n}\n\nconst TqCountdown = ({\n mode = 'date',\n target,\n from = '100',\n to = '0',\n format = 'blocks',\n units,\n labels,\n onComplete = 'keep-zero',\n completeMessage = '',\n ...rest\n}) => {\n const [mounted, setMounted] = useState(false)\n const [numericStart] = useState(() => Date.now())\n const [now, setNow] = useState(() => Date.now())\n\n useEffect(() => {\n setMounted(true)\n }, [])\n\n // Numeric mode ticks via its own interval.\n useEffect(() => {\n if (mode !== 'numeric') {\n return undefined\n }\n const id = setInterval(() => setNow(Date.now()), 1000)\n return () => clearInterval(id)\n }, [mode])\n\n const resolvedUnits = useMemo(() => resolveUnits(units), [units])\n const resolvedLabels = useMemo(() => resolveLabels(resolvedUnits, labels), [resolvedUnits, labels])\n\n // Deterministic placeholder (zeros) for SSR + first paint, so hydration matches.\n if (!mounted) {\n const zeros =\n mode === 'numeric'\n ? [{ value: String(from), label: resolvedLabels[0] || '' }]\n : resolvedUnits.map((u, i) => ({ value: '00', label: resolvedLabels[i] || '' }))\n return <div {...rest}>{renderSegments(zeros, format)}</div>\n }\n\n if (mode === 'numeric') {\n const fromN = parseFloat(from) || 0\n const toN = parseFloat(to) || 0\n const elapsed = Math.max(0, Math.floor((now - numericStart) / 1000))\n const current = fromN > toN ? Math.max(toN, fromN - elapsed) : Math.min(toN, fromN + elapsed)\n const completed = current === toN\n if (completed && onComplete === 'hide') {\n return <div {...rest} />\n }\n if (completed && onComplete === 'show-message') {\n return (\n <div {...rest}>\n <span className=\"tq-countdown-complete\">{completeMessage}</span>\n </div>\n )\n }\n return <div {...rest}>{renderSegments([{ value: String(current), label: resolvedLabels[0] || '' }], format)}</div>\n }\n\n const renderer = ({ days, hours, minutes, seconds, completed }) => {\n if (completed && onComplete === 'hide') {\n return <span className=\"tq-countdown-complete\" />\n }\n if (completed && onComplete === 'show-message') {\n return <span className=\"tq-countdown-complete\">{completeMessage}</span>\n }\n const segments = dateSegments({ days, hours, minutes, seconds }, resolvedUnits, resolvedLabels)\n return renderSegments(segments, format)\n }\n\n
|
|
18
|
+
return "import React, { useState, useEffect, useMemo } from 'react'\nimport Countdown, { zeroPad } from 'react-countdown'\n\nconst ALL_UNITS = ['days', 'hours', 'minutes', 'seconds']\nconst DEFAULT_LABELS = { days: 'Days', hours: 'Hours', minutes: 'Minutes', seconds: 'Seconds' }\n\nconst parseCsv = (value) =>\n typeof value === 'string' ? value.split(',').map((s) => s.trim()).filter(Boolean) : []\n\nconst resolveUnits = (units) => {\n const parsed = parseCsv(units).filter((u) => ALL_UNITS.indexOf(u) !== -1)\n return parsed.length > 0 ? parsed : ALL_UNITS\n}\n\nconst resolveLabels = (units, labels) => {\n const raw = parseCsv(labels)\n return units.map((u, i) => raw[i] || DEFAULT_LABELS[u] || u)\n}\n\nconst wrapperStyle = (format) => ({\n display: 'flex',\n alignItems: 'center',\n gap: format === 'inline' ? 0 : 16,\n fontVariantNumeric: 'tabular-nums',\n})\n\nconst renderSegments = (segments, format) => {\n if (format === 'inline') {\n return (\n <div className=\"tq-countdown tq-countdown-inline\" style={wrapperStyle('inline')}>\n <span className=\"tq-countdown-value\">{segments.map((s) => s.value).join(':')}</span>\n </div>\n )\n }\n return (\n <div className=\"tq-countdown tq-countdown-blocks\" style={wrapperStyle('blocks')}>\n {segments.map((s, i) => (\n <div\n key={i}\n className=\"tq-countdown-unit\"\n style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}\n >\n <span className=\"tq-countdown-value\" style={{ fontSize: '2em', fontWeight: 700, lineHeight: 1 }}>\n {s.value}\n </span>\n {s.label ? (\n <span className=\"tq-countdown-label\" style={{ fontSize: '0.75em', opacity: 0.7 }}>\n {s.label}\n </span>\n ) : null}\n </div>\n ))}\n </div>\n )\n}\n\nconst dateSegments = (timeDelta, units, labels) => {\n const showDays = units.indexOf('days') !== -1\n const hours = showDays ? timeDelta.hours : timeDelta.days * 24 + timeDelta.hours\n const byUnit = {\n days: timeDelta.days,\n hours: hours,\n minutes: timeDelta.minutes,\n seconds: timeDelta.seconds,\n }\n return units.map((unit, i) => ({ value: zeroPad(byUnit[unit] != null ? byUnit[unit] : 0), label: labels[i] || '' }))\n}\n\nconst TqCountdown = ({\n mode = 'date',\n target,\n from = '100',\n to = '0',\n format = 'blocks',\n units,\n labels,\n onComplete = 'keep-zero',\n completeMessage = '',\n ...rest\n}) => {\n const [mounted, setMounted] = useState(false)\n const [numericStart] = useState(() => Date.now())\n const [now, setNow] = useState(() => Date.now())\n\n useEffect(() => {\n setMounted(true)\n }, [])\n\n // Numeric mode ticks via its own interval.\n useEffect(() => {\n if (mode !== 'numeric') {\n return undefined\n }\n const id = setInterval(() => setNow(Date.now()), 1000)\n return () => clearInterval(id)\n }, [mode])\n\n const resolvedUnits = useMemo(() => resolveUnits(units), [units])\n const resolvedLabels = useMemo(() => resolveLabels(resolvedUnits, labels), [resolvedUnits, labels])\n // Every hook must run on EVERY render: the mounted-guard and the numeric-mode\n // branch below both return early, so a hook placed after them would only run\n // on some renders (\"Rendered more hooks than during the previous render\").\n const targetDate = useMemo(() => {\n const d = new Date(target)\n return isNaN(d.getTime()) ? new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) : d\n }, [target])\n\n // Deterministic placeholder (zeros) for SSR + first paint, so hydration matches.\n if (!mounted) {\n const zeros =\n mode === 'numeric'\n ? [{ value: String(from), label: resolvedLabels[0] || '' }]\n : resolvedUnits.map((u, i) => ({ value: '00', label: resolvedLabels[i] || '' }))\n return <div {...rest}>{renderSegments(zeros, format)}</div>\n }\n\n if (mode === 'numeric') {\n const fromN = parseFloat(from) || 0\n const toN = parseFloat(to) || 0\n const elapsed = Math.max(0, Math.floor((now - numericStart) / 1000))\n const current = fromN > toN ? Math.max(toN, fromN - elapsed) : Math.min(toN, fromN + elapsed)\n const completed = current === toN\n if (completed && onComplete === 'hide') {\n return <div {...rest} />\n }\n if (completed && onComplete === 'show-message') {\n return (\n <div {...rest}>\n <span className=\"tq-countdown-complete\">{completeMessage}</span>\n </div>\n )\n }\n return <div {...rest}>{renderSegments([{ value: String(current), label: resolvedLabels[0] || '' }], format)}</div>\n }\n\n const renderer = ({ days, hours, minutes, seconds, completed }) => {\n if (completed && onComplete === 'hide') {\n return <span className=\"tq-countdown-complete\" />\n }\n if (completed && onComplete === 'show-message') {\n return <span className=\"tq-countdown-complete\">{completeMessage}</span>\n }\n const segments = dateSegments({ days, hours, minutes, seconds }, resolvedUnits, resolvedLabels)\n return renderSegments(segments, format)\n }\n\n return (\n <div {...rest}>\n <Countdown date={targetDate} renderer={renderer} overtime={false} />\n </div>\n )\n}\n\nexport default TqCountdown\n";
|
|
19
19
|
};
|
|
20
20
|
exports.generateCountdownComponentCode = generateCountdownComponentCode;
|
|
21
21
|
//# sourceMappingURL=component-generator.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"component-generator.js","sourceRoot":"","sources":["../../../src/countdown/component-generator.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;GAYG;AACI,IAAM,8BAA8B,GAAG;IAC5C,OAAO,
|
|
1
|
+
{"version":3,"file":"component-generator.js","sourceRoot":"","sources":["../../../src/countdown/component-generator.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;GAYG;AACI,IAAM,8BAA8B,GAAG;IAC5C,OAAO,6sKA0JR,CAAA;AACD,CAAC,CAAA;AA5JY,QAAA,8BAA8B,kCA4J1C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state-data-source-plugin.d.ts","sourceRoot":"","sources":["../../src/state-data-source-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,sBAAsB,EAQvB,MAAM,4BAA4B,CAAA;
|
|
1
|
+
{"version":3,"file":"state-data-source-plugin.d.ts","sourceRoot":"","sources":["../../src/state-data-source-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,sBAAsB,EAQvB,MAAM,4BAA4B,CAAA;AA0/BnC,eAAO,MAAM,2BAA2B,EAAE,sBAAsB,CAAC,EAAE,CAkKlE,CAAA"}
|