@saykit/transform-jsx 0.0.0 → 0.1.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/README.md +25 -0
- package/dist/generator.d.mts +7 -0
- package/dist/generator.mjs +26 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.mjs +96 -0
- package/dist/parser.d.mts +10 -0
- package/dist/parser.mjs +114 -0
- package/package.json +64 -3
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @saykit/transform-jsx
|
|
2
|
+
|
|
3
|
+
> JSX and TSX transformer for [SayKit](https://saykit.js.org).
|
|
4
|
+
|
|
5
|
+
Extracts and rewrites the `<Say>` macro (and its `<Say.Plural>`, `<Say.Ordinal>`, `<Say.Select>` sub-components) in `.jsx` and `.tsx` files. Used inside a `saykit.config.ts` bucket alongside [`@saykit/transform-js`](https://github.com/k0d13/saykit/tree/main/packages/transform-js).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add -D @saykit/transform-jsx
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts title="saykit.config.ts"
|
|
16
|
+
import js from '@saykit/transform-js';
|
|
17
|
+
import jsx from '@saykit/transform-jsx';
|
|
18
|
+
|
|
19
|
+
// inside a bucket:
|
|
20
|
+
transformer: [js(), jsx()],
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Documentation
|
|
24
|
+
|
|
25
|
+
See [saykit.js.org](https://saykit.js.org).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { CompositeMessage } from "@saykit/config/features/messages";
|
|
2
|
+
import * as t from "@babel/types";
|
|
3
|
+
|
|
4
|
+
//#region src/generator.d.ts
|
|
5
|
+
declare function generateSayJSXElement(message: CompositeMessage): t.JSXElement;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { generateSayJSXElement };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage } from "@saykit/config/features/messages";
|
|
2
|
+
import * as t from "@babel/types";
|
|
3
|
+
//#region src/generator.ts
|
|
4
|
+
function generateSayJSXElement(message) {
|
|
5
|
+
const id = message.descriptor.id ?? message.toHashString();
|
|
6
|
+
const children = generateChildExpressions(message.children);
|
|
7
|
+
const attributes = [t.jsxAttribute(t.jsxIdentifier("id"), t.stringLiteral(id)), ...children.map(([k, e]) => t.jsxAttribute(t.jsxIdentifier(Number.isNaN(+k) ? k : `_${k}`), t.jsxExpressionContainer(e)))];
|
|
8
|
+
return t.jsxElement(t.jsxOpeningElement(t.jsxIdentifier("Say"), attributes, true), null, []);
|
|
9
|
+
}
|
|
10
|
+
function generateChildExpressions(messages) {
|
|
11
|
+
return messages.reduce((c, m) => {
|
|
12
|
+
if (m instanceof ArgumentMessage) c.push([String(m.identifier), m.expression]);
|
|
13
|
+
if (m instanceof ElementMessage) {
|
|
14
|
+
c.push([String(m.identifier), m.expression]);
|
|
15
|
+
c.push(...generateChildExpressions(m.children));
|
|
16
|
+
}
|
|
17
|
+
if (m instanceof ChoiceMessage) {
|
|
18
|
+
c.push([String(m.identifier), m.expression]);
|
|
19
|
+
c.push(...generateChildExpressions(m.branches.map((b) => b.value)));
|
|
20
|
+
}
|
|
21
|
+
if (m instanceof CompositeMessage) c.push(...generateChildExpressions(m.children));
|
|
22
|
+
return c;
|
|
23
|
+
}, []);
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { generateSayJSXElement };
|
package/dist/index.d.mts
ADDED
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { generateSayJSXElement } from "./generator.mjs";
|
|
2
|
+
import { parseJSXElement } from "./parser.mjs";
|
|
3
|
+
import { generate } from "@babel/generator";
|
|
4
|
+
import * as parser from "@babel/parser";
|
|
5
|
+
import traverse_ from "@babel/traverse";
|
|
6
|
+
import { assignSequenceIdentifiers } from "@saykit/config/features/messages";
|
|
7
|
+
import { generateSayCallExpression } from "@saykit/transform-js/generator";
|
|
8
|
+
import { parseExpression } from "@saykit/transform-js/parser";
|
|
9
|
+
//#region src/index.ts
|
|
10
|
+
const traverse = traverse_.default || traverse_;
|
|
11
|
+
function createProgram(code, id) {
|
|
12
|
+
const program = parser.parse(code, {
|
|
13
|
+
sourceType: "module",
|
|
14
|
+
sourceFilename: id,
|
|
15
|
+
plugins: ["typescript", "jsx"],
|
|
16
|
+
allowImportExportEverywhere: false,
|
|
17
|
+
allowReturnOutsideFunction: true,
|
|
18
|
+
ranges: true,
|
|
19
|
+
attachComment: true,
|
|
20
|
+
tokens: true
|
|
21
|
+
});
|
|
22
|
+
return Object.assign(program, { toString() {
|
|
23
|
+
return generate(program, {
|
|
24
|
+
retainLines: true,
|
|
25
|
+
comments: true,
|
|
26
|
+
compact: false
|
|
27
|
+
}).code;
|
|
28
|
+
} });
|
|
29
|
+
}
|
|
30
|
+
function createJsxTransformer() {
|
|
31
|
+
return {
|
|
32
|
+
match(id) {
|
|
33
|
+
return [".jsx", ".tsx"].some((e) => id.endsWith(e));
|
|
34
|
+
},
|
|
35
|
+
extract(code, id) {
|
|
36
|
+
const program = createProgram(code, id);
|
|
37
|
+
const messages = [];
|
|
38
|
+
traverse(program, {
|
|
39
|
+
Expression(path) {
|
|
40
|
+
path.node.leadingComments = path.node.leadingComments ?? [];
|
|
41
|
+
const message = parseExpression(path.node);
|
|
42
|
+
if (message) {
|
|
43
|
+
assignSequenceIdentifiers(message, { current: 0 });
|
|
44
|
+
messages.push(message);
|
|
45
|
+
path.skip();
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
JSXElement(path) {
|
|
49
|
+
path.node.leadingComments = path.node.leadingComments ?? [];
|
|
50
|
+
const message = parseJSXElement(path.node);
|
|
51
|
+
if (message) {
|
|
52
|
+
assignSequenceIdentifiers(message, { current: 0 });
|
|
53
|
+
messages.push(message);
|
|
54
|
+
path.skip();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return messages.map((message) => ({
|
|
59
|
+
message: message.toICUString(),
|
|
60
|
+
translation: void 0,
|
|
61
|
+
id: message.descriptor.id,
|
|
62
|
+
context: message.descriptor.context,
|
|
63
|
+
comments: message.comments,
|
|
64
|
+
references: message.references
|
|
65
|
+
}));
|
|
66
|
+
},
|
|
67
|
+
transform(code, id) {
|
|
68
|
+
const program = createProgram(code, id);
|
|
69
|
+
traverse(program, {
|
|
70
|
+
Expression(path) {
|
|
71
|
+
path.node.leadingComments = path.node.leadingComments ?? [];
|
|
72
|
+
const message = parseExpression(path.node);
|
|
73
|
+
if (message) {
|
|
74
|
+
assignSequenceIdentifiers(message, { current: 0 });
|
|
75
|
+
const replacement = generateSayCallExpression(message);
|
|
76
|
+
path.replaceWith(replacement);
|
|
77
|
+
path.skip();
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
JSXElement(path) {
|
|
81
|
+
path.node.leadingComments = path.node.leadingComments ?? [];
|
|
82
|
+
const message = parseJSXElement(path.node);
|
|
83
|
+
if (message) {
|
|
84
|
+
assignSequenceIdentifiers(message, { current: 0 });
|
|
85
|
+
const replacement = generateSayJSXElement(message);
|
|
86
|
+
path.replaceWith(replacement);
|
|
87
|
+
path.skip();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
return program.toString();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
export { createJsxTransformer as default };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { CompositeMessage, ElementMessage } from "@saykit/config/features/messages";
|
|
2
|
+
import * as t from "@babel/types";
|
|
3
|
+
|
|
4
|
+
//#region src/parser.d.ts
|
|
5
|
+
declare function parseJSXContainerElement(element: t.JSXElement): CompositeMessage | null;
|
|
6
|
+
declare function parseJSXOpeningElement(element: t.JSXOpeningElement): CompositeMessage | null;
|
|
7
|
+
declare function parseJSXElement(element: t.JSXElement, fallback?: false): CompositeMessage | null;
|
|
8
|
+
declare function parseJSXElement(element: t.JSXElement, fallback: true): CompositeMessage | ElementMessage;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { parseJSXContainerElement, parseJSXElement, parseJSXOpeningElement };
|
package/dist/parser.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, ElementMessage, LiteralMessage } from "@saykit/config/features/messages";
|
|
2
|
+
import * as t from "@babel/types";
|
|
3
|
+
//#region src/parser.ts
|
|
4
|
+
function parseJSXContainerElement(element) {
|
|
5
|
+
if (element.openingElement.selfClosing) return null;
|
|
6
|
+
const processed = processJSXOpeningElement(element.openingElement);
|
|
7
|
+
if (!processed) return null;
|
|
8
|
+
const [accessor] = processed;
|
|
9
|
+
const children = element.children.reduce((p, c, i, a) => {
|
|
10
|
+
if (t.isJSXText(c)) {
|
|
11
|
+
let text = c.value.replace(/\s+/g, " ");
|
|
12
|
+
if (i === 0) text = text.trimStart();
|
|
13
|
+
if (i === a.length - 1) text = text.trimEnd();
|
|
14
|
+
if (text) p.push(new LiteralMessage(text));
|
|
15
|
+
}
|
|
16
|
+
if (t.isJSXElement(c)) p.push(parseJSXElement(c, true));
|
|
17
|
+
else if (t.isJSXFragment(c)) p.push(new ElementMessage(AUTO_INCREMENT_IDENTIFIER, [], c));
|
|
18
|
+
else if (t.isJSXExpressionContainer(c)) {
|
|
19
|
+
if (t.isExpression(c.expression)) p.push(new ArgumentMessage(getExpressionAsIdentifier(c.expression), c.expression));
|
|
20
|
+
}
|
|
21
|
+
return p;
|
|
22
|
+
}, []);
|
|
23
|
+
return new CompositeMessage({
|
|
24
|
+
id: findAttributeValueIfStringLiteralAsString(element.openingElement.attributes, "id"),
|
|
25
|
+
context: findAttributeValueIfStringLiteralAsString(element.openingElement.attributes, "context")
|
|
26
|
+
}, [], [], children, accessor);
|
|
27
|
+
}
|
|
28
|
+
function parseJSXOpeningElement(element) {
|
|
29
|
+
if (!element.selfClosing) return null;
|
|
30
|
+
const processed = processJSXOpeningElement(element);
|
|
31
|
+
if (!processed) return null;
|
|
32
|
+
const [accessor, kind] = processed;
|
|
33
|
+
if (typeof kind === "string" && [
|
|
34
|
+
"select",
|
|
35
|
+
"ordinal",
|
|
36
|
+
"plural"
|
|
37
|
+
].includes(kind)) {
|
|
38
|
+
const branches = element.attributes.reduce((b, a) => {
|
|
39
|
+
if (!t.isJSXAttribute(a)) return b;
|
|
40
|
+
let identifier = getAttributeNameAsString(a);
|
|
41
|
+
if (identifier === "_" || identifier === "id" || identifier === "context") return b;
|
|
42
|
+
if (identifier.startsWith("_") && identifier.length > 1 && !Number.isNaN(+identifier.slice(1))) identifier = identifier.slice(1);
|
|
43
|
+
if (t.isStringLiteral(a.value)) b.push({
|
|
44
|
+
identifier,
|
|
45
|
+
value: new LiteralMessage(a.value.value)
|
|
46
|
+
});
|
|
47
|
+
else if (t.isJSXExpressionContainer(a.value)) {
|
|
48
|
+
if (t.isJSXElement(a.value.expression)) b.push({
|
|
49
|
+
identifier,
|
|
50
|
+
value: parseJSXElement(a.value.expression, true)
|
|
51
|
+
});
|
|
52
|
+
else if (t.isJSXFragment(a.value.expression)) b.push({
|
|
53
|
+
identifier,
|
|
54
|
+
value: new ElementMessage(AUTO_INCREMENT_IDENTIFIER, [], a.value.expression)
|
|
55
|
+
});
|
|
56
|
+
else if (t.isExpression(a.value.expression)) b.push({
|
|
57
|
+
identifier,
|
|
58
|
+
value: new ArgumentMessage(getExpressionAsIdentifier(a.value.expression), a.value.expression)
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return b;
|
|
62
|
+
}, []);
|
|
63
|
+
const initialiser = findAttributeValueIfExpressionOrStringLiteral(element.attributes, "_");
|
|
64
|
+
if (!initialiser) return null;
|
|
65
|
+
const choice = new ChoiceMessage(kind, getExpressionAsIdentifier(initialiser), branches, initialiser);
|
|
66
|
+
return new CompositeMessage({
|
|
67
|
+
id: findAttributeValueIfStringLiteralAsString(element.attributes, "id"),
|
|
68
|
+
context: findAttributeValueIfStringLiteralAsString(element.attributes, "context")
|
|
69
|
+
}, [], [], [choice], accessor);
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
function processJSXOpeningElement(element) {
|
|
74
|
+
if (t.isJSXIdentifier(element.name) && element.name.name === "Say") return [element.name, null];
|
|
75
|
+
if (t.isJSXMemberExpression(element.name) && t.isJSXIdentifier(element.name.object) && element.name.object.name === "Say" && t.isJSXIdentifier(element.name.property)) return [element.name.object, element.name.property.name.toLowerCase()];
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
function parseJSXElement(element, fallback) {
|
|
79
|
+
const message = element.openingElement.selfClosing ? parseJSXOpeningElement(element.openingElement) : parseJSXContainerElement(element);
|
|
80
|
+
if (message) return message;
|
|
81
|
+
if (!fallback) return null;
|
|
82
|
+
if (element.openingElement.selfClosing) return new ElementMessage(AUTO_INCREMENT_IDENTIFIER, [], element);
|
|
83
|
+
const wrapped = parseJSXElement(t.jsxElement(t.jsxOpeningElement(t.jsxIdentifier("Say"), []), t.jsxClosingElement(t.jsxIdentifier("Say")), element.children), true);
|
|
84
|
+
if (wrapped) return new ElementMessage(AUTO_INCREMENT_IDENTIFIER, [wrapped], element);
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
function getAttributeNameAsString(attribute) {
|
|
88
|
+
if (t.isJSXIdentifier(attribute.name)) return attribute.name.name;
|
|
89
|
+
if (t.isJSXNamespacedName(attribute.name)) return attribute.name.name.name;
|
|
90
|
+
}
|
|
91
|
+
function findAttributeValueIfStringLiteralAsString(attributes, identifier) {
|
|
92
|
+
for (const attribute of attributes) {
|
|
93
|
+
if (!t.isJSXAttribute(attribute)) continue;
|
|
94
|
+
if (t.isJSXIdentifier(attribute.name) && attribute.name.name === identifier) {
|
|
95
|
+
if (t.isStringLiteral(attribute.value)) return attribute.value.value;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function findAttributeValueIfExpressionOrStringLiteral(attributes, identifier) {
|
|
100
|
+
for (const attribute of attributes) {
|
|
101
|
+
if (!t.isJSXAttribute(attribute)) continue;
|
|
102
|
+
if (t.isJSXIdentifier(attribute.name) && attribute.name.name === identifier) {
|
|
103
|
+
if (t.isJSXExpressionContainer(attribute.value) && t.isExpression(attribute.value.expression)) return attribute.value.expression;
|
|
104
|
+
if (t.isStringLiteral(attribute.value)) return attribute.value;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function getExpressionAsIdentifier(node) {
|
|
109
|
+
if (t.isIdentifier(node)) return node.name;
|
|
110
|
+
if (t.isJSXIdentifier(node)) return node.name;
|
|
111
|
+
return AUTO_INCREMENT_IDENTIFIER;
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
export { parseJSXContainerElement, parseJSXElement, parseJSXOpeningElement };
|
package/package.json
CHANGED
|
@@ -1,4 +1,65 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@saykit/transform-jsx",
|
|
3
|
-
"version": "0.
|
|
1
|
+
{
|
|
2
|
+
"name": "@saykit/transform-jsx",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "JSX and TSX source transformer for SayKit",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"i18n",
|
|
7
|
+
"jsx",
|
|
8
|
+
"saykit",
|
|
9
|
+
"transformer",
|
|
10
|
+
"tsx"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/k0d13/saykit#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/k0d13/saykit/issues"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/k0d13/saykit.git",
|
|
20
|
+
"directory": "packages/transform-jsx"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"!dist/**/*.map"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.mts",
|
|
30
|
+
"default": "./dist/index.mjs"
|
|
31
|
+
},
|
|
32
|
+
"./parser": {
|
|
33
|
+
"types": "./dist/parser.d.mts",
|
|
34
|
+
"default": "./dist/parser.mjs"
|
|
35
|
+
},
|
|
36
|
+
"./generator": {
|
|
37
|
+
"types": "./dist/generator.d.mts",
|
|
38
|
+
"default": "./dist/generator.mjs"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public",
|
|
43
|
+
"provenance": true
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@babel/generator": "^7.29.1",
|
|
47
|
+
"@babel/parser": "^7.29.3",
|
|
48
|
+
"@babel/traverse": "^7.29.0",
|
|
49
|
+
"@babel/types": "^7.29.0",
|
|
50
|
+
"@saykit/config": "^0.1.0",
|
|
51
|
+
"@saykit/transform-js": "^0.1.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@babel/core": "^7.29.0",
|
|
55
|
+
"@types/babel__core": "^7.20.5",
|
|
56
|
+
"@types/babel__generator": "^7.27.0",
|
|
57
|
+
"@types/babel__traverse": "^7.28.0"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"check": "tsc --noEmit",
|
|
61
|
+
"test": "vitest",
|
|
62
|
+
"coverage": "vitest run --coverage",
|
|
63
|
+
"build": "tsdown"
|
|
64
|
+
}
|
|
4
65
|
}
|