@saykit/transform-js 0.0.0 → 0.2.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 +36 -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 +78 -0
- package/dist/parser.d.mts +11 -0
- package/dist/parser.mjs +126 -0
- package/package.json +61 -3
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @saykit/transform-js
|
|
2
|
+
|
|
3
|
+
> JavaScript and TypeScript transformer for [SayKit](https://saykit.js.org).
|
|
4
|
+
|
|
5
|
+
[](https://codecov.io/gh/k0d13/saykit?flags%5B0%5D=transform-js)
|
|
6
|
+
|
|
7
|
+
Extracts and rewrites SayKit macros in `.js`, `.cjs`, `.mjs`, `.ts`, `.mts`, and `.cts` files. Used inside a `saykit.config.ts` bucket.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pnpm add -D @saykit/transform-js
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```ts title="saykit.config.ts"
|
|
18
|
+
import js from '@saykit/transform-js';
|
|
19
|
+
|
|
20
|
+
// inside a bucket:
|
|
21
|
+
transformer: js();
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
For JSX/TSX, combine with [`@saykit/transform-jsx`](https://github.com/k0d13/saykit/tree/main/packages/transform-jsx):
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import js from '@saykit/transform-js';
|
|
28
|
+
import jsx from '@saykit/transform-jsx';
|
|
29
|
+
|
|
30
|
+
// inside a bucket:
|
|
31
|
+
transformer: [js(), jsx()];
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Documentation
|
|
35
|
+
|
|
36
|
+
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 };
|
package/dist/index.d.mts
ADDED
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 };
|
package/dist/parser.mjs
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
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
|
+
/* v8 ignore next */
|
|
88
|
+
if (t.isJSXIdentifier(node)) return node.name;
|
|
89
|
+
return AUTO_INCREMENT_IDENTIFIER;
|
|
90
|
+
}
|
|
91
|
+
function parseExpression(expression, fallback) {
|
|
92
|
+
let message = null;
|
|
93
|
+
switch (true) {
|
|
94
|
+
case t.isTaggedTemplateExpression(expression):
|
|
95
|
+
message = parseTaggedTemplateExpression(expression);
|
|
96
|
+
break;
|
|
97
|
+
case t.isCallExpression(expression):
|
|
98
|
+
message = parseCallExpression(expression);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
if (message) return message;
|
|
102
|
+
else if (fallback) return new ArgumentMessage(getExpressionAsKey(expression), expression);
|
|
103
|
+
else return null;
|
|
104
|
+
}
|
|
105
|
+
function getPropertyNameAsString(key) {
|
|
106
|
+
if (t.isIdentifier(key)) return key.name;
|
|
107
|
+
if (t.isStringLiteral(key)) return key.value;
|
|
108
|
+
if (t.isNumericLiteral(key)) return key.value.toString();
|
|
109
|
+
if (t.isBigIntLiteral(key)) return key.value.toString();
|
|
110
|
+
return AUTO_INCREMENT_IDENTIFIER;
|
|
111
|
+
}
|
|
112
|
+
function findPropertyValueIfStringLiteralAsString(object, key) {
|
|
113
|
+
for (const property of object.properties) {
|
|
114
|
+
if (!t.isObjectProperty(property)) continue;
|
|
115
|
+
if (t.isIdentifier(property.key) && property.key.name === key && t.isStringLiteral(property.value)) return property.value.value;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function getTranslatorComments(comments) {
|
|
119
|
+
return comments.reduce((a, c) => {
|
|
120
|
+
const text = c.value.trim();
|
|
121
|
+
if (text.toLowerCase().startsWith("translators:")) a.push(text.slice(12).trim());
|
|
122
|
+
return a;
|
|
123
|
+
}, []);
|
|
124
|
+
}
|
|
125
|
+
//#endregion
|
|
126
|
+
export { parseCallExpression, parseExpression, parseTaggedTemplateExpression, processExpression };
|
package/package.json
CHANGED
|
@@ -1,4 +1,62 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@saykit/transform-js",
|
|
3
|
-
"version": "0.
|
|
1
|
+
{
|
|
2
|
+
"name": "@saykit/transform-js",
|
|
3
|
+
"version": "0.2.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.7",
|
|
47
|
+
"@babel/parser": "^7.29.7",
|
|
48
|
+
"@babel/traverse": "^7.29.7",
|
|
49
|
+
"@babel/types": "^7.29.7",
|
|
50
|
+
"@saykit/config": "^0.2.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@babel/core": "^7.29.7",
|
|
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
|
+
"build": "tsdown"
|
|
61
|
+
}
|
|
4
62
|
}
|