gt 2.21.4 → 2.22.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.
Files changed (118) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/api/collectUserEditDiffs.js +1 -1
  3. package/dist/auth/callbackPage.d.ts +1 -0
  4. package/dist/auth/callbackPage.js +77 -0
  5. package/dist/auth/callbackPage.js.map +1 -0
  6. package/dist/auth/credentialStore.d.ts +15 -0
  7. package/dist/auth/credentialStore.js +107 -0
  8. package/dist/auth/credentialStore.js.map +1 -0
  9. package/dist/auth/errors.d.ts +7 -0
  10. package/dist/auth/errors.js +23 -0
  11. package/dist/auth/errors.js.map +1 -0
  12. package/dist/auth/loopback.d.ts +9 -0
  13. package/dist/auth/loopback.js +73 -0
  14. package/dist/auth/loopback.js.map +1 -0
  15. package/dist/auth/loopback.test.d.ts +1 -0
  16. package/dist/auth/oauth.d.ts +39 -0
  17. package/dist/auth/oauth.js +260 -0
  18. package/dist/auth/oauth.js.map +1 -0
  19. package/dist/bin/bin-entry.js +2 -10
  20. package/dist/bin/bin-entry.js.map +1 -1
  21. package/dist/cli/base.d.ts +4 -8
  22. package/dist/cli/base.js +119 -60
  23. package/dist/cli/base.js.map +1 -1
  24. package/dist/cli/commands/api.js +6 -2
  25. package/dist/cli/commands/api.js.map +1 -1
  26. package/dist/cli/commands/upload.js +2 -2
  27. package/dist/cli/commands/upload.js.map +1 -1
  28. package/dist/cli/commands/utils/validation.d.ts +7 -7
  29. package/dist/cli/commands/utils/validation.js +7 -20
  30. package/dist/cli/commands/utils/validation.js.map +1 -1
  31. package/dist/config/generateSettings.js +4 -9
  32. package/dist/config/generateSettings.js.map +1 -1
  33. package/dist/config/optionPresets.js +16 -6
  34. package/dist/config/optionPresets.js.map +1 -1
  35. package/dist/config/validateSettings.d.ts +4 -2
  36. package/dist/config/validateSettings.js +9 -0
  37. package/dist/config/validateSettings.js.map +1 -1
  38. package/dist/console/index.d.ts +0 -2
  39. package/dist/console/index.js +2 -4
  40. package/dist/console/index.js.map +1 -1
  41. package/dist/console/inkPrompts.js +8 -6
  42. package/dist/console/inkPrompts.js.map +1 -1
  43. package/dist/console/inkTypes.d.ts +3 -0
  44. package/dist/console/logger.d.ts +13 -0
  45. package/dist/console/logger.js +60 -20
  46. package/dist/console/logger.js.map +1 -1
  47. package/dist/console/logging.d.ts +5 -3
  48. package/dist/console/logging.js +8 -6
  49. package/dist/console/logging.js.map +1 -1
  50. package/dist/console/promptParsing.d.ts +4 -3
  51. package/dist/console/promptParsing.js +7 -7
  52. package/dist/console/promptParsing.js.map +1 -1
  53. package/dist/formats/files/aggregateFiles.js +4 -4
  54. package/dist/formats/files/aggregateFiles.js.map +1 -1
  55. package/dist/formats/files/localeContent.js +2 -2
  56. package/dist/formats/files/localeContent.js.map +1 -1
  57. package/dist/formats/files/transformFormat.d.ts +2 -0
  58. package/dist/formats/files/transformFormat.js +8 -2
  59. package/dist/formats/files/transformFormat.js.map +1 -1
  60. package/dist/formats/json/extractJson.js +3 -3
  61. package/dist/formats/json/extractJson.js.map +1 -1
  62. package/dist/formats/json/mergeJson.js +6 -6
  63. package/dist/formats/json/mergeJson.js.map +1 -1
  64. package/dist/formats/xcstrings/mergeXcstrings.js +2 -2
  65. package/dist/formats/xcstrings/mergeXcstrings.js.map +1 -1
  66. package/dist/generated/version.d.ts +1 -1
  67. package/dist/generated/version.js +1 -1
  68. package/dist/generated/version.js.map +1 -1
  69. package/dist/git/setupMergeDrivers.js +1 -1
  70. package/dist/git/setupMergeDrivers.js.map +1 -1
  71. package/dist/index.js +2 -0
  72. package/dist/index.js.map +1 -1
  73. package/dist/main.js +2 -10
  74. package/dist/main.js.map +1 -1
  75. package/dist/react/jsx/utils/jsxParsing/addGTIdentifierToSyntaxTree.js +1 -1
  76. package/dist/react/jsx/utils/jsxParsing/parseJsx.js +1 -1
  77. package/dist/react/jsx/utils/parseString.js +1 -1
  78. package/dist/react/jsx/utils/stringParsing/derivation/index.js +1 -1
  79. package/dist/setup/developmentCredentials.d.ts +8 -0
  80. package/dist/setup/developmentCredentials.js +82 -0
  81. package/dist/setup/developmentCredentials.js.map +1 -0
  82. package/dist/setup/setupViteSPA.js +1 -1
  83. package/dist/setup/setupViteSPA.js.map +1 -1
  84. package/dist/setup/userInput.d.ts +1 -0
  85. package/dist/setup/userInput.js +7 -2
  86. package/dist/setup/userInput.js.map +1 -1
  87. package/dist/translation/extractInline.js +1 -1
  88. package/dist/translation/extractInline.js.map +1 -1
  89. package/dist/types/index.d.ts +7 -2
  90. package/dist/utils/api.d.ts +146 -67
  91. package/dist/utils/api.js +5 -94
  92. package/dist/utils/api.js.map +1 -1
  93. package/dist/utils/credentials.d.ts +37 -10
  94. package/dist/utils/credentials.js +267 -58
  95. package/dist/utils/credentials.js.map +1 -1
  96. package/dist/utils/loadEnv.d.ts +2 -0
  97. package/dist/utils/loadEnv.js +24 -0
  98. package/dist/utils/loadEnv.js.map +1 -0
  99. package/dist/utils/localizeStaticUrls.js +34 -6
  100. package/dist/utils/localizeStaticUrls.js.map +1 -1
  101. package/dist/utils/processOpenApi.js +2 -2
  102. package/dist/utils/processOpenApi.js.map +1 -1
  103. package/dist/utils/reactPackageCompatibility.js +1 -1
  104. package/dist/utils/reactPackageCompatibility.js.map +1 -1
  105. package/dist/workflows/steps/PollJobsStep.js +40 -53
  106. package/dist/workflows/steps/PollJobsStep.js.map +1 -1
  107. package/dist/workflows/utils/syncFonts.js +2 -2
  108. package/dist/workflows/utils/syncFonts.js.map +1 -1
  109. package/package.json +8 -7
  110. package/dist/next/config/parseNextConfig.d.ts +0 -10
  111. package/dist/next/config/parseNextConfig.js +0 -34
  112. package/dist/next/config/parseNextConfig.js.map +0 -1
  113. package/dist/utils/fetch.d.ts +0 -11
  114. package/dist/utils/fetch.js +0 -24
  115. package/dist/utils/fetch.js.map +0 -1
  116. package/dist/utils/gt.d.ts +0 -2
  117. package/dist/utils/gt.js +0 -7
  118. package/dist/utils/gt.js.map +0 -1
@@ -1,71 +1,280 @@
1
- import { logger } from "../console/logger.js";
2
- import { logErrorAndExit } from "../console/logging.js";
3
- import apiRequest from "./fetch.js";
4
- import chalk from "chalk";
1
+ import { randomUUID } from "node:crypto";
5
2
  import path from "node:path";
6
3
  import fs from "node:fs";
4
+ import { createDiagnosticMessage, formatDiagnosticErrorDetails } from "generaltranslation/internal";
5
+ import { execFile } from "node:child_process";
6
+ import { promisify } from "node:util";
7
+ import dotenv from "dotenv";
7
8
  //#region src/utils/credentials.ts
8
- async function retrieveCredentials(settings, keyType) {
9
- const { sessionId } = await generateCredentialsSession(settings.baseUrl, keyType);
10
- const urlToOpen = `${settings.dashboardUrl}/cli/wizard/${sessionId}`;
11
- await import("open").then((open) => open.default(urlToOpen, { wait: false }));
12
- logger.message(`${chalk.dim(`If the browser window didn't open automatically, please open the following link:`)}\n\n${chalk.cyan(urlToOpen)}`);
13
- const spinner = logger.createSpinner("dots");
14
- spinner.start("Waiting for response from dashboard...");
15
- const credentials = await new Promise((resolve) => {
16
- const interval = setInterval(async () => {
17
- try {
18
- const res = await apiRequest(settings.baseUrl, `/cli/wizard/${sessionId}`, { method: "GET" });
19
- if (res.status === 200) {
20
- resolve(await res.json());
21
- clearInterval(interval);
22
- clearTimeout(timeout);
23
- apiRequest(settings.baseUrl, `/cli/wizard/${sessionId}`, { method: "DELETE" });
24
- }
25
- } catch (err) {
26
- console.error(err);
27
- }
28
- }, 2e3);
29
- const timeout = setTimeout(() => {
30
- spinner.stop("Timed out");
31
- clearInterval(interval);
32
- logErrorAndExit("Timed out waiting for response from dashboard");
33
- }, 1e3 * 60 * 60);
9
+ const execFileAsync = promisify(execFile);
10
+ const unsafeCredentialsEnvError = createDiagnosticMessage({
11
+ whatHappened: "Cannot safely update .env.local",
12
+ reassurance: "The existing .env.local file was not changed",
13
+ fix: "Move multiline values away from project/key assignments or set the development credentials manually, then retry"
14
+ });
15
+ function unwritableEnvFileError(envFile) {
16
+ return createDiagnosticMessage({
17
+ whatHappened: `${envFile} is not a regular file`,
18
+ reassurance: "Nothing was changed",
19
+ fix: "Replace it with a regular file or a symlink to one, or set the development credentials manually, then retry"
20
+ });
21
+ }
22
+ function trackedEnvFileError(file) {
23
+ return createDiagnosticMessage({
24
+ whatHappened: `${file} is tracked by Git`,
25
+ why: "development credentials saved there would be committed",
26
+ reassurance: "Nothing was changed",
27
+ fix: `Run \`git rm --cached ${path.basename(file)}\` in ${path.dirname(file)} and add the file to .gitignore, then retry`
28
+ });
29
+ }
30
+ function unignoredEnvFileError(file) {
31
+ return createDiagnosticMessage({
32
+ whatHappened: `${file} is not ignored by Git`,
33
+ why: "development credentials saved there could be committed",
34
+ reassurance: "Nothing was changed",
35
+ fix: `Add the file to a .gitignore that applies to ${path.dirname(file)}, then retry`
36
+ });
37
+ }
38
+ function unignoredEnvFileReferentError(envFile, referent) {
39
+ return createDiagnosticMessage({
40
+ whatHappened: `${envFile} links to ${referent}, which Git does not ignore`,
41
+ why: "development credentials saved there could be committed",
42
+ reassurance: "Nothing was changed",
43
+ fix: `Add ${referent} to a .gitignore in its repository or set the development credentials manually, then retry`
44
+ });
45
+ }
46
+ function gitUnavailableError(file, error) {
47
+ return createDiagnosticMessage({
48
+ whatHappened: `Could not run Git to verify that ${file} is untracked and ignored`,
49
+ reassurance: "Nothing was changed",
50
+ fix: "Install Git or make it available on PATH, then retry",
51
+ details: formatDiagnosticErrorDetails(error)
52
+ });
53
+ }
54
+ function gitOverrideError(file, names) {
55
+ return createDiagnosticMessage({
56
+ whatHappened: `Git repository overrides are set in the environment: ${names.join(", ")}`,
57
+ why: `they could point the untracked and ignore checks for ${file} at a different repository or index`,
58
+ reassurance: "Nothing was changed",
59
+ fix: "Unset them (for example, run outside a Git hook), then retry"
60
+ });
61
+ }
62
+ function gitCheckFailedError(file, stderr) {
63
+ return createDiagnosticMessage({
64
+ whatHappened: `Git could not check whether ${file} is untracked and ignored`,
65
+ reassurance: "Nothing was changed",
66
+ fix: "Fix the reported Git problem, then retry",
67
+ details: stderr.trim()
34
68
  });
35
- spinner.stop("Received credentials");
36
- return credentials;
37
69
  }
38
- async function generateCredentialsSession(url, keyType) {
39
- const res = await apiRequest(url, "/cli/wizard/session", { body: { keyType } });
40
- if (!res.ok) logErrorAndExit("Failed to generate credentials session");
41
- return await res.json();
70
+ const FRAMEWORK_ENV_PREFIXES = {
71
+ "next-pages": "NEXT_PUBLIC_",
72
+ vite: "VITE_",
73
+ gatsby: "GATSBY_",
74
+ react: "REACT_APP_",
75
+ redwood: "REDWOOD_ENV_"
76
+ };
77
+ /** Names of the runtime variables init writes for this framework. */
78
+ function getDevelopmentEnvNames(framework) {
79
+ const prefix = (framework && FRAMEWORK_ENV_PREFIXES[framework]) ?? "";
80
+ return {
81
+ projectId: `${prefix}GT_PROJECT_ID`,
82
+ devApiKey: `${prefix}GT_DEV_API_KEY`
83
+ };
84
+ }
85
+ /**
86
+ * Whether the project already has a runtime key: the framework's development
87
+ * key, or an explicit production key for server runtimes (gt-next, gt-node).
88
+ * Browser frameworks only read their prefixed key, so there `settings.apiKey`
89
+ * is tooling auth alone. `settings.projectId` already resolves every
90
+ * framework prefix. Login is checked separately.
91
+ */
92
+ function areCredentialsSet(settings, framework) {
93
+ const { devApiKey } = getDevelopmentEnvNames(framework);
94
+ const browserOnly = Boolean(framework && FRAMEWORK_ENV_PREFIXES[framework]);
95
+ return Boolean(settings.projectId && (process.env[devApiKey] || !browserOnly && settings.apiKey));
96
+ }
97
+ function assignedName(line) {
98
+ return /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line)?.[1];
99
+ }
100
+ /**
101
+ * dotenv continues a quoted value onto later lines until its closing quote.
102
+ * Replacing such a line would orphan the continuation, even when a later
103
+ * duplicate hides the multiline value from dotenv's effective values.
104
+ */
105
+ function opensMultilineValue(line) {
106
+ const value = line.slice(line.indexOf("=") + 1).trim();
107
+ const quote = value[0];
108
+ return [
109
+ "\"",
110
+ "'",
111
+ "`"
112
+ ].includes(quote) && !value.slice(1).replaceAll(`\\${quote}`, "").includes(quote);
113
+ }
114
+ function upsertEnvAssignment(content, name, value) {
115
+ const assignsName = (line) => assignedName(line) === name;
116
+ const lines = content.split("\n");
117
+ const first = lines.findIndex(assignsName);
118
+ if (first === -1) return `${content}${content && !content.endsWith("\n") ? "\n" : ""}${name}=${value}\n`;
119
+ return lines.map((line, index) => index === first ? `${name}=${value}` : line).filter((line, index) => index === first || !assignsName(line)).join("\n");
120
+ }
121
+ /**
122
+ * Where .env.local content lives and its mode. Follows an existing symlink so
123
+ * its referent is replaced, never the link itself; anything but a regular
124
+ * file (dangling link, directory) fails before any write.
125
+ */
126
+ async function resolveEnvFile(envFile) {
127
+ const entry = await fs.promises.lstat(envFile).catch((error) => {
128
+ if (error.code === "ENOENT") return void 0;
129
+ throw error;
130
+ });
131
+ if (!entry) return void 0;
132
+ const stat = await fs.promises.stat(envFile).catch((error) => {
133
+ if (error.code === "ENOENT") return void 0;
134
+ throw error;
135
+ });
136
+ if (!stat?.isFile()) throw new Error(unwritableEnvFileError(envFile));
137
+ return {
138
+ target: entry.isSymbolicLink() ? await fs.promises.realpath(envFile) : envFile,
139
+ mode: stat.mode & 511
140
+ };
141
+ }
142
+ const GIT_LOCATION_OVERRIDES = [
143
+ "GIT_DIR",
144
+ "GIT_WORK_TREE",
145
+ "GIT_INDEX_FILE",
146
+ "GIT_COMMON_DIR",
147
+ "GIT_CEILING_DIRECTORIES"
148
+ ];
149
+ /**
150
+ * How Git in the file's own repository treats the path, using Git's real
151
+ * index and effective ignore rules (comments, negations, nested .gitignore,
152
+ * excludes). Only a Git-confirmed non-repository counts as no repository;
153
+ * a missing binary, a repository override, or any other Git error fails
154
+ * closed.
155
+ */
156
+ async function inspectGitExposure(file) {
157
+ const overrides = GIT_LOCATION_OVERRIDES.filter((name) => process.env[name] !== void 0);
158
+ if (overrides.length > 0) throw new Error(gitOverrideError(file, overrides));
159
+ const cwd = path.dirname(file);
160
+ const run = async (args) => {
161
+ try {
162
+ await execFileAsync("git", args, {
163
+ cwd,
164
+ encoding: "utf8",
165
+ windowsHide: true,
166
+ env: {
167
+ ...process.env,
168
+ LC_ALL: "C"
169
+ }
170
+ });
171
+ return {
172
+ code: 0,
173
+ stderr: ""
174
+ };
175
+ } catch (error) {
176
+ const { code, stderr } = error;
177
+ if (typeof code !== "number") throw new Error(gitUnavailableError(file, error));
178
+ return {
179
+ code,
180
+ stderr: typeof stderr === "string" ? stderr : ""
181
+ };
182
+ }
183
+ };
184
+ const toplevel = await run(["rev-parse", "--show-toplevel"]);
185
+ if (toplevel.code === 128 && /not a git repository/i.test(toplevel.stderr)) return "no-repository";
186
+ if (toplevel.code !== 0) throw new Error(gitCheckFailedError(file, toplevel.stderr));
187
+ const tracked = await run([
188
+ "ls-files",
189
+ "--error-unmatch",
190
+ "--",
191
+ file
192
+ ]);
193
+ if (tracked.code === 0) return "tracked";
194
+ if (tracked.code !== 1) throw new Error(gitCheckFailedError(file, tracked.stderr));
195
+ const ignored = await run([
196
+ "check-ignore",
197
+ "-q",
198
+ "--",
199
+ file
200
+ ]);
201
+ if (ignored.code === 0) return "ignored";
202
+ if (ignored.code === 1) return "unignored";
203
+ throw new Error(gitCheckFailedError(file, ignored.stderr));
204
+ }
205
+ /**
206
+ * Everything that must hold before credentials may be saved to .env.local,
207
+ * so callers can fail before minting a key: the path is a regular file (or a
208
+ * link to one) or absent, and neither it nor a link's referent is tracked by
209
+ * Git. A referent inside any worktree must already be ignored there; only
210
+ * .env.local itself gets an ignore rule added, by setCredentials.
211
+ */
212
+ async function inspectCredentialsEnvFile(cwd = process.cwd()) {
213
+ const envFile = path.resolve(cwd, ".env.local");
214
+ const existing = await resolveEnvFile(envFile);
215
+ const exposure = await inspectGitExposure(envFile);
216
+ if (exposure === "tracked") throw new Error(trackedEnvFileError(envFile));
217
+ if (existing && existing.target !== envFile) {
218
+ const referentExposure = await inspectGitExposure(existing.target);
219
+ if (referentExposure === "tracked") throw new Error(trackedEnvFileError(existing.target));
220
+ if (referentExposure === "unignored") throw new Error(unignoredEnvFileReferentError(envFile, existing.target));
221
+ }
222
+ return {
223
+ envFile,
224
+ existing,
225
+ exposure
226
+ };
227
+ }
228
+ async function appendGitignoreRule(cwd) {
229
+ const gitignoreFile = path.join(cwd, ".gitignore");
230
+ const content = await fs.promises.readFile(gitignoreFile, "utf8").catch((error) => {
231
+ if (error.code === "ENOENT") return "";
232
+ throw error;
233
+ });
234
+ const separator = content && !content.endsWith("\n") ? "\n" : "";
235
+ await fs.promises.appendFile(gitignoreFile, `${separator}.env.local\n`, "utf8");
42
236
  }
43
- function areCredentialsSet() {
44
- return process.env.GT_PROJECT_ID && (process.env.GT_API_KEY || process.env.GT_DEV_API_KEY);
237
+ async function writeEnvFileAtomically(target, content, mode) {
238
+ const temporaryPath = path.join(path.dirname(target), `.env.local.${randomUUID()}.tmp`);
239
+ try {
240
+ await fs.promises.writeFile(temporaryPath, content, {
241
+ encoding: "utf8",
242
+ flag: "wx",
243
+ mode
244
+ });
245
+ await fs.promises.chmod(temporaryPath, mode);
246
+ await fs.promises.rename(temporaryPath, target);
247
+ } catch (error) {
248
+ await fs.promises.rm(temporaryPath, { force: true });
249
+ throw error;
250
+ }
45
251
  }
46
252
  async function setCredentials(credentials, framework, cwd = process.cwd()) {
47
- const envFile = path.join(cwd, ".env.local");
48
- let envContent = "";
49
- if (!fs.existsSync(envFile)) {
50
- await fs.promises.writeFile(envFile, "", "utf8");
253
+ const { envFile, existing: existingEnvFile, exposure } = await inspectCredentialsEnvFile(cwd);
254
+ if (exposure === "unignored") {
255
+ await appendGitignoreRule(cwd);
256
+ if (await inspectGitExposure(envFile) !== "ignored") throw new Error(unignoredEnvFileError(envFile));
257
+ } else if (exposure === "no-repository") {
51
258
  const gitignoreFile = path.join(cwd, ".gitignore");
52
- if (fs.existsSync(gitignoreFile)) {
53
- if (!(await fs.promises.readFile(gitignoreFile, "utf8")).includes(".env.local")) await fs.promises.appendFile(gitignoreFile, "\n.env.local\n", "utf8");
54
- } else await fs.promises.writeFile(gitignoreFile, ".env.local\n", "utf8");
55
- } else envContent = await fs.promises.readFile(envFile, "utf8");
56
- let prefix = "";
57
- if (framework === "next-pages") prefix = "NEXT_PUBLIC_";
58
- else if (framework === "vite") prefix = "VITE_";
59
- else if (framework === "gatsby") prefix = "GATSBY_";
60
- else if (framework === "react") prefix = "REACT_APP_";
61
- else if (framework === "redwood") prefix = "REDWOOD_ENV_";
62
- envContent += `\n${prefix}GT_PROJECT_ID=${credentials.projectId}\n`;
63
- for (const apiKey of credentials.apiKeys) if (apiKey.type === "development") envContent += `${prefix || ""}GT_DEV_API_KEY=${apiKey.key}\n`;
64
- else envContent += `GT_API_KEY=${apiKey.key}\n`;
65
- envContent = envContent.replace(/\n{3,}/g, "\n\n").trim() + "\n";
66
- await fs.promises.writeFile(envFile, envContent, "utf8");
259
+ if (!fs.existsSync(gitignoreFile) || (await fs.promises.readFile(gitignoreFile, "utf8")).trimEnd().split(/\r?\n/).at(-1) !== ".env.local") await appendGitignoreRule(cwd);
260
+ }
261
+ let envContent = existingEnvFile ? await fs.promises.readFile(existingEnvFile.target, "utf8") : "";
262
+ const names = getDevelopmentEnvNames(framework);
263
+ const targetNames = Object.values(names);
264
+ const targetOpensMultiline = envContent.split("\n").some((line) => targetNames.includes(assignedName(line) ?? "") && opensMultilineValue(line));
265
+ const original = dotenv.parse(envContent);
266
+ const expected = {
267
+ ...original,
268
+ [names.projectId]: credentials.projectId,
269
+ [names.devApiKey]: credentials.apiKey
270
+ };
271
+ envContent = upsertEnvAssignment(envContent, names.projectId, credentials.projectId);
272
+ envContent = upsertEnvAssignment(envContent, names.devApiKey, credentials.apiKey);
273
+ const updated = dotenv.parse(envContent);
274
+ if (targetOpensMultiline || targetNames.some((name) => original[name]?.includes("\n")) || Object.keys(updated).length !== Object.keys(expected).length || Object.entries(expected).some(([name, value]) => updated[name] !== value)) throw new Error(unsafeCredentialsEnvError);
275
+ await writeEnvFileAtomically(existingEnvFile?.target ?? envFile, envContent, existingEnvFile?.mode ?? 384);
67
276
  }
68
277
  //#endregion
69
- export { areCredentialsSet, generateCredentialsSession, retrieveCredentials, setCredentials };
278
+ export { areCredentialsSet, getDevelopmentEnvNames, inspectCredentialsEnvFile, setCredentials };
70
279
 
71
280
  //# sourceMappingURL=credentials.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"credentials.js","names":[],"sources":["../../src/utils/credentials.ts"],"sourcesContent":["import { logErrorAndExit } from '../console/logging.js';\nimport { logger } from '../console/logger.js';\nimport path from 'node:path';\nimport fs from 'node:fs';\nimport { Settings, SupportedFrameworks } from '../types/index.js';\nimport chalk from 'chalk';\nimport apiRequest from './fetch.js';\n// Type for credentials returned from the dashboard\ntype Credentials = {\n apiKeys: ApiKey[];\n projectId: string;\n};\n\ntype ApiKey = {\n key: string;\n type: 'development' | 'production';\n};\n\n// Fetches project ID and API key by opening the dashboard in the browser\nexport async function retrieveCredentials(\n settings: Settings,\n keyType: 'development' | 'production' | 'all'\n): Promise<Credentials> {\n // Generate a session ID\n const { sessionId } = await generateCredentialsSession(\n settings.baseUrl,\n keyType\n );\n\n const urlToOpen = `${settings.dashboardUrl}/cli/wizard/${sessionId}`;\n await import('open').then((open) =>\n open.default(urlToOpen, {\n wait: false,\n })\n );\n\n logger.message(\n `${chalk.dim(\n `If the browser window didn't open automatically, please open the following link:`\n )}\\n\\n${chalk.cyan(urlToOpen)}`\n );\n\n const spinner = logger.createSpinner('dots');\n spinner.start('Waiting for response from dashboard...');\n\n const credentials = await new Promise<Credentials>((resolve) => {\n const interval = setInterval(async () => {\n // Ping the dashboard to see if the credentials are set\n try {\n const res = await apiRequest(\n settings.baseUrl,\n `/cli/wizard/${sessionId}`,\n {\n method: 'GET',\n }\n );\n if (res.status === 200) {\n const data = await res.json();\n resolve(data as Credentials);\n clearInterval(interval);\n clearTimeout(timeout);\n apiRequest(settings.baseUrl, `/cli/wizard/${sessionId}`, {\n method: 'DELETE',\n });\n }\n } catch (err) {\n console.error(err);\n }\n }, 2000);\n // timeout after 1 hour\n const timeout = setTimeout(\n () => {\n spinner.stop('Timed out');\n clearInterval(interval);\n logErrorAndExit('Timed out waiting for response from dashboard');\n },\n 1000 * 60 * 60\n );\n });\n spinner.stop('Received credentials');\n return credentials;\n}\n\nexport async function generateCredentialsSession(\n url: string,\n keyType: 'development' | 'production' | 'all'\n): Promise<{\n sessionId: string;\n}> {\n const res = await apiRequest(url, '/cli/wizard/session', {\n body: { keyType },\n });\n if (!res.ok) {\n logErrorAndExit('Failed to generate credentials session');\n }\n return await res.json();\n}\n\n// Checks if the credentials are set in the environment variables\nexport function areCredentialsSet() {\n return (\n process.env.GT_PROJECT_ID &&\n (process.env.GT_API_KEY || process.env.GT_DEV_API_KEY)\n );\n}\n\n// Sets the credentials in .env.local file\nexport async function setCredentials(\n credentials: Credentials,\n framework?: SupportedFrameworks,\n cwd: string = process.cwd()\n) {\n const envFile = path.join(cwd, '.env.local');\n let envContent = '';\n\n // Check if .env.local exists, create it if it doesn't\n if (!fs.existsSync(envFile)) {\n // File doesn't exist, create it\n await fs.promises.writeFile(envFile, '', 'utf8');\n\n // Add .env.local to .gitignore if it exists\n const gitignoreFile = path.join(cwd, '.gitignore');\n if (fs.existsSync(gitignoreFile)) {\n const gitignoreContent = await fs.promises.readFile(\n gitignoreFile,\n 'utf8'\n );\n if (!gitignoreContent.includes('.env.local')) {\n await fs.promises.appendFile(gitignoreFile, '\\n.env.local\\n', 'utf8');\n }\n } else {\n // Create .gitignore file with .env.local\n await fs.promises.writeFile(gitignoreFile, '.env.local\\n', 'utf8');\n }\n } else {\n // Read existing content\n envContent = await fs.promises.readFile(envFile, 'utf8');\n }\n\n // Always append the credentials to the file\n let prefix = '';\n if (framework === 'next-pages') {\n prefix = 'NEXT_PUBLIC_';\n } else if (framework === 'vite') {\n prefix = 'VITE_';\n } else if (framework === 'gatsby') {\n prefix = 'GATSBY_';\n } else if (framework === 'react') {\n prefix = 'REACT_APP_';\n } else if (framework === 'redwood') {\n prefix = 'REDWOOD_ENV_';\n }\n\n envContent += `\\n${prefix}GT_PROJECT_ID=${credentials.projectId}\\n`;\n\n for (const apiKey of credentials.apiKeys) {\n if (apiKey.type === 'development') {\n envContent += `${prefix || ''}GT_DEV_API_KEY=${apiKey.key}\\n`;\n } else {\n envContent += `GT_API_KEY=${apiKey.key}\\n`;\n }\n }\n\n // Ensure we don't have excessive newlines\n envContent = envContent.replace(/\\n{3,}/g, '\\n\\n').trim() + '\\n';\n\n // Write the updated content back to the file\n await fs.promises.writeFile(envFile, envContent, 'utf8');\n}\n"],"mappings":";;;;;;;AAmBA,eAAsB,oBACpB,UACA,SACsB;CAEtB,MAAM,EAAE,cAAc,MAAM,2BAC1B,SAAS,SACT,QACD;CAED,MAAM,YAAY,GAAG,SAAS,aAAa,cAAc;AACzD,OAAM,OAAO,QAAQ,MAAM,SACzB,KAAK,QAAQ,WAAW,EACtB,MAAM,OACP,CAAC,CACH;AAED,QAAO,QACL,GAAG,MAAM,IACP,mFACD,CAAC,MAAM,MAAM,KAAK,UAAU,GAC9B;CAED,MAAM,UAAU,OAAO,cAAc,OAAO;AAC5C,SAAQ,MAAM,yCAAyC;CAEvD,MAAM,cAAc,MAAM,IAAI,SAAsB,YAAY;EAC9D,MAAM,WAAW,YAAY,YAAY;AAEvC,OAAI;IACF,MAAM,MAAM,MAAM,WAChB,SAAS,SACT,eAAe,aACf,EACE,QAAQ,OACT,CACF;AACD,QAAI,IAAI,WAAW,KAAK;AAEtB,aAAQ,MADW,IAAI,MAAM,CACD;AAC5B,mBAAc,SAAS;AACvB,kBAAa,QAAQ;AACrB,gBAAW,SAAS,SAAS,eAAe,aAAa,EACvD,QAAQ,UACT,CAAC;;YAEG,KAAK;AACZ,YAAQ,MAAM,IAAI;;KAEnB,IAAK;EAER,MAAM,UAAU,iBACR;AACJ,WAAQ,KAAK,YAAY;AACzB,iBAAc,SAAS;AACvB,mBAAgB,gDAAgD;KAElE,MAAO,KAAK,GACb;GACD;AACF,SAAQ,KAAK,uBAAuB;AACpC,QAAO;;AAGT,eAAsB,2BACpB,KACA,SAGC;CACD,MAAM,MAAM,MAAM,WAAW,KAAK,uBAAuB,EACvD,MAAM,EAAE,SAAS,EAClB,CAAC;AACF,KAAI,CAAC,IAAI,GACP,iBAAgB,yCAAyC;AAE3D,QAAO,MAAM,IAAI,MAAM;;AAIzB,SAAgB,oBAAoB;AAClC,QACE,QAAQ,IAAI,kBACX,QAAQ,IAAI,cAAc,QAAQ,IAAI;;AAK3C,eAAsB,eACpB,aACA,WACA,MAAc,QAAQ,KAAK,EAC3B;CACA,MAAM,UAAU,KAAK,KAAK,KAAK,aAAa;CAC5C,IAAI,aAAa;AAGjB,KAAI,CAAC,GAAG,WAAW,QAAQ,EAAE;AAE3B,QAAM,GAAG,SAAS,UAAU,SAAS,IAAI,OAAO;EAGhD,MAAM,gBAAgB,KAAK,KAAK,KAAK,aAAa;AAClD,MAAI,GAAG,WAAW,cAAc;OAK1B,EAAC,MAJ0B,GAAG,SAAS,SACzC,eACA,OACD,EACqB,SAAS,aAAa,CAC1C,OAAM,GAAG,SAAS,WAAW,eAAe,kBAAkB,OAAO;QAIvE,OAAM,GAAG,SAAS,UAAU,eAAe,gBAAgB,OAAO;OAIpE,cAAa,MAAM,GAAG,SAAS,SAAS,SAAS,OAAO;CAI1D,IAAI,SAAS;AACb,KAAI,cAAc,aAChB,UAAS;UACA,cAAc,OACvB,UAAS;UACA,cAAc,SACvB,UAAS;UACA,cAAc,QACvB,UAAS;UACA,cAAc,UACvB,UAAS;AAGX,eAAc,KAAK,OAAO,gBAAgB,YAAY,UAAU;AAEhE,MAAK,MAAM,UAAU,YAAY,QAC/B,KAAI,OAAO,SAAS,cAClB,eAAc,GAAG,UAAU,GAAG,iBAAiB,OAAO,IAAI;KAE1D,eAAc,cAAc,OAAO,IAAI;AAK3C,cAAa,WAAW,QAAQ,WAAW,OAAO,CAAC,MAAM,GAAG;AAG5D,OAAM,GAAG,SAAS,UAAU,SAAS,YAAY,OAAO"}
1
+ {"version":3,"file":"credentials.js","names":[],"sources":["../../src/utils/credentials.ts"],"sourcesContent":["import { execFile } from 'node:child_process';\nimport { randomUUID } from 'node:crypto';\nimport path from 'node:path';\nimport fs from 'node:fs';\nimport { promisify } from 'node:util';\nimport dotenv from 'dotenv';\nimport { Settings, SupportedFrameworks } from '../types/index.js';\nimport {\n createDiagnosticMessage,\n formatDiagnosticErrorDetails,\n} from 'generaltranslation/internal';\n\nconst execFileAsync = promisify(execFile);\n\nconst unsafeCredentialsEnvError = createDiagnosticMessage({\n whatHappened: 'Cannot safely update .env.local',\n reassurance: 'The existing .env.local file was not changed',\n fix: 'Move multiline values away from project/key assignments or set the development credentials manually, then retry',\n});\n\nfunction unwritableEnvFileError(envFile: string): string {\n return createDiagnosticMessage({\n whatHappened: `${envFile} is not a regular file`,\n reassurance: 'Nothing was changed',\n fix: 'Replace it with a regular file or a symlink to one, or set the development credentials manually, then retry',\n });\n}\n\nfunction trackedEnvFileError(file: string): string {\n return createDiagnosticMessage({\n whatHappened: `${file} is tracked by Git`,\n why: 'development credentials saved there would be committed',\n reassurance: 'Nothing was changed',\n fix: `Run \\`git rm --cached ${path.basename(file)}\\` in ${path.dirname(file)} and add the file to .gitignore, then retry`,\n });\n}\n\nfunction unignoredEnvFileError(file: string): string {\n return createDiagnosticMessage({\n whatHappened: `${file} is not ignored by Git`,\n why: 'development credentials saved there could be committed',\n reassurance: 'Nothing was changed',\n fix: `Add the file to a .gitignore that applies to ${path.dirname(file)}, then retry`,\n });\n}\n\nfunction unignoredEnvFileReferentError(envFile: string, referent: string) {\n return createDiagnosticMessage({\n whatHappened: `${envFile} links to ${referent}, which Git does not ignore`,\n why: 'development credentials saved there could be committed',\n reassurance: 'Nothing was changed',\n fix: `Add ${referent} to a .gitignore in its repository or set the development credentials manually, then retry`,\n });\n}\n\nfunction gitUnavailableError(file: string, error: unknown): string {\n return createDiagnosticMessage({\n whatHappened: `Could not run Git to verify that ${file} is untracked and ignored`,\n reassurance: 'Nothing was changed',\n fix: 'Install Git or make it available on PATH, then retry',\n details: formatDiagnosticErrorDetails(error),\n });\n}\n\nfunction gitOverrideError(file: string, names: string[]): string {\n return createDiagnosticMessage({\n whatHappened: `Git repository overrides are set in the environment: ${names.join(', ')}`,\n why: `they could point the untracked and ignore checks for ${file} at a different repository or index`,\n reassurance: 'Nothing was changed',\n fix: 'Unset them (for example, run outside a Git hook), then retry',\n });\n}\n\nfunction gitCheckFailedError(file: string, stderr: string): string {\n return createDiagnosticMessage({\n whatHappened: `Git could not check whether ${file} is untracked and ignored`,\n reassurance: 'Nothing was changed',\n fix: 'Fix the reported Git problem, then retry',\n details: stderr.trim(),\n });\n}\n\nexport type Credentials = { apiKey: string; projectId: string };\n\n// Client-side frameworks only expose variables carrying their public prefix.\nconst FRAMEWORK_ENV_PREFIXES: Partial<\n Record<NonNullable<SupportedFrameworks>, string>\n> = {\n 'next-pages': 'NEXT_PUBLIC_',\n vite: 'VITE_',\n gatsby: 'GATSBY_',\n react: 'REACT_APP_',\n redwood: 'REDWOOD_ENV_',\n};\n\n/** Names of the runtime variables init writes for this framework. */\nexport function getDevelopmentEnvNames(framework?: SupportedFrameworks) {\n const prefix = (framework && FRAMEWORK_ENV_PREFIXES[framework]) ?? '';\n return {\n projectId: `${prefix}GT_PROJECT_ID`,\n devApiKey: `${prefix}GT_DEV_API_KEY`,\n };\n}\n\n/**\n * Whether the project already has a runtime key: the framework's development\n * key, or an explicit production key for server runtimes (gt-next, gt-node).\n * Browser frameworks only read their prefixed key, so there `settings.apiKey`\n * is tooling auth alone. `settings.projectId` already resolves every\n * framework prefix. Login is checked separately.\n */\nexport function areCredentialsSet(\n settings: Pick<Settings, 'projectId' | 'apiKey'>,\n framework?: SupportedFrameworks\n): boolean {\n const { devApiKey } = getDevelopmentEnvNames(framework);\n const browserOnly = Boolean(framework && FRAMEWORK_ENV_PREFIXES[framework]);\n return Boolean(\n settings.projectId &&\n (process.env[devApiKey] || (!browserOnly && settings.apiKey))\n );\n}\n\nfunction assignedName(line: string): string | undefined {\n return /^\\s*(?:export\\s+)?([A-Za-z_][A-Za-z0-9_]*)\\s*=/.exec(line)?.[1];\n}\n\n/**\n * dotenv continues a quoted value onto later lines until its closing quote.\n * Replacing such a line would orphan the continuation, even when a later\n * duplicate hides the multiline value from dotenv's effective values.\n */\nfunction opensMultilineValue(line: string): boolean {\n const value = line.slice(line.indexOf('=') + 1).trim();\n const quote = value[0];\n return (\n ['\"', \"'\", '`'].includes(quote) &&\n !value.slice(1).replaceAll(`\\\\${quote}`, '').includes(quote)\n );\n}\n\nfunction upsertEnvAssignment(\n content: string,\n name: string,\n value: string\n): string {\n const assignsName = (line: string) => assignedName(line) === name;\n const lines = content.split('\\n');\n const first = lines.findIndex(assignsName);\n if (first === -1) {\n const separator = content && !content.endsWith('\\n') ? '\\n' : '';\n return `${content}${separator}${name}=${value}\\n`;\n }\n // Replace the first assignment in place and drop stale duplicates.\n return lines\n .map((line, index) => (index === first ? `${name}=${value}` : line))\n .filter((line, index) => index === first || !assignsName(line))\n .join('\\n');\n}\n\n/**\n * Where .env.local content lives and its mode. Follows an existing symlink so\n * its referent is replaced, never the link itself; anything but a regular\n * file (dangling link, directory) fails before any write.\n */\nasync function resolveEnvFile(\n envFile: string\n): Promise<{ target: string; mode: number } | undefined> {\n const entry = await fs.promises.lstat(envFile).catch((error) => {\n if (error.code === 'ENOENT') return undefined;\n throw error;\n });\n if (!entry) return undefined;\n // stat follows the link and fails on a dangling one.\n const stat = await fs.promises.stat(envFile).catch((error) => {\n if (error.code === 'ENOENT') return undefined;\n throw error;\n });\n if (!stat?.isFile()) throw new Error(unwritableEnvFileError(envFile));\n return {\n target: entry.isSymbolicLink()\n ? await fs.promises.realpath(envFile)\n : envFile,\n mode: stat.mode & 0o777,\n };\n}\n\ntype GitExposure = 'no-repository' | 'tracked' | 'ignored' | 'unignored';\n\n// Git honors these over discovering the repository from the working\n// directory, so an inherited value could select another repository, hide this\n// one, or substitute an empty index. Configuration variables stay in effect.\nconst GIT_LOCATION_OVERRIDES = [\n 'GIT_DIR',\n 'GIT_WORK_TREE',\n 'GIT_INDEX_FILE',\n 'GIT_COMMON_DIR',\n 'GIT_CEILING_DIRECTORIES',\n];\n\n/**\n * How Git in the file's own repository treats the path, using Git's real\n * index and effective ignore rules (comments, negations, nested .gitignore,\n * excludes). Only a Git-confirmed non-repository counts as no repository;\n * a missing binary, a repository override, or any other Git error fails\n * closed.\n */\nasync function inspectGitExposure(file: string): Promise<GitExposure> {\n const overrides = GIT_LOCATION_OVERRIDES.filter(\n (name) => process.env[name] !== undefined\n );\n if (overrides.length > 0) throw new Error(gitOverrideError(file, overrides));\n const cwd = path.dirname(file);\n const run = async (args: string[]) => {\n try {\n await execFileAsync('git', args, {\n cwd,\n encoding: 'utf8',\n windowsHide: true,\n // The no-repository check reads Git's untranslated message.\n env: { ...process.env, LC_ALL: 'C' },\n });\n return { code: 0, stderr: '' };\n } catch (error) {\n const { code, stderr } = error as { code?: unknown; stderr?: unknown };\n if (typeof code !== 'number')\n throw new Error(gitUnavailableError(file, error));\n return { code, stderr: typeof stderr === 'string' ? stderr : '' };\n }\n };\n\n const toplevel = await run(['rev-parse', '--show-toplevel']);\n if (toplevel.code === 128 && /not a git repository/i.test(toplevel.stderr)) {\n return 'no-repository';\n }\n if (toplevel.code !== 0) {\n throw new Error(gitCheckFailedError(file, toplevel.stderr));\n }\n // check-ignore ignores tracked files, so the index is consulted first.\n const tracked = await run(['ls-files', '--error-unmatch', '--', file]);\n if (tracked.code === 0) return 'tracked';\n if (tracked.code !== 1)\n throw new Error(gitCheckFailedError(file, tracked.stderr));\n const ignored = await run(['check-ignore', '-q', '--', file]);\n if (ignored.code === 0) return 'ignored';\n if (ignored.code === 1) return 'unignored';\n throw new Error(gitCheckFailedError(file, ignored.stderr));\n}\n\ntype CredentialsEnvFile = {\n envFile: string;\n existing: Awaited<ReturnType<typeof resolveEnvFile>>;\n exposure: GitExposure;\n};\n\n/**\n * Everything that must hold before credentials may be saved to .env.local,\n * so callers can fail before minting a key: the path is a regular file (or a\n * link to one) or absent, and neither it nor a link's referent is tracked by\n * Git. A referent inside any worktree must already be ignored there; only\n * .env.local itself gets an ignore rule added, by setCredentials.\n */\nexport async function inspectCredentialsEnvFile(\n cwd: string = process.cwd()\n): Promise<CredentialsEnvFile> {\n const envFile = path.resolve(cwd, '.env.local');\n const existing = await resolveEnvFile(envFile);\n const exposure = await inspectGitExposure(envFile);\n if (exposure === 'tracked') throw new Error(trackedEnvFileError(envFile));\n if (existing && existing.target !== envFile) {\n const referentExposure = await inspectGitExposure(existing.target);\n if (referentExposure === 'tracked') {\n throw new Error(trackedEnvFileError(existing.target));\n }\n if (referentExposure === 'unignored') {\n throw new Error(unignoredEnvFileReferentError(envFile, existing.target));\n }\n }\n return { envFile, existing, exposure };\n}\n\nasync function appendGitignoreRule(cwd: string): Promise<void> {\n const gitignoreFile = path.join(cwd, '.gitignore');\n const content = await fs.promises\n .readFile(gitignoreFile, 'utf8')\n .catch((error) => {\n if (error.code === 'ENOENT') return '';\n throw error;\n });\n const separator = content && !content.endsWith('\\n') ? '\\n' : '';\n await fs.promises.appendFile(\n gitignoreFile,\n `${separator}.env.local\\n`,\n 'utf8'\n );\n}\n\n// Same-directory temporary file and rename, as in auth/credentialStore.ts:\n// a failed write never truncates the existing file.\nasync function writeEnvFileAtomically(\n target: string,\n content: string,\n mode: number\n): Promise<void> {\n const temporaryPath = path.join(\n path.dirname(target),\n `.env.local.${randomUUID()}.tmp`\n );\n try {\n await fs.promises.writeFile(temporaryPath, content, {\n encoding: 'utf8',\n flag: 'wx',\n mode,\n });\n await fs.promises.chmod(temporaryPath, mode);\n await fs.promises.rename(temporaryPath, target);\n } catch (error) {\n await fs.promises.rm(temporaryPath, { force: true });\n throw error;\n }\n}\n\n// Sets the credentials in .env.local file\nexport async function setCredentials(\n credentials: Credentials,\n framework?: SupportedFrameworks,\n cwd: string = process.cwd()\n) {\n const {\n envFile,\n existing: existingEnvFile,\n exposure,\n } = await inspectCredentialsEnvFile(cwd);\n\n if (exposure === 'unignored') {\n // A rule appended to the same-directory .gitignore is the last match at\n // the deepest level, so it wins over earlier negations; verify anyway.\n await appendGitignoreRule(cwd);\n if ((await inspectGitExposure(envFile)) !== 'ignored') {\n throw new Error(unignoredEnvFileError(envFile));\n }\n } else if (exposure === 'no-repository') {\n // Without a repository there are no effective rules to consult, so this\n // is unverified: seed a .gitignore so a later `git init` ignores the file.\n const gitignoreFile = path.join(cwd, '.gitignore');\n if (\n !fs.existsSync(gitignoreFile) ||\n (await fs.promises.readFile(gitignoreFile, 'utf8'))\n .trimEnd()\n .split(/\\r?\\n/)\n .at(-1) !== '.env.local'\n ) {\n await appendGitignoreRule(cwd);\n }\n }\n\n let envContent = existingEnvFile\n ? await fs.promises.readFile(existingEnvFile.target, 'utf8')\n : '';\n\n // Only the hot-reload key: the CLI itself acts as the signed-in user, and\n // CI keys are created deliberately rather than dropped into .env.local.\n // Other lines, comments, and any GT_API_KEY are left untouched.\n const names = getDevelopmentEnvNames(framework);\n const targetNames = Object.values(names);\n const targetOpensMultiline = envContent\n .split('\\n')\n .some(\n (line) =>\n targetNames.includes(assignedName(line) ?? '') &&\n opensMultilineValue(line)\n );\n const original = dotenv.parse(envContent);\n const expected = {\n ...original,\n [names.projectId]: credentials.projectId,\n [names.devApiKey]: credentials.apiKey,\n };\n envContent = upsertEnvAssignment(\n envContent,\n names.projectId,\n credentials.projectId\n );\n envContent = upsertEnvAssignment(\n envContent,\n names.devApiKey,\n credentials.apiKey\n );\n\n const updated = dotenv.parse(envContent);\n if (\n targetOpensMultiline ||\n targetNames.some((name) => original[name]?.includes('\\n')) ||\n Object.keys(updated).length !== Object.keys(expected).length ||\n Object.entries(expected).some(([name, value]) => updated[name] !== value)\n ) {\n throw new Error(unsafeCredentialsEnvError);\n }\n\n await writeEnvFileAtomically(\n existingEnvFile?.target ?? envFile,\n envContent,\n existingEnvFile?.mode ?? 0o600\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ /** Load executable startup env files without contaminating command stdout. */
2
+ export declare function loadEnv(): void;
@@ -0,0 +1,24 @@
1
+ import dotenv from "dotenv";
2
+ //#region src/utils/loadEnv.ts
3
+ /** Load executable startup env files without contaminating command stdout. */
4
+ function loadEnv() {
5
+ const log = console.log;
6
+ console.log = console.error;
7
+ try {
8
+ dotenv.config({ path: ".env" });
9
+ dotenv.config({
10
+ path: ".env.local",
11
+ override: true
12
+ });
13
+ dotenv.config({
14
+ path: ".env.production",
15
+ override: true
16
+ });
17
+ } finally {
18
+ console.log = log;
19
+ }
20
+ }
21
+ //#endregion
22
+ export { loadEnv };
23
+
24
+ //# sourceMappingURL=loadEnv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loadEnv.js","names":[],"sources":["../../src/utils/loadEnv.ts"],"sourcesContent":["import dotenv from 'dotenv';\n\n/** Load executable startup env files without contaminating command stdout. */\nexport function loadEnv(): void {\n /* eslint-disable no-console -- dotenv 16's vault warnings bypass quiet via console.log. */\n const log = console.log;\n // Keep this override synchronous; remove it when dotenv supports stderr diagnostics.\n console.log = console.error;\n try {\n dotenv.config({ path: '.env' });\n dotenv.config({ path: '.env.local', override: true });\n dotenv.config({ path: '.env.production', override: true });\n } finally {\n console.log = log;\n }\n /* eslint-enable no-console */\n}\n"],"mappings":";;;AAGA,SAAgB,UAAgB;CAE9B,MAAM,MAAM,QAAQ;AAEpB,SAAQ,MAAM,QAAQ;AACtB,KAAI;AACF,SAAO,OAAO,EAAE,MAAM,QAAQ,CAAC;AAC/B,SAAO,OAAO;GAAE,MAAM;GAAc,UAAU;GAAM,CAAC;AACrD,SAAO,OAAO;GAAE,MAAM;GAAmB,UAAU;GAAM,CAAC;WAClD;AACR,UAAQ,MAAM"}
@@ -3,6 +3,7 @@ import { parseMdxForRoundTrip, restoreAnchorIds } from "./mdxAnchorSyntax.js";
3
3
  import * as fs$1 from "fs";
4
4
  import { parse } from "@babel/parser";
5
5
  import micromatch from "micromatch";
6
+ import * as path$1 from "path";
6
7
  import { unified } from "unified";
7
8
  import remarkMdx from "remark-mdx";
8
9
  import remarkFrontmatter from "remark-frontmatter";
@@ -18,6 +19,28 @@ const { isMatch } = micromatch;
18
19
  * locale prefix were added. Extend deliberately.
19
20
  */
20
21
  const LOCALIZABLE_URL_ATTRIBUTES = new Set(["href"]);
22
+ const PAGE_EXTENSIONS = [".mdx", ".md"];
23
+ const INDEX_PAGE = "index";
24
+ /**
25
+ * The page file a root-relative docs URL points to, relative to the working
26
+ * directory like file mapping keys: `/a/b#x` -> `a/b.mdx`, `a/b.md`, or
27
+ * `a/b/index.mdx`.
28
+ */
29
+ function findPage(url) {
30
+ const base = url.split(/[?#]/)[0].replace(/^\/+|\/+$/g, "");
31
+ return PAGE_EXTENSIONS.flatMap((ext) => [`${base}${ext}`, path$1.join(base, `${INDEX_PAGE}${ext}`)]).find((page) => fs$1.existsSync(page));
32
+ }
33
+ /**
34
+ * Whether a URL points to a source page with no translation on disk, either at
35
+ * its mapped output path or at the localized URL. URLs with no source page,
36
+ * such as pages generated from an OpenAPI spec, are not missing a translation.
37
+ */
38
+ function translationMissing(url, localizedUrl, localizedFiles) {
39
+ const sourcePage = findPage(url);
40
+ if (!sourcePage) return false;
41
+ const mappedPage = localizedFiles[sourcePage];
42
+ return !(mappedPage && fs$1.existsSync(mappedPage)) && !findPage(localizedUrl);
43
+ }
21
44
  /**
22
45
  * Localize URL string literals that live inside an MDX expression's raw source.
23
46
  *
@@ -110,6 +133,8 @@ function localizeUrlsInExpressionSource(source, transformUrl, rootStringIsUrl =
110
133
  async function localizeStaticUrls(settings, targetLocales, includeFiles) {
111
134
  if (!settings.files || Object.keys(settings.files.placeholderPaths).length === 1 && settings.files.placeholderPaths.gt) return;
112
135
  const { resolvedPaths: sourceFiles } = settings.files;
136
+ const localizeOptions = settings.options?.experimentalLocalizeStaticUrls;
137
+ const skipUntranslatedPages = typeof localizeOptions === "object" && localizeOptions.skipUntranslatedPages === true;
113
138
  const locales = targetLocales || settings.locales;
114
139
  const fileMapping = createFileMapping(sourceFiles, settings.files.placeholderPaths, settings.files.transformPaths ?? {}, settings.files.transformFormats ?? {}, settings.locales, settings.defaultLocale);
115
140
  const processPromises = [];
@@ -127,10 +152,10 @@ async function localizeStaticUrls(settings, targetLocales, includeFiles) {
127
152
  }
128
153
  }
129
154
  const mappingPromises = Object.entries(fileMapping).filter(([locale]) => locales.includes(locale)).map(async ([locale, filesMap]) => {
130
- const targetFiles = Object.values(filesMap).filter((p) => (p.endsWith(".md") || p.endsWith(".mdx")) && (!includeFiles || includeFiles.has(p)));
155
+ const targetFiles = Object.values(filesMap).filter((p) => PAGE_EXTENSIONS.some((ext) => p.endsWith(ext)) && (!includeFiles || includeFiles.has(p)));
131
156
  await Promise.all(targetFiles.map(async (filePath) => {
132
157
  if (!fs$1.existsSync(filePath)) return;
133
- const result = localizeStaticUrlsForFile(await fs$1.promises.readFile(filePath, "utf8"), settings.defaultLocale, locale, settings.options?.experimentalHideDefaultLocale || false, settings.options?.docsUrlPattern, settings.options?.excludeStaticUrls, settings.options?.baseDomain);
158
+ const result = localizeStaticUrlsForFile(await fs$1.promises.readFile(filePath, "utf8"), settings.defaultLocale, locale, settings.options?.experimentalHideDefaultLocale || false, settings.options?.docsUrlPattern, settings.options?.excludeStaticUrls, settings.options?.baseDomain, skipUntranslatedPages ? filesMap : void 0);
134
159
  if (result.hasChanges) await fs$1.promises.writeFile(filePath, result.content);
135
160
  }));
136
161
  });
@@ -200,7 +225,7 @@ function transformUrlPath(originalUrl, patternHead, targetLocale, defaultLocale,
200
225
  /**
201
226
  * AST-based transformation for MDX files using remark-mdx
202
227
  */
203
- function transformMdxUrls(mdxContent, defaultLocale, targetLocale, hideDefaultLocale, pattern = "/[locale]", exclude = [], baseDomain) {
228
+ function transformMdxUrls(mdxContent, defaultLocale, targetLocale, hideDefaultLocale, pattern = "/[locale]", exclude = [], baseDomain, localizedFiles) {
204
229
  const transformedUrls = [];
205
230
  if (!pattern.startsWith("/")) pattern = "/" + pattern;
206
231
  const patternHead = pattern.split("[locale]")[0];
@@ -238,8 +263,10 @@ function transformMdxUrls(mdxContent, defaultLocale, targetLocale, hideDefaultLo
238
263
  }
239
264
  if (!shouldProcessUrl(originalUrl, patternHead, targetLocale, defaultLocale, baseDomain)) return null;
240
265
  if (baseDomain && shouldProcessAbsoluteUrl(originalUrl, baseDomain)) {
241
- const transformedPath = transformUrlPath(originalUrl.substring(baseDomain.length), patternHead, targetLocale, defaultLocale, hideDefaultLocale);
266
+ const afterDomain = originalUrl.substring(baseDomain.length);
267
+ const transformedPath = transformUrlPath(afterDomain, patternHead, targetLocale, defaultLocale, hideDefaultLocale);
242
268
  if (!transformedPath) return null;
269
+ if (localizedFiles && translationMissing(afterDomain, transformedPath, localizedFiles)) return null;
243
270
  transformedUrls.push({
244
271
  originalPath: originalUrl,
245
272
  newPath: transformedPath,
@@ -251,6 +278,7 @@ function transformMdxUrls(mdxContent, defaultLocale, targetLocale, hideDefaultLo
251
278
  const newUrl = transformUrlPath(originalUrl, patternHead, targetLocale, defaultLocale, hideDefaultLocale);
252
279
  if (!newUrl) return null;
253
280
  if (isUrlExcluded(originalUrl, exclude, defaultLocale)) return null;
281
+ if (localizedFiles && translationMissing(originalUrl, newUrl, localizedFiles)) return null;
254
282
  transformedUrls.push({
255
283
  originalPath: originalUrl,
256
284
  newPath: newUrl,
@@ -345,8 +373,8 @@ function transformMdxUrls(mdxContent, defaultLocale, targetLocale, hideDefaultLo
345
373
  transformedUrls
346
374
  };
347
375
  }
348
- function localizeStaticUrlsForFile(file, defaultLocale, targetLocale, hideDefaultLocale, pattern = "/[locale]", exclude = [], baseDomain) {
349
- return transformMdxUrls(file, defaultLocale, targetLocale, hideDefaultLocale, pattern, exclude, baseDomain || "");
376
+ function localizeStaticUrlsForFile(file, defaultLocale, targetLocale, hideDefaultLocale, pattern = "/[locale]", exclude = [], baseDomain, localizedFiles) {
377
+ return transformMdxUrls(file, defaultLocale, targetLocale, hideDefaultLocale, pattern, exclude, baseDomain || "", localizedFiles);
350
378
  }
351
379
  function checkIfPathMatchesPattern(originalUrlArray, patternHeadArray) {
352
380
  for (let i = 0; i < patternHeadArray.length; i++) if (patternHeadArray[i] !== originalUrlArray?.[i]) return false;