frakment 0.1.0 → 0.3.0
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/bin/index.js +3 -2
- package/package.json +1 -1
- package/src/commands/add.js +44 -8
- package/src/commands/add.test.mjs +43 -1
- package/src/commands/init.js +16 -16
- package/templates/lib/animation.ts +237 -6
package/bin/index.js
CHANGED
|
@@ -25,6 +25,7 @@ program
|
|
|
25
25
|
Examples:
|
|
26
26
|
${chalk.dim("$ frakment init")}
|
|
27
27
|
${chalk.dim("$ frakment add button")}
|
|
28
|
+
${chalk.dim("$ frakment add button bubble")}
|
|
28
29
|
${chalk.dim("$ frakment list")}
|
|
29
30
|
`
|
|
30
31
|
);
|
|
@@ -35,8 +36,8 @@ program
|
|
|
35
36
|
.action(init);
|
|
36
37
|
|
|
37
38
|
program
|
|
38
|
-
.command("add <
|
|
39
|
-
.description("Add
|
|
39
|
+
.command("add <components...>")
|
|
40
|
+
.description("Add one or more components from the frakment registry")
|
|
40
41
|
.option(
|
|
41
42
|
"--dry-run",
|
|
42
43
|
"Show planned file and dependency changes without writing"
|
package/package.json
CHANGED
package/src/commands/add.js
CHANGED
|
@@ -16,6 +16,29 @@ const DEPENDENCY_PATTERN =
|
|
|
16
16
|
/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*(?:@[0-9A-Za-z*~^<>=|.+_-]+)?$/u;
|
|
17
17
|
const MAX_REGISTRY_PAYLOAD_BYTES = 10 * 1024 * 1024;
|
|
18
18
|
|
|
19
|
+
const DEFAULT_ALIASES = {
|
|
20
|
+
components: "@/components",
|
|
21
|
+
ui: "@/components/ui",
|
|
22
|
+
utils: "@/lib/utils",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const normalizeAliases = (input) => {
|
|
26
|
+
const aliases = input ?? {};
|
|
27
|
+
const components = aliases.components ?? DEFAULT_ALIASES.components;
|
|
28
|
+
const ui = aliases.ui ?? `${components}/ui`;
|
|
29
|
+
const utils = aliases.utils ?? DEFAULT_ALIASES.utils;
|
|
30
|
+
return { components, ui, utils };
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const rewriteImports = (content, aliases) => {
|
|
34
|
+
const a = normalizeAliases(aliases);
|
|
35
|
+
let out = content;
|
|
36
|
+
out = out.replaceAll("@/components/ui/", `${a.ui}/`);
|
|
37
|
+
out = out.replaceAll("@/components/", `${a.components}/`);
|
|
38
|
+
out = out.replace(/@\/lib\/utils\b/g, a.utils);
|
|
39
|
+
return out;
|
|
40
|
+
};
|
|
41
|
+
|
|
19
42
|
const isRecord = (value) =>
|
|
20
43
|
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
44
|
|
|
@@ -194,7 +217,7 @@ export async function assertSafePath(cwd, relativePath) {
|
|
|
194
217
|
return target;
|
|
195
218
|
}
|
|
196
219
|
|
|
197
|
-
async function writeFileSafely(file, cwd, options) {
|
|
220
|
+
async function writeFileSafely(file, cwd, options, aliases) {
|
|
198
221
|
const targetPath = await assertSafePath(cwd, file.path);
|
|
199
222
|
if (existsSync(targetPath) && !options.overwrite) {
|
|
200
223
|
const { overwrite } = await prompts({
|
|
@@ -212,12 +235,21 @@ async function writeFileSafely(file, cwd, options) {
|
|
|
212
235
|
const output =
|
|
213
236
|
file.encoding === "base64"
|
|
214
237
|
? Buffer.from(file.content, "base64")
|
|
215
|
-
: file.content.replaceAll("\r\n", "\n");
|
|
238
|
+
: rewriteImports(file.content, aliases).replaceAll("\r\n", "\n");
|
|
216
239
|
await writeFile(targetPath, output);
|
|
217
240
|
return file.path;
|
|
218
241
|
}
|
|
219
242
|
|
|
220
|
-
export async function add(
|
|
243
|
+
export async function add(componentNamesInput, options) {
|
|
244
|
+
const componentNames = Array.isArray(componentNamesInput)
|
|
245
|
+
? [...new Set(componentNamesInput)]
|
|
246
|
+
: [componentNamesInput];
|
|
247
|
+
if (componentNames.length === 0) {
|
|
248
|
+
logger.error(
|
|
249
|
+
`${chalk.red("Error:")} At least one component name is required.`
|
|
250
|
+
);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
221
253
|
let cwd;
|
|
222
254
|
const preflight = ora("Preflight checks").start();
|
|
223
255
|
try {
|
|
@@ -237,7 +269,9 @@ export async function add(componentName, options) {
|
|
|
237
269
|
let config;
|
|
238
270
|
let registryUrl;
|
|
239
271
|
try {
|
|
240
|
-
|
|
272
|
+
for (const name of componentNames) {
|
|
273
|
+
assertComponentName(name);
|
|
274
|
+
}
|
|
241
275
|
const configRaw = await readFile(configPath, "utf-8");
|
|
242
276
|
config = JSON.parse(configRaw.replace(/^\uFEFF/u, ""));
|
|
243
277
|
registryUrl = getRegistryUrl(options.registry ?? config.registry);
|
|
@@ -249,7 +283,8 @@ export async function add(componentName, options) {
|
|
|
249
283
|
}
|
|
250
284
|
|
|
251
285
|
const registrySpinner = ora("Checking registry").start();
|
|
252
|
-
const
|
|
286
|
+
const roots = new Set(componentNames);
|
|
287
|
+
const queue = [...roots];
|
|
253
288
|
const fetched = new Map();
|
|
254
289
|
const npmDeps = new Map();
|
|
255
290
|
|
|
@@ -277,6 +312,7 @@ export async function add(componentName, options) {
|
|
|
277
312
|
}
|
|
278
313
|
}
|
|
279
314
|
registrySpinner.succeed("Checking registry.");
|
|
315
|
+
const aliases = normalizeAliases(config.aliases);
|
|
280
316
|
const isSrcLayout = config.tailwind?.css?.startsWith("src/") ?? false;
|
|
281
317
|
const resolveTargetPath = (p) => {
|
|
282
318
|
if (!isSrcLayout) {
|
|
@@ -288,8 +324,8 @@ export async function add(componentName, options) {
|
|
|
288
324
|
return `src/${p}`;
|
|
289
325
|
};
|
|
290
326
|
|
|
291
|
-
const order = [...fetched.keys()].filter((n) => n
|
|
292
|
-
order.push(
|
|
327
|
+
const order = [...fetched.keys()].filter((n) => !roots.has(n));
|
|
328
|
+
order.push(...roots);
|
|
293
329
|
const written = [];
|
|
294
330
|
|
|
295
331
|
const getMissingDependencies = async () => {
|
|
@@ -320,7 +356,7 @@ export async function add(componentName, options) {
|
|
|
320
356
|
for (const name of order) {
|
|
321
357
|
for (const file of fetched.get(name).files ?? []) {
|
|
322
358
|
const targetFile = { ...file, path: resolveTargetPath(file.path) };
|
|
323
|
-
const result = await writeFileSafely(targetFile, cwd, options);
|
|
359
|
+
const result = await writeFileSafely(targetFile, cwd, options, aliases);
|
|
324
360
|
if (result) {
|
|
325
361
|
written.push(result);
|
|
326
362
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
assertSafePath,
|
|
5
|
+
rewriteImports,
|
|
6
|
+
validateRegistryPayload,
|
|
7
|
+
} from "./add.js";
|
|
4
8
|
|
|
5
9
|
const unsignedPayload = {
|
|
6
10
|
dependencies: ["@frakment/react@^0.0.3", "motion@^14.10.0"],
|
|
@@ -87,3 +91,41 @@ describe("registry target paths", () => {
|
|
|
87
91
|
).rejects.toThrow("Absolute path rejected");
|
|
88
92
|
});
|
|
89
93
|
});
|
|
94
|
+
|
|
95
|
+
describe("rewriteImports", () => {
|
|
96
|
+
const src = `import { Button } from "@/components/ui/button";
|
|
97
|
+
import { cn } from "@/lib/utils";
|
|
98
|
+
import { Card } from "@/components/card";
|
|
99
|
+
export { cn, Button, Card };
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
it("is a no-op with default aliases", () => {
|
|
103
|
+
expect(rewriteImports(src, undefined)).toBe(src);
|
|
104
|
+
expect(rewriteImports(src, {})).toBe(src);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("rewrites ui, components, and utils prefixes", () => {
|
|
108
|
+
const out = rewriteImports(src, {
|
|
109
|
+
components: "~/components",
|
|
110
|
+
ui: "~/ui",
|
|
111
|
+
utils: "~/lib/cn",
|
|
112
|
+
});
|
|
113
|
+
expect(out).toContain('from "~/ui/button"');
|
|
114
|
+
expect(out).toContain('from "~/lib/cn"');
|
|
115
|
+
expect(out).toContain('from "~/components/card"');
|
|
116
|
+
expect(out).not.toContain("@/components/ui/");
|
|
117
|
+
expect(out).not.toContain("@/lib/utils");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("derives ui from components when ui is omitted", () => {
|
|
121
|
+
const out = rewriteImports(src, { components: "~/components" });
|
|
122
|
+
expect(out).toContain('from "~/components/ui/button"');
|
|
123
|
+
expect(out).toContain('from "~/components/card"');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("does not rewrite unrelated @/lib paths", () => {
|
|
127
|
+
const input = 'import { springSoft } from "@/lib/animation";\n';
|
|
128
|
+
const out = rewriteImports(input, { utils: "~/lib/cn" });
|
|
129
|
+
expect(out).toBe(input);
|
|
130
|
+
});
|
|
131
|
+
});
|
package/src/commands/init.js
CHANGED
|
@@ -135,28 +135,28 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
135
135
|
logger.log(" - lib/utils.ts");
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
if (
|
|
138
|
+
if (existsSync(path.join(cwd, "lib/animation.ts"))) {
|
|
139
|
+
const existing = await readFile(
|
|
140
|
+
path.join(cwd, "lib/animation.ts"),
|
|
141
|
+
"utf-8"
|
|
142
|
+
);
|
|
143
|
+
if (!existing.includes("parseAnimation")) {
|
|
144
|
+
logger.warn(
|
|
145
|
+
"lib/animation.ts is outdated (missing animation DSL parser). Delete it and re-run `frakment init`."
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
139
149
|
const s = ora("Creating lib/animation.ts").start();
|
|
140
150
|
await mkdir(path.join(cwd, "lib"), { recursive: true });
|
|
141
|
-
await
|
|
142
|
-
path.join(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
export const easeInOut = [0.4, 0, 0.2, 1] as const
|
|
146
|
-
export const easeInOutQuad = [0.45, 0, 0.55, 1] as const
|
|
147
|
-
export const easeOutExpo = [0.16, 1, 0.3, 1] as const
|
|
148
|
-
export const easeSmoothOut = [0.32, 0.72, 0, 1] as const
|
|
149
|
-
|
|
150
|
-
export const durationFast = 0.2
|
|
151
|
-
export const durationNormal = 0.3
|
|
152
|
-
export const durationSlow = 0.45
|
|
153
|
-
|
|
154
|
-
export const delayTight = 0.02
|
|
155
|
-
`
|
|
151
|
+
const animation = await readFile(
|
|
152
|
+
path.join(TEMPLATES_DIR, "lib/animation.ts"),
|
|
153
|
+
"utf-8"
|
|
156
154
|
);
|
|
155
|
+
await writeFile(path.join(cwd, "lib/animation.ts"), animation);
|
|
157
156
|
s.succeed("Created 1 file:");
|
|
158
157
|
logger.log(" - lib/animation.ts");
|
|
159
158
|
}
|
|
159
|
+
|
|
160
160
|
const pkgPath = path.join(cwd, "package.json");
|
|
161
161
|
const pkgRaw = await readFile(pkgPath, "utf-8");
|
|
162
162
|
const pkg = JSON.parse(pkgRaw.replace(/^\uFEFF/u, ""));
|
|
@@ -1,12 +1,243 @@
|
|
|
1
|
-
|
|
1
|
+
// Design tokens
|
|
2
|
+
export const springSoft = {
|
|
2
3
|
damping: 30,
|
|
3
4
|
stiffness: 200,
|
|
4
5
|
type: "spring",
|
|
5
6
|
} as const;
|
|
6
7
|
|
|
7
|
-
export const
|
|
8
|
-
export const
|
|
9
|
-
export const
|
|
8
|
+
export const easeInOut = [0.4, 0, 0.2, 1] as const;
|
|
9
|
+
export const easeInOutQuad = [0.45, 0, 0.55, 1] as const;
|
|
10
|
+
export const easeOutExpo = [0.16, 1, 0.3, 1] as const;
|
|
11
|
+
export const easeSmoothOut = [0.32, 0.72, 0, 1] as const;
|
|
10
12
|
|
|
11
|
-
export const
|
|
12
|
-
export const
|
|
13
|
+
export const durationFast = 0.2;
|
|
14
|
+
export const durationNormal = 0.3;
|
|
15
|
+
export const durationSlow = 0.45;
|
|
16
|
+
|
|
17
|
+
export const delayTight = 0.02;
|
|
18
|
+
|
|
19
|
+
// Animation DSL parser
|
|
20
|
+
export interface AnimationKeyframes {
|
|
21
|
+
times?: number[];
|
|
22
|
+
values: number[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AnimationConfig {
|
|
26
|
+
keyframes?: AnimationKeyframes;
|
|
27
|
+
transition: Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const NAMED_EASINGS: Record<string, readonly number[]> = {
|
|
31
|
+
easeInOut,
|
|
32
|
+
easeInOutQuad,
|
|
33
|
+
easeOutExpo,
|
|
34
|
+
easeSmoothOut,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const MOTION_NATIVE_EASINGS = new Set([
|
|
38
|
+
"linear",
|
|
39
|
+
"circIn",
|
|
40
|
+
"circOut",
|
|
41
|
+
"circInOut",
|
|
42
|
+
"backIn",
|
|
43
|
+
"backOut",
|
|
44
|
+
"backInOut",
|
|
45
|
+
"anticipate",
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
const DURATION_RE = /^(\d+(?:\.\d+)?)(ms|s)?$/;
|
|
49
|
+
const DELAY_RE = /^delay\(([^)]+)\)$/;
|
|
50
|
+
const REPEAT_RE = /^repeat\(([^)]+)\)$/;
|
|
51
|
+
|
|
52
|
+
function warn(message: string): void {
|
|
53
|
+
if (typeof console !== "undefined" && typeof console.warn === "function") {
|
|
54
|
+
console.warn(`[motion-dsl] ${message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function toSeconds(raw: string): number | null {
|
|
59
|
+
const m = raw.trim().match(DURATION_RE);
|
|
60
|
+
if (!m) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const n = Number.parseFloat(m[1]);
|
|
64
|
+
if (Number.isNaN(n)) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return m[2] === "ms" ? n / 1000 : n;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseNumbers(raw: string): number[] {
|
|
71
|
+
return raw.split(",").map((s) => Number.parseFloat(s.trim()));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function tokenize(input: string): string[] {
|
|
75
|
+
const out: string[] = [];
|
|
76
|
+
let depth = 0;
|
|
77
|
+
let cur = "";
|
|
78
|
+
for (const ch of input) {
|
|
79
|
+
if (ch === "(" || ch === "[") {
|
|
80
|
+
depth++;
|
|
81
|
+
}
|
|
82
|
+
if (ch === ")" || ch === "]") {
|
|
83
|
+
depth--;
|
|
84
|
+
}
|
|
85
|
+
if (/\s/.test(ch) && depth === 0) {
|
|
86
|
+
if (cur) {
|
|
87
|
+
out.push(cur);
|
|
88
|
+
}
|
|
89
|
+
cur = "";
|
|
90
|
+
} else {
|
|
91
|
+
cur += ch;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (cur) {
|
|
95
|
+
out.push(cur);
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function parseAnimation(input?: false | string): AnimationConfig | null {
|
|
101
|
+
if (input === undefined || input === false) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
if (typeof input !== "string") {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const trimmed = input.trim();
|
|
109
|
+
if (!trimmed) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const tokens = tokenize(trimmed);
|
|
114
|
+
if (tokens.length === 0) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const head = tokens[0];
|
|
119
|
+
let config: AnimationConfig;
|
|
120
|
+
|
|
121
|
+
if (head.startsWith("spring(")) {
|
|
122
|
+
const args = parseNumbers(head.slice(7, -1));
|
|
123
|
+
if (args.length !== 2 || args.some(Number.isNaN)) {
|
|
124
|
+
warn(`invalid spring: "${head}" - expected spring(stiffness, damping)`);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
config = {
|
|
128
|
+
transition: { damping: args[1], stiffness: args[0], type: "spring" },
|
|
129
|
+
};
|
|
130
|
+
} else if (head.startsWith("inertia(")) {
|
|
131
|
+
const args = parseNumbers(head.slice(8, -1));
|
|
132
|
+
if (args.length !== 2 || args.some(Number.isNaN)) {
|
|
133
|
+
warn(
|
|
134
|
+
`invalid inertia: "${head}" - expected inertia(power, timeConstant)`
|
|
135
|
+
);
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
config = {
|
|
139
|
+
transition: { power: args[0], timeConstant: args[1], type: "inertia" },
|
|
140
|
+
};
|
|
141
|
+
} else if (head.startsWith("cubic-bezier(")) {
|
|
142
|
+
const args = parseNumbers(head.slice(13, -1));
|
|
143
|
+
if (args.length !== 4 || args.some(Number.isNaN)) {
|
|
144
|
+
warn(`invalid cubic-bezier: "${head}" - expected 4 numbers`);
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
config = {
|
|
148
|
+
transition: {
|
|
149
|
+
ease: args as [number, number, number, number],
|
|
150
|
+
type: "tween",
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
} else if (head.startsWith("steps(")) {
|
|
154
|
+
const args = parseNumbers(head.slice(6, -1));
|
|
155
|
+
if (args.length !== 1 || Number.isNaN(args[0])) {
|
|
156
|
+
warn(`invalid steps: "${head}" - expected steps(count)`);
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
config = { transition: { ease: "linear", type: "tween" } };
|
|
160
|
+
} else if (head.startsWith("keyframes(")) {
|
|
161
|
+
const inner = head.slice(10, -1);
|
|
162
|
+
const arrays = inner.match(/\[[^\]]*\]/g);
|
|
163
|
+
if (!arrays || arrays.length === 0) {
|
|
164
|
+
warn(
|
|
165
|
+
`invalid keyframes: "${head}" - expected keyframes([values], [times]?)`
|
|
166
|
+
);
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
const values = arrays[0]
|
|
170
|
+
.slice(1, -1)
|
|
171
|
+
.split(",")
|
|
172
|
+
.map((s) => Number.parseFloat(s.trim()));
|
|
173
|
+
|
|
174
|
+
if (values.some(Number.isNaN)) {
|
|
175
|
+
warn(`invalid keyframes values: "${arrays[0]}"`);
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
const times = arrays[1]
|
|
179
|
+
? arrays[1]
|
|
180
|
+
.slice(1, -1)
|
|
181
|
+
.split(",")
|
|
182
|
+
.map((s) => Number.parseFloat(s.trim()))
|
|
183
|
+
: undefined;
|
|
184
|
+
if (times?.some(Number.isNaN)) {
|
|
185
|
+
warn(`invalid keyframes times: "${arrays[1]}"`);
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
config = { keyframes: { times, values }, transition: { type: "tween" } };
|
|
189
|
+
} else if (head in NAMED_EASINGS) {
|
|
190
|
+
config = {
|
|
191
|
+
transition: {
|
|
192
|
+
ease: [...NAMED_EASINGS[head]] as [number, number, number, number],
|
|
193
|
+
type: "tween",
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
} else if (MOTION_NATIVE_EASINGS.has(head)) {
|
|
197
|
+
config = { transition: { ease: head, type: "tween" } };
|
|
198
|
+
} else {
|
|
199
|
+
warn(`unknown easing or syntax: "${head}"`);
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const isTween = config.transition.type === "tween";
|
|
204
|
+
|
|
205
|
+
for (const token of tokens.slice(1)) {
|
|
206
|
+
const dur = toSeconds(token);
|
|
207
|
+
if (dur !== null) {
|
|
208
|
+
if (isTween) {
|
|
209
|
+
(config.transition as { duration?: number }).duration = dur;
|
|
210
|
+
}
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const delayMatch = token.match(DELAY_RE);
|
|
214
|
+
if (delayMatch) {
|
|
215
|
+
const d = toSeconds(delayMatch[1]);
|
|
216
|
+
if (d === null) {
|
|
217
|
+
warn(`invalid delay: "${token}"`);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
(config.transition as { delay?: number }).delay = d;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const repeatMatch = token.match(REPEAT_RE);
|
|
224
|
+
if (repeatMatch) {
|
|
225
|
+
const v = repeatMatch[1].trim();
|
|
226
|
+
if (v === "infinite") {
|
|
227
|
+
(config.transition as { repeat?: number }).repeat =
|
|
228
|
+
Number.POSITIVE_INFINITY;
|
|
229
|
+
} else {
|
|
230
|
+
const n = Number.parseInt(v, 10);
|
|
231
|
+
if (Number.isNaN(n) || n < 0) {
|
|
232
|
+
warn(`invalid repeat: "${token}"`);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
(config.transition as { repeat?: number }).repeat = n;
|
|
236
|
+
}
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
warn(`unexpected token: "${token}"`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return config;
|
|
243
|
+
}
|