texlite 0.9.1 → 0.9.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/DESIGN.md +36 -6
- package/OPERATIONS.md +70 -3
- package/README.md +11 -3
- package/README.zh-CN.md +11 -2
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/dist/client/assets/AdminUsers-huwQK7Ly.js +1 -0
- package/dist/client/assets/CitationLibraryDialog-C5C3sPdw.js +1 -0
- package/dist/client/assets/Dashboard-DtHSc55D.js +2 -0
- package/dist/client/assets/{GitDialog-C9Loz4Al.js → GitDialog-BI7QIMsh.js} +1 -1
- package/dist/client/assets/{HistoryDialog-3UNN_jhU.js → HistoryDialog-DA7O9aF2.js} +1 -1
- package/dist/client/assets/LatexEditor-BYWWx5Km.js +138 -0
- package/dist/client/assets/{PdfPreview-BKhcS-2c.js → PdfPreview-mO9WS01q.js} +1 -1
- package/dist/client/assets/ProjectIconPickerDialog-B0q_r5dX.js +1 -0
- package/dist/client/assets/{ProjectNavigationDialogs-CZKTnxrl.js → ProjectNavigationDialogs-DwWYbB1A.js} +1 -1
- package/dist/client/assets/ProjectSettings-BpPFgdoj.js +1 -0
- package/dist/client/assets/ProjectWorkspace-CI2zKon5.js +32 -0
- package/dist/client/assets/{SelectionHistoryDialog-D3dO83lg.js → SelectionHistoryDialog-pWMI5izu.js} +2 -2
- package/dist/client/assets/SystemMetricsDialog-BmlO7oNg.js +1 -0
- package/dist/client/assets/TagManagementDialog-h4Ves4FR.js +1 -0
- package/dist/client/assets/{WordCountDialog-TwpyAP3m.js → WordCountDialog-DlBSP4y7.js} +1 -1
- package/dist/client/assets/check-BAdi_Lxq.js +1 -0
- package/dist/client/assets/editor-DZS7-wrd.js +14 -0
- package/dist/client/assets/{hard-drive-XHSiS_A7.js → hard-drive-DP2OHKMC.js} +1 -1
- package/dist/client/assets/index-CBrIrzgg.css +1 -0
- package/dist/client/assets/{index-Cd6-F29b.js → index-i5IY_PMw.js} +1 -1
- package/dist/client/assets/index-teah8iB2.js +12 -0
- package/dist/client/assets/katex-D9PXdmFF.css +1 -0
- package/dist/client/assets/{key-round-C-CXCWnz.js → key-round-D8sCqL2b.js} +1 -1
- package/dist/client/assets/{latexFormatterWorker-DYkYtcFy.js → latexFormatterWorker-BRmsns8a.js} +2 -2
- package/dist/client/assets/{minimize-2-BMDPVaF-.js → minimize-2-6tf519Ts.js} +1 -1
- package/dist/client/assets/{minus-BIa7-KuM.js → minus-CqvESp9M.js} +1 -1
- package/dist/client/assets/{pdf-BB1XvBIF.js → pdf-DUYcbPMK.js} +1 -1
- package/dist/client/assets/{plus-BKDWy4Ui.js → plus-DIrweesj.js} +1 -1
- package/dist/client/assets/{refresh-cw-Bo7-a7-I.js → refresh-cw-BDcwKu_d.js} +1 -1
- package/dist/client/assets/{search-Bz9Ob5mT.js → search-C9_Jyrnx.js} +1 -1
- package/dist/client/assets/{sigma-DxlHf9gv.js → sigma-p9-fBP0d.js} +1 -1
- package/dist/client/assets/spellCheck-Y0RlmXbl.js +1 -0
- package/dist/client/assets/{tags-IMZehjzE.js → tags-DjF3z21M.js} +1 -1
- package/dist/client/assets/{type-DTWpFQNQ.js → type-CNrDJnzR.js} +1 -1
- package/dist/client/assets/{user-plus--aVpd8K6.js → user-plus-C7exNawX.js} +1 -1
- package/dist/client/assets/x-BEVz0wdw.js +1 -0
- package/dist/client/index.html +6 -4
- package/dist/server/app.js +130 -83
- package/dist/server/cli.js +4 -2
- package/dist/server/collaboration.js +6 -19
- package/dist/server/compileArtifacts.js +3 -2
- package/dist/server/config.js +27 -0
- package/dist/server/db.js +91 -10
- package/dist/server/index.js +4 -2
- package/dist/server/instanceLock.js +4 -2
- package/dist/server/latexCompletion.js +126 -63
- package/dist/server/latexDocumentGraph.js +217 -0
- package/dist/server/latexSpellMask.js +3 -4
- package/dist/server/pm2.js +3 -2
- package/dist/server/projectOutline.js +103 -91
- package/dist/server/projectReferences.js +109 -0
- package/dist/server/routes/auth.js +5 -2
- package/dist/server/routes/comments.js +10 -4
- package/dist/server/routes/compile.js +2 -2
- package/dist/server/routes/projectFiles.js +12 -3
- package/dist/server/routes/projectReferences.js +36 -0
- package/dist/server/routes/projectShared.js +33 -12
- package/dist/server/routes/projects.js +5 -0
- package/dist/server/routes/system.js +1 -0
- package/dist/server/zip.js +2 -16
- package/dist/shared/basePath.js +44 -0
- package/dist/shared/bibtexReferences.js +135 -0
- package/dist/shared/collaborationProtocol.js +31 -0
- package/dist/shared/latexCommandSnippets.js +91 -0
- package/dist/shared/latexDependencies.js +131 -0
- package/dist/shared/latexLiterals.js +215 -0
- package/dist/shared/latexReferenceCommands.js +75 -0
- package/dist/shared/latexReferences.js +193 -0
- package/dist/shared/latexRoot.js +21 -0
- package/dist/shared/latexScanner.js +183 -0
- package/dist/shared/writingChecks.js +4 -0
- package/package.json +11 -4
- package/texlite.config.example.json +2 -4
- package/dist/client/assets/AdminUsers-Dazar9cj.js +0 -1
- package/dist/client/assets/CitationLibraryDialog-VSUiSPgG.js +0 -1
- package/dist/client/assets/Dashboard-CoE5ESfb.js +0 -2
- package/dist/client/assets/LatexEditor-Bq76v8sO.js +0 -65
- package/dist/client/assets/ProjectIconPickerDialog-BoWMjQVm.js +0 -1
- package/dist/client/assets/ProjectSettings-DiOQVJ40.js +0 -1
- package/dist/client/assets/ProjectWorkspace-DqLwkGpT.js +0 -27
- package/dist/client/assets/SystemMetricsDialog-0zCwLtH_.js +0 -1
- package/dist/client/assets/TagManagementDialog-C6KDwtvd.js +0 -1
- package/dist/client/assets/check-sZxOOKtS.js +0 -1
- package/dist/client/assets/chevron-left-32SrOwa5.js +0 -1
- package/dist/client/assets/editor-CiK7TK0p.js +0 -14
- package/dist/client/assets/index-BRr3Kuxh.css +0 -1
- package/dist/client/assets/index-BjzELSQ-.js +0 -12
- package/dist/client/assets/katex-CEK31ho9.css +0 -1
- package/dist/client/assets/spellCheck-BfZEnpwY.js +0 -1
- package/dist/client/assets/x-Tbo3aA0J.js +0 -1
package/dist/server/app.js
CHANGED
|
@@ -29,6 +29,7 @@ import { registerCitationRoutes } from "./routes/citations.js";
|
|
|
29
29
|
import { registerCommentRoutes } from "./routes/comments.js";
|
|
30
30
|
import { registerProjectMemberRoutes } from "./routes/projectMembers.js";
|
|
31
31
|
import { registerProjectFileRoutes } from "./routes/projectFiles.js";
|
|
32
|
+
import { registerProjectReferenceRoutes } from "./routes/projectReferences.js";
|
|
32
33
|
import { registerProjectHistoryRoutes } from "./routes/projectHistory.js";
|
|
33
34
|
import { registerProjectGitRoutes } from "./routes/projectGit.js";
|
|
34
35
|
import { registerProjectCatalogRoutes } from "./routes/projects.js";
|
|
@@ -37,10 +38,36 @@ import { registerUserManagementRoutes } from "./routes/users.js";
|
|
|
37
38
|
import { registerWordCountRoutes } from "./routes/wordCount.js";
|
|
38
39
|
import { HarperService } from "./harper.js";
|
|
39
40
|
import { TexcountService } from "./texcount.js";
|
|
41
|
+
import { basePathHref, basePathPrefix, withoutBasePath } from "../shared/basePath.js";
|
|
40
42
|
// Retained public helper for callers and tests; implementation lives with the file routes.
|
|
41
43
|
export { escapeGlobPattern } from "./routes/projectShared.js";
|
|
42
44
|
const SESSION_CLEANUP_INTERVAL_MS = 15 * 60_000;
|
|
43
45
|
const now = () => new Date().toISOString();
|
|
46
|
+
/** Inject the runtime mount point into a single, path-independent Vite build. */
|
|
47
|
+
export function renderClientIndex(source, basePath) {
|
|
48
|
+
const href = escapeHtmlAttribute(basePathHref(basePath));
|
|
49
|
+
const value = escapeHtmlAttribute(basePath);
|
|
50
|
+
const baseElement = `<base href="${href}" />`;
|
|
51
|
+
const metaElement = `<meta name="texlite-base-path" content="${value}" />`;
|
|
52
|
+
let hadBase = false;
|
|
53
|
+
let rendered = source.replace(/<base\s+href=(?:"[^"]*"|'[^']*')\s*\/?\s*>/i, () => {
|
|
54
|
+
hadBase = true;
|
|
55
|
+
return baseElement;
|
|
56
|
+
});
|
|
57
|
+
if (!hadBase)
|
|
58
|
+
rendered = rendered.replace(/<head(?:\s[^>]*)?>/i, (head) => `${head}\n ${baseElement}`);
|
|
59
|
+
let hadMeta = false;
|
|
60
|
+
rendered = rendered.replace(/<meta\s+name=(?:"texlite-base-path"|'texlite-base-path')\s+content=(?:"[^"]*"|'[^']*')\s*\/?\s*>/i, () => {
|
|
61
|
+
hadMeta = true;
|
|
62
|
+
return metaElement;
|
|
63
|
+
});
|
|
64
|
+
if (!hadMeta)
|
|
65
|
+
rendered = rendered.replace(baseElement, `${baseElement}\n ${metaElement}`);
|
|
66
|
+
return rendered;
|
|
67
|
+
}
|
|
68
|
+
function escapeHtmlAttribute(value) {
|
|
69
|
+
return value.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">");
|
|
70
|
+
}
|
|
44
71
|
export async function buildApp(config, db, options = {}) {
|
|
45
72
|
const app = Fastify({
|
|
46
73
|
logger: options.logger ?? true,
|
|
@@ -161,7 +188,10 @@ export async function buildApp(config, db, options = {}) {
|
|
|
161
188
|
const startedAt = requestStarts.get(request);
|
|
162
189
|
if (startedAt === undefined)
|
|
163
190
|
return;
|
|
164
|
-
const
|
|
191
|
+
const registeredRoute = request.routeOptions.url;
|
|
192
|
+
const route = registeredRoute
|
|
193
|
+
? withoutBasePath(config.basePath, registeredRoute) ?? registeredRoute
|
|
194
|
+
: null;
|
|
165
195
|
if (!route)
|
|
166
196
|
return;
|
|
167
197
|
const metric = {
|
|
@@ -193,93 +223,110 @@ export async function buildApp(config, db, options = {}) {
|
|
|
193
223
|
void apiError(reply, 500, "SERVER_ERROR");
|
|
194
224
|
}
|
|
195
225
|
});
|
|
196
|
-
registerSystemRoutes(app, {
|
|
197
|
-
config,
|
|
198
|
-
db,
|
|
199
|
-
queue,
|
|
200
|
-
collaboration,
|
|
201
|
-
latexCompletions,
|
|
202
|
-
projectOutlines,
|
|
203
|
-
metrics,
|
|
204
|
-
eventLoopDelay
|
|
205
|
-
});
|
|
206
|
-
registerCollaborationRoutes(app, { db, collaboration, metrics });
|
|
207
|
-
registerAuthRoutes(app, { config, db, loginLimiter });
|
|
208
|
-
registerCitationRoutes(app, { db });
|
|
209
|
-
registerUserManagementRoutes(app, {
|
|
210
|
-
config,
|
|
211
|
-
db,
|
|
212
|
-
collaboration,
|
|
213
|
-
projectMutations,
|
|
214
|
-
latexCompletions,
|
|
215
|
-
projectOutlines
|
|
216
|
-
});
|
|
217
|
-
registerCommentRoutes(app, { config, db, collaboration, projectMutations });
|
|
218
|
-
registerProjectMemberRoutes(app, { db, collaboration, projectMutations });
|
|
219
|
-
registerProjectFileRoutes(app, {
|
|
220
|
-
config,
|
|
221
|
-
db,
|
|
222
|
-
collaboration,
|
|
223
|
-
projectMutations,
|
|
224
|
-
latexCompletions,
|
|
225
|
-
projectOutlines,
|
|
226
|
-
metrics,
|
|
227
|
-
recordHistory
|
|
228
|
-
});
|
|
229
|
-
registerProjectHistoryRoutes(app, { config, db, history, editHistory, projectMutations, recordHistory,
|
|
230
|
-
clearPendingEdits: (id) => { editRetry.clear(id); failedEdits.delete(id); signalHistory(id); },
|
|
231
|
-
scheduleHistoryRetention: (id) => historyRetention.schedule(id) });
|
|
232
|
-
registerProjectGitRoutes(app, { config, db, collaboration, projectMutations, projectGit, recordHistory });
|
|
233
|
-
registerProjectCatalogRoutes(app, {
|
|
234
|
-
config,
|
|
235
|
-
db,
|
|
236
|
-
collaboration,
|
|
237
|
-
projectMutations,
|
|
238
|
-
latexCompletions,
|
|
239
|
-
projectOutlines,
|
|
240
|
-
harper,
|
|
241
|
-
recordHistory
|
|
242
|
-
});
|
|
243
|
-
registerWordCountRoutes(app, { config, db, projectMutations, texcount });
|
|
244
|
-
registerCompileRoutes(app, { config, db, collaboration, projectMutations, compileCoordinator, metrics, pruneCompileRuns });
|
|
245
|
-
// The factory owns shared services and lifecycle hooks; route modules own endpoint behavior.
|
|
246
|
-
app.addHook("onRequest", async (request) => {
|
|
247
|
-
if (request.url.startsWith("/api/") && request.url !== "/api/auth/login" && request.url !== "/api/health" && request.url !== "/api/config") {
|
|
248
|
-
const token = request.cookies.texlite_session;
|
|
249
|
-
if (token && !currentUser(request, db)) {
|
|
250
|
-
db.prepare("DELETE FROM sessions WHERE id = ?").run(digestToken(token));
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
226
|
app.addHook("onClose", async () => collaboration.destroy());
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
else {
|
|
272
|
-
reply.header("Cache-Control", "public, max-age=3600");
|
|
227
|
+
const indexPath = path.join(config.clientDir, "index.html");
|
|
228
|
+
const renderedIndex = fs.existsSync(indexPath)
|
|
229
|
+
? renderClientIndex(fs.readFileSync(indexPath, "utf8"), config.basePath)
|
|
230
|
+
: null;
|
|
231
|
+
const routePrefix = basePathPrefix(config.basePath);
|
|
232
|
+
await app.register(async (routes) => {
|
|
233
|
+
routes.addHook("onRequest", async (request) => {
|
|
234
|
+
const requestPath = withoutBasePath(config.basePath, request.url) ?? request.url;
|
|
235
|
+
if (requestPath.startsWith("/api/")
|
|
236
|
+
&& requestPath !== "/api/auth/login"
|
|
237
|
+
&& requestPath !== "/api/health"
|
|
238
|
+
&& requestPath !== "/api/config") {
|
|
239
|
+
const token = request.cookies.texlite_session;
|
|
240
|
+
if (token && !currentUser(request, db)) {
|
|
241
|
+
db.prepare("DELETE FROM sessions WHERE id = ?").run(digestToken(token));
|
|
273
242
|
}
|
|
274
243
|
}
|
|
275
244
|
});
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
245
|
+
registerSystemRoutes(routes, {
|
|
246
|
+
config,
|
|
247
|
+
db,
|
|
248
|
+
queue,
|
|
249
|
+
collaboration,
|
|
250
|
+
latexCompletions,
|
|
251
|
+
projectOutlines,
|
|
252
|
+
metrics,
|
|
253
|
+
eventLoopDelay
|
|
281
254
|
});
|
|
282
|
-
|
|
255
|
+
registerCollaborationRoutes(routes, { db, collaboration, metrics });
|
|
256
|
+
registerAuthRoutes(routes, { config, db, loginLimiter });
|
|
257
|
+
registerCitationRoutes(routes, { db });
|
|
258
|
+
registerUserManagementRoutes(routes, {
|
|
259
|
+
config,
|
|
260
|
+
db,
|
|
261
|
+
collaboration,
|
|
262
|
+
projectMutations,
|
|
263
|
+
latexCompletions,
|
|
264
|
+
projectOutlines
|
|
265
|
+
});
|
|
266
|
+
registerCommentRoutes(routes, { config, db, collaboration, projectMutations });
|
|
267
|
+
registerProjectMemberRoutes(routes, { db, collaboration, projectMutations });
|
|
268
|
+
registerProjectFileRoutes(routes, {
|
|
269
|
+
config,
|
|
270
|
+
db,
|
|
271
|
+
collaboration,
|
|
272
|
+
projectMutations,
|
|
273
|
+
latexCompletions,
|
|
274
|
+
projectOutlines,
|
|
275
|
+
metrics,
|
|
276
|
+
recordHistory
|
|
277
|
+
});
|
|
278
|
+
registerProjectReferenceRoutes(routes, { config, db, projectMutations });
|
|
279
|
+
registerProjectHistoryRoutes(routes, { config, db, history, editHistory, projectMutations, recordHistory,
|
|
280
|
+
clearPendingEdits: (id) => { editRetry.clear(id); failedEdits.delete(id); signalHistory(id); },
|
|
281
|
+
scheduleHistoryRetention: (id) => historyRetention.schedule(id) });
|
|
282
|
+
registerProjectGitRoutes(routes, { config, db, collaboration, projectMutations, projectGit, recordHistory });
|
|
283
|
+
registerProjectCatalogRoutes(routes, {
|
|
284
|
+
config,
|
|
285
|
+
db,
|
|
286
|
+
collaboration,
|
|
287
|
+
projectMutations,
|
|
288
|
+
latexCompletions,
|
|
289
|
+
projectOutlines,
|
|
290
|
+
harper,
|
|
291
|
+
recordHistory
|
|
292
|
+
});
|
|
293
|
+
registerWordCountRoutes(routes, { config, db, projectMutations, texcount });
|
|
294
|
+
registerCompileRoutes(routes, { config, db, collaboration, projectMutations, compileCoordinator, metrics, pruneCompileRuns });
|
|
295
|
+
// The factory owns shared services and lifecycle hooks; route modules own endpoint behavior.
|
|
296
|
+
if (renderedIndex !== null) {
|
|
297
|
+
await routes.register(staticPlugin, {
|
|
298
|
+
root: config.clientDir,
|
|
299
|
+
wildcard: false,
|
|
300
|
+
index: false,
|
|
301
|
+
globIgnore: ["index.html"],
|
|
302
|
+
// Vite fingerprints everything under assets/. These large JS/WASM
|
|
303
|
+
// files are safe to cache indefinitely; a new build produces a new
|
|
304
|
+
// URL. The rendered SPA shell below always remains fresh.
|
|
305
|
+
cacheControl: false,
|
|
306
|
+
setHeaders(reply, filePath) {
|
|
307
|
+
const relativePath = path.relative(config.clientDir, filePath).split(path.sep).join("/");
|
|
308
|
+
if (relativePath.startsWith("assets/")) {
|
|
309
|
+
reply.header("Cache-Control", "public, max-age=31536000, immutable");
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
reply.header("Cache-Control", "public, max-age=3600");
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
const sendIndex = async (_request, reply) => {
|
|
317
|
+
reply.header("Cache-Control", "no-store");
|
|
318
|
+
return reply.type("text/html; charset=utf-8").send(renderedIndex);
|
|
319
|
+
};
|
|
320
|
+
routes.get("/", sendIndex);
|
|
321
|
+
routes.get("/index.html", sendIndex);
|
|
322
|
+
routes.get("/*", async (request, reply) => {
|
|
323
|
+
const requestPath = withoutBasePath(config.basePath, request.url) ?? request.url;
|
|
324
|
+
if (requestPath.startsWith("/api/"))
|
|
325
|
+
return apiError(reply, 404, "API_NOT_FOUND");
|
|
326
|
+
return sendIndex(request, reply);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}, { prefix: routePrefix });
|
|
283
330
|
const cleanupExpiredSessions = () => {
|
|
284
331
|
try {
|
|
285
332
|
pruneExpiredSessions(db, now());
|
package/dist/server/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import { assertEnvironment, hostRequirementsSatisfied, inspectHostEnvironment, i
|
|
|
12
12
|
import { serve } from "./index.js";
|
|
13
13
|
import { processStatus, restartManaged, startManaged, stopManaged, streamLogs, waitForOnline } from "./pm2.js";
|
|
14
14
|
import { defaultDataDirectory, resolveConfigPath } from "./runtimePaths.js";
|
|
15
|
+
import { basePathHref } from "../shared/basePath.js";
|
|
15
16
|
const HELP = `TexLite ${packageVersion()}
|
|
16
17
|
|
|
17
18
|
Usage:
|
|
@@ -106,7 +107,7 @@ function writeInitialConfig(configPath, siteName, adminEmail) {
|
|
|
106
107
|
siteName,
|
|
107
108
|
adminEmail,
|
|
108
109
|
sessionDays: CONFIG_DEFAULTS.sessionDays,
|
|
109
|
-
server: { host: CONFIG_DEFAULTS.host, port: CONFIG_DEFAULTS.port },
|
|
110
|
+
server: { host: CONFIG_DEFAULTS.host, port: CONFIG_DEFAULTS.port, basePath: CONFIG_DEFAULTS.basePath },
|
|
110
111
|
storage: { dataDir: dataDirectory },
|
|
111
112
|
uploads: { maxFileSizeMB: CONFIG_DEFAULTS.maxFileSizeMB },
|
|
112
113
|
pdf: {
|
|
@@ -277,6 +278,7 @@ async function printConfig(options) {
|
|
|
277
278
|
siteName: config.siteName,
|
|
278
279
|
host: config.host,
|
|
279
280
|
port: config.port,
|
|
281
|
+
basePath: config.basePath,
|
|
280
282
|
dataDir: config.dataDir,
|
|
281
283
|
databasePath: config.databasePath,
|
|
282
284
|
projectsDir: config.projectsDir,
|
|
@@ -304,7 +306,7 @@ async function start(options, restart = false) {
|
|
|
304
306
|
const managed = await waitForOnline(config);
|
|
305
307
|
const status = managed.description?.pm2_env?.status ?? "unknown";
|
|
306
308
|
output(`TexLite ${status}: ${config.siteName}`);
|
|
307
|
-
output(`Address: http://${config.host}:${config.port}`);
|
|
309
|
+
output(`Address: http://${config.host}:${config.port}${basePathHref(config.basePath)}`);
|
|
308
310
|
output(`Configuration file: ${configPath}`);
|
|
309
311
|
output(`PM2 process: ${managed.name}`);
|
|
310
312
|
}
|
|
@@ -12,26 +12,14 @@ import { listProjectFilesAsync, outputRoot, resolveSourcePath, safeRelativePath,
|
|
|
12
12
|
import { collaborationProjectAccessFromStatement, canEdit, prepareCollaborationProjectAccessStatement } from "./projects.js";
|
|
13
13
|
import { reanchorFileComments } from "./anchors.js";
|
|
14
14
|
import { hashText } from "./editHistory.js";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const MESSAGE_QUERY_AWARENESS =
|
|
18
|
-
const MESSAGE_FLUSH = 4;
|
|
19
|
-
const MESSAGE_PROTOCOL = 5;
|
|
20
|
-
const MESSAGE_MAINTENANCE = 6;
|
|
21
|
-
const MESSAGE_PERMISSION = 7;
|
|
22
|
-
// A small, non-Yjs handshake message used for ephemeral compile metadata.
|
|
23
|
-
// Source text still uses the normal Yjs sync protocol; this message prevents
|
|
24
|
-
// an old IndexedDB metadata entry from being treated as an active compile.
|
|
25
|
-
const MESSAGE_COMPILE_STATES = 8;
|
|
26
|
-
// Ephemeral, per-file formatter leases. These are deliberately kept out of
|
|
27
|
-
// the Yjs document and source persistence state.
|
|
28
|
-
const MESSAGE_FORMAT_LEASE = 9;
|
|
15
|
+
import { COLLABORATION_PROTOCOL_VERSION, CollaborationMessageType, isCollaborationCleanMode, isCollaborationCompileStatus } from "../shared/collaborationProtocol.js";
|
|
16
|
+
export { COLLABORATION_PROTOCOL_VERSION } from "../shared/collaborationProtocol.js";
|
|
17
|
+
const { Sync: MESSAGE_SYNC, Awareness: MESSAGE_AWARENESS, QueryAwareness: MESSAGE_QUERY_AWARENESS, Flush: MESSAGE_FLUSH, Protocol: MESSAGE_PROTOCOL, Maintenance: MESSAGE_MAINTENANCE, Permission: MESSAGE_PERMISSION, CompileStates: MESSAGE_COMPILE_STATES, FormatLease: MESSAGE_FORMAT_LEASE } = CollaborationMessageType;
|
|
29
18
|
/**
|
|
30
19
|
* Increment this when a wire-level collaboration change cannot be decoded by
|
|
31
20
|
* an older browser. The epoch marker is rotated at the same time, which makes
|
|
32
21
|
* already-open older pages discard their local draft and reload safely.
|
|
33
22
|
*/
|
|
34
|
-
export const COLLABORATION_PROTOCOL_VERSION = 3;
|
|
35
23
|
const VERSIONED_EPOCH_PREFIX = `${COLLABORATION_PROTOCOL_VERSION}:`;
|
|
36
24
|
const SOURCE_PREFIX = "source:";
|
|
37
25
|
const MAX_PROJECT_SESSIONS = 10;
|
|
@@ -1576,7 +1564,7 @@ export function maxCollaborativeFileBytes(config) {
|
|
|
1576
1564
|
return Math.min(config.maxUploadBytes, MAX_COLLABORATIVE_FILE_BYTES);
|
|
1577
1565
|
}
|
|
1578
1566
|
export function isCollaborativeTextFile(filePath) {
|
|
1579
|
-
return /(?:\.tex|\.bib|\.sty|\.cls|\.txt|\.md|latexmkrc)$/i.test(filePath);
|
|
1567
|
+
return /(?:\.tex|\.bib|\.bst|\.sty|\.cls|\.txt|\.md|latexmkrc)$/i.test(filePath);
|
|
1580
1568
|
}
|
|
1581
1569
|
function collaborativeEntrySignature(entry) {
|
|
1582
1570
|
return `${entry.path}\0${entry.size ?? 0}\0${entry.mtimeMs ?? 0}`;
|
|
@@ -1760,9 +1748,8 @@ function isSharedCompileState(mainFile, value) {
|
|
|
1760
1748
|
const requestedBy = state.requestedBy;
|
|
1761
1749
|
return state.mainFile === mainFile
|
|
1762
1750
|
&& typeof state.runId === "string" && state.runId.length > 0
|
|
1763
|
-
&& (state.status
|
|
1764
|
-
|
|
1765
|
-
&& (state.status !== "cleaned" || state.cleanMode === "cache" || state.cleanMode === "artifacts")
|
|
1751
|
+
&& isCollaborationCompileStatus(state.status)
|
|
1752
|
+
&& (state.status !== "cleaned" || isCollaborationCleanMode(state.cleanMode))
|
|
1766
1753
|
&& (state.stale === undefined || typeof state.stale === "boolean")
|
|
1767
1754
|
&& typeof state.updatedAt === "string"
|
|
1768
1755
|
&& Boolean(requestedBy && typeof requestedBy === "object"
|
|
@@ -4,6 +4,7 @@ import { publishedCompileArtifacts } from "./compiler.js";
|
|
|
4
4
|
import { outputRoot, resolveSourcePath, safeRelativePath, sourceRoot, texFileStem } from "./files.js";
|
|
5
5
|
import { isMainDocumentCandidateSync } from "./latexRoot.js";
|
|
6
6
|
import { MAX_TEXT_PREVIEW_BYTES } from "./limits.js";
|
|
7
|
+
import { withBasePath } from "../shared/basePath.js";
|
|
7
8
|
export function pdfLoadingMode(config, pdfSizeBytes) {
|
|
8
9
|
if (config.pdfLoadingStrategy === "full" || config.pdfLoadingStrategy === "range") {
|
|
9
10
|
return config.pdfLoadingStrategy;
|
|
@@ -77,8 +78,8 @@ export function compileMainFile(config, projectId, defaultMainFile, value) {
|
|
|
77
78
|
}
|
|
78
79
|
return mainFile;
|
|
79
80
|
}
|
|
80
|
-
export function compilePdfUrl(projectId, mainFile, version) {
|
|
81
|
-
return `/api/projects/${projectId}/pdf?mainFile=${encodeURIComponent(mainFile)}&run=${encodeURIComponent(version)}
|
|
81
|
+
export function compilePdfUrl(config, projectId, mainFile, version) {
|
|
82
|
+
return withBasePath(config.basePath, `/api/projects/${projectId}/pdf?mainFile=${encodeURIComponent(mainFile)}&run=${encodeURIComponent(version)}`);
|
|
82
83
|
}
|
|
83
84
|
export function listCompileArtifacts(directory) {
|
|
84
85
|
const artifacts = [];
|
package/dist/server/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { defaultDataDirectory, packageClientDirectory, resolveConfigPath } from "./runtimePaths.js";
|
|
4
|
+
import { normalizeBasePath, ROOT_BASE_PATH } from "../shared/basePath.js";
|
|
4
5
|
export const LATEX_ENGINES = ["pdflatex", "xelatex", "lualatex"];
|
|
5
6
|
export const PDF_LOADING_STRATEGIES = ["auto", "full", "range"];
|
|
6
7
|
/** Effective values used when the corresponding file/env setting is omitted. */
|
|
@@ -9,6 +10,7 @@ export const CONFIG_DEFAULTS = {
|
|
|
9
10
|
adminEmail: "",
|
|
10
11
|
host: "127.0.0.1",
|
|
11
12
|
port: 3000,
|
|
13
|
+
basePath: ROOT_BASE_PATH,
|
|
12
14
|
dataDir: defaultDataDirectory(),
|
|
13
15
|
clientDir: packageClientDirectory(),
|
|
14
16
|
sessionDays: 14,
|
|
@@ -74,12 +76,14 @@ export function loadConfig(configPathOverride) {
|
|
|
74
76
|
const historyMaxStorageMB = integerSetting("history.maxStorageMB", process.env.TEXLITE_HISTORY_MAX_STORAGE_MB, fileConfig.history?.maxStorageMB, CONFIG_DEFAULTS.historyMaxStorageMB, CONFIG_LIMITS.historyMaxStorageMB);
|
|
75
77
|
const editHistoryMaxStorageMB = integerSetting("editHistory.maxStorageMB", process.env.TEXLITE_EDIT_HISTORY_MAX_STORAGE_MB, fileConfig.editHistory?.maxStorageMB, CONFIG_DEFAULTS.editHistoryMaxStorageMB, CONFIG_LIMITS.editHistoryMaxStorageMB);
|
|
76
78
|
const gitOperationTimeoutSeconds = integerSetting("git.operationTimeoutSeconds", process.env.TEXLITE_GIT_TIMEOUT, fileConfig.git?.operationTimeoutSeconds, CONFIG_DEFAULTS.gitOperationTimeoutSeconds, CONFIG_LIMITS.gitOperationTimeoutSeconds);
|
|
79
|
+
const basePath = resolveBasePath(fileConfig);
|
|
77
80
|
const config = {
|
|
78
81
|
configPath,
|
|
79
82
|
siteName: stringSetting("siteName", process.env.TEXLITE_SITE_NAME, fileConfig.siteName, CONFIG_DEFAULTS.siteName, { min: 1, max: 120 }),
|
|
80
83
|
adminEmail: stringSetting("adminEmail", process.env.TEXLITE_ADMIN_EMAIL, fileConfig.adminEmail, CONFIG_DEFAULTS.adminEmail, { min: 0, max: 320 }),
|
|
81
84
|
host: stringSetting("server.host", process.env.TEXLITE_HOST, fileConfig.server?.host, CONFIG_DEFAULTS.host, { min: 1, max: 255 }),
|
|
82
85
|
port: integerSetting("server.port", process.env.TEXLITE_PORT, fileConfig.server?.port, CONFIG_DEFAULTS.port, CONFIG_LIMITS.port),
|
|
86
|
+
basePath,
|
|
83
87
|
dataDir,
|
|
84
88
|
databasePath: path.join(dataDir, "texlite.db"),
|
|
85
89
|
projectsDir: path.join(dataDir, "projects"),
|
|
@@ -105,6 +109,17 @@ export function loadConfig(configPathOverride) {
|
|
|
105
109
|
validateConfig(config);
|
|
106
110
|
return config;
|
|
107
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Read only the configured public mount path. Vite uses this during local UI
|
|
114
|
+
* development, where checking server data paths would be unnecessary and can
|
|
115
|
+
* prevent the frontend from starting before the API server is configured.
|
|
116
|
+
*/
|
|
117
|
+
export function loadBasePath(configPathOverride) {
|
|
118
|
+
const configPath = resolveConfigPath(configPathOverride);
|
|
119
|
+
const fileConfig = readConfigFile(configPath);
|
|
120
|
+
validateFileConfig(fileConfig);
|
|
121
|
+
return resolveBasePath(fileConfig);
|
|
122
|
+
}
|
|
108
123
|
/** Validate an already materialized configuration (useful for startup/tests). */
|
|
109
124
|
export function validateConfig(config) {
|
|
110
125
|
if (!config.dataDir || path.parse(config.dataDir).root === config.dataDir) {
|
|
@@ -126,6 +141,9 @@ export function validateConfig(config) {
|
|
|
126
141
|
throw configurationError("latex.defaultEngine", "must be one of the engines listed in latex.allowedEngines");
|
|
127
142
|
}
|
|
128
143
|
validateInteger("server.port", config.port, CONFIG_LIMITS.port);
|
|
144
|
+
if (normalizeBasePath(config.basePath) !== config.basePath) {
|
|
145
|
+
throw configurationError("server.basePath", "must be / or a normalized URL path such as /texlite or /tools/texlite");
|
|
146
|
+
}
|
|
129
147
|
validateInteger("sessionDays", config.sessionDays, CONFIG_LIMITS.sessionDays);
|
|
130
148
|
validateInteger("latex.compileTimeoutSeconds", config.compileTimeoutMs / 1000, CONFIG_LIMITS.compileTimeoutSeconds);
|
|
131
149
|
validateInteger("latex.maxCompileJobs", config.maxCompileJobs, CONFIG_LIMITS.maxCompileJobs);
|
|
@@ -139,6 +157,14 @@ export function validateConfig(config) {
|
|
|
139
157
|
validateInteger("editHistory.maxStorageMB", config.editHistoryMaxStorageBytes / (1024 * 1024), CONFIG_LIMITS.editHistoryMaxStorageMB);
|
|
140
158
|
validateInteger("git.operationTimeoutSeconds", config.gitOperationTimeoutMs / 1000, CONFIG_LIMITS.gitOperationTimeoutSeconds);
|
|
141
159
|
}
|
|
160
|
+
function resolveBasePath(fileConfig) {
|
|
161
|
+
const configuredBasePath = stringSetting("server.basePath", process.env.TEXLITE_BASE_PATH, fileConfig.server?.basePath, CONFIG_DEFAULTS.basePath, { min: 1, max: 1_024 });
|
|
162
|
+
const basePath = normalizeBasePath(configuredBasePath);
|
|
163
|
+
if (!basePath) {
|
|
164
|
+
throw configurationError("server.basePath", `must be / or a URL path such as /texlite or /tools/texlite; received ${displayValue(configuredBasePath)}`);
|
|
165
|
+
}
|
|
166
|
+
return basePath;
|
|
167
|
+
}
|
|
142
168
|
function isEngine(value) {
|
|
143
169
|
return typeof value === "string" && LATEX_ENGINES.includes(value);
|
|
144
170
|
}
|
|
@@ -168,6 +194,7 @@ function validateFileConfig(config) {
|
|
|
168
194
|
const server = optionalSection(config.server, "server");
|
|
169
195
|
optionalString(server?.host, "server.host", { min: 1, max: 255 });
|
|
170
196
|
optionalInteger(server?.port, "server.port", CONFIG_LIMITS.port);
|
|
197
|
+
optionalString(server?.basePath, "server.basePath", { min: 1, max: 1_024 });
|
|
171
198
|
const storage = optionalSection(config.storage, "storage");
|
|
172
199
|
optionalString(storage?.dataDir, "storage.dataDir", { min: 1, max: 4_096 });
|
|
173
200
|
const uploads = optionalSection(config.uploads, "uploads");
|
package/dist/server/db.js
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import Database from "better-sqlite3";
|
|
3
|
+
const migrationsTable = "texlite_schema_migrations";
|
|
4
|
+
/**
|
|
5
|
+
* Keep this list append-only. Existing migration bodies must never be changed
|
|
6
|
+
* after release: a database records the version only after that migration's
|
|
7
|
+
* transaction has committed.
|
|
8
|
+
*
|
|
9
|
+
* Version 1 deliberately folds the untracked historical schema setup into a
|
|
10
|
+
* single baseline. It upgrades every database released before versioned
|
|
11
|
+
* migrations while preserving the current schema for new installations.
|
|
12
|
+
*/
|
|
13
|
+
const databaseMigrations = [
|
|
14
|
+
// Append future migrations below this entry. Never insert before or modify
|
|
15
|
+
// the released baseline: recorded databases will intentionally skip it.
|
|
16
|
+
{ version: 1, name: "baseline_schema_and_legacy_upgrade", apply: applyBaselineMigration }
|
|
17
|
+
];
|
|
3
18
|
export function openDatabase(config) {
|
|
4
19
|
fs.mkdirSync(config.dataDir, { recursive: true, mode: 0o700 });
|
|
5
20
|
fs.mkdirSync(config.projectsDir, { recursive: true, mode: 0o700 });
|
|
@@ -8,10 +23,67 @@ export function openDatabase(config) {
|
|
|
8
23
|
// per-transaction WAL fsync: a sudden host failure can lose the most recent
|
|
9
24
|
// acknowledged transaction, but the database remains consistent.
|
|
10
25
|
db.exec("PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA busy_timeout = 5000;");
|
|
11
|
-
|
|
26
|
+
try {
|
|
27
|
+
migrate(db);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
// A failed migration must not leave an open SQLite handle holding a lock
|
|
31
|
+
// while startup unwinds. Its transaction has already been rolled back.
|
|
32
|
+
db.close();
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
12
35
|
return db;
|
|
13
36
|
}
|
|
14
37
|
function migrate(db) {
|
|
38
|
+
const context = {
|
|
39
|
+
migrateLegacyProjectTags: tableExists(db, "project_tags") && !tableExists(db, "user_tags"),
|
|
40
|
+
backfillAdminCanCreateProjects: missingColumn(db, "users", "can_create_projects"),
|
|
41
|
+
backfillProjectLastModifiedBy: missingColumn(db, "projects", "last_modified_by"),
|
|
42
|
+
backfillCompileRunMainFile: missingColumn(db, "compile_runs", "main_file"),
|
|
43
|
+
backfillEditSegmentBytes: missingColumn(db, "project_edit_segments", "steps_bytes")
|
|
44
|
+
};
|
|
45
|
+
db.exec(`CREATE TABLE IF NOT EXISTS ${migrationsTable} (
|
|
46
|
+
version INTEGER PRIMARY KEY,
|
|
47
|
+
name TEXT NOT NULL,
|
|
48
|
+
applied_at TEXT NOT NULL
|
|
49
|
+
)`);
|
|
50
|
+
const applied = db.prepare(`SELECT version, name FROM ${migrationsTable}`).all()
|
|
51
|
+
.map((migration) => ({ version: Number(migration.version), name: migration.name }));
|
|
52
|
+
const knownMigrations = new Map(databaseMigrations.map((migration) => [migration.version, migration]));
|
|
53
|
+
const unknownMigration = applied.find((migration) => !knownMigrations.has(migration.version));
|
|
54
|
+
if (unknownMigration !== undefined) {
|
|
55
|
+
const latestVersion = databaseMigrations.at(-1)?.version ?? 0;
|
|
56
|
+
const direction = unknownMigration.version > latestVersion ? "newer than" : "not recognized by";
|
|
57
|
+
throw new Error(`Database schema version ${unknownMigration.version} is ${direction} this TexLite release.`);
|
|
58
|
+
}
|
|
59
|
+
const renamedMigration = applied.find((migration) => knownMigrations.get(migration.version)?.name !== migration.name);
|
|
60
|
+
if (renamedMigration) {
|
|
61
|
+
throw new Error(`Database migration ${renamedMigration.version} does not match this TexLite release.`);
|
|
62
|
+
}
|
|
63
|
+
const appliedVersions = new Set(applied.map((migration) => migration.version));
|
|
64
|
+
const missingBeforeApplied = databaseMigrations.find((migration) => !appliedVersions.has(migration.version)
|
|
65
|
+
&& applied.some((appliedMigration) => appliedMigration.version > migration.version));
|
|
66
|
+
if (missingBeforeApplied) {
|
|
67
|
+
throw new Error(`Database migration ${missingBeforeApplied.version} is missing before a later migration.`);
|
|
68
|
+
}
|
|
69
|
+
const record = db.prepare(`INSERT INTO ${migrationsTable} (version, name, applied_at) VALUES (?, ?, ?)`);
|
|
70
|
+
for (const migration of databaseMigrations) {
|
|
71
|
+
if (appliedVersions.has(migration.version))
|
|
72
|
+
continue;
|
|
73
|
+
db.transaction(() => {
|
|
74
|
+
migration.apply(db, context);
|
|
75
|
+
record.run(migration.version, migration.name, new Date().toISOString());
|
|
76
|
+
})();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function tableExists(db, table) {
|
|
80
|
+
return Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(table));
|
|
81
|
+
}
|
|
82
|
+
function missingColumn(db, table, column) {
|
|
83
|
+
return tableExists(db, table)
|
|
84
|
+
&& !db.prepare("SELECT 1 FROM pragma_table_info(?) WHERE name = ?").get(table, column);
|
|
85
|
+
}
|
|
86
|
+
function applyBaselineMigration(db, context) {
|
|
15
87
|
db.exec(`
|
|
16
88
|
CREATE TABLE IF NOT EXISTS users (
|
|
17
89
|
id TEXT PRIMARY KEY,
|
|
@@ -282,10 +354,10 @@ function migrate(db) {
|
|
|
282
354
|
`);
|
|
283
355
|
const editColumns = db.prepare("PRAGMA table_info(project_edit_segments)").all();
|
|
284
356
|
if (!editColumns.some((column) => column.name === "steps_bytes")) {
|
|
285
|
-
db.
|
|
286
|
-
|
|
357
|
+
db.exec("ALTER TABLE project_edit_segments ADD COLUMN steps_bytes INTEGER NOT NULL DEFAULT 0");
|
|
358
|
+
if (context.backfillEditSegmentBytes) {
|
|
287
359
|
db.exec("UPDATE project_edit_segments SET steps_bytes = LENGTH(CAST(steps_json AS BLOB))");
|
|
288
|
-
}
|
|
360
|
+
}
|
|
289
361
|
}
|
|
290
362
|
const projectColumns = db.prepare("PRAGMA table_info(projects)").all();
|
|
291
363
|
if (!projectColumns.some((column) => column.name === "latexmkrc")) {
|
|
@@ -328,14 +400,23 @@ function migrate(db) {
|
|
|
328
400
|
db.exec("ALTER TABLE citation_library_entries ADD COLUMN revision INTEGER NOT NULL DEFAULT 1");
|
|
329
401
|
}
|
|
330
402
|
db.exec(`
|
|
331
|
-
UPDATE users SET can_create_projects = 1 WHERE role = 'admin';
|
|
332
|
-
UPDATE projects SET last_modified_by = owner_id WHERE last_modified_by IS NULL;
|
|
333
|
-
UPDATE compile_runs SET main_file = COALESCE(
|
|
334
|
-
(SELECT project.main_file FROM projects project WHERE project.id = compile_runs.project_id), ''
|
|
335
|
-
) WHERE main_file = '';
|
|
336
403
|
CREATE INDEX IF NOT EXISTS compile_runs_project_main_created
|
|
337
404
|
ON compile_runs(project_id, main_file, created_at DESC);
|
|
338
|
-
|
|
405
|
+
`);
|
|
406
|
+
if (context.backfillProjectLastModifiedBy) {
|
|
407
|
+
db.exec("UPDATE projects SET last_modified_by = owner_id WHERE last_modified_by IS NULL");
|
|
408
|
+
}
|
|
409
|
+
if (context.backfillAdminCanCreateProjects) {
|
|
410
|
+
db.exec("UPDATE users SET can_create_projects = 1 WHERE role = 'admin'");
|
|
411
|
+
}
|
|
412
|
+
if (context.backfillCompileRunMainFile) {
|
|
413
|
+
db.exec(`UPDATE compile_runs SET main_file = COALESCE(
|
|
414
|
+
(SELECT project.main_file FROM projects project WHERE project.id = compile_runs.project_id), ''
|
|
415
|
+
) WHERE main_file = ''`);
|
|
416
|
+
}
|
|
417
|
+
if (!context.migrateLegacyProjectTags)
|
|
418
|
+
return;
|
|
419
|
+
db.exec(`
|
|
339
420
|
INSERT OR IGNORE INTO tags (id, name, color, created_by, created_at, updated_at)
|
|
340
421
|
SELECT legacy.id, legacy.name, legacy.color, p.owner_id, legacy.created_at, legacy.created_at
|
|
341
422
|
FROM project_tags legacy
|
package/dist/server/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { buildApp } from "./app.js";
|
|
|
6
6
|
import { assertEnvironment } from "./environment.js";
|
|
7
7
|
import { acquireDataDirectoryLock } from "./instanceLock.js";
|
|
8
8
|
import { LogRotation } from "./logRotation.js";
|
|
9
|
+
import { basePathHref } from "../shared/basePath.js";
|
|
9
10
|
export async function startServer(configPath) {
|
|
10
11
|
const config = loadConfig(configPath);
|
|
11
12
|
const lock = acquireDataDirectoryLock(config);
|
|
@@ -23,11 +24,12 @@ export async function startServer(configPath) {
|
|
|
23
24
|
app = await buildApp(config, db);
|
|
24
25
|
await app.listen({ host: config.host, port: config.port });
|
|
25
26
|
app.log.info(`Environment ready: ${environment.map((item) => `${item.name} ${item.version}`).join(", ")}`);
|
|
26
|
-
|
|
27
|
+
const address = `http://${config.host}:${config.port}${basePathHref(config.basePath)}`;
|
|
28
|
+
app.log.info(`${config.siteName} running at ${address}`);
|
|
27
29
|
if (typeof process.send === "function")
|
|
28
30
|
process.send("ready");
|
|
29
31
|
return {
|
|
30
|
-
address
|
|
32
|
+
address,
|
|
31
33
|
close: async () => {
|
|
32
34
|
try {
|
|
33
35
|
if (app)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { basePathHref } from "../shared/basePath.js";
|
|
4
5
|
export const LOCK_FILE_NAME = ".texlite.lock";
|
|
5
6
|
export function isProcessAlive(pid) {
|
|
6
7
|
if (!Number.isInteger(pid) || pid <= 0)
|
|
@@ -45,7 +46,8 @@ export function acquireDataDirectoryLock(config) {
|
|
|
45
46
|
token: randomUUID(),
|
|
46
47
|
configPath: config.configPath,
|
|
47
48
|
host: config.host,
|
|
48
|
-
port: config.port
|
|
49
|
+
port: config.port,
|
|
50
|
+
basePath: config.basePath
|
|
49
51
|
};
|
|
50
52
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
51
53
|
try {
|
|
@@ -103,7 +105,7 @@ export function acquireDataDirectoryLock(config) {
|
|
|
103
105
|
throw new Error(`The data directory is already locked by another TexLite instance:\n` +
|
|
104
106
|
` - PID: ${existing.pid}\n` +
|
|
105
107
|
` - Config: ${existing.configPath}\n` +
|
|
106
|
-
` - Address: http://${existing.host}:${existing.port}\n` +
|
|
108
|
+
` - Address: http://${existing.host}:${existing.port}${basePathHref(existing.basePath ?? "/")}\n` +
|
|
107
109
|
` - Started: ${existing.startedAt}\n` +
|
|
108
110
|
` - Data directory: ${config.dataDir}\n` +
|
|
109
111
|
`Do not run multiple TexLite instances with the same data directory.`);
|