fluent-transpiler 0.4.0 → 0.5.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 CHANGED
@@ -35,12 +35,14 @@ npm i -D fluent-transpiler
35
35
  ## CLI
36
36
 
37
37
  ```bash
38
- Usage: ftl [options] <input>
38
+ Usage: ftl [options] <inputs...>
39
39
 
40
40
  Compile Fluent (.ftl) files to JavaScript (.js or .mjs)
41
41
 
42
42
  Arguments:
43
- input Path to the Fluent file to compile
43
+ inputs Paths to the Fluent file(s) to compile.
44
+ Multiple files are joined in order;
45
+ ids must be unique across the set.
44
46
 
45
47
  Options:
46
48
  --locale <locale...> What locale(s) to be used. Multiple can be set to allow for fallback. i.e. en-CA
@@ -79,3 +81,20 @@ const ftl = await readFile('./path/to/en.ftl', { encoding: 'utf8' })
79
81
  const js = fluentTranspiler(ftl, { locale: 'en-CA' })
80
82
  await writeFile('./path/to/en.mjs', js, 'utf8')
81
83
  ```
84
+
85
+ ### Joining multiple files
86
+
87
+ `compile` also accepts an array of source strings, and `compileFiles` reads and
88
+ joins files from disk. Sources are concatenated in the order supplied; top-level
89
+ message and term ids must be unique across the set.
90
+
91
+ ```javascript
92
+ import { writeFile } from 'node:fs/promises'
93
+ import { compileFiles } from 'fluent-transpiler'
94
+
95
+ const js = await compileFiles(
96
+ ['./common.ftl', './brand.ftl', './app.ftl'],
97
+ { locale: 'en-CA' },
98
+ )
99
+ await writeFile('./en.mjs', js, 'utf8')
100
+ ```
package/cli.js CHANGED
@@ -2,9 +2,9 @@
2
2
  // Copyright 2026 will Farrell, and fluent-transpiler contributors.
3
3
  // SPDX-License-Identifier: MIT
4
4
 
5
- import { readFile, stat, writeFile } from "node:fs/promises";
5
+ import { stat, writeFile } from "node:fs/promises";
6
6
  import { Command, Option } from "commander";
7
- import compile from "./index.js";
7
+ import { compileFiles } from "./index.js";
8
8
 
9
9
  const fileExists = async (filepath) => {
10
10
  const stats = await stat(filepath);
@@ -16,7 +16,10 @@ const fileExists = async (filepath) => {
16
16
  new Command()
17
17
  .name("ftl")
18
18
  .description("Compile Fluent (.ftl) files to JavaScript (.js or .mjs)")
19
- .argument("<input>", "Path to the Fluent file to compile")
19
+ .argument(
20
+ "<inputs...>",
21
+ "Paths to the Fluent file(s) to compile. Multiple files are joined in order; ids must be unique across the set.",
22
+ )
20
23
  .requiredOption(
21
24
  "--locale <locale...>",
22
25
  "What locale(s) to be used. Multiple can be set to allow for fallback. i.e. en-CA",
@@ -68,14 +71,14 @@ new Command()
68
71
  "Path to store the resulting JavaScript file. Will be in ESM.",
69
72
  ),
70
73
  )
71
- .action(async (input, options) => {
74
+ .action(async (inputs, options) => {
72
75
  options.comments = options.comments ?? false;
73
76
  try {
74
- await fileExists(input);
75
-
76
- const ftl = await readFile(input, { encoding: "utf8" });
77
+ for (const input of inputs) {
78
+ await fileExists(input);
79
+ }
77
80
 
78
- const js = compile(ftl, options);
81
+ const js = await compileFiles(inputs, options);
79
82
  if (options.output) {
80
83
  await writeFile(options.output, js, "utf8");
81
84
  } else {
package/index.d.ts CHANGED
@@ -28,7 +28,22 @@ export interface CompileOptions {
28
28
 
29
29
  /**
30
30
  * Compile Fluent (.ftl) source into a JavaScript ESM string.
31
+ * Pass an array of source strings to join multiple files into one module;
32
+ * top-level message/term ids must be unique across the set.
31
33
  */
32
- export declare function compile(src: string, opts?: CompileOptions): string;
34
+ export declare function compile(
35
+ src: string | string[],
36
+ opts?: CompileOptions,
37
+ ): string;
38
+
39
+ /**
40
+ * Read and compile one or more Fluent (.ftl) files into a single JavaScript
41
+ * ESM string. Files are joined in the order supplied. Top-level message/term
42
+ * ids must be unique across the set.
43
+ */
44
+ export declare function compileFiles(
45
+ paths: string[],
46
+ opts?: CompileOptions,
47
+ ): Promise<string>;
33
48
 
34
49
  export default compile;
package/index.js CHANGED
@@ -1,9 +1,42 @@
1
1
  // Copyright 2026 will Farrell, and fluent-transpiler contributors.
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
+ import { readFile } from "node:fs/promises";
4
5
  import { parse } from "@fluent/syntax";
5
6
  import { camelCase, constantCase, pascalCase, snakeCase } from "change-case";
6
7
 
8
+ const collectTopLevelIds = (src) => {
9
+ const { body } = parse(src);
10
+ const ids = [];
11
+ for (const node of body) {
12
+ if (node.type === "Message" || node.type === "Term") {
13
+ ids.push(node.id.name);
14
+ }
15
+ }
16
+ return ids;
17
+ };
18
+
19
+ const checkDuplicates = (sources) => {
20
+ const seen = new Map();
21
+ const duplicates = [];
22
+ for (const { label, src } of sources) {
23
+ for (const id of collectTopLevelIds(src)) {
24
+ const prior = seen.get(id);
25
+ if (prior !== undefined && prior !== label) {
26
+ duplicates.push({ id, a: prior, b: label });
27
+ } else if (prior === undefined) {
28
+ seen.set(id, label);
29
+ }
30
+ }
31
+ }
32
+ if (duplicates.length) {
33
+ const lines = duplicates.map(
34
+ (d) => ` - "${d.id}" defined in ${d.a} and ${d.b}`,
35
+ );
36
+ throw new Error(`Duplicate id(s) found:\n${lines.join("\n")}`);
37
+ }
38
+ };
39
+
7
40
  const reservedWords = new Set([
8
41
  "abstract",
9
42
  "arguments",
@@ -81,6 +114,11 @@ const exportDefault = `(id, params) => {
81
114
  }
82
115
  `;
83
116
  export const compile = (src, opts) => {
117
+ if (Array.isArray(src)) {
118
+ const sources = src.map((s, i) => ({ label: `source[${i}]`, src: s }));
119
+ checkDuplicates(sources);
120
+ src = src.join("\n\n");
121
+ }
84
122
  const options = {
85
123
  comments: true,
86
124
  errorOnJunk: true,
@@ -510,4 +548,15 @@ const variableNotation = {
510
548
  constantCase,
511
549
  };
512
550
 
551
+ export const compileFiles = async (paths, opts) => {
552
+ const sources = await Promise.all(
553
+ paths.map(async (path) => ({
554
+ label: path,
555
+ src: await readFile(path, { encoding: "utf8" }),
556
+ })),
557
+ );
558
+ checkDuplicates(sources);
559
+ return compile(sources.map((s) => s.src).join("\n\n"), opts);
560
+ };
561
+
513
562
  export default compile;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  ".github"
4
4
  ],
5
5
  "name": "fluent-transpiler",
6
- "version": "0.4.0",
6
+ "version": "0.5.0",
7
7
  "description": "Transpile Fluent (ftl) files into optimized, tree-shakable, JavaScript EcmaScript Modules (esm).",
8
8
  "main": "index.js",
9
9
  "types": "index.d.ts",
@@ -40,14 +40,21 @@
40
40
  "test:unit": "node --test --test-force-exit --experimental-test-coverage --test-coverage-lines=100 --test-coverage-branches=100 --test-coverage-functions=100 ./**/*.test.js",
41
41
  "test:types": "tstyche",
42
42
  "test:perf": "node --test --test-concurrency=1 ./**/*.perf.js",
43
- "test:sast": "npm run test:sast:license && npm run test:sast:lockfile && npm run test:sast:semgrep && npm run test:sast:trufflehog && npm run test:sast:trivy",
43
+ "test:sast": "npm run test:sast:license && npm run test:sast:lockfile && npm run test:sast:semgrep && npm run test:sast:trufflehog && npm run test:sast:gitleaks && npm run test:sast:actionlint && npm run test:sast:zizmor && npm run test:sast:trivy",
44
+ "test:sast:actionlint": "actionlint",
45
+ "test:sast:gitleaks": "gitleaks detect --source . --redact --no-banner",
44
46
  "test:sast:license": "license-check-and-add check -f license.json",
45
47
  "test:sast:lockfile": "lockfile-lint --path package-lock.json --type npm --allowed-schemes \"https:\" --allowed-hosts npm --validate-integrity --validate-package-names",
46
48
  "test:sast:semgrep": "semgrep scan --config auto",
47
49
  "test:sast:trivy": "trivy fs --scanners vuln,license --include-dev-deps --ignored-licenses 0BSD,Apache-2.0,BSD-1-Clause,BSD-2-Clause,BSD-3-Clause,CC0-1.0,CC-BY-4.0,ISC,MIT,Python-2.0 --exit-code 1 --disable-telemetry .",
48
50
  "test:sast:trufflehog": "trufflehog filesystem --only-verified --log-level=-1 ./",
51
+ "test:sast:zizmor": "zizmor .github/workflows/",
49
52
  "test:dast": "npm run test:dast:fuzz",
50
53
  "test:dast:fuzz": "node --test ./**/*.fuzz.js",
54
+ "rm": "npm run rm:macos && npm run rm:node_modules && npm run rm:lock",
55
+ "rm:macos": "find . -name '.DS_Store' -type f -delete",
56
+ "rm:lock": "find . -name 'package-lock.json' -type f -delete",
57
+ "rm:node_modules": "find . -name 'node_modules' -type d -prune -exec rm -rf '{}' +",
51
58
  "release:license:add": "license-check-and-add add -f license.json",
52
59
  "release:license:remove": "license-check-and-add remove -f license.json"
53
60
  },
@@ -80,14 +87,14 @@
80
87
  },
81
88
  "devDependencies": {
82
89
  "@biomejs/biome": "^2.0.0",
83
- "@commitlint/cli": "^20.0.0",
84
- "@commitlint/config-conventional": "^20.0.0",
90
+ "@commitlint/cli": "^21.0.0",
91
+ "@commitlint/config-conventional": "^21.0.0",
85
92
  "@fluent/bundle": "^0.19.0",
86
93
  "fast-check": "^4.0.0",
87
94
  "husky": "^9.0.0",
88
95
  "license-check-and-add": "4.0.5",
89
96
  "tinybench": "^6.0.0",
90
- "tstyche": "^6.0.0"
97
+ "tstyche": "^7.0.0"
91
98
  },
92
99
  "funding": {
93
100
  "type": "github",