next-yak 0.0.18 → 0.0.20
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/loaders/__tests__/classifier.test.ts +133 -0
- package/loaders/__tests__/tsloader.test.ts +143 -48
- package/loaders/babel-yak-plugin.cjs +61 -10
- package/loaders/cssloader.cjs +9 -11
- package/loaders/lib/getStyledComponentName.cjs +2 -2
- package/loaders/lib/getYakImports.cjs +29 -20
- package/loaders/lib/quasiClassifier.cjs +78 -21
- package/loaders/lib/replaceQuasiExpressionTokens.cjs +17 -7
- package/loaders/lib/stripCssComments.cjs +49 -42
- package/loaders/tsloader.cjs +39 -23
- package/package.json +1 -1
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import quasiClassifier from '../lib/quasiClassifier.cjs';
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
|
|
4
|
+
describe('quasiClassifier', () => {
|
|
5
|
+
it('should classify empty quasi', () => {
|
|
6
|
+
expect(quasiClassifier('', [])).toEqual({
|
|
7
|
+
empty: true,
|
|
8
|
+
unknownSelector: false,
|
|
9
|
+
insideCssValue: false,
|
|
10
|
+
currentNestingScopes: [],
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
it("should recognize a comment as empty", () => {
|
|
14
|
+
expect(quasiClassifier("/* comment */", [])).toEqual({
|
|
15
|
+
empty: true,
|
|
16
|
+
unknownSelector: false,
|
|
17
|
+
insideCssValue: false,
|
|
18
|
+
currentNestingScopes: [],
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
it("should find incomplete css - unknownSelector", () => {
|
|
22
|
+
expect(quasiClassifier("{ color: blue;", [])).toEqual({
|
|
23
|
+
empty: false,
|
|
24
|
+
unknownSelector: true,
|
|
25
|
+
insideCssValue: false,
|
|
26
|
+
currentNestingScopes: [],
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
it("should find incomplete css - insideCssValue", () => {
|
|
30
|
+
expect(quasiClassifier("color: ", [
|
|
31
|
+
".foo"
|
|
32
|
+
])).toEqual({
|
|
33
|
+
empty: false,
|
|
34
|
+
unknownSelector: false,
|
|
35
|
+
insideCssValue: true,
|
|
36
|
+
currentNestingScopes: [".foo"],
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
it("should find incomplete css - insideCssValue after a media query", () => {
|
|
40
|
+
expect(quasiClassifier("} .foo { color: ", [
|
|
41
|
+
"@media (min-width: 640px)"
|
|
42
|
+
])).toEqual({
|
|
43
|
+
empty: false,
|
|
44
|
+
unknownSelector: false,
|
|
45
|
+
insideCssValue: true,
|
|
46
|
+
currentNestingScopes: [".foo"],
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
it("should find incomplete css - insideCssValue in a multi value", () => {
|
|
50
|
+
expect(quasiClassifier("} .foo { transition: 200ms ", [
|
|
51
|
+
"@media (min-width: 640px)"
|
|
52
|
+
])).toEqual({
|
|
53
|
+
empty: false,
|
|
54
|
+
unknownSelector: false,
|
|
55
|
+
insideCssValue: true,
|
|
56
|
+
currentNestingScopes: [".foo"],
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
it("should find nesting scopes", () => {
|
|
60
|
+
expect(quasiClassifier(`
|
|
61
|
+
.foo {
|
|
62
|
+
.bar {
|
|
63
|
+
@supports (display: grid) {
|
|
64
|
+
`, [
|
|
65
|
+
"@media (min-width: 640px)"
|
|
66
|
+
])).toEqual({
|
|
67
|
+
empty: false,
|
|
68
|
+
unknownSelector: false,
|
|
69
|
+
insideCssValue: false,
|
|
70
|
+
currentNestingScopes: [
|
|
71
|
+
"@media (min-width: 640px)",
|
|
72
|
+
".foo",
|
|
73
|
+
".bar",
|
|
74
|
+
"@supports (display: grid)",
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
it("should find nesting scopes with properties", () => {
|
|
79
|
+
expect(quasiClassifier(`
|
|
80
|
+
.foo {
|
|
81
|
+
color: purple;
|
|
82
|
+
.x {
|
|
83
|
+
color: green;
|
|
84
|
+
}
|
|
85
|
+
.bar {
|
|
86
|
+
color: orange;
|
|
87
|
+
@supports (display: grid) {
|
|
88
|
+
color: blue;
|
|
89
|
+
`, [
|
|
90
|
+
"@media (min-width: 640px)"
|
|
91
|
+
])).toEqual({
|
|
92
|
+
empty: false,
|
|
93
|
+
unknownSelector: false,
|
|
94
|
+
insideCssValue: false,
|
|
95
|
+
currentNestingScopes: [
|
|
96
|
+
"@media (min-width: 640px)",
|
|
97
|
+
".foo",
|
|
98
|
+
".bar",
|
|
99
|
+
"@supports (display: grid)",
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should find nesting scopes with properties and closing nestings", () => {
|
|
105
|
+
expect(quasiClassifier(`
|
|
106
|
+
.foo {
|
|
107
|
+
/* }}}}} */
|
|
108
|
+
color: purple;
|
|
109
|
+
.x {
|
|
110
|
+
color: green;
|
|
111
|
+
}
|
|
112
|
+
.bar {
|
|
113
|
+
:before {
|
|
114
|
+
content: '{';
|
|
115
|
+
}
|
|
116
|
+
color: orange;
|
|
117
|
+
@supports (display: grid) {
|
|
118
|
+
color: blue;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
`, [
|
|
122
|
+
"@media (min-width: 640px)"
|
|
123
|
+
])).toEqual({
|
|
124
|
+
empty: false,
|
|
125
|
+
unknownSelector: false,
|
|
126
|
+
insideCssValue: false,
|
|
127
|
+
currentNestingScopes: [
|
|
128
|
+
"@media (min-width: 640px)",
|
|
129
|
+
".foo",
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -166,13 +166,12 @@ const FancyButton = styled(Button)\`
|
|
|
166
166
|
const FancyButton = styled(Button)(__styleYak.FancyButton_2);"
|
|
167
167
|
`);
|
|
168
168
|
});
|
|
169
|
-
});
|
|
170
169
|
|
|
171
|
-
it("should support attrs on intrinsic elements", async () => {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
170
|
+
it("should support attrs on intrinsic elements", async () => {
|
|
171
|
+
expect(
|
|
172
|
+
await tsloader.call(
|
|
173
|
+
loaderContext,
|
|
174
|
+
`
|
|
176
175
|
import { styled } from "next-yak";
|
|
177
176
|
|
|
178
177
|
const headline = styled.input.attrs({
|
|
@@ -181,21 +180,21 @@ const headline = styled.input.attrs({
|
|
|
181
180
|
color: red;
|
|
182
181
|
\`;
|
|
183
182
|
`
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
)
|
|
184
|
+
).toMatchInlineSnapshot(`
|
|
186
185
|
"import { styled } from \\"next-yak\\";
|
|
187
186
|
import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
|
|
188
187
|
const headline = styled.input.attrs({
|
|
189
188
|
type: \\"text\\"
|
|
190
189
|
})(__styleYak.headline_0);"
|
|
191
190
|
`);
|
|
192
|
-
});
|
|
191
|
+
});
|
|
193
192
|
|
|
194
|
-
it("should support attrs on wrapped elements", async () => {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
193
|
+
it("should support attrs on wrapped elements", async () => {
|
|
194
|
+
expect(
|
|
195
|
+
await tsloader.call(
|
|
196
|
+
loaderContext,
|
|
197
|
+
`
|
|
199
198
|
import { styled } from "next-yak";
|
|
200
199
|
|
|
201
200
|
const headline = styled.input\`
|
|
@@ -208,8 +207,8 @@ const newHeadline = styled(headline).attrs({
|
|
|
208
207
|
color: black;
|
|
209
208
|
\`;
|
|
210
209
|
`
|
|
211
|
-
|
|
212
|
-
|
|
210
|
+
)
|
|
211
|
+
).toMatchInlineSnapshot(`
|
|
213
212
|
"import { styled } from \\"next-yak\\";
|
|
214
213
|
import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
|
|
215
214
|
const headline = styled.input(__styleYak.headline_0);
|
|
@@ -217,13 +216,13 @@ const newHeadline = styled(headline).attrs({
|
|
|
217
216
|
type: \\"text\\"
|
|
218
217
|
})(__styleYak.newHeadline_1);"
|
|
219
218
|
`);
|
|
220
|
-
});
|
|
219
|
+
});
|
|
221
220
|
|
|
222
|
-
it("should support css variables with spaces", async () => {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
221
|
+
it("should support css variables with spaces", async () => {
|
|
222
|
+
expect(
|
|
223
|
+
await tsloader.call(
|
|
224
|
+
loaderContext,
|
|
225
|
+
`
|
|
227
226
|
import styles from "./page.module.css";
|
|
228
227
|
import { css } from "next-yak";
|
|
229
228
|
import { easing } from "styleguide";
|
|
@@ -235,8 +234,8 @@ const headline = css\`
|
|
|
235
234
|
\${css\`color: blue\`}
|
|
236
235
|
\`;
|
|
237
236
|
`
|
|
238
|
-
|
|
239
|
-
|
|
237
|
+
)
|
|
238
|
+
).toMatchInlineSnapshot(`
|
|
240
239
|
"import styles from \\"./page.module.css\\";
|
|
241
240
|
import { css } from \\"next-yak\\";
|
|
242
241
|
import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
|
|
@@ -250,13 +249,13 @@ const headline = css\`
|
|
|
250
249
|
}
|
|
251
250
|
});"
|
|
252
251
|
`);
|
|
253
|
-
});
|
|
252
|
+
});
|
|
254
253
|
|
|
255
|
-
it("should convert keyframes", async () => {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
254
|
+
it("should convert keyframes", async () => {
|
|
255
|
+
expect(
|
|
256
|
+
await tsloader.call(
|
|
257
|
+
loaderContext,
|
|
258
|
+
`
|
|
260
259
|
import styles from "./page.module.css";
|
|
261
260
|
import { styled, keyframes } from "next-yak";
|
|
262
261
|
|
|
@@ -273,8 +272,8 @@ const FadeInButton = styled.button\`
|
|
|
273
272
|
animation: 1s \${fadeIn} ease-out;
|
|
274
273
|
\`
|
|
275
274
|
`
|
|
276
|
-
|
|
277
|
-
|
|
275
|
+
)
|
|
276
|
+
).toMatchInlineSnapshot(`
|
|
278
277
|
"import styles from \\"./page.module.css\\";
|
|
279
278
|
import { styled, keyframes } from \\"next-yak\\";
|
|
280
279
|
import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
|
|
@@ -285,13 +284,13 @@ const FadeInButton = styled.button\`
|
|
|
285
284
|
}
|
|
286
285
|
});"
|
|
287
286
|
`);
|
|
288
|
-
});
|
|
287
|
+
});
|
|
289
288
|
|
|
290
|
-
it("should allow to target components", async () => {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
289
|
+
it("should allow to target components", async () => {
|
|
290
|
+
expect(
|
|
291
|
+
await tsloader.call(
|
|
292
|
+
loaderContext,
|
|
293
|
+
`
|
|
295
294
|
import { styled, keyframes } from "next-yak";
|
|
296
295
|
|
|
297
296
|
const Link = styled.a\`
|
|
@@ -317,21 +316,21 @@ const Wrapper = styled.div\`
|
|
|
317
316
|
\`
|
|
318
317
|
|
|
319
318
|
`
|
|
320
|
-
|
|
321
|
-
|
|
319
|
+
)
|
|
320
|
+
).toMatchInlineSnapshot(`
|
|
322
321
|
"import { styled, keyframes } from \\"next-yak\\";
|
|
323
322
|
import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
|
|
324
323
|
const Link = styled.a(__styleYak.Link_0);
|
|
325
324
|
const Icon = styled.svg(__styleYak.Icon_1);
|
|
326
325
|
const Wrapper = styled.div(__styleYak.Wrapper_2);"
|
|
327
326
|
`);
|
|
328
|
-
});
|
|
327
|
+
});
|
|
329
328
|
|
|
330
|
-
it("should allow to target components even if they don't have styles", async () => {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
329
|
+
it("should allow to target components even if they don't have styles", async () => {
|
|
330
|
+
expect(
|
|
331
|
+
await tsloader.call(
|
|
332
|
+
loaderContext,
|
|
333
|
+
`
|
|
335
334
|
import { styled, keyframes } from "next-yak";
|
|
336
335
|
|
|
337
336
|
const Link = styled.a\`
|
|
@@ -347,12 +346,108 @@ const Wrapper = styled.div\`
|
|
|
347
346
|
\`
|
|
348
347
|
|
|
349
348
|
`
|
|
350
|
-
|
|
351
|
-
|
|
349
|
+
)
|
|
350
|
+
).toMatchInlineSnapshot(`
|
|
352
351
|
"import { styled, keyframes } from \\"next-yak\\";
|
|
353
352
|
import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
|
|
354
353
|
const Link = styled.a();
|
|
355
354
|
const Icon = styled.svg(__styleYak.Icon_1);
|
|
356
355
|
const Wrapper = styled.div(__styleYak.Wrapper_2);"
|
|
357
356
|
`);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("should show error when mixin is used in nested selector", async () => {
|
|
360
|
+
await expect(() =>
|
|
361
|
+
tsloader.call(
|
|
362
|
+
loaderContext,
|
|
363
|
+
`
|
|
364
|
+
import { styled, css } from "next-yak";
|
|
365
|
+
|
|
366
|
+
const bold = css\`
|
|
367
|
+
font-weight: bold;
|
|
368
|
+
\`
|
|
369
|
+
|
|
370
|
+
const Icon = styled.div\`
|
|
371
|
+
@media (min-width: 640px) {
|
|
372
|
+
.bar {
|
|
373
|
+
\${bold}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
\`
|
|
377
|
+
`
|
|
378
|
+
)
|
|
379
|
+
).rejects.toThrowErrorMatchingInlineSnapshot(`
|
|
380
|
+
"/some/special/path/page.tsx: Expressions are not allowed inside nested selectors:
|
|
381
|
+
line 11: found \\"bold\\" inside \\"@media (min-width: 640px) { .bar {\\""
|
|
382
|
+
`);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("should show error when mixin is used in nested selector inside a css", async () => {
|
|
386
|
+
await expect(() =>
|
|
387
|
+
tsloader.call(
|
|
388
|
+
loaderContext,
|
|
389
|
+
`
|
|
390
|
+
import { styled, css } from "next-yak";
|
|
391
|
+
|
|
392
|
+
const bold = css\`
|
|
393
|
+
font-weight: bold;
|
|
394
|
+
\`
|
|
395
|
+
|
|
396
|
+
const Icon = styled.div\`
|
|
397
|
+
@media (min-width: 640px) {
|
|
398
|
+
.bar {
|
|
399
|
+
\${() => css\`\${bold}\`}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
\`
|
|
403
|
+
`
|
|
404
|
+
)
|
|
405
|
+
).rejects.toThrowErrorMatchingInlineSnapshot(`
|
|
406
|
+
"/some/special/path/page.tsx: Expressions are not allowed inside nested selectors:
|
|
407
|
+
line 11: found Expression inside \\"@media (min-width: 640px) { .bar {\\""
|
|
408
|
+
`);
|
|
409
|
+
});
|
|
410
|
+
it("should show error when a dynamic selector is used", async () => {
|
|
411
|
+
await expect(() =>
|
|
412
|
+
tsloader.call(
|
|
413
|
+
loaderContext,
|
|
414
|
+
`
|
|
415
|
+
import { styled, css } from "next-yak";
|
|
416
|
+
|
|
417
|
+
const test = "bar";
|
|
418
|
+
|
|
419
|
+
const Icon = styled.div\`
|
|
420
|
+
\${test} {
|
|
421
|
+
font-weight: bold;
|
|
422
|
+
}
|
|
423
|
+
\`
|
|
424
|
+
`
|
|
425
|
+
)
|
|
426
|
+
).rejects.toThrowErrorMatchingInlineSnapshot(`
|
|
427
|
+
"/some/special/path/page.tsx: Expressions are not allowed as selectors:
|
|
428
|
+
line 7: found \${test}"
|
|
429
|
+
`);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("should show error when a dynamic selector is used after a comma", async () => {
|
|
433
|
+
await expect(() =>
|
|
434
|
+
tsloader.call(
|
|
435
|
+
loaderContext,
|
|
436
|
+
`
|
|
437
|
+
import { styled, css } from "next-yak";
|
|
438
|
+
|
|
439
|
+
const test = "bar";
|
|
440
|
+
|
|
441
|
+
const Icon = styled.div\`
|
|
442
|
+
\${test}, baz {
|
|
443
|
+
font-weight: bold;
|
|
444
|
+
}
|
|
445
|
+
\`
|
|
446
|
+
`
|
|
447
|
+
)
|
|
448
|
+
).rejects.toThrowErrorMatchingInlineSnapshot(`
|
|
449
|
+
"/some/special/path/page.tsx: Expressions are not allowed as selectors:
|
|
450
|
+
line 7: found \${test}"
|
|
451
|
+
`);
|
|
452
|
+
});
|
|
358
453
|
});
|
|
@@ -197,15 +197,41 @@ module.exports = function (babel, options) {
|
|
|
197
197
|
// Replace the tagged template expression with a call to the 'styled' function
|
|
198
198
|
const newArguments = new Set();
|
|
199
199
|
const quasis = path.node.quasi.quasis;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
)
|
|
200
|
+
/** @type {string[]} */
|
|
201
|
+
let currentNestingScopes = [];
|
|
202
|
+
const quasiTypes = quasis.map((quasi) => {
|
|
203
|
+
const classification = quasiClassifier(
|
|
204
|
+
quasi.value.raw,
|
|
205
|
+
currentNestingScopes
|
|
206
|
+
);
|
|
207
|
+
currentNestingScopes = classification.currentNestingScopes;
|
|
208
|
+
return classification;
|
|
209
|
+
});
|
|
203
210
|
const expressions = path.node.quasi.expressions;
|
|
204
211
|
|
|
205
212
|
let cssVariablesInlineStyle;
|
|
206
213
|
|
|
207
214
|
for (let i = 0; i < quasis.length; i++) {
|
|
208
|
-
|
|
215
|
+
const type = quasiTypes[i];
|
|
216
|
+
if (type.unknownSelector) {
|
|
217
|
+
const expression = expressions[i - 1];
|
|
218
|
+
if (!expression) {
|
|
219
|
+
throw new Error(`Invalid css "${quasis[i].value.raw}"`);
|
|
220
|
+
}
|
|
221
|
+
let errorText = "Expressions are not allowed as selectors";
|
|
222
|
+
const line = expression.loc?.start.line || -1;
|
|
223
|
+
if (expression.start && expression.end) {
|
|
224
|
+
errorText += `:\n${
|
|
225
|
+
line !== -1 ? `line ${line}:` : ""
|
|
226
|
+
} found \${${this.file.code.slice(
|
|
227
|
+
expression.start,
|
|
228
|
+
expression.end
|
|
229
|
+
)}}`;
|
|
230
|
+
}
|
|
231
|
+
throw new InvalidPositionError(errorText);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (type.empty) {
|
|
209
235
|
const expression = expressions[i];
|
|
210
236
|
if (expression) {
|
|
211
237
|
newArguments.add(expression);
|
|
@@ -225,11 +251,7 @@ module.exports = function (babel, options) {
|
|
|
225
251
|
while (i < quasis.length - 1) {
|
|
226
252
|
const type = quasiTypes[i];
|
|
227
253
|
// expressions after a partial css are converted into css variables
|
|
228
|
-
if (
|
|
229
|
-
type.partialStart ||
|
|
230
|
-
type.partialEnd ||
|
|
231
|
-
(isMerging && type.empty)
|
|
232
|
-
) {
|
|
254
|
+
if (type.insideCssValue || (isMerging && type.empty)) {
|
|
233
255
|
isMerging = true;
|
|
234
256
|
// expression: `x`
|
|
235
257
|
// { style: { --v0: x}}
|
|
@@ -254,6 +276,8 @@ module.exports = function (babel, options) {
|
|
|
254
276
|
hashedFile = murmurhash2_32_gc(relativePath);
|
|
255
277
|
}
|
|
256
278
|
|
|
279
|
+
// expression: `x`
|
|
280
|
+
// { style: { --v0: x}}
|
|
257
281
|
cssVariablesInlineStyle.properties.push(
|
|
258
282
|
t.objectProperty(
|
|
259
283
|
t.stringLiteral(`--🦬${hashedFile}${this.varIndex++}`),
|
|
@@ -261,10 +285,26 @@ module.exports = function (babel, options) {
|
|
|
261
285
|
)
|
|
262
286
|
);
|
|
263
287
|
} else if (type.empty) {
|
|
264
|
-
// empty quasis can be ignored
|
|
288
|
+
// empty quasis can be ignored in typescript
|
|
265
289
|
// e.g. `transition: color ${duration} ${easing};`
|
|
290
|
+
// ^
|
|
266
291
|
} else {
|
|
267
292
|
if (expressions[i]) {
|
|
293
|
+
if (quasiTypes[i].currentNestingScopes.length > 0) {
|
|
294
|
+
const errorExpression = expressions[i];
|
|
295
|
+
const name =
|
|
296
|
+
errorExpression.type === "Identifier"
|
|
297
|
+
? `"${errorExpression.name}"`
|
|
298
|
+
: "Expression";
|
|
299
|
+
const line = errorExpression.loc?.start.line || -1;
|
|
300
|
+
throw new InvalidPositionError(
|
|
301
|
+
`Expressions are not allowed inside nested selectors:\n${
|
|
302
|
+
line !== -1 ? `line ${line}: ` : ""
|
|
303
|
+
}found ${name} inside "${quasiTypes[
|
|
304
|
+
i
|
|
305
|
+
].currentNestingScopes.join(" { ")} {"`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
268
308
|
newArguments.add(expressions[i]);
|
|
269
309
|
}
|
|
270
310
|
break;
|
|
@@ -303,3 +343,14 @@ module.exports = function (babel, options) {
|
|
|
303
343
|
},
|
|
304
344
|
};
|
|
305
345
|
};
|
|
346
|
+
|
|
347
|
+
class InvalidPositionError extends Error {
|
|
348
|
+
/**
|
|
349
|
+
* @param {string} message
|
|
350
|
+
*/
|
|
351
|
+
constructor(message) {
|
|
352
|
+
super(message);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
module.exports.InvalidPositionError = InvalidPositionError;
|
package/loaders/cssloader.cjs
CHANGED
|
@@ -34,7 +34,7 @@ module.exports = async function cssLoader(source) {
|
|
|
34
34
|
imports.forEach(({ localName, importedName }) => {
|
|
35
35
|
replaces[localName] = constantValues[importedName];
|
|
36
36
|
});
|
|
37
|
-
})
|
|
37
|
+
}),
|
|
38
38
|
);
|
|
39
39
|
|
|
40
40
|
// parse source with babel
|
|
@@ -127,7 +127,7 @@ module.exports = async function cssLoader(source) {
|
|
|
127
127
|
const isStyledLiteral =
|
|
128
128
|
t.isMemberExpression(tag) &&
|
|
129
129
|
t.isIdentifier(
|
|
130
|
-
/** @type {babel.types.MemberExpression} */ (tag).object
|
|
130
|
+
/** @type {babel.types.MemberExpression} */ (tag).object,
|
|
131
131
|
) &&
|
|
132
132
|
/** @type {babel.types.Identifier} */ (
|
|
133
133
|
/** @type {babel.types.MemberExpression} */ (tag).object
|
|
@@ -136,7 +136,7 @@ module.exports = async function cssLoader(source) {
|
|
|
136
136
|
const isStyledCall =
|
|
137
137
|
t.isCallExpression(tag) &&
|
|
138
138
|
t.isIdentifier(
|
|
139
|
-
/** @type {babel.types.CallExpression} */ (tag).callee
|
|
139
|
+
/** @type {babel.types.CallExpression} */ (tag).callee,
|
|
140
140
|
) &&
|
|
141
141
|
/** @type {babel.types.Identifier} */ (
|
|
142
142
|
/** @type {babel.types.CallExpression} */ (tag).callee
|
|
@@ -176,7 +176,7 @@ module.exports = async function cssLoader(source) {
|
|
|
176
176
|
}
|
|
177
177
|
return false;
|
|
178
178
|
},
|
|
179
|
-
t
|
|
179
|
+
t,
|
|
180
180
|
);
|
|
181
181
|
|
|
182
182
|
// Keep the same selector for all quasis belonging to the same css block
|
|
@@ -184,14 +184,12 @@ module.exports = async function cssLoader(source) {
|
|
|
184
184
|
const literalSelector = localIdent(
|
|
185
185
|
variableName,
|
|
186
186
|
literalSelectorIndex,
|
|
187
|
-
isKeyFrameLiteral ? "keyframes" : "selector"
|
|
187
|
+
isKeyFrameLiteral ? "keyframes" : "selector",
|
|
188
188
|
);
|
|
189
189
|
|
|
190
190
|
// Replace the tagged template expression with a call to the 'styled' function
|
|
191
191
|
const quasis = path.node.quasi.quasis;
|
|
192
|
-
const quasiTypes = quasis.map((quasi) =>
|
|
193
|
-
quasiClassifier(quasi.value.raw)
|
|
194
|
-
);
|
|
192
|
+
const quasiTypes = quasis.map((quasi) => quasiClassifier(quasi.value.raw, []));
|
|
195
193
|
|
|
196
194
|
for (let i = 0; i < quasis.length; i++) {
|
|
197
195
|
const quasi = quasis[i];
|
|
@@ -206,8 +204,8 @@ module.exports = async function cssLoader(source) {
|
|
|
206
204
|
const type = quasiTypes[i];
|
|
207
205
|
// expressions after a partial css are converted into css variables
|
|
208
206
|
if (
|
|
209
|
-
type.
|
|
210
|
-
type.
|
|
207
|
+
type.unknownSelector ||
|
|
208
|
+
type.insideCssValue ||
|
|
211
209
|
(isMerging && type.empty)
|
|
212
210
|
) {
|
|
213
211
|
isMerging = true;
|
|
@@ -243,7 +241,7 @@ module.exports = async function cssLoader(source) {
|
|
|
243
241
|
if (isStyledLiteral || isStyledCall || isAttrsCall) {
|
|
244
242
|
variableNameToStyledClassName.set(
|
|
245
243
|
variableName,
|
|
246
|
-
localIdent(variableName, literalSelectorIndex, "selector")
|
|
244
|
+
localIdent(variableName, literalSelectorIndex, "selector"),
|
|
247
245
|
);
|
|
248
246
|
}
|
|
249
247
|
},
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
const getStyledComponentName = (taggedTemplateExpressionPath) => {
|
|
15
15
|
const variableDeclaratorPath = taggedTemplateExpressionPath.findParent(
|
|
16
|
-
(path) => path.isVariableDeclarator()
|
|
16
|
+
(path) => path.isVariableDeclarator(),
|
|
17
17
|
);
|
|
18
18
|
if (
|
|
19
19
|
!variableDeclaratorPath ||
|
|
@@ -22,7 +22,7 @@ const getStyledComponentName = (taggedTemplateExpressionPath) => {
|
|
|
22
22
|
) {
|
|
23
23
|
throw new Error(
|
|
24
24
|
"Could not find variable declaration for styled component at " +
|
|
25
|
-
taggedTemplateExpressionPath.node.loc
|
|
25
|
+
taggedTemplateExpressionPath.node.loc,
|
|
26
26
|
);
|
|
27
27
|
}
|
|
28
28
|
return variableDeclaratorPath.node.id.name;
|
|
@@ -1,38 +1,47 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
3
|
* Finds all imports in a given code string which import from a .yak file
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
5
|
* Uses regex to work with typescript and javascript
|
|
6
6
|
* Does not support lazy imports
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
8
|
* @param {string} code
|
|
9
9
|
* @returns { { imports: { localName: string, importedName: string }[], from: string }[] }
|
|
10
10
|
*/
|
|
11
11
|
const getYakImports = (code) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
|
|
12
|
+
const codeWithoutComments = code.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
13
|
+
const allImports = codeWithoutComments.matchAll(
|
|
14
|
+
/(^|\n|;)\s*import\s+(?:(\w+(?:\s+as\s+\w+)?)\s*,?\s*)?(?:{([^}]*)})?\s+from\s+["']([^'"]+\.yak)["'](;|\n)/g,
|
|
15
|
+
);
|
|
16
|
+
return [...allImports].map(([, , defaultImport, namedImports, from]) => {
|
|
17
|
+
// parse named imports to { localName: string, importedName: string }[]
|
|
18
|
+
const imports =
|
|
19
|
+
namedImports?.split(",").map((namedImport) => {
|
|
20
|
+
const [importedName, localName = importedName] = namedImport
|
|
21
|
+
.replace(/^type\s+/, "")
|
|
22
|
+
.trim()
|
|
23
|
+
.split(/\s+as\s+/);
|
|
24
|
+
return { localName, importedName };
|
|
25
|
+
}) ?? [];
|
|
26
|
+
// parse default import to { localName: string, importedName: string }[]
|
|
27
|
+
if (defaultImport) {
|
|
28
|
+
imports.push(parseDefaultImport(defaultImport));
|
|
29
|
+
}
|
|
30
|
+
return { imports, from };
|
|
31
|
+
});
|
|
26
32
|
};
|
|
27
33
|
|
|
28
34
|
/**
|
|
29
35
|
* Parses a default import string
|
|
30
36
|
* @param {string} defaultImport
|
|
31
37
|
* @returns {{ localName: string, importedName: string }}
|
|
32
|
-
*/
|
|
38
|
+
*/
|
|
33
39
|
function parseDefaultImport(defaultImport) {
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
const defaultImportArray = defaultImport.split(/\s+as\s+/);
|
|
41
|
+
return {
|
|
42
|
+
localName: defaultImportArray[1] ?? defaultImportArray[0],
|
|
43
|
+
importedName: defaultImportArray[0],
|
|
44
|
+
};
|
|
36
45
|
}
|
|
37
46
|
|
|
38
|
-
module.exports = getYakImports;
|
|
47
|
+
module.exports = getYakImports;
|
|
@@ -3,31 +3,88 @@ const stripCssComments = require("./stripCssComments.cjs");
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Checks a quasiValue and returns its type
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* - empty: no expressions, no text
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
11
|
-
* @param {string} quasiValue
|
|
8
|
+
* - unknownSelector: starts with a `{` e.g. `{ opacity: 0.5; }` or `,` e.g. `, bar { ... }`
|
|
9
|
+
* - insideCssValue: does not end with a `{` or `}` or `;` e.g. `color: `
|
|
10
|
+
*
|
|
11
|
+
* @param {string} quasiValue
|
|
12
|
+
* @param {string[]} currentNestingScopes - the current nesting scope
|
|
13
|
+
*
|
|
12
14
|
* @returns {{
|
|
13
15
|
* empty: boolean,
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
+
* unknownSelector: boolean,
|
|
17
|
+
* insideCssValue: boolean,
|
|
18
|
+
* currentNestingScopes: string[],
|
|
16
19
|
* }}
|
|
17
20
|
*/
|
|
18
|
-
module.exports = function quasiClassifier(quasiValue) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
partialStart: false,
|
|
24
|
-
partialEnd: false,
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
21
|
+
module.exports = function quasiClassifier(quasiValue, currentNestingScopes) {
|
|
22
|
+
// TODO - for better performance we could move the comment skipping logic
|
|
23
|
+
// directly in the for loop below instead of calling stripCssComments
|
|
24
|
+
const trimmedCssString = stripCssComments(quasiValue).trim();
|
|
25
|
+
if (trimmedCssString === "") {
|
|
28
26
|
return {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
empty: true,
|
|
28
|
+
unknownSelector: false,
|
|
29
|
+
insideCssValue: false,
|
|
30
|
+
currentNestingScopes,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** @type {'"' | "'" | false} */
|
|
34
|
+
let isInsideString = false;
|
|
35
|
+
let currentCharacter = "";
|
|
36
|
+
let newNestingLevel = [...currentNestingScopes];
|
|
37
|
+
let currentSelector = "";
|
|
38
|
+
for (let index = 0; index < trimmedCssString.length; index++) {
|
|
39
|
+
currentCharacter = trimmedCssString[index];
|
|
40
|
+
if (
|
|
41
|
+
trimmedCssString[index - 1] !== "\\" &&
|
|
42
|
+
(currentCharacter === '"' || currentCharacter === "'")
|
|
43
|
+
) {
|
|
44
|
+
if (isInsideString === currentCharacter) {
|
|
45
|
+
isInsideString = false;
|
|
46
|
+
} else if (!isInsideString) {
|
|
47
|
+
isInsideString = currentCharacter;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (isInsideString) {
|
|
51
|
+
continue;
|
|
32
52
|
}
|
|
33
|
-
|
|
53
|
+
if (currentCharacter === "{") {
|
|
54
|
+
const selector = currentSelector.trim();
|
|
55
|
+
if (selector !== "") {
|
|
56
|
+
newNestingLevel.push(selector);
|
|
57
|
+
}
|
|
58
|
+
// after an opening bracket a new selector might start e.g.:
|
|
59
|
+
// .class {
|
|
60
|
+
// .nested-class {
|
|
61
|
+
currentSelector = "";
|
|
62
|
+
} else if (currentCharacter === "}") {
|
|
63
|
+
newNestingLevel.pop();
|
|
64
|
+
// after a closing bracket a new selector might start e.g.:
|
|
65
|
+
// .class {
|
|
66
|
+
// color: red;
|
|
67
|
+
// }
|
|
68
|
+
// .other-class {
|
|
69
|
+
currentSelector = "";
|
|
70
|
+
} else if (currentCharacter === ";") {
|
|
71
|
+
// after a semi-colon a nested selector might start e.g.:
|
|
72
|
+
// .class {
|
|
73
|
+
// color: red;
|
|
74
|
+
// .nested-class {
|
|
75
|
+
currentSelector = "";
|
|
76
|
+
} else {
|
|
77
|
+
currentSelector += currentCharacter;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
empty: false,
|
|
83
|
+
unknownSelector: trimmedCssString[0] === "{" || trimmedCssString[0] === ",",
|
|
84
|
+
insideCssValue:
|
|
85
|
+
currentCharacter !== "{" &&
|
|
86
|
+
currentCharacter !== "}" &&
|
|
87
|
+
currentCharacter !== ";",
|
|
88
|
+
currentNestingScopes: newNestingLevel,
|
|
89
|
+
};
|
|
90
|
+
};
|
|
@@ -35,10 +35,13 @@ module.exports = function replaceTokensInQuasiExpressions(quasi, replacer, t) {
|
|
|
35
35
|
}
|
|
36
36
|
replaceExpressionAndMergeQuasis(quasi, i, replacement);
|
|
37
37
|
i--;
|
|
38
|
-
}
|
|
38
|
+
}
|
|
39
39
|
// replace member expressions e.g. ${query.xs}
|
|
40
40
|
// replace deeply nested member expressions e.g. ${query.xs.min}
|
|
41
|
-
else if (
|
|
41
|
+
else if (
|
|
42
|
+
t.isMemberExpression(expression) &&
|
|
43
|
+
t.isIdentifier(expression.object)
|
|
44
|
+
) {
|
|
42
45
|
/** @type {any} */
|
|
43
46
|
let replacement = replacer(expression.object.name);
|
|
44
47
|
if (replacement === false) {
|
|
@@ -60,7 +63,7 @@ module.exports = function replaceTokensInQuasiExpressions(quasi, replacer, t) {
|
|
|
60
63
|
i--;
|
|
61
64
|
}
|
|
62
65
|
}
|
|
63
|
-
}
|
|
66
|
+
};
|
|
64
67
|
|
|
65
68
|
/**
|
|
66
69
|
* Replace tokens with predefined values
|
|
@@ -69,9 +72,16 @@ module.exports = function replaceTokensInQuasiExpressions(quasi, replacer, t) {
|
|
|
69
72
|
* @param {unknown} replacement
|
|
70
73
|
*/
|
|
71
74
|
function replaceExpressionAndMergeQuasis(quasi, expressionIndex, replacement) {
|
|
72
|
-
const stringReplacement =
|
|
75
|
+
const stringReplacement =
|
|
76
|
+
typeof replacement === "string"
|
|
77
|
+
? replacement
|
|
78
|
+
: replacement == null
|
|
79
|
+
? ""
|
|
80
|
+
: JSON.stringify(replacement);
|
|
73
81
|
quasi.expressions.splice(expressionIndex, 1);
|
|
74
|
-
quasi.quasis[expressionIndex].value.raw +=
|
|
75
|
-
|
|
82
|
+
quasi.quasis[expressionIndex].value.raw +=
|
|
83
|
+
stringReplacement + quasi.quasis[expressionIndex + 1].value.raw;
|
|
84
|
+
quasi.quasis[expressionIndex].value.cooked +=
|
|
85
|
+
stringReplacement + quasi.quasis[expressionIndex + 1].value.cooked;
|
|
76
86
|
quasi.quasis.splice(expressionIndex + 1, 1);
|
|
77
|
-
}
|
|
87
|
+
}
|
|
@@ -1,51 +1,58 @@
|
|
|
1
1
|
/// @ts-check
|
|
2
2
|
// from https://github.com/sindresorhus/strip-css-comments/tree/main
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* @param {string} cssString
|
|
4
|
+
*
|
|
5
|
+
* @param {string} cssString
|
|
6
6
|
*/
|
|
7
7
|
module.exports = function stripCssComments(cssString) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
/** @type {string | false} */
|
|
9
|
+
let isInsideString = false;
|
|
10
|
+
let currentCharacter = "";
|
|
11
|
+
let comment = "";
|
|
12
|
+
let returnValue = "";
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
for (let index = 0; index < cssString.length; index++) {
|
|
15
|
+
currentCharacter = cssString[index];
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
if (
|
|
18
|
+
cssString[index - 1] !== "\\" &&
|
|
19
|
+
(currentCharacter === '"' || currentCharacter === "'")
|
|
20
|
+
) {
|
|
21
|
+
if (isInsideString === currentCharacter) {
|
|
22
|
+
isInsideString = false;
|
|
23
|
+
} else if (!isInsideString) {
|
|
24
|
+
isInsideString = currentCharacter;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
// Find beginning of `/*` type comment
|
|
29
|
+
if (
|
|
30
|
+
!isInsideString &&
|
|
31
|
+
currentCharacter === "/" &&
|
|
32
|
+
cssString[index + 1] === "*"
|
|
33
|
+
) {
|
|
34
|
+
let index2 = index + 2;
|
|
28
35
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
36
|
+
// Iterate over comment
|
|
37
|
+
for (; index2 < cssString.length; index2++) {
|
|
38
|
+
// Find end of comment
|
|
39
|
+
if (cssString[index2] === "*" && cssString[index2 + 1] === "/") {
|
|
40
|
+
if (cssString[index2 + 2] === "\n") {
|
|
41
|
+
index2++;
|
|
42
|
+
} else if (cssString[index2 + 2] + cssString[index2 + 3] === "\r\n") {
|
|
43
|
+
index2 += 2;
|
|
44
|
+
}
|
|
45
|
+
comment = "";
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
// Store comment text
|
|
49
|
+
comment += cssString[index2];
|
|
50
|
+
}
|
|
51
|
+
// Resume iteration over CSS string from the end of the comment
|
|
52
|
+
index = index2 + 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
returnValue += currentCharacter;
|
|
56
|
+
}
|
|
57
|
+
return returnValue;
|
|
58
|
+
};
|
package/loaders/tsloader.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/// @ts-check
|
|
2
2
|
const babel = require("@babel/core");
|
|
3
3
|
const getYakImports = require("./lib/getYakImports.cjs");
|
|
4
|
+
const InvalidPositionError = require("./babel-yak-plugin.cjs").InvalidPositionError;
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Loader for typescript files that use yak, it replaces the css template literal with a call to the 'styled' function
|
|
@@ -20,34 +21,49 @@ module.exports = async function tsloader(source) {
|
|
|
20
21
|
const isYakFile = /\.yak\.(j|t)sx?$/.test(resourcePath.matches);
|
|
21
22
|
// The user may import constants from a .yak file
|
|
22
23
|
// e.g. import { primary } from './colors.yak'
|
|
23
|
-
//
|
|
24
|
+
//
|
|
24
25
|
// However .yak files inside .yak files are not be compiled
|
|
25
26
|
// to avoid performance overhead
|
|
26
|
-
const importedYakConstantNames = isYakFile
|
|
27
|
-
|
|
27
|
+
const importedYakConstantNames = isYakFile
|
|
28
|
+
? []
|
|
29
|
+
: getYakImports(source)
|
|
30
|
+
.map(({ imports }) => imports.map(({ localName }) => localName))
|
|
31
|
+
.flat(2);
|
|
32
|
+
const replaces = Object.fromEntries(
|
|
33
|
+
importedYakConstantNames.map((name) => [name, null])
|
|
34
|
+
);
|
|
28
35
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
/** @type {babel.BabelFileResult | null} */
|
|
37
|
+
let result = null;
|
|
38
|
+
try {
|
|
39
|
+
// Compile the typescript file with babel - this will:
|
|
40
|
+
// - inject the import to the css-module (with .yak.module.css extension)
|
|
41
|
+
// - replace the css template literal with styles from the css-module
|
|
42
|
+
result = babel.transformSync(source, {
|
|
43
|
+
filename: resourcePath,
|
|
44
|
+
configFile: false,
|
|
45
|
+
plugins: [
|
|
46
|
+
[
|
|
47
|
+
"@babel/plugin-syntax-typescript",
|
|
48
|
+
{ isTSX: this.resourcePath.endsWith(".tsx") },
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
require.resolve("./babel-yak-plugin.cjs"),
|
|
52
|
+
{
|
|
53
|
+
replaces,
|
|
54
|
+
rootContext,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
39
57
|
],
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
],
|
|
48
|
-
});
|
|
58
|
+
});
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error instanceof InvalidPositionError) {
|
|
61
|
+
return callback(new Error(error.message));
|
|
62
|
+
}
|
|
63
|
+
return callback(new Error("babel transform failed"));
|
|
64
|
+
}
|
|
49
65
|
if (!result?.code) {
|
|
50
|
-
|
|
66
|
+
return callback(new Error("babel transform failed"));
|
|
51
67
|
}
|
|
52
68
|
return callback(null, result.code, result.map);
|
|
53
69
|
};
|