@saykit/transform-jsx 0.0.0-beta-20260418085054

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.
@@ -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 };
@@ -0,0 +1,6 @@
1
+ import { Transformer } from "@saykit/config";
2
+
3
+ //#region src/index.d.ts
4
+ declare function createJsxTransformer(): Transformer;
5
+ //#endregion
6
+ export { createJsxTransformer as default };
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 };
@@ -0,0 +1,112 @@
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((c, e) => {
10
+ if (t.isJSXText(e)) {
11
+ const text = e.value.replace(/\s+/g, " ");
12
+ c.push(new LiteralMessage(text));
13
+ }
14
+ if (t.isJSXElement(e)) c.push(parseJSXElement(e, true));
15
+ else if (t.isJSXFragment(e)) c.push(new ElementMessage(AUTO_INCREMENT_IDENTIFIER, [], e));
16
+ else if (t.isJSXExpressionContainer(e)) {
17
+ if (t.isExpression(e.expression)) c.push(new ArgumentMessage(getExpressionAsIdentifier(e.expression), e.expression));
18
+ }
19
+ return c;
20
+ }, []);
21
+ return new CompositeMessage({
22
+ id: findAttributeValueIfStringLiteralAsString(element.openingElement.attributes, "id"),
23
+ context: findAttributeValueIfStringLiteralAsString(element.openingElement.attributes, "context")
24
+ }, [], [], children, accessor);
25
+ }
26
+ function parseJSXOpeningElement(element) {
27
+ if (!element.selfClosing) return null;
28
+ const processed = processJSXOpeningElement(element);
29
+ if (!processed) return null;
30
+ const [accessor, kind] = processed;
31
+ if (typeof kind === "string" && [
32
+ "select",
33
+ "ordinal",
34
+ "plural"
35
+ ].includes(kind)) {
36
+ const branches = element.attributes.reduce((b, a) => {
37
+ if (!t.isJSXAttribute(a)) return b;
38
+ let identifier = getAttributeNameAsString(a);
39
+ if (identifier === "_" || identifier === "id" || identifier === "context") return b;
40
+ if (identifier.startsWith("_") && identifier.length > 1 && !Number.isNaN(+identifier.slice(1))) identifier = identifier.slice(1);
41
+ if (t.isStringLiteral(a.value)) b.push({
42
+ identifier,
43
+ value: new LiteralMessage(a.value.value)
44
+ });
45
+ else if (t.isJSXExpressionContainer(a.value)) {
46
+ if (t.isJSXElement(a.value.expression)) b.push({
47
+ identifier,
48
+ value: parseJSXElement(a.value.expression, true)
49
+ });
50
+ else if (t.isJSXFragment(a.value.expression)) b.push({
51
+ identifier,
52
+ value: new ElementMessage(AUTO_INCREMENT_IDENTIFIER, [], a.value.expression)
53
+ });
54
+ else if (t.isExpression(a.value.expression)) b.push({
55
+ identifier,
56
+ value: new ArgumentMessage(getExpressionAsIdentifier(a.value.expression), a.value.expression)
57
+ });
58
+ }
59
+ return b;
60
+ }, []);
61
+ const initialiser = findAttributeValueIfExpressionOrStringLiteral(element.attributes, "_");
62
+ if (!initialiser) return null;
63
+ const choice = new ChoiceMessage(kind, getExpressionAsIdentifier(initialiser), branches, initialiser);
64
+ return new CompositeMessage({
65
+ id: findAttributeValueIfStringLiteralAsString(element.attributes, "id"),
66
+ context: findAttributeValueIfStringLiteralAsString(element.attributes, "context")
67
+ }, [], [], [choice], accessor);
68
+ }
69
+ return null;
70
+ }
71
+ function processJSXOpeningElement(element) {
72
+ if (t.isJSXIdentifier(element.name) && element.name.name === "Say") return [element.name, null];
73
+ 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()];
74
+ return null;
75
+ }
76
+ function parseJSXElement(element, fallback) {
77
+ const message = element.openingElement.selfClosing ? parseJSXOpeningElement(element.openingElement) : parseJSXContainerElement(element);
78
+ if (message) return message;
79
+ if (!fallback) return null;
80
+ if (element.openingElement.selfClosing) return new ElementMessage(AUTO_INCREMENT_IDENTIFIER, [], element);
81
+ const wrapped = parseJSXElement(t.jsxElement(t.jsxOpeningElement(t.jsxIdentifier("Say"), []), t.jsxClosingElement(t.jsxIdentifier("Say")), element.children), true);
82
+ if (wrapped) return new ElementMessage(AUTO_INCREMENT_IDENTIFIER, [wrapped], element);
83
+ return null;
84
+ }
85
+ function getAttributeNameAsString(attribute) {
86
+ if (t.isJSXIdentifier(attribute.name)) return attribute.name.name;
87
+ if (t.isJSXNamespacedName(attribute.name)) return attribute.name.name.name;
88
+ }
89
+ function findAttributeValueIfStringLiteralAsString(attributes, identifier) {
90
+ for (const attribute of attributes) {
91
+ if (!t.isJSXAttribute(attribute)) continue;
92
+ if (t.isJSXIdentifier(attribute.name) && attribute.name.name === identifier) {
93
+ if (t.isStringLiteral(attribute.value)) return attribute.value.value;
94
+ }
95
+ }
96
+ }
97
+ function findAttributeValueIfExpressionOrStringLiteral(attributes, identifier) {
98
+ for (const attribute of attributes) {
99
+ if (!t.isJSXAttribute(attribute)) continue;
100
+ if (t.isJSXIdentifier(attribute.name) && attribute.name.name === identifier) {
101
+ if (t.isJSXExpressionContainer(attribute.value) && t.isExpression(attribute.value.expression)) return attribute.value.expression;
102
+ if (t.isStringLiteral(attribute.value)) return attribute.value;
103
+ }
104
+ }
105
+ }
106
+ function getExpressionAsIdentifier(node) {
107
+ if (t.isIdentifier(node)) return node.name;
108
+ if (t.isJSXIdentifier(node)) return node.name;
109
+ return AUTO_INCREMENT_IDENTIFIER;
110
+ }
111
+ //#endregion
112
+ export { parseJSXContainerElement, parseJSXElement, parseJSXOpeningElement };
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@saykit/transform-jsx",
3
+ "version": "0.0.0-beta-20260418085054",
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.2",
48
+ "@babel/traverse": "^7.29.0",
49
+ "@babel/types": "^7.29.0",
50
+ "@saykit/config": "^0.0.0-beta-20260418085054",
51
+ "@saykit/transform-js": "^0.0.0-beta-20260418085054"
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
+ }
65
+ }