@lang-tag/cli 0.18.1 → 0.20.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/index.js CHANGED
@@ -2,18 +2,19 @@
2
2
  import { program } from "commander";
3
3
  import * as process$1 from "node:process";
4
4
  import process__default from "node:process";
5
- import fs, { readFileSync, existsSync, readdirSync, statSync } from "fs";
5
+ import fs, { readFileSync, existsSync, writeFileSync, readdirSync, statSync } from "fs";
6
6
  import { globby } from "globby";
7
7
  import * as path from "path";
8
8
  import path__default, { dirname, resolve, join, sep } from "path";
9
9
  import JSON5 from "json5";
10
10
  import { mkdir, writeFile, readFile } from "fs/promises";
11
- import path$1, { resolve as resolve$1 } from "pathe";
12
11
  import { pathToFileURL, fileURLToPath } from "url";
12
+ import path$1, { resolve as resolve$1 } from "pathe";
13
13
  import "case";
14
14
  import { N as NamespaceCollector } from "./chunks/namespace-collector.js";
15
15
  import micromatch from "micromatch";
16
16
  import * as acorn from "acorn";
17
+ import { Project } from "ts-morph";
17
18
  import mustache from "mustache";
18
19
  import checkbox from "@inquirer/checkbox";
19
20
  import confirm from "@inquirer/confirm";
@@ -82,22 +83,83 @@ class $LT_TagProcessor {
82
83
  const matches = [];
83
84
  let currentIndex = 0;
84
85
  const skipRanges = this.buildSkipRanges(fileContent);
85
- const startPattern = new RegExp(
86
- `${optionalVariableAssignment}${tagName}\\(\\s*\\{`,
86
+ const tagNamePattern = new RegExp(
87
+ `${optionalVariableAssignment}${tagName}`,
87
88
  "g"
88
89
  );
89
90
  while (true) {
90
- startPattern.lastIndex = currentIndex;
91
- const startMatch = startPattern.exec(fileContent);
92
- if (!startMatch) break;
93
- const matchStartIndex = startMatch.index;
94
- const variableName = startMatch[1] || void 0;
91
+ tagNamePattern.lastIndex = currentIndex;
92
+ const tagNameMatch = tagNamePattern.exec(fileContent);
93
+ if (!tagNameMatch) break;
94
+ const tagNameStartIndex = tagNameMatch.index;
95
+ const variableName = tagNameMatch[1] || void 0;
96
+ if (this.isInSkipRange(tagNameStartIndex, skipRanges)) {
97
+ currentIndex = tagNameStartIndex + 1;
98
+ continue;
99
+ }
100
+ let i = tagNameStartIndex + tagNameMatch[0].length;
101
+ let genericType = void 0;
102
+ let matchStartIndex = tagNameStartIndex;
103
+ while (i < fileContent.length && /\s/.test(fileContent[i])) {
104
+ i++;
105
+ }
106
+ if (i < fileContent.length && fileContent[i] === "<") {
107
+ const genericStart = i;
108
+ let angleCount = 1;
109
+ i++;
110
+ while (i < fileContent.length && angleCount > 0) {
111
+ const char = fileContent[i];
112
+ if (char === "<") angleCount++;
113
+ else if (char === ">") angleCount--;
114
+ else if ((char === '"' || char === "'" || char === "`") && !this.isInSkipRange(i, skipRanges)) {
115
+ i++;
116
+ while (i < fileContent.length) {
117
+ if (fileContent[i] === "\\") {
118
+ i += 2;
119
+ continue;
120
+ }
121
+ if (fileContent[i] === char) {
122
+ i++;
123
+ break;
124
+ }
125
+ i++;
126
+ }
127
+ continue;
128
+ }
129
+ i++;
130
+ }
131
+ if (angleCount === 0) {
132
+ genericType = fileContent.substring(genericStart + 1, i - 1).trim();
133
+ matchStartIndex = tagNameStartIndex;
134
+ } else {
135
+ currentIndex = tagNameStartIndex + 1;
136
+ continue;
137
+ }
138
+ } else {
139
+ matchStartIndex = tagNameStartIndex;
140
+ }
141
+ while (i < fileContent.length && /\s/.test(fileContent[i])) {
142
+ i++;
143
+ }
144
+ if (i >= fileContent.length || fileContent[i] !== "(" || i + 1 >= fileContent.length) {
145
+ currentIndex = tagNameStartIndex + 1;
146
+ continue;
147
+ }
148
+ i++;
149
+ while (i < fileContent.length && /\s/.test(fileContent[i])) {
150
+ i++;
151
+ }
152
+ if (i >= fileContent.length || fileContent[i] !== "{") {
153
+ currentIndex = tagNameStartIndex + 1;
154
+ continue;
155
+ }
156
+ const braceStartIndex = i;
95
157
  if (this.isInSkipRange(matchStartIndex, skipRanges)) {
96
158
  currentIndex = matchStartIndex + 1;
97
159
  continue;
98
160
  }
99
161
  let braceCount = 1;
100
- let i = matchStartIndex + startMatch[0].length;
162
+ i++;
101
163
  while (i < fileContent.length && braceCount > 0) {
102
164
  if (fileContent[i] === "{") braceCount++;
103
165
  if (fileContent[i] === "}") braceCount--;
@@ -107,10 +169,7 @@ class $LT_TagProcessor {
107
169
  currentIndex = matchStartIndex + 1;
108
170
  continue;
109
171
  }
110
- let parameter1Text = fileContent.substring(
111
- matchStartIndex + startMatch[0].length - 1,
112
- i
113
- );
172
+ let parameter1Text = fileContent.substring(braceStartIndex, i);
114
173
  let parameter2Text;
115
174
  while (i < fileContent.length && (fileContent[i] === " " || fileContent[i] === "\n" || fileContent[i] === " ")) {
116
175
  i++;
@@ -205,6 +264,7 @@ class $LT_TagProcessor {
205
264
  matches.push({
206
265
  fullMatch,
207
266
  variableName,
267
+ genericType,
208
268
  parameter1Text,
209
269
  parameter2Text,
210
270
  parameterTranslations,
@@ -258,7 +318,8 @@ class $LT_TagProcessor {
258
318
  }
259
319
  const arg1 = this.config.translationArgPosition === 1 ? newTranslationsString : newConfigString;
260
320
  const arg2 = this.config.translationArgPosition === 1 ? newConfigString : newTranslationsString;
261
- let tagFunction = `${this.config.tagName}(${arg1}`;
321
+ const genericTypePart = tag.genericType ? `<${tag.genericType}>` : "";
322
+ let tagFunction = `${this.config.tagName}${genericTypePart}(${arg1}`;
262
323
  if (arg2) tagFunction += `, ${arg2}`;
263
324
  tagFunction += ")";
264
325
  if (tag.variableName)
@@ -484,7 +545,9 @@ async function $LT_CollectCandidateFilesWithTags(props) {
484
545
  langTagConfig: config
485
546
  });
486
547
  }
487
- tags = $LT_FilterEmptyNamespaceTags(tags, logger);
548
+ if (!props.skipEmptyNamespaceCheck) {
549
+ tags = $LT_FilterEmptyNamespaceTags(tags, logger);
550
+ }
488
551
  const relativeFilePath = path__default.relative(cwd, filePath);
489
552
  candidates.push({ relativeFilePath, tags });
490
553
  }
@@ -825,9 +888,30 @@ async function $LT_WriteToCollections({
825
888
  }
826
889
  await config.collect.collector.postWrite(changedCollections);
827
890
  }
891
+ function deepFreezeObject(obj) {
892
+ const propNames = Object.getOwnPropertyNames(obj);
893
+ for (const name of propNames) {
894
+ const value = obj[name];
895
+ if (value && typeof value === "object") {
896
+ deepFreezeObject(value);
897
+ }
898
+ }
899
+ return Object.freeze(obj);
900
+ }
901
+ function formatFileUrlForDisplay(filePath) {
902
+ return pathToFileURL(filePath).href.replace(/\[/g, "%5B").replace(/\]/g, "%5D").replace(/\(/g, "%28").replace(/\)/g, "%29");
903
+ }
904
+ function formatExecutionTime(milliseconds) {
905
+ if (milliseconds >= 1e3) {
906
+ const seconds = milliseconds / 1e3;
907
+ return `${seconds.toFixed(1)}s`;
908
+ }
909
+ return `${Math.round(milliseconds)}ms`;
910
+ }
828
911
  const LANG_TAG_DEFAULT_CONFIG = {
829
912
  tagName: "lang",
830
913
  isLibrary: false,
914
+ enforceLibraryTagPrefix: true,
831
915
  includes: ["src/**/*.{js,ts,jsx,tsx}"],
832
916
  excludes: ["node_modules", "dist", "build"],
833
917
  localesDirectory: "locales",
@@ -888,6 +972,7 @@ This will enable import of language tags from external packages.
888
972
  }
889
973
  },
890
974
  translationArgPosition: 1,
975
+ hideDistDir: "dist",
891
976
  onConfigGeneration: async (event) => {
892
977
  event.logger.info(
893
978
  "Config generation event is not configured. Add onConfigGeneration handler to customize config generation."
@@ -926,6 +1011,9 @@ async function $LT_ReadConfig(projectPath) {
926
1011
  if (!config.collect.collector) {
927
1012
  throw new Error("Collector not found! (config.collect.collector)");
928
1013
  }
1014
+ if (config.isLibrary && (config.enforceLibraryTagPrefix ?? true) && config.tagName && !config.tagName.startsWith("_")) {
1015
+ config.tagName = `_${config.tagName}`;
1016
+ }
929
1017
  return config;
930
1018
  } catch (error) {
931
1019
  throw error;
@@ -1270,9 +1358,9 @@ async function logTagConflictInfo(tagInfo, prefix, conflictPath, translationArgP
1270
1358
  console.error("Failed to colorize config:", error);
1271
1359
  }
1272
1360
  }
1273
- const encodedPath = encodeURI(filePath);
1361
+ const fileUrl = formatFileUrlForDisplay(filePath);
1274
1362
  console.log(
1275
- `${ANSI.gray}${prefix}${ANSI.reset} ${ANSI.cyan}file://${encodedPath}${ANSI.reset}${ANSI.gray}:${lineNum}${ANSI.reset}`
1363
+ `${ANSI.gray}${prefix}${ANSI.reset} ${ANSI.cyan}${fileUrl}${ANSI.reset}${ANSI.gray}:${lineNum}${ANSI.reset}`
1276
1364
  );
1277
1365
  printLines(colorizedWhole.split("\n"), startLine, errorLines, condense);
1278
1366
  } catch (error) {
@@ -1402,9 +1490,14 @@ async function $LT_GetCommandEssentials() {
1402
1490
  };
1403
1491
  }
1404
1492
  async function $LT_CMD_Collect(options) {
1493
+ const startTime = Date.now();
1405
1494
  const { config, logger } = await $LT_GetCommandEssentials();
1406
1495
  logger.info("Collecting translations from source files...");
1407
- const files = await $LT_CollectCandidateFilesWithTags({ config, logger });
1496
+ const files = await $LT_CollectCandidateFilesWithTags({
1497
+ config,
1498
+ logger,
1499
+ skipEmptyNamespaceCheck: config.isLibrary
1500
+ });
1408
1501
  if (config.debug) {
1409
1502
  for (let file of files) {
1410
1503
  logger.debug("Found {count} translations tags inside: {file}", {
@@ -1413,8 +1506,19 @@ async function $LT_CMD_Collect(options) {
1413
1506
  });
1414
1507
  }
1415
1508
  }
1509
+ const totalTags = files.reduce((sum, file) => sum + file.tags.length, 0);
1416
1510
  if (config.isLibrary) {
1511
+ if (totalTags === 0 && (config.enforceLibraryTagPrefix ?? true) && config.tagName) {
1512
+ const baseTagName = config.tagName.startsWith("_") ? config.tagName.substring(1) : config.tagName;
1513
+ console.log("");
1514
+ logger.warn(
1515
+ '⚠️ No translation tags found in your library code.\n This might be because enforceLibraryTagPrefix is enabled.\n Remember: your tag function must be named {prefixedBaseTagName} (with "_" prefix), not {baseTagName}.\n Example: export function {prefixedBaseTagName}(...) instead of export function {baseTagName}(...)\n The prefix prevents the tag from appearing in TypeScript autocomplete after compilation.',
1516
+ { prefixedBaseTagName: `_${baseTagName}`, baseTagName }
1517
+ );
1518
+ }
1417
1519
  await $LT_WriteAsExportFile({ config, logger, files });
1520
+ const executionTime = formatExecutionTime(Date.now() - startTime);
1521
+ logger.debug("Collection completed ({time})", { time: executionTime });
1418
1522
  return;
1419
1523
  }
1420
1524
  try {
@@ -1423,10 +1527,6 @@ async function $LT_CMD_Collect(options) {
1423
1527
  files,
1424
1528
  config
1425
1529
  });
1426
- const totalTags = files.reduce(
1427
- (sum, file) => sum + file.tags.length,
1428
- 0
1429
- );
1430
1530
  logger.debug("Found {totalTags} translation tags", { totalTags });
1431
1531
  await $LT_WriteToCollections({
1432
1532
  config,
@@ -1434,15 +1534,252 @@ async function $LT_CMD_Collect(options) {
1434
1534
  logger,
1435
1535
  clean: options?.clean
1436
1536
  });
1537
+ const executionTime = formatExecutionTime(Date.now() - startTime);
1538
+ logger.debug("Collection completed ({time})", { time: executionTime });
1437
1539
  } catch (e) {
1438
1540
  const prefix = "LangTagConflictResolution:";
1439
1541
  if (e.message.startsWith(prefix)) {
1440
1542
  logger.error(e.message.substring(prefix.length));
1543
+ const executionTime = formatExecutionTime(Date.now() - startTime);
1544
+ logger.debug("Collection completed ({time})", {
1545
+ time: executionTime
1546
+ });
1441
1547
  return;
1442
1548
  }
1443
1549
  throw e;
1444
1550
  }
1445
1551
  }
1552
+ function $LT_HideExportsInDtsFile(dtsFilePath, variableNames) {
1553
+ const originalContent = readFileSync(dtsFilePath, "utf-8");
1554
+ const project = new Project({
1555
+ skipAddingFilesFromTsConfig: true,
1556
+ skipFileDependencyResolution: true,
1557
+ skipLoadingLibFiles: true
1558
+ });
1559
+ const sourceFile = project.addSourceFileAtPath(dtsFilePath);
1560
+ let hasChanges = false;
1561
+ const exportsToHide = [];
1562
+ const processedStatements = /* @__PURE__ */ new Set();
1563
+ for (const declaration of sourceFile.getVariableDeclarations()) {
1564
+ const name = declaration.getName();
1565
+ if (variableNames.has(name)) {
1566
+ const parent = declaration.getParent();
1567
+ if (parent && parent.getKindName() === "VariableDeclarationList") {
1568
+ const varList = parent;
1569
+ const grandParent = varList.getParent();
1570
+ if (grandParent && grandParent.getKindName() === "VariableStatement") {
1571
+ const varStatement = grandParent;
1572
+ if (varStatement.hasExportKeyword()) {
1573
+ if (!processedStatements.has(varStatement)) {
1574
+ processedStatements.add(varStatement);
1575
+ exportsToHide.push(name);
1576
+ varStatement.toggleModifier("export", false);
1577
+ hasChanges = true;
1578
+ }
1579
+ }
1580
+ }
1581
+ }
1582
+ }
1583
+ }
1584
+ if (!hasChanges) {
1585
+ return {
1586
+ hiddenCount: 0,
1587
+ modifiedContent: originalContent,
1588
+ originalContent
1589
+ };
1590
+ }
1591
+ const modifiedContent = sourceFile.getFullText();
1592
+ return {
1593
+ hiddenCount: exportsToHide.length,
1594
+ modifiedContent,
1595
+ originalContent
1596
+ };
1597
+ }
1598
+ function extractPrefixesFromIncludes(includes) {
1599
+ const prefixes = /* @__PURE__ */ new Set();
1600
+ for (const pattern of includes) {
1601
+ const groupMatch = pattern.match(/^\(([^)]+)\)\/\*\*/);
1602
+ if (groupMatch) {
1603
+ const options = groupMatch[1].split("|").map((s) => s.trim());
1604
+ options.forEach((opt) => prefixes.add(opt));
1605
+ continue;
1606
+ }
1607
+ const simpleMatch = pattern.match(/^([^/]+)\/\*\*/);
1608
+ if (simpleMatch) {
1609
+ prefixes.add(simpleMatch[1]);
1610
+ continue;
1611
+ }
1612
+ const singleMatch = pattern.match(/^([^/]+)\//);
1613
+ if (singleMatch) {
1614
+ prefixes.add(singleMatch[1]);
1615
+ }
1616
+ }
1617
+ return Array.from(prefixes);
1618
+ }
1619
+ function findMatchingDtsFile(sourceFilePath, sourceRelativePath, dtsFileMap, config) {
1620
+ const sourceBaseName = path__default.basename(
1621
+ sourceFilePath,
1622
+ path__default.extname(sourceFilePath)
1623
+ );
1624
+ const relativePathKey = sourceRelativePath.replace(
1625
+ /\.(ts|tsx|js|jsx)$/,
1626
+ ""
1627
+ );
1628
+ let dtsFilePath = dtsFileMap.get(relativePathKey);
1629
+ if (dtsFilePath) {
1630
+ return { dtsFilePath, strategy: "relative-path" };
1631
+ }
1632
+ if (config?.includes) {
1633
+ const prefixes = extractPrefixesFromIncludes(config.includes);
1634
+ for (const prefix of prefixes) {
1635
+ const prefixPattern = new RegExp(
1636
+ `^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/`
1637
+ );
1638
+ if (prefixPattern.test(sourceRelativePath)) {
1639
+ const strippedPath = sourceRelativePath.replace(
1640
+ prefixPattern,
1641
+ ""
1642
+ );
1643
+ const strippedPathKey = strippedPath.replace(
1644
+ /\.(ts|tsx|js|jsx)$/,
1645
+ ""
1646
+ );
1647
+ dtsFilePath = dtsFileMap.get(strippedPathKey);
1648
+ if (dtsFilePath) {
1649
+ return {
1650
+ dtsFilePath,
1651
+ strategy: "includes-prefix-stripped"
1652
+ };
1653
+ }
1654
+ }
1655
+ }
1656
+ }
1657
+ dtsFilePath = dtsFileMap.get(sourceBaseName);
1658
+ if (dtsFilePath) {
1659
+ return { dtsFilePath, strategy: "base-name" };
1660
+ }
1661
+ return { dtsFilePath: null, strategy: null };
1662
+ }
1663
+ async function $LT_MatchSourceToDtsFiles(files, distPath, cwd, config) {
1664
+ const sourceFileToVariables = /* @__PURE__ */ new Map();
1665
+ for (const file of files) {
1666
+ const sourceFilePath = path__default.resolve(cwd, file.relativeFilePath);
1667
+ const variableNames = /* @__PURE__ */ new Set();
1668
+ for (const tag of file.tags) {
1669
+ if (tag.variableName) {
1670
+ variableNames.add(tag.variableName);
1671
+ }
1672
+ }
1673
+ if (variableNames.size > 0) {
1674
+ sourceFileToVariables.set(sourceFilePath, variableNames);
1675
+ }
1676
+ }
1677
+ if (sourceFileToVariables.size === 0) {
1678
+ return [];
1679
+ }
1680
+ const dtsFiles = await globby("**/*.d.ts", {
1681
+ cwd: distPath,
1682
+ absolute: true
1683
+ });
1684
+ if (dtsFiles.length === 0) {
1685
+ return [];
1686
+ }
1687
+ const dtsFileMap = /* @__PURE__ */ new Map();
1688
+ for (const dtsFile of dtsFiles) {
1689
+ const relativeDtsPath = path__default.relative(distPath, dtsFile);
1690
+ const baseName = path__default.basename(dtsFile, ".d.ts");
1691
+ const relativePathWithoutExt = relativeDtsPath.replace(/\.d\.ts$/, "");
1692
+ dtsFileMap.set(relativePathWithoutExt, dtsFile);
1693
+ dtsFileMap.set(baseName, dtsFile);
1694
+ }
1695
+ const matches = [];
1696
+ const processedDtsFiles = /* @__PURE__ */ new Set();
1697
+ for (const [sourceFilePath, variableNames] of sourceFileToVariables) {
1698
+ const sourceRelativePath = path__default.relative(cwd, sourceFilePath);
1699
+ const match = findMatchingDtsFile(
1700
+ sourceFilePath,
1701
+ sourceRelativePath,
1702
+ dtsFileMap,
1703
+ config
1704
+ );
1705
+ if (!match.dtsFilePath) {
1706
+ continue;
1707
+ }
1708
+ if (processedDtsFiles.has(match.dtsFilePath)) {
1709
+ continue;
1710
+ }
1711
+ processedDtsFiles.add(match.dtsFilePath);
1712
+ matches.push({
1713
+ sourceFilePath,
1714
+ sourceRelativePath,
1715
+ dtsFilePath: match.dtsFilePath,
1716
+ variableNames
1717
+ });
1718
+ }
1719
+ return matches;
1720
+ }
1721
+ async function $LT_CMD_HideCompiledExports(options) {
1722
+ const { config, logger } = await $LT_GetCommandEssentials();
1723
+ const distDir = options?.distDir || config.hideDistDir || "dist";
1724
+ const distPath = path__default.resolve(process__default.cwd(), distDir);
1725
+ if (!existsSync(distPath)) {
1726
+ logger.warn("Dist directory does not exist: {distPath}", { distPath });
1727
+ return;
1728
+ }
1729
+ logger.info("Scanning source files for lang-tag variables...");
1730
+ const files = await $LT_CollectCandidateFilesWithTags({
1731
+ config,
1732
+ logger,
1733
+ skipEmptyNamespaceCheck: true
1734
+ });
1735
+ const matches = await $LT_MatchSourceToDtsFiles(
1736
+ files,
1737
+ distPath,
1738
+ process__default.cwd(),
1739
+ config
1740
+ );
1741
+ if (matches.length === 0) {
1742
+ logger.info(
1743
+ "No lang-tag variables found in source files or no matching .d.ts files found."
1744
+ );
1745
+ return;
1746
+ }
1747
+ logger.info("Found {count} .d.ts files to process", {
1748
+ count: matches.length
1749
+ });
1750
+ let hiddenCount = 0;
1751
+ for (const match of matches) {
1752
+ try {
1753
+ const result = $LT_HideExportsInDtsFile(
1754
+ match.dtsFilePath,
1755
+ match.variableNames
1756
+ );
1757
+ if (!result.hiddenCount) {
1758
+ continue;
1759
+ }
1760
+ writeFileSync(match.dtsFilePath, result.modifiedContent, "utf-8");
1761
+ hiddenCount += result.hiddenCount;
1762
+ const hiddenVariables = Array.from(match.variableNames).join(", ");
1763
+ logger.debug(
1764
+ "Hidden exports from {file}: {variables} (from {sourceFile})",
1765
+ {
1766
+ file: path__default.relative(process__default.cwd(), match.dtsFilePath),
1767
+ variables: hiddenVariables,
1768
+ sourceFile: match.sourceRelativePath
1769
+ }
1770
+ );
1771
+ } catch (error) {
1772
+ logger.warn("Error processing file {file}: {error}", {
1773
+ file: match.sourceRelativePath,
1774
+ error: error.message || String(error)
1775
+ });
1776
+ }
1777
+ }
1778
+ logger.success("Hidden {hiddenCount} exports from {fileCount} files.", {
1779
+ hiddenCount,
1780
+ fileCount: matches.length
1781
+ });
1782
+ }
1446
1783
  async function $LT_CollectExportFiles(logger) {
1447
1784
  const nodeModulesPath = path$1.join(process__default.cwd(), "node_modules");
1448
1785
  if (!fs.existsSync(nodeModulesPath)) {
@@ -1561,9 +1898,11 @@ async function generateImportFiles(config, logger, importManager) {
1561
1898
  const content = renderTemplate$2(templateData);
1562
1899
  await $LT_EnsureDirectoryExists(dirname(filePath));
1563
1900
  await writeFile(filePath, content, "utf-8");
1901
+ const fileUrl = formatFileUrlForDisplay(filePath);
1564
1902
  logger.success('Created tag file: "{file}"', {
1565
1903
  file: importedFile.pathRelativeToImportDir
1566
1904
  });
1905
+ logger.debug(" └── link: {url}", { url: fileUrl });
1567
1906
  }
1568
1907
  }
1569
1908
  class ImportManager {
@@ -1641,12 +1980,12 @@ async function $LT_ImportLibraries(config, logger) {
1641
1980
  logger.warn("No tags were imported from any library files");
1642
1981
  return;
1643
1982
  }
1983
+ await $LT_EnsureDirectoryExists(config.import.dir);
1644
1984
  await generateImportFiles(config, logger, importManager);
1645
1985
  if (config.import.onImportFinish) config.import.onImportFinish();
1646
1986
  }
1647
1987
  async function $LT_ImportTranslations() {
1648
1988
  const { config, logger } = await $LT_GetCommandEssentials();
1649
- await $LT_EnsureDirectoryExists(config.import.dir);
1650
1989
  logger.info("Importing translations from libraries...");
1651
1990
  await $LT_ImportLibraries(config, logger);
1652
1991
  logger.success("Successfully imported translations from libraries.");
@@ -2062,7 +2401,10 @@ async function detectInitTagOptions(options, config) {
2062
2401
  const isTypeScript = options.typescript !== void 0 ? options.typescript : detectTypeScript(packageJson);
2063
2402
  const isReact = options.react !== void 0 ? options.react : detectReact(packageJson);
2064
2403
  const isLibrary = options.library !== void 0 ? options.library : config.isLibrary;
2065
- const tagName = options.name || config.tagName || "lang";
2404
+ let tagName = options.name || config.tagName || "lang";
2405
+ if (isLibrary && (config.enforceLibraryTagPrefix ?? true) && !tagName.startsWith("_")) {
2406
+ tagName = `_${tagName}`;
2407
+ }
2066
2408
  const fileExtension = isLibrary && isReact ? isTypeScript ? "tsx" : "jsx" : isTypeScript ? "ts" : "js";
2067
2409
  return {
2068
2410
  tagName,
@@ -2163,22 +2505,22 @@ async function $LT_CMD_InitTagFile(options = {}) {
2163
2505
  "2. Create your translation objects and use the tag function"
2164
2506
  );
2165
2507
  logger.info('3. Run "lang-tag collect" to extract translations');
2508
+ if (renderOptions.isLibrary && renderOptions.tagName.startsWith("_") && (config.enforceLibraryTagPrefix ?? true)) {
2509
+ console.log("");
2510
+ logger.info(
2511
+ '📌 Important: Library tag prefix enforcement is enabled\n Your tag uses "_" prefix: {tagName} (instead of {baseTagName})\n This prevents the tag from appearing in TypeScript autocomplete after compilation\n Always use {tagName} (with prefix) in your library code\n This is a best practice for library internals - it keeps your API clean\n The prefix is automatically added by the enforceLibraryTagPrefix option\n To disable this behavior, set enforceLibraryTagPrefix: false in your config',
2512
+ {
2513
+ tagName: renderOptions.tagName,
2514
+ baseTagName: renderOptions.tagName.substring(1)
2515
+ }
2516
+ );
2517
+ }
2166
2518
  } catch (error) {
2167
2519
  logger.error("Failed to write file: {error}", {
2168
2520
  error: error?.message
2169
2521
  });
2170
2522
  }
2171
2523
  }
2172
- function deepFreezeObject(obj) {
2173
- const propNames = Object.getOwnPropertyNames(obj);
2174
- for (const name of propNames) {
2175
- const value = obj[name];
2176
- if (value && typeof value === "object") {
2177
- deepFreezeObject(value);
2178
- }
2179
- }
2180
- return Object.freeze(obj);
2181
- }
2182
2524
  async function checkAndRegenerateFileLangTags(config, logger, file, path2) {
2183
2525
  let libraryImportsDir = config.import.dir;
2184
2526
  if (!libraryImportsDir.endsWith(sep)) libraryImportsDir += sep;
@@ -2212,13 +2554,23 @@ async function checkAndRegenerateFileLangTags(config, logger, file, path2) {
2212
2554
  event.isSaved = true;
2213
2555
  event.savedConfig = updatedConfig;
2214
2556
  logger.debug(
2215
- 'Called save for "{path}" with config "{config}" triggered by: ("{trigger}")',
2557
+ 'Called save for "{path}"{varName} with config "{config}" triggered by: ("{trigger}")',
2216
2558
  {
2217
2559
  path: path2,
2218
2560
  config: JSON.stringify(updatedConfig),
2219
- trigger: triggerName || "-"
2561
+ trigger: triggerName || "-",
2562
+ varName: tag.variableName ? `(${tag.variableName})` : ""
2220
2563
  }
2221
2564
  );
2565
+ },
2566
+ getCurrentConfig: () => {
2567
+ if (event.savedConfig !== void 0 && event.savedConfig !== null) {
2568
+ return { ...event.savedConfig };
2569
+ }
2570
+ if (event.config) {
2571
+ return { ...event.config };
2572
+ }
2573
+ return {};
2222
2574
  }
2223
2575
  };
2224
2576
  await config.onConfigGeneration(event);
@@ -2233,10 +2585,10 @@ async function checkAndRegenerateFileLangTags(config, logger, file, path2) {
2233
2585
  if (replacements.length) {
2234
2586
  const newContent = processor.replaceTags(fileContent, replacements);
2235
2587
  await writeFile(file, newContent, "utf-8");
2236
- const encodedFile = encodeURI(file);
2588
+ const fileUrl = formatFileUrlForDisplay(file);
2237
2589
  logger.info(
2238
- 'Lang tag configurations written for file "{path}" (file://{file}:{line})',
2239
- { path: path2, file: encodedFile, line: lastUpdatedLine }
2590
+ 'Lang tag configurations written for file "{path}" ({url}:{line})',
2591
+ { path: path2, url: fileUrl, line: lastUpdatedLine }
2240
2592
  );
2241
2593
  return true;
2242
2594
  }
@@ -2249,6 +2601,7 @@ function isConfigSame(c1, c2) {
2249
2601
  return false;
2250
2602
  }
2251
2603
  async function $LT_CMD_RegenerateTags() {
2604
+ const startTime = Date.now();
2252
2605
  const { config, logger } = await $LT_GetCommandEssentials();
2253
2606
  const files = await globby(config.includes, {
2254
2607
  cwd: process.cwd(),
@@ -2269,11 +2622,13 @@ async function $LT_CMD_RegenerateTags() {
2269
2622
  dirty = true;
2270
2623
  }
2271
2624
  }
2625
+ const executionTime = formatExecutionTime(Date.now() - startTime);
2272
2626
  if (!dirty) {
2273
2627
  logger.info(
2274
2628
  "No changes were made based on the current configuration and files"
2275
2629
  );
2276
2630
  }
2631
+ logger.debug("Regeneration completed ({time})", { time: executionTime });
2277
2632
  }
2278
2633
  function getBasePath(pattern) {
2279
2634
  const globStartIndex = pattern.indexOf("*");
@@ -2382,6 +2737,14 @@ function createCli() {
2382
2737
  ).action(async (options) => {
2383
2738
  await $LT_CMD_InitTagFile(options);
2384
2739
  });
2740
+ program.command("hide-compiled-exports").alias("hce").description(
2741
+ "Hide compiled .d.ts exports of lang-tag variables (remove export modifier, keep type)"
2742
+ ).option(
2743
+ "-d, --dist-dir <dir>",
2744
+ 'Dist directory to process (default: from config or "dist")'
2745
+ ).action(async (options) => {
2746
+ await $LT_CMD_HideCompiledExports({ distDir: options.distDir });
2747
+ });
2385
2748
  return program;
2386
2749
  }
2387
2750
  createCli().parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lang-tag/cli",
3
- "version": "0.18.1",
3
+ "version": "0.20.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -45,7 +45,8 @@
45
45
  "json5": "^2.2.3",
46
46
  "micromatch": "^4.0.8",
47
47
  "mustache": "^4.2.0",
48
- "pathe": "^2.0.3"
48
+ "pathe": "^2.0.3",
49
+ "ts-morph": "^24.0.0"
49
50
  },
50
51
  "keywords": [
51
52
  "i18n",
@@ -1,8 +1,8 @@
1
1
  {{#isTypeScript}}
2
2
  import {
3
- CallableTranslations,
4
- LangTagTranslations,
5
- LangTagTranslationsConfig,
3
+ type CallableTranslations,
4
+ type LangTagTranslations,
5
+ type LangTagTranslationsConfig,
6
6
  createCallableTranslations
7
7
  } from 'lang-tag';
8
8
  {{/isTypeScript}}
@@ -10,7 +10,7 @@ import {
10
10
  import { createCallableTranslations } from 'lang-tag';
11
11
  {{/isTypeScript}}
12
12
  {{#isReact}}
13
- import React, { ReactNode, useMemo } from 'react';
13
+ import React, { type ReactNode, useMemo } from 'react';
14
14
  {{/isReact}}
15
15
 
16
16
  {{#isTypeScript}}