@saykit/transform-js 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 ADDED
@@ -0,0 +1,34 @@
1
+ # @saykit/transform-js
2
+
3
+ > JavaScript and TypeScript transformer for [SayKit](https://saykit.js.org).
4
+
5
+ Extracts and rewrites SayKit macros in `.js`, `.cjs`, `.mjs`, `.ts`, `.mts`, and `.cts` files. Used inside a `saykit.config.ts` bucket.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pnpm add -D @saykit/transform-js
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts title="saykit.config.ts"
16
+ import js from '@saykit/transform-js';
17
+
18
+ // inside a bucket:
19
+ transformer: js();
20
+ ```
21
+
22
+ For JSX/TSX, combine with [`@saykit/transform-jsx`](https://github.com/k0d13/saykit/tree/main/packages/transform-jsx):
23
+
24
+ ```ts
25
+ import js from '@saykit/transform-js';
26
+ import jsx from '@saykit/transform-jsx';
27
+
28
+ // inside a bucket:
29
+ transformer: [js(), jsx()];
30
+ ```
31
+
32
+ ## Documentation
33
+
34
+ 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 generateSayCallExpression(message: CompositeMessage): t.CallExpression;
6
+ //#endregion
7
+ export { generateSayCallExpression };
@@ -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 generateSayCallExpression(message) {
5
+ const id = message.descriptor.id ?? message.toHashString();
6
+ const children = generateChildExpressions(message.children);
7
+ const properties = t.objectExpression([t.objectProperty(t.identifier("id"), t.stringLiteral(id)), ...children.map(([ident, expr]) => t.objectProperty(t.identifier(ident), expr))]);
8
+ return t.callExpression(t.memberExpression(message.accessor, t.identifier("call")), [properties]);
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 { generateSayCallExpression };
@@ -0,0 +1,6 @@
1
+ import { Transformer } from "@saykit/config";
2
+
3
+ //#region src/index.d.ts
4
+ declare function createJsTransformer(): Transformer;
5
+ //#endregion
6
+ export { createJsTransformer as default };
package/dist/index.mjs ADDED
@@ -0,0 +1,78 @@
1
+ import { generateSayCallExpression } from "./generator.mjs";
2
+ import { parseExpression } 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
+ //#region src/index.ts
8
+ const traverse = traverse_.default || traverse_;
9
+ function createProgram(code, id) {
10
+ const program = parser.parse(code, {
11
+ sourceType: "module",
12
+ sourceFilename: id,
13
+ plugins: ["typescript"],
14
+ allowImportExportEverywhere: false,
15
+ allowReturnOutsideFunction: true,
16
+ ranges: true,
17
+ attachComment: true,
18
+ tokens: true
19
+ });
20
+ return Object.assign(program, { toString() {
21
+ return generate(program, {
22
+ retainLines: true,
23
+ comments: true,
24
+ compact: false
25
+ }).code;
26
+ } });
27
+ }
28
+ function createJsTransformer() {
29
+ return {
30
+ match(id) {
31
+ return [
32
+ ".js",
33
+ ".cjs",
34
+ ".mjs",
35
+ ".ts",
36
+ ".mts",
37
+ ".cts"
38
+ ].some((e) => id.endsWith(e));
39
+ },
40
+ extract(code, id) {
41
+ const program = createProgram(code, id);
42
+ const messages = [];
43
+ traverse(program, { Expression(path) {
44
+ path.node.leadingComments = path.node.leadingComments ?? [];
45
+ const message = parseExpression(path.node);
46
+ if (message) {
47
+ assignSequenceIdentifiers(message, { current: 0 });
48
+ messages.push(message);
49
+ path.skip();
50
+ }
51
+ } });
52
+ return messages.map((message) => ({
53
+ message: message.toICUString(),
54
+ translation: void 0,
55
+ id: message.descriptor.id,
56
+ context: message.descriptor.context,
57
+ comments: message.comments,
58
+ references: message.references
59
+ }));
60
+ },
61
+ transform(code, id) {
62
+ const program = createProgram(code, id);
63
+ traverse(program, { Expression(path) {
64
+ path.node.leadingComments = path.node.leadingComments ?? [];
65
+ const message = parseExpression(path.node);
66
+ if (message) {
67
+ assignSequenceIdentifiers(message, { current: 0 });
68
+ const replacement = generateSayCallExpression(message);
69
+ path.replaceWith(replacement);
70
+ path.skip();
71
+ }
72
+ } });
73
+ return program.toString();
74
+ }
75
+ };
76
+ }
77
+ //#endregion
78
+ export { createJsTransformer as default };
@@ -0,0 +1,11 @@
1
+ import { ArgumentMessage, CompositeMessage } from "@saykit/config/features/messages";
2
+ import * as t from "@babel/types";
3
+
4
+ //#region src/parser.d.ts
5
+ declare function parseTaggedTemplateExpression(tagged: t.TaggedTemplateExpression): CompositeMessage | null;
6
+ declare function parseCallExpression(call: t.CallExpression): CompositeMessage | null;
7
+ declare function processExpression(expression: t.Node): [t.Expression, t.ObjectExpression | null, string | null] | null;
8
+ declare function parseExpression(expression: t.Expression, fallback?: false): CompositeMessage | null;
9
+ declare function parseExpression(expression: t.Expression, fallback: true): CompositeMessage | ArgumentMessage;
10
+ //#endregion
11
+ export { parseCallExpression, parseExpression, parseTaggedTemplateExpression, processExpression };
@@ -0,0 +1,125 @@
1
+ import { AUTO_INCREMENT_IDENTIFIER, ArgumentMessage, ChoiceMessage, CompositeMessage, LiteralMessage } from "@saykit/config/features/messages";
2
+ import * as t from "@babel/types";
3
+ //#region src/parser.ts
4
+ function parseTaggedTemplateExpression(tagged) {
5
+ const processed = processExpression(tagged.tag);
6
+ if (!processed) return null;
7
+ const [accessor, descriptor] = processed;
8
+ const children = tagged.quasi.quasis.reduce((c, q, i) => {
9
+ c.push(new LiteralMessage(q.value.cooked ?? q.value.raw));
10
+ if (t.isExpression(tagged.quasi.expressions[i])) c.push(parseExpression(tagged.quasi.expressions[i], true));
11
+ return c;
12
+ }, []);
13
+ return new CompositeMessage({
14
+ id: descriptor ? findPropertyValueIfStringLiteralAsString(descriptor, "id") : void 0,
15
+ context: descriptor ? findPropertyValueIfStringLiteralAsString(descriptor, "context") : void 0
16
+ }, getTranslatorComments(tagged.leadingComments ?? []), tagged.loc ? [`${tagged.loc.filename}:${tagged.loc.start.line}`] : [], children, accessor);
17
+ }
18
+ function parseCallExpression(call) {
19
+ const processed = processExpression(call.callee);
20
+ if (!processed) return null;
21
+ const [accessor, descriptor, kind] = processed;
22
+ if (typeof kind === "string" && [
23
+ "select",
24
+ "ordinal",
25
+ "plural"
26
+ ].includes(kind)) {
27
+ if (call.arguments.length !== 2) return null;
28
+ if (!t.isExpression(call.arguments[0])) return null;
29
+ if (!t.isObjectExpression(call.arguments[1])) return null;
30
+ const branches = call.arguments[1].properties.reduce((c, p) => {
31
+ if (!t.isObjectProperty(p) || !t.isExpression(p.value)) return c;
32
+ let message = null;
33
+ if (!message && t.isStringLiteral(p.value)) message = new LiteralMessage(p.value.value);
34
+ if (!message) message = parseExpression(p.value, true);
35
+ c.push({
36
+ identifier: getPropertyNameAsString(p.key),
37
+ value: message
38
+ });
39
+ return c;
40
+ }, []);
41
+ const value = call.arguments[0];
42
+ const choice = new ChoiceMessage(kind, getExpressionAsKey(value), branches, value);
43
+ return new CompositeMessage({
44
+ id: descriptor ? findPropertyValueIfStringLiteralAsString(descriptor, "id") : void 0,
45
+ context: descriptor ? findPropertyValueIfStringLiteralAsString(descriptor, "context") : void 0
46
+ }, [], call.loc ? [`${call.loc.filename}:${call.loc.start.line}`] : [], [choice], accessor);
47
+ }
48
+ return null;
49
+ }
50
+ function processExpression(expression) {
51
+ if (t.isIdentifier(expression) && expression.name === "say") return [
52
+ expression,
53
+ null,
54
+ null
55
+ ];
56
+ if (t.isCallExpression(expression)) {
57
+ const inner = processExpression(expression.callee);
58
+ if (inner && expression.arguments.length === 1 && t.isObjectExpression(expression.arguments[0])) return [
59
+ inner[0],
60
+ expression.arguments[0],
61
+ null
62
+ ];
63
+ else if (inner) return [
64
+ inner[0],
65
+ null,
66
+ null
67
+ ];
68
+ }
69
+ if (t.isMemberExpression(expression)) {
70
+ const innerProperty = processExpression(expression.property);
71
+ if (innerProperty) return [
72
+ expression,
73
+ innerProperty[1],
74
+ null
75
+ ];
76
+ const innerObject = processExpression(expression.object);
77
+ if (innerObject && t.isIdentifier(expression.property)) return [
78
+ innerObject[0],
79
+ innerObject[1],
80
+ expression.property.name
81
+ ];
82
+ }
83
+ return null;
84
+ }
85
+ function getExpressionAsKey(node) {
86
+ if (t.isIdentifier(node)) return node.name;
87
+ if (t.isJSXIdentifier(node)) return node.name;
88
+ return AUTO_INCREMENT_IDENTIFIER;
89
+ }
90
+ function parseExpression(expression, fallback) {
91
+ let message = null;
92
+ switch (true) {
93
+ case t.isTaggedTemplateExpression(expression):
94
+ message = parseTaggedTemplateExpression(expression);
95
+ break;
96
+ case t.isCallExpression(expression):
97
+ message = parseCallExpression(expression);
98
+ break;
99
+ }
100
+ if (message) return message;
101
+ else if (fallback) return new ArgumentMessage(getExpressionAsKey(expression), expression);
102
+ else return null;
103
+ }
104
+ function getPropertyNameAsString(key) {
105
+ if (t.isIdentifier(key)) return key.name;
106
+ if (t.isStringLiteral(key)) return key.value;
107
+ if (t.isNumericLiteral(key)) return key.value.toString();
108
+ if (t.isBigIntLiteral(key)) return key.value.toString();
109
+ return AUTO_INCREMENT_IDENTIFIER;
110
+ }
111
+ function findPropertyValueIfStringLiteralAsString(object, key) {
112
+ for (const property of object.properties) {
113
+ if (!t.isObjectProperty(property)) continue;
114
+ if (t.isIdentifier(property.key) && property.key.name === key && t.isStringLiteral(property.value)) return property.value.value;
115
+ }
116
+ }
117
+ function getTranslatorComments(comments) {
118
+ return comments.reduce((a, c) => {
119
+ const text = c.value.trim();
120
+ if (text.toLowerCase().startsWith("translators:")) a.push(text.slice(12).trim());
121
+ return a;
122
+ }, []);
123
+ }
124
+ //#endregion
125
+ export { parseCallExpression, parseExpression, parseTaggedTemplateExpression, processExpression };
package/package.json CHANGED
@@ -1,4 +1,64 @@
1
- {
2
- "name": "@saykit/transform-js",
3
- "version": "0.0.0"
1
+ {
2
+ "name": "@saykit/transform-js",
3
+ "version": "0.1.0",
4
+ "description": "JavaScript and TypeScript transformer for SayKit",
5
+ "keywords": [
6
+ "i18n",
7
+ "javascript",
8
+ "saykit",
9
+ "transformer",
10
+ "typescript"
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-js"
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
+ },
52
+ "devDependencies": {
53
+ "@babel/core": "^7.29.0",
54
+ "@types/babel__core": "^7.20.5",
55
+ "@types/babel__generator": "^7.27.0",
56
+ "@types/babel__traverse": "^7.28.0"
57
+ },
58
+ "scripts": {
59
+ "check": "tsc --noEmit",
60
+ "test": "vitest",
61
+ "coverage": "vitest run --coverage",
62
+ "build": "tsdown"
63
+ }
4
64
  }