Skip to content

Commit 1da4773

Browse files
raju-opticlaude
andcommitted
[FSSDK-12933] Refactor message generator to use TS compiler API
Replace the dynamic-import-based message_generator.ts with a static TS-parser approach (scripts/generate-messages.js) that preserves declaration order and eliminates the jiti runtime dependency. Add a test script and CI job for the generator. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0105135 commit 1da4773

6 files changed

Lines changed: 229 additions & 39 deletions

File tree

.github/workflows/javascript.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,21 @@ jobs:
115115
# npm install
116116
# npm run test-ci
117117

118+
generate_messages_test:
119+
runs-on: ubuntu-latest
120+
steps:
121+
- uses: actions/checkout@v3
122+
- name: Set up Node
123+
uses: actions/setup-node@v3
124+
with:
125+
node-version: 20
126+
cache-dependency-path: ./package-lock.json
127+
cache: 'npm'
128+
- name: Test generate-messages script
129+
run: |
130+
npm install
131+
node scripts/test-generate-messages.js
132+
118133
unit_tests:
119134
runs-on: ubuntu-latest
120135
strategy:

message_generator.ts

Lines changed: 0 additions & 36 deletions
This file was deleted.

package-lock.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
"coveralls": "nyc --reporter=lcov npm test",
7979
"prepare": "npm run build",
8080
"prepublishOnly": "npm test",
81-
"genmsg": "jiti message_generator ./lib/message/error_message.ts ./lib/message/log_message.ts",
81+
"genmsg": "node ./scripts/generate-messages.js ./lib/message/error_message.ts ./lib/message/log_message.ts",
8282
"test-typescript": "node ./scripts/run-ts-example.js",
8383
"test-commonjs": "node ./scripts/run-cjs-example.js"
8484
},
@@ -126,7 +126,6 @@
126126
"eslint-plugin-local-rules": "^3.0.2",
127127
"eslint-plugin-prettier": "^3.1.2",
128128
"happy-dom": "^20.0.11",
129-
"jiti": "^2.4.1",
130129
"minimatch": "^9.0.5",
131130
"mocha": "^10.2.0",
132131
"mocha-lcov-reporter": "^1.3.0",

scripts/generate-messages.js

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Copyright 2026, Optimizely
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
const ts = require('typescript');
20+
const fs = require('fs');
21+
const path = require('path');
22+
23+
function extractMessages(filePath) {
24+
const source = fs.readFileSync(filePath, 'utf-8');
25+
const sourceFile = ts.createSourceFile(
26+
filePath,
27+
source,
28+
ts.ScriptTarget.Latest,
29+
true,
30+
);
31+
32+
const exports = [];
33+
34+
for (const statement of sourceFile.statements) {
35+
if (!ts.isVariableStatement(statement)) continue;
36+
37+
const hasExport = statement.modifiers?.some(
38+
(m) => m.kind === ts.SyntaxKind.ExportKeyword,
39+
);
40+
if (!hasExport) continue;
41+
42+
for (const decl of statement.declarationList.declarations) {
43+
const name = decl.name.getText(sourceFile);
44+
if (name === 'messages' || name === '__platforms') continue;
45+
if (!decl.initializer || !ts.isStringLiteral(decl.initializer)) continue;
46+
47+
exports.push({ name, value: decl.initializer.text });
48+
}
49+
}
50+
51+
return exports;
52+
}
53+
54+
function buildOutputAst(exports) {
55+
const { factory } = ts;
56+
const statements = [];
57+
58+
exports.forEach((exp, i) => {
59+
statements.push(
60+
factory.createVariableStatement(
61+
[factory.createModifier(ts.SyntaxKind.ExportKeyword)],
62+
factory.createVariableDeclarationList(
63+
[factory.createVariableDeclaration(exp.name, undefined, undefined, factory.createStringLiteral(String(i)))],
64+
ts.NodeFlags.Const,
65+
),
66+
),
67+
);
68+
});
69+
70+
statements.push(
71+
factory.createVariableStatement(
72+
[factory.createModifier(ts.SyntaxKind.ExportKeyword)],
73+
factory.createVariableDeclarationList(
74+
[
75+
factory.createVariableDeclaration(
76+
'messages',
77+
undefined,
78+
undefined,
79+
factory.createArrayLiteralExpression(
80+
exports.map((exp) => factory.createStringLiteral(exp.value)),
81+
true,
82+
),
83+
),
84+
],
85+
ts.NodeFlags.Const,
86+
),
87+
),
88+
);
89+
90+
return factory.createSourceFile(statements, factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None);
91+
}
92+
93+
function generate(filePath) {
94+
const absPath = path.resolve(filePath);
95+
const parsed = path.parse(absPath);
96+
const genPath = path.join(parsed.dir, `${parsed.name}.gen${parsed.ext}`);
97+
98+
console.log('generating messages for:', absPath);
99+
100+
const exports = extractMessages(absPath);
101+
const outputAst = buildOutputAst(exports);
102+
103+
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
104+
const output = printer.printFile(outputAst);
105+
106+
fs.writeFileSync(genPath, output, 'utf-8');
107+
console.log('generated file path:', genPath);
108+
}
109+
110+
const files = process.argv.slice(2);
111+
112+
if (files.length === 0) {
113+
console.error('Usage: generate-messages.js <file1.ts> [file2.ts] ...');
114+
process.exit(1);
115+
}
116+
117+
for (const file of files) {
118+
generate(file);
119+
}
120+
121+
console.log('successfully generated messages');

scripts/test-generate-messages.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#!/usr/bin/env node
2+
3+
const assert = require('node:assert/strict');
4+
const { execFileSync } = require('node:child_process');
5+
const fs = require('node:fs');
6+
const os = require('node:os');
7+
const path = require('node:path');
8+
const ts = require('typescript');
9+
10+
const SCRIPT = path.resolve(__dirname, 'generate-messages.js');
11+
12+
const FILE1_EXPECTED = [
13+
{ name: 'HELLO', value: 'Hello, %s!' },
14+
{ name: 'GOODBYE', value: 'Goodbye, %s!' },
15+
{ name: 'WARNING', value: 'Warning: %s' },
16+
];
17+
18+
const FILE2_EXPECTED = [
19+
{ name: 'ERR_NOT_FOUND', value: 'Resource %s not found' },
20+
{ name: 'ERR_TIMEOUT', value: 'Request timed out after %s ms' },
21+
{ name: 'ERR_INVALID', value: 'Invalid input: %s' },
22+
{ name: 'ERR_PERMISSION', value: 'Permission denied for %s' },
23+
];
24+
25+
function buildFileContent(expected, { includeMessages = true, includePlatforms = false } = {}) {
26+
let content = expected.map((e) => `export const ${e.name} = '${e.value}';`).join('\n');
27+
if (includeMessages) content += `\n\nexport const messages: string[] = [];`;
28+
if (includePlatforms) content += `\nexport const __platforms: string[] = ['__universal__'];`;
29+
return content;
30+
}
31+
32+
function compileAndLoad(tsPath) {
33+
const source = fs.readFileSync(tsPath, 'utf-8');
34+
const { outputText } = ts.transpileModule(source, {
35+
compilerOptions: { module: ts.ModuleKind.CommonJS },
36+
});
37+
const jsPath = tsPath.replace(/\.ts$/, '.js');
38+
fs.writeFileSync(jsPath, outputText, 'utf-8');
39+
return require(jsPath);
40+
}
41+
42+
function runTest() {
43+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gen-msg-test-'));
44+
45+
try {
46+
const file1 = path.join(tmpDir, 'file1.ts');
47+
const file2 = path.join(tmpDir, 'file2.ts');
48+
fs.writeFileSync(file1, buildFileContent(FILE1_EXPECTED, { includePlatforms: true }), 'utf-8');
49+
fs.writeFileSync(file2, buildFileContent(FILE2_EXPECTED), 'utf-8');
50+
51+
execFileSync(process.execPath, [SCRIPT, file1, file2], { stdio: 'pipe' });
52+
53+
const genFile1 = path.join(tmpDir, 'file1.gen.ts');
54+
const genFile2 = path.join(tmpDir, 'file2.gen.ts');
55+
assert.ok(fs.existsSync(genFile1), 'file1.gen.ts should be created');
56+
assert.ok(fs.existsSync(genFile2), 'file2.gen.ts should be created');
57+
58+
for (const [expected, genPath, label] of [
59+
[FILE1_EXPECTED, genFile1, 'file1'],
60+
[FILE2_EXPECTED, genFile2, 'file2'],
61+
]) {
62+
const mod = compileAndLoad(genPath);
63+
64+
assert.ok(Array.isArray(mod.messages), `${label}: messages should be an array`);
65+
assert.equal(mod.messages.length, expected.length,
66+
`${label}: messages array length should be ${expected.length}, got ${mod.messages.length}`);
67+
68+
assert.equal(mod.__platforms, undefined, `${label}: __platforms should not be exported`);
69+
70+
const constantCount = Object.keys(mod).filter((k) => k !== 'messages').length;
71+
assert.equal(constantCount, expected.length,
72+
`${label}: should have ${expected.length} constants, got ${constantCount}`);
73+
74+
for (const exp of expected) {
75+
assert.ok(exp.name in mod,
76+
`${label}: constant "${exp.name}" should be present`);
77+
78+
const index = Number(mod[exp.name]);
79+
assert.equal(mod.messages[index], exp.value,
80+
`${label}: messages[${index}] should be "${exp.value}", got "${mod.messages[index]}"`);
81+
}
82+
}
83+
84+
console.log('All tests passed.');
85+
} finally {
86+
fs.rmSync(tmpDir, { recursive: true, force: true });
87+
}
88+
}
89+
90+
runTest();

0 commit comments

Comments
 (0)