Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/javascript.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,21 @@ jobs:
# npm install
# npm run test-ci

generate_messages_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Node
uses: actions/setup-node@v3
with:
node-version: 20
cache-dependency-path: ./package-lock.json
cache: 'npm'
- name: Test generate-messages script
run: |
npm install
node scripts/test-generate-messages.js

unit_tests:
runs-on: ubuntu-latest
strategy:
Expand Down
36 changes: 0 additions & 36 deletions message_generator.ts

This file was deleted.

3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"coveralls": "nyc --reporter=lcov npm test",
"prepare": "npm run build",
"prepublishOnly": "npm test",
"genmsg": "jiti message_generator ./lib/message/error_message.ts ./lib/message/log_message.ts",
"genmsg": "./scripts/generate-messages.js ./lib/message/error_message.ts ./lib/message/log_message.ts",
"test-typescript": "node ./scripts/run-ts-example.js",
"test-commonjs": "node ./scripts/run-cjs-example.js"
},
Expand Down Expand Up @@ -126,7 +126,6 @@
"eslint-plugin-local-rules": "^3.0.2",
"eslint-plugin-prettier": "^3.1.2",
"happy-dom": "^20.0.11",
"jiti": "^2.4.1",
"minimatch": "^9.0.5",
"mocha": "^10.2.0",
"mocha-lcov-reporter": "^1.3.0",
Expand Down
121 changes: 121 additions & 0 deletions scripts/generate-messages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/usr/bin/env node

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#! is UNIX only! Not that it matters a lot but build:win could potentially be broken for this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. But other scripts are also using this currently. Windows build is relevant only for devs' local machine, but we can use wsl nowadays. We can come back and fix the windows build later if needed


/**
* Copyright 2026, Optimizely
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

const ts = require('typescript');
const fs = require('fs');
const path = require('path');

function extractMessages(filePath) {
const source = fs.readFileSync(filePath, 'utf-8');
const sourceFile = ts.createSourceFile(
filePath,
source,
ts.ScriptTarget.Latest,
true,
);

const exports = [];

for (const statement of sourceFile.statements) {
if (!ts.isVariableStatement(statement)) continue;

const hasExport = statement.modifiers?.some(
(m) => m.kind === ts.SyntaxKind.ExportKeyword,
);
if (!hasExport) continue;

for (const decl of statement.declarationList.declarations) {
const name = decl.name.getText(sourceFile);
if (name === 'messages' || name === '__platforms') continue;
if (!decl.initializer || !ts.isStringLiteral(decl.initializer)) continue;

exports.push({ name, value: decl.initializer.text });
}
}

return exports;
}

function buildOutputAst(exports) {
const { factory } = ts;
const statements = [];

exports.forEach((exp, i) => {
statements.push(
factory.createVariableStatement(
[factory.createModifier(ts.SyntaxKind.ExportKeyword)],
factory.createVariableDeclarationList(
[factory.createVariableDeclaration(exp.name, undefined, undefined, factory.createStringLiteral(String(i)))],
ts.NodeFlags.Const,
),
),
);
});

statements.push(
factory.createVariableStatement(
[factory.createModifier(ts.SyntaxKind.ExportKeyword)],
factory.createVariableDeclarationList(
[
factory.createVariableDeclaration(
'messages',
undefined,
undefined,
factory.createArrayLiteralExpression(
exports.map((exp) => factory.createStringLiteral(exp.value)),
true,
),
),
],
ts.NodeFlags.Const,
),
),
);

return factory.createSourceFile(statements, factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None);
}

function generate(filePath) {
const absPath = path.resolve(filePath);
const parsed = path.parse(absPath);
const genPath = path.join(parsed.dir, `${parsed.name}.gen${parsed.ext}`);

console.log('generating messages for:', absPath);

const exports = extractMessages(absPath);
const outputAst = buildOutputAst(exports);

const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
const output = printer.printFile(outputAst);

fs.writeFileSync(genPath, output, 'utf-8');
console.log('generated file path:', genPath);
}

const files = process.argv.slice(2);

if (files.length === 0) {
console.error('Usage: generate-messages.js <file1.ts> [file2.ts] ...');
process.exit(1);
}

for (const file of files) {
generate(file);
}

console.log('successfully generated messages');
90 changes: 90 additions & 0 deletions scripts/test-generate-messages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env node

const assert = require('node:assert/strict');
const { execFileSync } = require('node:child_process');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const ts = require('typescript');

const SCRIPT = path.resolve(__dirname, 'generate-messages.js');

const FILE1_EXPECTED = [
{ name: 'HELLO', value: 'Hello, %s!' },
{ name: 'GOODBYE', value: 'Goodbye, %s!' },
{ name: 'WARNING', value: 'Warning: %s' },
];

const FILE2_EXPECTED = [
{ name: 'ERR_NOT_FOUND', value: 'Resource %s not found' },
{ name: 'ERR_TIMEOUT', value: 'Request timed out after %s ms' },
{ name: 'ERR_INVALID', value: 'Invalid input: %s' },
{ name: 'ERR_PERMISSION', value: 'Permission denied for %s' },
];

function buildFileContent(expected, { includeMessages = true, includePlatforms = false } = {}) {
let content = expected.map((e) => `export const ${e.name} = '${e.value}';`).join('\n');
if (includeMessages) content += `\n\nexport const messages: string[] = [];`;
if (includePlatforms) content += `\nexport const __platforms: string[] = ['__universal__'];`;
return content;
}

function compileAndLoad(tsPath) {
const source = fs.readFileSync(tsPath, 'utf-8');
const { outputText } = ts.transpileModule(source, {
compilerOptions: { module: ts.ModuleKind.CommonJS },
});
const jsPath = tsPath.replace(/\.ts$/, '.js');
fs.writeFileSync(jsPath, outputText, 'utf-8');
return require(jsPath);
}

function runTest() {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gen-msg-test-'));

try {
const file1 = path.join(tmpDir, 'file1.ts');
const file2 = path.join(tmpDir, 'file2.ts');
fs.writeFileSync(file1, buildFileContent(FILE1_EXPECTED, { includePlatforms: true }), 'utf-8');
fs.writeFileSync(file2, buildFileContent(FILE2_EXPECTED), 'utf-8');

execFileSync(process.execPath, [SCRIPT, file1, file2], { stdio: 'pipe' });

const genFile1 = path.join(tmpDir, 'file1.gen.ts');
const genFile2 = path.join(tmpDir, 'file2.gen.ts');
assert.ok(fs.existsSync(genFile1), 'file1.gen.ts should be created');
assert.ok(fs.existsSync(genFile2), 'file2.gen.ts should be created');

for (const [expected, genPath, label] of [
[FILE1_EXPECTED, genFile1, 'file1'],
[FILE2_EXPECTED, genFile2, 'file2'],
]) {
const mod = compileAndLoad(genPath);

assert.ok(Array.isArray(mod.messages), `${label}: messages should be an array`);
assert.equal(mod.messages.length, expected.length,
`${label}: messages array length should be ${expected.length}, got ${mod.messages.length}`);

assert.equal(mod.__platforms, undefined, `${label}: __platforms should not be exported`);

const constantCount = Object.keys(mod).filter((k) => k !== 'messages').length;
assert.equal(constantCount, expected.length,
`${label}: should have ${expected.length} constants, got ${constantCount}`);

for (const exp of expected) {
assert.ok(exp.name in mod,
`${label}: constant "${exp.name}" should be present`);

const index = Number(mod[exp.name]);
assert.equal(mod.messages[index], exp.value,
`${label}: messages[${index}] should be "${exp.value}", got "${mod.messages[index]}"`);
}
}

console.log('All tests passed.');
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
}

runTest();
Loading