glotfile 0.8.1 → 0.8.3
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/dist/server/server.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Hono as Hono2 } from "hono";
|
|
3
3
|
import { serve } from "@hono/node-server";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
|
-
import { dirname as dirname4, join as
|
|
5
|
+
import { dirname as dirname4, join as join18, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
|
|
6
6
|
import { readFile, stat } from "fs/promises";
|
|
7
7
|
import { createServer } from "net";
|
|
8
8
|
import open from "open";
|
|
@@ -639,7 +639,7 @@ function setSourceValue(state, key, value) {
|
|
|
639
639
|
function setTargetValue(state, key, locale, value, clock = systemClock) {
|
|
640
640
|
const entry = requireKey(state, key);
|
|
641
641
|
if (entry.plural) throw new GlotfileError(`Key is a plural; use the plural setters: ${key}`);
|
|
642
|
-
entry.values[locale] = { value: value.trim(), state: "reviewed", updatedAt: clock() };
|
|
642
|
+
entry.values[canonLocale(locale)] = { value: value.trim(), state: "reviewed", updatedAt: clock() };
|
|
643
643
|
}
|
|
644
644
|
function formSignature(forms) {
|
|
645
645
|
return Object.entries(forms).sort(([a], [b]) => a.localeCompare(b)).map(([cat, val]) => `${cat}:${normalizeSource(val ?? "")}`).join("|");
|
|
@@ -662,8 +662,9 @@ function setSourcePluralForms(state, key, forms) {
|
|
|
662
662
|
}
|
|
663
663
|
function setPluralForms(state, key, locale, forms, clock = systemClock) {
|
|
664
664
|
const entry = requirePlural(state, key);
|
|
665
|
-
|
|
666
|
-
|
|
665
|
+
const loc = canonLocale(locale);
|
|
666
|
+
if (loc === state.config.sourceLocale) throw new GlotfileError("Use setSourcePluralForms for the source locale");
|
|
667
|
+
entry.values[loc] = { forms: normalizeForms(forms), state: "reviewed", updatedAt: clock() };
|
|
667
668
|
}
|
|
668
669
|
function convertToPlural(state, key, arg) {
|
|
669
670
|
const entry = requireKey(state, key);
|
|
@@ -691,12 +692,15 @@ function convertToScalar(state, key) {
|
|
|
691
692
|
}
|
|
692
693
|
function clearValue(state, key, locale) {
|
|
693
694
|
const entry = requireKey(state, key);
|
|
694
|
-
|
|
695
|
-
|
|
695
|
+
const loc = canonLocale(locale);
|
|
696
|
+
if (loc === state.config.sourceLocale) throw new GlotfileError("Cannot clear the source value");
|
|
697
|
+
delete entry.values[loc];
|
|
696
698
|
}
|
|
697
699
|
function setKeyState(state, key, locale, next) {
|
|
700
|
+
if (!STATES.includes(next)) throw new GlotfileError(`Unknown translation state: ${next}`);
|
|
698
701
|
const entry = requireKey(state, key);
|
|
699
|
-
const
|
|
702
|
+
const loc = canonLocale(locale);
|
|
703
|
+
const lv = entry.values[loc];
|
|
700
704
|
if (!lv) throw new GlotfileError(`No value for ${key} @ ${locale}`);
|
|
701
705
|
lv.state = next;
|
|
702
706
|
}
|
|
@@ -769,14 +773,16 @@ function removeCustomWord(state, word) {
|
|
|
769
773
|
function applyMachineTranslation(state, key, locale, value, clock = systemClock, force = false) {
|
|
770
774
|
const entry = requireKey(state, key);
|
|
771
775
|
if (entry.plural) throw new GlotfileError(`Key is a plural; use applyMachineTranslationForms: ${key}`);
|
|
772
|
-
|
|
773
|
-
entry.values[
|
|
776
|
+
const loc = canonLocale(locale);
|
|
777
|
+
if (!force && entry.values[loc]?.state === "reviewed") return false;
|
|
778
|
+
entry.values[loc] = { value: value.trim(), state: "machine", source: "ai", updatedAt: clock() };
|
|
774
779
|
return true;
|
|
775
780
|
}
|
|
776
781
|
function applyMachineTranslationForms(state, key, locale, forms, clock = systemClock, force = false) {
|
|
777
782
|
const entry = requirePlural(state, key);
|
|
778
|
-
|
|
779
|
-
entry.values[
|
|
783
|
+
const loc = canonLocale(locale);
|
|
784
|
+
if (!force && entry.values[loc]?.state === "reviewed") return false;
|
|
785
|
+
entry.values[loc] = { forms: normalizeForms(forms), state: "machine", source: "ai", updatedAt: clock() };
|
|
780
786
|
return true;
|
|
781
787
|
}
|
|
782
788
|
|
|
@@ -905,7 +911,21 @@ var PATTERNS = {
|
|
|
905
911
|
// t('key') — word boundary before t, not preceded by dot (excludes i18n.t which is above)
|
|
906
912
|
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*'([^']+)'/g,
|
|
907
913
|
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*"([^"]+)"/g,
|
|
908
|
-
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*`([^`$\n]+)`/g
|
|
914
|
+
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*`([^`$\n]+)`/g,
|
|
915
|
+
// vue-i18n pluralization: $tc('key') and the destructured bare tc('key').
|
|
916
|
+
/\$tc\s*\(\s*'([^']+)'/g,
|
|
917
|
+
/\$tc\s*\(\s*"([^"]+)"/g,
|
|
918
|
+
/(?<!\.)(?<![a-zA-Z0-9_$])\btc\s*\(\s*'([^']+)'/g,
|
|
919
|
+
/(?<!\.)(?<![a-zA-Z0-9_$])\btc\s*\(\s*"([^"]+)"/g,
|
|
920
|
+
// React-i18next <Trans i18nKey="key" /> (attribute order tolerated).
|
|
921
|
+
/<Trans\b[^>]*\bi18nKey\s*=\s*'([^']+)'/g,
|
|
922
|
+
/<Trans\b[^>]*\bi18nKey\s*=\s*"([^"]+)"/g,
|
|
923
|
+
// A renamed translate() wrapper (covers the common `const { t: translate }`
|
|
924
|
+
// alias by name; arbitrary aliases aren't resolved). Method `.translate()`
|
|
925
|
+
// is excluded. Over-matching here only keeps keys "used" — the safe direction
|
|
926
|
+
// for prune, which deletes only keys with no match at all.
|
|
927
|
+
/(?<!\.)(?<![a-zA-Z0-9_$])\btranslate\s*\(\s*'([^']+)'/g,
|
|
928
|
+
/(?<!\.)(?<![a-zA-Z0-9_$])\btranslate\s*\(\s*"([^"]+)"/g
|
|
909
929
|
],
|
|
910
930
|
gettext: [
|
|
911
931
|
/\b(?:gettext|ngettext)\s*\(\s*'([^']+)'/g,
|
|
@@ -939,7 +959,7 @@ var PREFIX_PATTERNS = {
|
|
|
939
959
|
/(?<!\.)(?<![a-zA-Z0-9_$])\bt\s*\(\s*`([^`$]*)\$\{/g
|
|
940
960
|
]
|
|
941
961
|
};
|
|
942
|
-
var CACHE_VERSION =
|
|
962
|
+
var CACHE_VERSION = 7;
|
|
943
963
|
var EXT_SCANNER = {
|
|
944
964
|
".php": "laravel",
|
|
945
965
|
".vue": "js-i18n",
|
|
@@ -2300,28 +2320,33 @@ var i18nextJson = {
|
|
|
2300
2320
|
export(state, output) {
|
|
2301
2321
|
const files = [];
|
|
2302
2322
|
const warnings = [];
|
|
2303
|
-
const
|
|
2323
|
+
const { indent, finalNewline } = resolveFormat(state, output);
|
|
2324
|
+
const fmt = { indent, sortKeys: true, finalNewline };
|
|
2325
|
+
const emptyAs = resolveEmptyAs(output, "omit");
|
|
2304
2326
|
const collided = /* @__PURE__ */ new Set();
|
|
2305
2327
|
warnings.push(...localeCollisionWarnings(output, state.config.locales, DEFAULT_LOCALE_CASE3));
|
|
2306
2328
|
for (const locale of state.config.locales) {
|
|
2307
2329
|
const obj = {};
|
|
2308
2330
|
for (const [key, entry] of Object.entries(state.keys)) {
|
|
2309
|
-
const lv = entry.values[locale];
|
|
2310
|
-
if (!lv) continue;
|
|
2311
2331
|
const segments = key.split(".");
|
|
2312
2332
|
const leaf = segments[segments.length - 1];
|
|
2313
2333
|
const parent = segments.slice(0, -1);
|
|
2314
2334
|
if (entry.plural) {
|
|
2315
|
-
|
|
2335
|
+
const forms = resolveForms(entry, locale, state.config.sourceLocale, emptyAs);
|
|
2336
|
+
if (!forms) continue;
|
|
2316
2337
|
for (const cat of PLURAL_CATEGORIES) {
|
|
2317
|
-
const body =
|
|
2338
|
+
const body = forms[cat];
|
|
2318
2339
|
if (body === void 0) continue;
|
|
2319
2340
|
if (setNested(obj, [...parent, `${leaf}_${cat}`], toI18next(body))) collided.add(key);
|
|
2320
2341
|
}
|
|
2321
2342
|
continue;
|
|
2322
2343
|
}
|
|
2323
|
-
|
|
2324
|
-
if (
|
|
2344
|
+
const raw = resolveScalar(entry, locale, state.config.sourceLocale, emptyAs);
|
|
2345
|
+
if (raw === null) continue;
|
|
2346
|
+
if (raw && isIcuPluralOrSelect(raw)) {
|
|
2347
|
+
warnings.push({ code: "lossy-plural", key, locale, message: "i18next-json does not yet convert ICU plural/select; written unconverted" });
|
|
2348
|
+
}
|
|
2349
|
+
if (setNested(obj, segments, toI18next(raw))) collided.add(key);
|
|
2325
2350
|
}
|
|
2326
2351
|
files.push({ path: resolvePath(output.path, resolveLocaleToken(output, locale, DEFAULT_LOCALE_CASE3)), contents: serializeJson(obj, fmt) });
|
|
2327
2352
|
}
|
|
@@ -2594,7 +2619,8 @@ function attrEscape(s) {
|
|
|
2594
2619
|
return xmlEscape2(s).replace(/"/g, """);
|
|
2595
2620
|
}
|
|
2596
2621
|
function angularXMeta(placeholders, name) {
|
|
2597
|
-
|
|
2622
|
+
const meta = placeholders?.[name];
|
|
2623
|
+
return /^[A-Z][A-Z0-9_]*$/.test(name) || meta?.origin === "x" ? meta : void 0;
|
|
2598
2624
|
}
|
|
2599
2625
|
function renderInterpolations(text, ids, placeholders) {
|
|
2600
2626
|
let out = "";
|
|
@@ -2841,7 +2867,7 @@ function checkOutputs(state, root) {
|
|
|
2841
2867
|
}
|
|
2842
2868
|
|
|
2843
2869
|
// src/server/api.ts
|
|
2844
|
-
import { readFileSync as readFileSync23, existsSync as existsSync13, readdirSync as
|
|
2870
|
+
import { readFileSync as readFileSync23, existsSync as existsSync13, readdirSync as readdirSync15, statSync as statSync10, rmSync as rmSync6 } from "fs";
|
|
2845
2871
|
import { dirname as dirname3, resolve as resolve9, basename, relative as relative4, sep as sep2 } from "path";
|
|
2846
2872
|
|
|
2847
2873
|
// src/server/ai/anthropic.ts
|
|
@@ -3316,6 +3342,7 @@ var AnthropicProvider = class {
|
|
|
3316
3342
|
}, { signal });
|
|
3317
3343
|
this.recordUsage(res.usage);
|
|
3318
3344
|
const text = res.content.find((b) => b.type === "text")?.text ?? "";
|
|
3345
|
+
if (res.stop_reason === "max_tokens") throw new MalformedReplyError(text);
|
|
3319
3346
|
return parseReplyItems(text);
|
|
3320
3347
|
}
|
|
3321
3348
|
};
|
|
@@ -3401,6 +3428,7 @@ var OpenAIProvider = class {
|
|
|
3401
3428
|
]
|
|
3402
3429
|
}, { signal });
|
|
3403
3430
|
const text = res.choices?.[0]?.message?.content ?? "";
|
|
3431
|
+
if (res.choices?.[0]?.finish_reason === "length") throw new MalformedReplyError(text);
|
|
3404
3432
|
return parseReplyItems(text);
|
|
3405
3433
|
}
|
|
3406
3434
|
};
|
|
@@ -3510,8 +3538,11 @@ var BedrockProvider = class {
|
|
|
3510
3538
|
const res = await this.client.send(this.makeCommand(this.buildInput(batch)), { abortSignal: signal });
|
|
3511
3539
|
const blocks = res.output?.message?.content ?? [];
|
|
3512
3540
|
const tool = blocks.find((b) => b.toolUse)?.toolUse;
|
|
3513
|
-
if (tool?.input?.items) return tool.input.items;
|
|
3514
3541
|
const text = blocks.find((b) => b.text)?.text ?? "";
|
|
3542
|
+
if (res.stopReason === "max_tokens") {
|
|
3543
|
+
throw new MalformedReplyError(text || JSON.stringify(tool?.input ?? {}));
|
|
3544
|
+
}
|
|
3545
|
+
if (tool?.input?.items) return tool.input.items;
|
|
3515
3546
|
return parseReplyItems(text);
|
|
3516
3547
|
}
|
|
3517
3548
|
};
|
|
@@ -3777,8 +3808,30 @@ function attachScreenshotsForProvider(reqs, state, projectRoot, supportsVision)
|
|
|
3777
3808
|
return { skipped: keys.size };
|
|
3778
3809
|
}
|
|
3779
3810
|
var DEFAULT_LOCALE_CONCURRENCY = 3;
|
|
3780
|
-
|
|
3811
|
+
function isTransientError(err) {
|
|
3812
|
+
const e = err;
|
|
3813
|
+
if (e && typeof e.status === "number") return e.status === 429 || e.status >= 500;
|
|
3814
|
+
const code = e?.code;
|
|
3815
|
+
return code === "ECONNRESET" || code === "ETIMEDOUT" || code === "ECONNREFUSED" || code === "EPIPE" || code === "EAI_AGAIN";
|
|
3816
|
+
}
|
|
3817
|
+
function sleep(ms, signal) {
|
|
3818
|
+
if (ms <= 0 || signal?.aborted) return Promise.resolve();
|
|
3819
|
+
return new Promise((resolve11) => {
|
|
3820
|
+
const t = setTimeout(() => {
|
|
3821
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3822
|
+
resolve11();
|
|
3823
|
+
}, ms);
|
|
3824
|
+
const onAbort = () => {
|
|
3825
|
+
clearTimeout(t);
|
|
3826
|
+
resolve11();
|
|
3827
|
+
};
|
|
3828
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
3829
|
+
});
|
|
3830
|
+
}
|
|
3831
|
+
async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAULT_LOCALE_CONCURRENCY, signal, batchSize = Infinity, retry = {}) {
|
|
3781
3832
|
if (!reqs.length) return [];
|
|
3833
|
+
const maxRetries = retry.retries ?? 3;
|
|
3834
|
+
const delayMs = retry.delayMs ?? ((attempt) => 250 * 2 ** attempt);
|
|
3782
3835
|
const byLocale = /* @__PURE__ */ new Map();
|
|
3783
3836
|
for (const req of reqs) {
|
|
3784
3837
|
let group = byLocale.get(req.targetLocale);
|
|
@@ -3813,10 +3866,20 @@ async function runLocaleParallel(reqs, provider, hooks = {}, concurrency = DEFAU
|
|
|
3813
3866
|
started.add(locale);
|
|
3814
3867
|
hooks.onLocaleStart?.(locale);
|
|
3815
3868
|
}
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3869
|
+
let batchResults;
|
|
3870
|
+
for (let attempt = 0; ; attempt++) {
|
|
3871
|
+
try {
|
|
3872
|
+
batchResults = await provider.translate(batch, (_localeDone, _localeTotal, results) => {
|
|
3873
|
+
done += results.length;
|
|
3874
|
+
hooks.onBatchComplete?.(done, total, results, locale);
|
|
3875
|
+
}, signal, (raw, size) => hooks.onMalformedReply?.(raw, size, locale));
|
|
3876
|
+
break;
|
|
3877
|
+
} catch (err) {
|
|
3878
|
+
if (attempt >= maxRetries || signal?.aborted || !isTransientError(err)) throw err;
|
|
3879
|
+
hooks.onRetry?.(locale, attempt + 1, err);
|
|
3880
|
+
await sleep(delayMs(attempt), signal);
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3820
3883
|
allResults.push(...batchResults);
|
|
3821
3884
|
const left = remaining.get(locale) - 1;
|
|
3822
3885
|
remaining.set(locale, left);
|
|
@@ -3881,21 +3944,56 @@ function clearPendingBatch(projectRoot) {
|
|
|
3881
3944
|
}
|
|
3882
3945
|
|
|
3883
3946
|
// src/server/log.ts
|
|
3884
|
-
import { appendFileSync,
|
|
3947
|
+
import { appendFileSync, existsSync as existsSync9, openSync, fstatSync, readSync, closeSync, statSync as statSync2, readFileSync as readFileSync9 } from "fs";
|
|
3885
3948
|
import { resolve as resolve6 } from "path";
|
|
3886
3949
|
function logPath(projectRoot) {
|
|
3887
3950
|
return resolve6(projectRoot, ".glotfile", "log.jsonl");
|
|
3888
3951
|
}
|
|
3952
|
+
var MAX_LOG_BYTES = 5 * 1024 * 1024;
|
|
3953
|
+
var TRIM_LOG_TO_BYTES = 4 * 1024 * 1024;
|
|
3889
3954
|
function appendLog(projectRoot, entry) {
|
|
3890
3955
|
ensureGlotfileDir(projectRoot);
|
|
3891
|
-
appendFileSync(logPath(projectRoot), JSON.stringify(entry) + "\n", "utf8");
|
|
3892
|
-
}
|
|
3893
|
-
function readLog(projectRoot, limit = 100) {
|
|
3894
3956
|
const path = logPath(projectRoot);
|
|
3895
|
-
|
|
3957
|
+
appendFileSync(path, JSON.stringify(entry) + "\n", "utf8");
|
|
3958
|
+
trimLog(path);
|
|
3959
|
+
}
|
|
3960
|
+
function trimLog(path, maxBytes = MAX_LOG_BYTES, targetBytes = TRIM_LOG_TO_BYTES) {
|
|
3961
|
+
if (!existsSync9(path) || statSync2(path).size <= maxBytes) return;
|
|
3896
3962
|
const lines = readFileSync9(path, "utf8").split("\n").filter((l) => l.trim() !== "");
|
|
3897
|
-
const
|
|
3898
|
-
|
|
3963
|
+
const kept = [];
|
|
3964
|
+
let bytes = 0;
|
|
3965
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
3966
|
+
const lineBytes = Buffer.byteLength(lines[i], "utf8") + 1;
|
|
3967
|
+
if (kept.length > 0 && bytes + lineBytes > targetBytes) break;
|
|
3968
|
+
kept.unshift(lines[i]);
|
|
3969
|
+
bytes += lineBytes;
|
|
3970
|
+
}
|
|
3971
|
+
writeFileAtomic(path, kept.length ? kept.join("\n") + "\n" : "");
|
|
3972
|
+
}
|
|
3973
|
+
function readLastLines(path, n, chunkSize = 64 * 1024) {
|
|
3974
|
+
if (n <= 0 || !existsSync9(path)) return [];
|
|
3975
|
+
const fd = openSync(path, "r");
|
|
3976
|
+
try {
|
|
3977
|
+
let pos = fstatSync(fd).size;
|
|
3978
|
+
if (pos === 0) return [];
|
|
3979
|
+
const chunks = [];
|
|
3980
|
+
while (pos > 0) {
|
|
3981
|
+
const size = Math.min(chunkSize, pos);
|
|
3982
|
+
pos -= size;
|
|
3983
|
+
const buf = Buffer.alloc(size);
|
|
3984
|
+
readSync(fd, buf, 0, size, pos);
|
|
3985
|
+
chunks.unshift(buf);
|
|
3986
|
+
const segments = Buffer.concat(chunks).toString("utf8").split("\n");
|
|
3987
|
+
const complete = (pos > 0 ? segments.slice(1) : segments).filter((l) => l.trim() !== "");
|
|
3988
|
+
if (complete.length >= n || pos === 0) return complete.slice(-n);
|
|
3989
|
+
}
|
|
3990
|
+
return [];
|
|
3991
|
+
} finally {
|
|
3992
|
+
closeSync(fd);
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3995
|
+
function readLog(projectRoot, limit = 100) {
|
|
3996
|
+
return readLastLines(logPath(projectRoot), limit).map((l) => JSON.parse(l)).reverse();
|
|
3899
3997
|
}
|
|
3900
3998
|
|
|
3901
3999
|
// src/server/ai/batch-run.ts
|
|
@@ -4222,13 +4320,13 @@ function estimateTranslation(state, ai, opts) {
|
|
|
4222
4320
|
import { relative as relative3 } from "path";
|
|
4223
4321
|
|
|
4224
4322
|
// src/server/import/detect.ts
|
|
4225
|
-
import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync11, statSync as
|
|
4323
|
+
import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync11, statSync as statSync3 } from "fs";
|
|
4226
4324
|
import { join as join6 } from "path";
|
|
4227
4325
|
var LOCALE_RE = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4228
4326
|
var VUE_DIR_CANDIDATES = ["src/locale", "src/locales", "src/i18n/locales", "locales", "lang"];
|
|
4229
4327
|
function safeIsDir(p) {
|
|
4230
4328
|
try {
|
|
4231
|
-
return
|
|
4329
|
+
return statSync3(p).isDirectory();
|
|
4232
4330
|
} catch {
|
|
4233
4331
|
return false;
|
|
4234
4332
|
}
|
|
@@ -4264,7 +4362,7 @@ function detectVue(root, forced = false) {
|
|
|
4264
4362
|
if (enough) {
|
|
4265
4363
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4266
4364
|
try {
|
|
4267
|
-
return
|
|
4365
|
+
return statSync3(join6(localeRoot, `${loc}.json`)).size;
|
|
4268
4366
|
} catch {
|
|
4269
4367
|
return 0;
|
|
4270
4368
|
}
|
|
@@ -4301,7 +4399,7 @@ function detectApple(root) {
|
|
|
4301
4399
|
locales,
|
|
4302
4400
|
sourceLocale: pickSource(locales, (loc) => {
|
|
4303
4401
|
try {
|
|
4304
|
-
return
|
|
4402
|
+
return statSync3(join6(dir, `${loc}.lproj`, "Localizable.strings")).size;
|
|
4305
4403
|
} catch {
|
|
4306
4404
|
return 0;
|
|
4307
4405
|
}
|
|
@@ -4363,7 +4461,7 @@ function detectI18next(root) {
|
|
|
4363
4461
|
if (locales.length === 0) continue;
|
|
4364
4462
|
const sourceLocale = pickSource(locales, (loc) => {
|
|
4365
4463
|
try {
|
|
4366
|
-
return readdirSync3(join6(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum +
|
|
4464
|
+
return readdirSync3(join6(localeRoot, loc)).filter((f) => f.endsWith(".json")).reduce((sum, f) => sum + statSync3(join6(localeRoot, loc, f)).size, 0);
|
|
4367
4465
|
} catch {
|
|
4368
4466
|
return 0;
|
|
4369
4467
|
}
|
|
@@ -4462,10 +4560,9 @@ import { join as join7 } from "path";
|
|
|
4462
4560
|
function flattenObject(value, prefix, warnings) {
|
|
4463
4561
|
const out = {};
|
|
4464
4562
|
const walk = (node, path) => {
|
|
4465
|
-
if (typeof node === "string") {
|
|
4466
|
-
out
|
|
4467
|
-
|
|
4468
|
-
out[path] = String(node);
|
|
4563
|
+
if (typeof node === "string" || typeof node === "number" || typeof node === "boolean") {
|
|
4564
|
+
if (path in out) warnings.push(`duplicate flattened key "${path}" \u2014 keeping the first value`);
|
|
4565
|
+
else out[path] = typeof node === "string" ? node : String(node);
|
|
4469
4566
|
} else if (Array.isArray(node)) {
|
|
4470
4567
|
node.forEach((el, i) => walk(el, path ? `${path}.${i}` : String(i)));
|
|
4471
4568
|
} else if (node && typeof node === "object") {
|
|
@@ -4510,7 +4607,7 @@ var vueI18nJson2 = {
|
|
|
4510
4607
|
};
|
|
4511
4608
|
|
|
4512
4609
|
// src/server/import/parsers/laravel-php.ts
|
|
4513
|
-
import { readdirSync as readdirSync5, statSync as
|
|
4610
|
+
import { readdirSync as readdirSync5, statSync as statSync4 } from "fs";
|
|
4514
4611
|
import { join as join8, relative as relative2 } from "path";
|
|
4515
4612
|
import { execFileSync } from "child_process";
|
|
4516
4613
|
|
|
@@ -4521,14 +4618,14 @@ function laravelToCanonical(value) {
|
|
|
4521
4618
|
|
|
4522
4619
|
// src/server/import/parsers/laravel-php.ts
|
|
4523
4620
|
function listDirs2(dir) {
|
|
4524
|
-
return readdirSync5(dir).filter((e) =>
|
|
4621
|
+
return readdirSync5(dir).filter((e) => statSync4(join8(dir, e)).isDirectory());
|
|
4525
4622
|
}
|
|
4526
4623
|
function listPhpFiles(dir) {
|
|
4527
4624
|
const out = [];
|
|
4528
4625
|
const walk = (d) => {
|
|
4529
4626
|
for (const e of readdirSync5(d)) {
|
|
4530
4627
|
const full = join8(d, e);
|
|
4531
|
-
if (
|
|
4628
|
+
if (statSync4(full).isDirectory()) walk(full);
|
|
4532
4629
|
else if (e.endsWith(".php")) out.push(full);
|
|
4533
4630
|
}
|
|
4534
4631
|
};
|
|
@@ -4649,7 +4746,7 @@ var flutterArb2 = {
|
|
|
4649
4746
|
};
|
|
4650
4747
|
|
|
4651
4748
|
// src/server/import/parsers/apple-strings.ts
|
|
4652
|
-
import { readdirSync as readdirSync7, readFileSync as readFileSync14, statSync as
|
|
4749
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync14, statSync as statSync5 } from "fs";
|
|
4653
4750
|
import { join as join10 } from "path";
|
|
4654
4751
|
var LOCALE_RE4 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4655
4752
|
var TABLE = "Localizable.strings";
|
|
@@ -4718,25 +4815,34 @@ function parseStrings(text, file, warnings) {
|
|
|
4718
4815
|
while (i < n && !/[\s=;]/.test(text[i])) raw += text[i++];
|
|
4719
4816
|
return raw.length ? raw : null;
|
|
4720
4817
|
};
|
|
4818
|
+
const recover = () => {
|
|
4819
|
+
while (i < n && text[i] !== ";") i++;
|
|
4820
|
+
if (i >= n) return false;
|
|
4821
|
+
i++;
|
|
4822
|
+
return true;
|
|
4823
|
+
};
|
|
4721
4824
|
while (true) {
|
|
4722
4825
|
skipTrivia();
|
|
4723
4826
|
if (i >= n) break;
|
|
4724
4827
|
const key = readToken();
|
|
4725
4828
|
if (key === null) {
|
|
4726
4829
|
warnings.push(`apple-strings: malformed entry in ${file} near offset ${i}`);
|
|
4727
|
-
break;
|
|
4830
|
+
if (!recover()) break;
|
|
4831
|
+
continue;
|
|
4728
4832
|
}
|
|
4729
4833
|
skipTrivia();
|
|
4730
4834
|
if (text[i] !== "=") {
|
|
4731
4835
|
warnings.push(`apple-strings: expected '=' after key "${key}" in ${file}`);
|
|
4732
|
-
break;
|
|
4836
|
+
if (!recover()) break;
|
|
4837
|
+
continue;
|
|
4733
4838
|
}
|
|
4734
4839
|
i++;
|
|
4735
4840
|
skipTrivia();
|
|
4736
4841
|
const value = readToken();
|
|
4737
4842
|
if (value === null) {
|
|
4738
4843
|
warnings.push(`apple-strings: missing value for key "${key}" in ${file}`);
|
|
4739
|
-
break;
|
|
4844
|
+
if (!recover()) break;
|
|
4845
|
+
continue;
|
|
4740
4846
|
}
|
|
4741
4847
|
skipTrivia();
|
|
4742
4848
|
if (text[i] === ";") i++;
|
|
@@ -4757,7 +4863,7 @@ var appleStrings2 = {
|
|
|
4757
4863
|
const file = join10(localeRoot, dir, TABLE);
|
|
4758
4864
|
let text;
|
|
4759
4865
|
try {
|
|
4760
|
-
if (!
|
|
4866
|
+
if (!statSync5(file).isFile()) continue;
|
|
4761
4867
|
text = readFileSync14(file, "utf8");
|
|
4762
4868
|
} catch {
|
|
4763
4869
|
continue;
|
|
@@ -4788,6 +4894,7 @@ function parseAttrs(s) {
|
|
|
4788
4894
|
for (const m of s.matchAll(/([\w-]+)="([^"]*)"/g)) out[m[1]] = decodeEntities(m[2]);
|
|
4789
4895
|
return out;
|
|
4790
4896
|
}
|
|
4897
|
+
var ANGULAR_CONVENTION_ID = /^[A-Z][A-Z0-9_]*$/;
|
|
4791
4898
|
function decodeLocations(body) {
|
|
4792
4899
|
const out = [];
|
|
4793
4900
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -4821,6 +4928,7 @@ function decodeInline(raw, addMeta) {
|
|
|
4821
4928
|
const meta = {};
|
|
4822
4929
|
if (attrs["ctype"]) meta.type = attrs["ctype"];
|
|
4823
4930
|
if (equiv !== void 0) meta.example = equiv;
|
|
4931
|
+
if (!ANGULAR_CONVENTION_ID.test(id)) meta.origin = "x";
|
|
4824
4932
|
addMeta(id, meta);
|
|
4825
4933
|
}
|
|
4826
4934
|
last = m.index + m[0].length;
|
|
@@ -5033,7 +5141,7 @@ var gettextPo2 = {
|
|
|
5033
5141
|
};
|
|
5034
5142
|
|
|
5035
5143
|
// src/server/import/parsers/i18next-json.ts
|
|
5036
|
-
import { readdirSync as readdirSync10, readFileSync as readFileSync17, statSync as
|
|
5144
|
+
import { readdirSync as readdirSync10, readFileSync as readFileSync17, statSync as statSync6 } from "fs";
|
|
5037
5145
|
import { join as join13 } from "path";
|
|
5038
5146
|
var LOCALE_RE7 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5039
5147
|
var PLURAL_SUFFIX_RE = /^(.+)_(zero|one|two|few|many|other)$/;
|
|
@@ -5041,7 +5149,7 @@ var PLURAL_ARG = "count";
|
|
|
5041
5149
|
var DEFAULT_NAMESPACE = "translation";
|
|
5042
5150
|
function safeIsDir2(p) {
|
|
5043
5151
|
try {
|
|
5044
|
-
return
|
|
5152
|
+
return statSync6(p).isDirectory();
|
|
5045
5153
|
} catch {
|
|
5046
5154
|
return false;
|
|
5047
5155
|
}
|
|
@@ -5141,6 +5249,15 @@ function decodeDouble(body) {
|
|
|
5141
5249
|
}
|
|
5142
5250
|
const n = body[++i];
|
|
5143
5251
|
if (n === void 0) break;
|
|
5252
|
+
const hexLen = n === "x" ? 2 : n === "u" ? 4 : n === "U" ? 8 : 0;
|
|
5253
|
+
if (hexLen) {
|
|
5254
|
+
const hex = body.slice(i + 1, i + 1 + hexLen);
|
|
5255
|
+
if (hex.length === hexLen && /^[0-9a-fA-F]+$/.test(hex)) {
|
|
5256
|
+
out += String.fromCodePoint(parseInt(hex, 16));
|
|
5257
|
+
i += hexLen;
|
|
5258
|
+
continue;
|
|
5259
|
+
}
|
|
5260
|
+
}
|
|
5144
5261
|
out += n === "n" ? "\n" : n === "r" ? "\r" : n === "t" ? " " : n;
|
|
5145
5262
|
}
|
|
5146
5263
|
return out;
|
|
@@ -5355,7 +5472,7 @@ var railsYaml2 = {
|
|
|
5355
5472
|
};
|
|
5356
5473
|
|
|
5357
5474
|
// src/server/import/parsers/apple-stringsdict.ts
|
|
5358
|
-
import { readdirSync as readdirSync12, readFileSync as readFileSync19, statSync as
|
|
5475
|
+
import { readdirSync as readdirSync12, readFileSync as readFileSync19, statSync as statSync7 } from "fs";
|
|
5359
5476
|
import { join as join15 } from "path";
|
|
5360
5477
|
var LOCALE_RE9 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
5361
5478
|
var TABLE2 = "Localizable.stringsdict";
|
|
@@ -5495,7 +5612,7 @@ var appleStringsdict2 = {
|
|
|
5495
5612
|
const file = join15(localeRoot, dir, TABLE2);
|
|
5496
5613
|
let text;
|
|
5497
5614
|
try {
|
|
5498
|
-
if (!
|
|
5615
|
+
if (!statSync7(file).isFile()) continue;
|
|
5499
5616
|
text = readFileSync19(file, "utf8");
|
|
5500
5617
|
} catch {
|
|
5501
5618
|
continue;
|
|
@@ -5817,7 +5934,7 @@ function refreshLocationUsage(projectRoot, format) {
|
|
|
5817
5934
|
}
|
|
5818
5935
|
|
|
5819
5936
|
// src/server/export-run.ts
|
|
5820
|
-
import { existsSync as existsSync12, readFileSync as readFileSync20, readdirSync as readdirSync13, rmdirSync, statSync as
|
|
5937
|
+
import { existsSync as existsSync12, readFileSync as readFileSync20, readdirSync as readdirSync13, rmdirSync, statSync as statSync8, unlinkSync } from "fs";
|
|
5821
5938
|
import { dirname as dirname2, resolve as resolve7, sep } from "path";
|
|
5822
5939
|
function effectiveLocales(config) {
|
|
5823
5940
|
const limit = config.exportLocales;
|
|
@@ -5860,7 +5977,7 @@ function pruneStaleLocaleFiles(output, validTokens, projectRoot) {
|
|
|
5860
5977
|
if (!segment.includes("{locale}") && !segment.includes("{namespace}")) {
|
|
5861
5978
|
const next = resolve7(dir, segment);
|
|
5862
5979
|
if (isLast) {
|
|
5863
|
-
if (stale(locale) && existsSync12(next) &&
|
|
5980
|
+
if (stale(locale) && existsSync12(next) && statSync8(next).isFile()) {
|
|
5864
5981
|
unlinkSync(next);
|
|
5865
5982
|
deleted++;
|
|
5866
5983
|
removeEmptyDirs(dir, root);
|
|
@@ -6043,6 +6160,122 @@ function aiConfigError(ai) {
|
|
|
6043
6160
|
return null;
|
|
6044
6161
|
}
|
|
6045
6162
|
|
|
6163
|
+
// src/server/events.ts
|
|
6164
|
+
function createEventHub() {
|
|
6165
|
+
const senders = /* @__PURE__ */ new Set();
|
|
6166
|
+
return {
|
|
6167
|
+
subscribe(send) {
|
|
6168
|
+
senders.add(send);
|
|
6169
|
+
return () => senders.delete(send);
|
|
6170
|
+
},
|
|
6171
|
+
broadcast(event, data) {
|
|
6172
|
+
for (const send of [...senders]) {
|
|
6173
|
+
try {
|
|
6174
|
+
send(event, data);
|
|
6175
|
+
} catch {
|
|
6176
|
+
}
|
|
6177
|
+
}
|
|
6178
|
+
},
|
|
6179
|
+
size() {
|
|
6180
|
+
return senders.size;
|
|
6181
|
+
}
|
|
6182
|
+
};
|
|
6183
|
+
}
|
|
6184
|
+
|
|
6185
|
+
// src/server/watch.ts
|
|
6186
|
+
import { statSync as statSync9, readdirSync as readdirSync14 } from "fs";
|
|
6187
|
+
import { join as join17 } from "path";
|
|
6188
|
+
import { createHash as createHash2 } from "crypto";
|
|
6189
|
+
function hashState(state) {
|
|
6190
|
+
return createHash2("sha1").update(serializeJson(state, state.config.format)).digest("hex");
|
|
6191
|
+
}
|
|
6192
|
+
function signature(statePath) {
|
|
6193
|
+
const fmt = detectFormat(statePath);
|
|
6194
|
+
if (fmt === "none") return "none";
|
|
6195
|
+
if (fmt === "single") {
|
|
6196
|
+
const s = statSync9(statePath);
|
|
6197
|
+
return `single:${s.size}:${s.mtimeMs}`;
|
|
6198
|
+
}
|
|
6199
|
+
const dir = splitDirFor(statePath);
|
|
6200
|
+
const parts = [];
|
|
6201
|
+
for (const rel of ["config.json", "keys.json"]) {
|
|
6202
|
+
try {
|
|
6203
|
+
const s = statSync9(join17(dir, rel));
|
|
6204
|
+
parts.push(`${rel}:${s.size}:${s.mtimeMs}`);
|
|
6205
|
+
} catch {
|
|
6206
|
+
}
|
|
6207
|
+
}
|
|
6208
|
+
try {
|
|
6209
|
+
for (const name of readdirSync14(join17(dir, "locales")).sort()) {
|
|
6210
|
+
if (!name.endsWith(".json")) continue;
|
|
6211
|
+
const s = statSync9(join17(dir, "locales", name));
|
|
6212
|
+
parts.push(`${name}:${s.size}:${s.mtimeMs}`);
|
|
6213
|
+
}
|
|
6214
|
+
} catch {
|
|
6215
|
+
}
|
|
6216
|
+
return `split:${parts.join("|")}`;
|
|
6217
|
+
}
|
|
6218
|
+
function createStateWatcher(opts) {
|
|
6219
|
+
const intervalMs = opts.intervalMs ?? 750;
|
|
6220
|
+
let statePath = opts.statePath;
|
|
6221
|
+
let lastSig = "";
|
|
6222
|
+
let lastHash = "";
|
|
6223
|
+
let timer;
|
|
6224
|
+
function baseline() {
|
|
6225
|
+
try {
|
|
6226
|
+
lastSig = signature(statePath);
|
|
6227
|
+
lastHash = hashState(loadState(statePath));
|
|
6228
|
+
} catch {
|
|
6229
|
+
lastSig = "";
|
|
6230
|
+
lastHash = "";
|
|
6231
|
+
}
|
|
6232
|
+
}
|
|
6233
|
+
function check() {
|
|
6234
|
+
let sig;
|
|
6235
|
+
try {
|
|
6236
|
+
sig = signature(statePath);
|
|
6237
|
+
} catch {
|
|
6238
|
+
return;
|
|
6239
|
+
}
|
|
6240
|
+
if (sig === lastSig) return;
|
|
6241
|
+
let hash;
|
|
6242
|
+
try {
|
|
6243
|
+
hash = hashState(loadState(statePath));
|
|
6244
|
+
} catch {
|
|
6245
|
+
lastSig = sig;
|
|
6246
|
+
return;
|
|
6247
|
+
}
|
|
6248
|
+
lastSig = sig;
|
|
6249
|
+
if (hash !== lastHash) {
|
|
6250
|
+
lastHash = hash;
|
|
6251
|
+
opts.onChange();
|
|
6252
|
+
}
|
|
6253
|
+
}
|
|
6254
|
+
function noteWrite(state) {
|
|
6255
|
+
try {
|
|
6256
|
+
lastSig = signature(statePath);
|
|
6257
|
+
} catch {
|
|
6258
|
+
lastSig = "";
|
|
6259
|
+
}
|
|
6260
|
+
lastHash = hashState(state);
|
|
6261
|
+
}
|
|
6262
|
+
function retarget(next) {
|
|
6263
|
+
statePath = next;
|
|
6264
|
+
baseline();
|
|
6265
|
+
}
|
|
6266
|
+
function start() {
|
|
6267
|
+
if (timer) return;
|
|
6268
|
+
timer = setInterval(check, intervalMs);
|
|
6269
|
+
timer.unref?.();
|
|
6270
|
+
}
|
|
6271
|
+
function stop() {
|
|
6272
|
+
if (timer) clearInterval(timer);
|
|
6273
|
+
timer = void 0;
|
|
6274
|
+
}
|
|
6275
|
+
baseline();
|
|
6276
|
+
return { check, noteWrite, retarget, start, stop };
|
|
6277
|
+
}
|
|
6278
|
+
|
|
6046
6279
|
// src/server/api.ts
|
|
6047
6280
|
var sanitize = (s) => s.replace(/[^\w.\-]+/g, "_");
|
|
6048
6281
|
var screenshotDirName = (statePath) => basename(statePath).replace(/\.[^.]+$/, "") + "-screenshots";
|
|
@@ -6076,6 +6309,14 @@ function createApi(deps) {
|
|
|
6076
6309
|
const app = new Hono();
|
|
6077
6310
|
const load = () => loadState(deps.statePath);
|
|
6078
6311
|
const projectRoot = dirname3(resolve9(deps.statePath));
|
|
6312
|
+
const hub = deps.eventHub ?? createEventHub();
|
|
6313
|
+
const watcher = createStateWatcher({
|
|
6314
|
+
statePath: deps.statePath,
|
|
6315
|
+
intervalMs: deps.watchIntervalMs,
|
|
6316
|
+
onChange: () => hub.broadcast("state-changed", JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString() }))
|
|
6317
|
+
});
|
|
6318
|
+
deps.onWatcher?.(watcher);
|
|
6319
|
+
if (deps.watch) watcher.start();
|
|
6079
6320
|
let translateQueue = Promise.resolve();
|
|
6080
6321
|
const withTranslateLock = (fn) => {
|
|
6081
6322
|
const next = translateQueue.then(fn, fn);
|
|
@@ -6097,12 +6338,30 @@ function createApi(deps) {
|
|
|
6097
6338
|
};
|
|
6098
6339
|
const persist = (s) => {
|
|
6099
6340
|
saveState(deps.statePath, s);
|
|
6341
|
+
watcher.noteWrite(s);
|
|
6100
6342
|
scheduleAutoExport(s);
|
|
6101
6343
|
};
|
|
6102
6344
|
const logChange = (entry) => appendLog(projectRoot, { ...entry, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
6103
6345
|
const valueText = (s, key, locale) => s.keys[key]?.values[locale]?.value;
|
|
6104
6346
|
const uiPrefsPath = deps.uiPrefsPath ?? defaultUiPrefsPath();
|
|
6105
6347
|
app.get("/state", (c) => c.json(load()));
|
|
6348
|
+
app.get("/events", (c) => streamSSE(c, async (stream) => {
|
|
6349
|
+
const send = (event, data) => {
|
|
6350
|
+
void stream.writeSSE({ event, data });
|
|
6351
|
+
};
|
|
6352
|
+
const unsubscribe = hub.subscribe(send);
|
|
6353
|
+
stream.onAbort(unsubscribe);
|
|
6354
|
+
await stream.writeSSE({ event: "ready", data: "" });
|
|
6355
|
+
try {
|
|
6356
|
+
while (!stream.aborted) {
|
|
6357
|
+
await stream.sleep(3e4);
|
|
6358
|
+
if (stream.aborted) break;
|
|
6359
|
+
await stream.writeSSE({ event: "ping", data: "" });
|
|
6360
|
+
}
|
|
6361
|
+
} finally {
|
|
6362
|
+
unsubscribe();
|
|
6363
|
+
}
|
|
6364
|
+
}));
|
|
6106
6365
|
app.get("/ui-prefs", (c) => c.json(loadUiPrefs(uiPrefsPath)));
|
|
6107
6366
|
app.put("/ui-prefs", async (c) => {
|
|
6108
6367
|
const body = await c.req.json();
|
|
@@ -6190,7 +6449,7 @@ function createApi(deps) {
|
|
|
6190
6449
|
if (depth > 4) return;
|
|
6191
6450
|
let entries = [];
|
|
6192
6451
|
try {
|
|
6193
|
-
entries =
|
|
6452
|
+
entries = readdirSync15(dir);
|
|
6194
6453
|
} catch {
|
|
6195
6454
|
return;
|
|
6196
6455
|
}
|
|
@@ -6204,7 +6463,7 @@ function createApi(deps) {
|
|
|
6204
6463
|
filePath = abs;
|
|
6205
6464
|
} else {
|
|
6206
6465
|
try {
|
|
6207
|
-
if (
|
|
6466
|
+
if (statSync10(abs).isDirectory()) walk(abs, depth + 1);
|
|
6208
6467
|
} catch {
|
|
6209
6468
|
}
|
|
6210
6469
|
continue;
|
|
@@ -6235,6 +6494,7 @@ function createApi(deps) {
|
|
|
6235
6494
|
if (!existsSync13(resolved)) return c.json({ error: "file not found" }, 400);
|
|
6236
6495
|
loadState(resolved);
|
|
6237
6496
|
deps.statePath = resolved;
|
|
6497
|
+
watcher.retarget(resolved);
|
|
6238
6498
|
return c.json({ ok: true, path: resolved, name: basename(resolved), dir: projectRoot, project: basename(projectRoot) });
|
|
6239
6499
|
});
|
|
6240
6500
|
app.post("/keys", async (c) => {
|
|
@@ -7209,7 +7469,7 @@ function createApi(deps) {
|
|
|
7209
7469
|
|
|
7210
7470
|
// src/server/server.ts
|
|
7211
7471
|
var here = dirname4(fileURLToPath(import.meta.url));
|
|
7212
|
-
var DEFAULT_UI_DIR =
|
|
7472
|
+
var DEFAULT_UI_DIR = join18(here, "..", "ui");
|
|
7213
7473
|
var MIME = {
|
|
7214
7474
|
".html": "text/html; charset=utf-8",
|
|
7215
7475
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -7235,9 +7495,22 @@ async function readFileResponse(absPath) {
|
|
|
7235
7495
|
return null;
|
|
7236
7496
|
}
|
|
7237
7497
|
}
|
|
7498
|
+
function isLocalHost(hostname) {
|
|
7499
|
+
const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
7500
|
+
return h === "localhost" || h === "127.0.0.1" || h === "::1" || h.endsWith(".localhost");
|
|
7501
|
+
}
|
|
7238
7502
|
function buildApp(opts) {
|
|
7239
7503
|
const app = new Hono2();
|
|
7240
|
-
|
|
7504
|
+
app.use("*", async (c, next) => {
|
|
7505
|
+
if (!isLocalHost(new URL(c.req.url).hostname)) return c.text("Forbidden: non-local Host header", 403);
|
|
7506
|
+
return next();
|
|
7507
|
+
});
|
|
7508
|
+
const apiDeps = {
|
|
7509
|
+
statePath: opts.statePath,
|
|
7510
|
+
autoExport: true,
|
|
7511
|
+
watch: opts.watch,
|
|
7512
|
+
onWatcher: opts.onWatcher
|
|
7513
|
+
};
|
|
7241
7514
|
app.route("/api", createApi(apiDeps));
|
|
7242
7515
|
app.get("/:dir/*", async (c, next) => {
|
|
7243
7516
|
const dirSeg = c.req.param("dir");
|
|
@@ -7263,7 +7536,7 @@ function buildApp(opts) {
|
|
|
7263
7536
|
const file = await readFileResponse(target);
|
|
7264
7537
|
if (file) return file;
|
|
7265
7538
|
}
|
|
7266
|
-
const index = await readFileResponse(
|
|
7539
|
+
const index = await readFileResponse(join18(root, "index.html"));
|
|
7267
7540
|
if (index) return index;
|
|
7268
7541
|
return c.notFound();
|
|
7269
7542
|
});
|
|
@@ -7299,13 +7572,19 @@ function findAvailablePort(start) {
|
|
|
7299
7572
|
});
|
|
7300
7573
|
}
|
|
7301
7574
|
async function startServer(opts) {
|
|
7302
|
-
|
|
7575
|
+
let watcher;
|
|
7576
|
+
const app = buildApp({ ...opts, watch: opts.watch ?? true, onWatcher: (w) => {
|
|
7577
|
+
watcher = w;
|
|
7578
|
+
} });
|
|
7303
7579
|
const port = await findAvailablePort(opts.dev ? DEV_PORT : DEFAULT_PORT);
|
|
7304
7580
|
return new Promise((resolveP) => {
|
|
7305
7581
|
const server = serve({ fetch: app.fetch, hostname: "127.0.0.1", port }, (info) => {
|
|
7306
7582
|
const url = `http://127.0.0.1:${info.port}`;
|
|
7307
7583
|
if (opts.open !== false && !opts.dev) void open(url);
|
|
7308
|
-
resolveP({ url, close: () =>
|
|
7584
|
+
resolveP({ url, close: () => {
|
|
7585
|
+
watcher?.stop();
|
|
7586
|
+
server.close();
|
|
7587
|
+
} });
|
|
7309
7588
|
backgroundScan(opts.statePath);
|
|
7310
7589
|
});
|
|
7311
7590
|
});
|