@tsplc/build 0.0.0-stage → 0.2.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 (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -2
  3. package/dist/cli.js +1696 -0
  4. package/dist/cli.js.map +34 -0
  5. package/dist/index.js +1683 -0
  6. package/dist/index.js.map +31 -0
  7. package/dist/libraries.js +128 -0
  8. package/dist/libraries.js.map +10 -0
  9. package/docker/Dockerfile +173 -0
  10. package/docker/Dockerfile.dockerignore +18 -0
  11. package/docker/Dockerfile.source +15 -0
  12. package/docker/build-image +38 -0
  13. package/docker/codesys-build +5 -0
  14. package/docker/config.ts +8 -0
  15. package/docker/control-linux-sl/Dockerfile +38 -0
  16. package/docker/edge-gateway/Dockerfile +17 -0
  17. package/docker/entrypoint.sh +10 -0
  18. package/docker/install/NOTICE.md +21 -0
  19. package/docker/install/core.sh +36 -0
  20. package/docker/install/decode.c +12 -0
  21. package/docker/install/device/Runtime.ico +0 -0
  22. package/docker/install/device/Runtime.png +0 -0
  23. package/docker/install/device/codesyscontrol.devdesc.xml +1502 -0
  24. package/docker/install/extract.py +43 -0
  25. package/docker/install/helper-launcher.c +46 -0
  26. package/docker/install/install.sh +94 -0
  27. package/docker/install/prerequisites.sh +15 -0
  28. package/docker/install/verify.py +133 -0
  29. package/docker/install/verify.sh +34 -0
  30. package/docker/install/wine-ncrypt/COPYING.LIB +502 -0
  31. package/docker/install/wine-ncrypt/README.md +28 -0
  32. package/docker/install/wine-ncrypt/enable-oaep.py +34 -0
  33. package/docker/install/wine-ncrypt/main.c +619 -0
  34. package/docker/install/wine-ncrypt/oaep.patch +11 -0
  35. package/docker/install/wine-signal/README.md +22 -0
  36. package/docker/install/wine-signal/build.sh +10 -0
  37. package/docker/install/wine-signal/check.sh +34 -0
  38. package/docker/install/wine-signal/noop-i386.c +2 -0
  39. package/docker/install/wine-signal/signal-state.c +105 -0
  40. package/docker/install/wine-signal/tests/avx-registers.c +53 -0
  41. package/docker/install/wine-signal/tests/frame-and-handler.c +192 -0
  42. package/docker/install/wine-signal/tests/loader-i386.c +8 -0
  43. package/docker/install/wine-signal/tests/signal-return.c +47 -0
  44. package/docker/install/wine-signal/wine +4 -0
  45. package/docker/runner.ts +156 -0
  46. package/package.json +53 -4
  47. package/python/build.py +660 -0
  48. package/python/codesys_login.py +100 -0
  49. package/python/codesys_online.py +327 -0
  50. package/python/test_build.py +444 -0
  51. package/python/test_codesys_login.py +120 -0
  52. package/python/test_codesys_online.py +184 -0
  53. package/python/test_installer_packages.py +216 -0
  54. package/python/test_installer_verification.py +307 -0
  55. package/python/test_worker.py +142 -0
  56. package/python/worker.py +102 -0
  57. package/runtime/install.sh +74 -0
  58. package/src/__tests__/fixtures/xml/fb-simple.xml +40 -0
  59. package/src/__tests__/fixtures/xml/fb-state-machine.xml +86 -0
  60. package/src/__tests__/fixtures/xml/prg-cyclic.xml +44 -0
  61. package/src/artifacts.ts +77 -0
  62. package/src/build.ts +87 -0
  63. package/src/cli.ts +116 -0
  64. package/src/config.ts +12 -0
  65. package/src/controller-online.ts +297 -0
  66. package/src/controller-session.ts +298 -0
  67. package/src/diagnostics.ts +68 -0
  68. package/src/docker-build.ts +163 -0
  69. package/src/docker-context.ts +35 -0
  70. package/src/docker-run.ts +103 -0
  71. package/src/host/codesys-command.ts +45 -0
  72. package/src/host/codesys-host.ts +145 -0
  73. package/src/host/codesys-worker.ts +348 -0
  74. package/src/host/windows-path.ts +73 -0
  75. package/src/index.ts +43 -0
  76. package/src/io.ts +127 -0
  77. package/src/libraries.ts +166 -0
  78. package/src/package-path.ts +5 -0
  79. package/src/run.ts +133 -0
  80. package/src/runtime-status.ts +21 -0
  81. package/src/sources.ts +248 -0
  82. package/src/st/declarations.ts +229 -0
  83. package/src/st/lexer.ts +125 -0
  84. package/src/st/parse.ts +202 -0
  85. package/src/st/reader.ts +50 -0
package/dist/index.js ADDED
@@ -0,0 +1,1683 @@
1
+ // packages/build/src/artifacts.ts
2
+ import { copyFile, cp, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import { z } from "zod";
5
+
6
+ // packages/build/src/config.ts
7
+ import {
8
+ configSchema,
9
+ controllerSchema,
10
+ cyclicTasksSchema,
11
+ dockerImage,
12
+ defineBuildConfig,
13
+ resolveConfig,
14
+ loadConfig
15
+ } from "@tsplc/config/build";
16
+
17
+ // packages/build/src/artifacts.ts
18
+ var buildFiles = {
19
+ project: "Application.project",
20
+ output: "runtime/Application.app",
21
+ checksum: "runtime/Application.crc",
22
+ receipt: "build.json",
23
+ devices: "eds",
24
+ archive: "Application.projectarchive"
25
+ };
26
+ var receiptSchema = z.object({
27
+ mode: z.literal("build"),
28
+ application: z.string().regex(/^[A-Za-z_][A-Za-z_0-9]*$/),
29
+ tasks: cyclicTasksSchema.optional()
30
+ });
31
+ var readBuild = async (directory) => {
32
+ const receipt = receiptSchema.safeParse(JSON.parse(await readFile(resolve(directory, buildFiles.receipt), "utf8")));
33
+ if (!receipt.success) {
34
+ throw new Error("CODESYS did not produce a valid build receipt", {
35
+ cause: receipt.error
36
+ });
37
+ }
38
+ for (const file of [buildFiles.project, buildFiles.output, buildFiles.checksum]) {
39
+ const artifact = await stat(resolve(directory, file));
40
+ if (!artifact.isFile() || artifact.size === 0) {
41
+ throw new Error(`CODESYS produced an empty artifact: ${file}`);
42
+ }
43
+ }
44
+ return {
45
+ ...receipt.data,
46
+ directory,
47
+ project: resolve(directory, buildFiles.project),
48
+ output: resolve(directory, buildFiles.output),
49
+ checksum: resolve(directory, buildFiles.checksum)
50
+ };
51
+ };
52
+
53
+ // packages/build/src/docker-build.ts
54
+ import { execFile } from "node:child_process";
55
+ import { randomUUID } from "node:crypto";
56
+ import { cp as cp3, mkdir as mkdir3, mkdtemp as mkdtemp2, readFile as readFile3, rm as rm3 } from "node:fs/promises";
57
+ import { tmpdir as tmpdir2 } from "node:os";
58
+ import { resolve as resolve5 } from "node:path";
59
+ import { promisify } from "node:util";
60
+
61
+ // packages/build/src/docker-context.ts
62
+ import { cp as cp2, mkdir, mkdtemp, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
63
+ import { createRequire } from "node:module";
64
+ import { tmpdir } from "node:os";
65
+ import { dirname, resolve as resolve3 } from "node:path";
66
+
67
+ // packages/build/src/package-path.ts
68
+ import { resolve as resolve2 } from "node:path";
69
+ import { fileURLToPath } from "node:url";
70
+ var packageDirectory = resolve2(fileURLToPath(new URL("..", import.meta.url)));
71
+
72
+ // packages/build/src/docker-context.ts
73
+ async function prepareDockerContext() {
74
+ const context = await mkdtemp(resolve3(tmpdir(), "tsplc-docker-"));
75
+ try {
76
+ for (const directory of ["src", "python", "docker", "runtime"])
77
+ await cp2(resolve3(packageDirectory, directory), resolve3(context, directory), {
78
+ recursive: true
79
+ });
80
+ const configRoot = resolve3(dirname(createRequire(import.meta.url).resolve("@tsplc/config/build")), "..");
81
+ await mkdir(resolve3(context, "shared-config"));
82
+ await cp2(resolve3(configRoot, "src"), resolve3(context, "shared-config/src"), {
83
+ recursive: true
84
+ });
85
+ await cp2(resolve3(configRoot, "templates"), resolve3(context, "shared-config/templates"), {
86
+ recursive: true
87
+ });
88
+ await cp2(resolve3(configRoot, "package.json"), resolve3(context, "shared-config/package.json"));
89
+ await writeFile2(resolve3(context, "package.json"), JSON.stringify({ type: "module", dependencies: { zod: "4.3.6", jiti: "2.7.0" } }));
90
+ return context;
91
+ } catch (cause) {
92
+ await rm2(context, { recursive: true, force: true });
93
+ throw cause;
94
+ }
95
+ }
96
+
97
+ // packages/build/src/diagnostics.ts
98
+ import { z as z2 } from "zod";
99
+ var compilerDiagnosticSchema = z2.object({
100
+ category: z2.string(),
101
+ text: z2.string(),
102
+ severity: z2.string().nullable(),
103
+ project: z2.string().nullable(),
104
+ object: z2.object({
105
+ name: z2.string(),
106
+ guid: z2.string(),
107
+ path: z2.array(z2.string()),
108
+ isApplicationChild: z2.boolean().nullable()
109
+ }).passthrough().nullable(),
110
+ position: z2.string().nullable(),
111
+ positionText: z2.string().nullable(),
112
+ positionOffset: z2.int().nullable(),
113
+ length: z2.int().nullable(),
114
+ prefix: z2.string().nullable(),
115
+ number: z2.int().nullable()
116
+ }).passthrough();
117
+
118
+ class CodesysCompilerError extends Error {
119
+ diagnostics;
120
+ constructor(message, diagnostics) {
121
+ super(message);
122
+ this.diagnostics = diagnostics;
123
+ this.name = "CodesysCompilerError";
124
+ }
125
+ }
126
+ var buildFailureFile = ".codesys/docker/failure.json";
127
+ var controllerFailureFile = ".codesys/controller/failure.txt";
128
+ var buildFailureSchema = z2.strictObject({
129
+ operationId: z2.uuid(),
130
+ message: z2.string(),
131
+ diagnostics: z2.array(compilerDiagnosticSchema)
132
+ });
133
+
134
+ // packages/build/src/docker-run.ts
135
+ import { spawn } from "node:child_process";
136
+ import { createHash } from "node:crypto";
137
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile3 } from "node:fs/promises";
138
+ import { basename, posix, relative, resolve as resolve4, sep } from "node:path";
139
+ var runDocker = (args, failure) => new Promise((accept, reject) => {
140
+ const child = spawn("docker", args, {
141
+ stdio: ["inherit", process.env["TSPLC_LOG_STDERR"] === "1" ? 2 : "inherit", "inherit"]
142
+ });
143
+ const interrupt = () => child.kill("SIGINT");
144
+ const terminate = () => child.kill("SIGTERM");
145
+ process.on("SIGINT", interrupt);
146
+ process.on("SIGTERM", terminate);
147
+ const cleanup = () => {
148
+ process.off("SIGINT", interrupt);
149
+ process.off("SIGTERM", terminate);
150
+ };
151
+ child.once("error", (cause) => {
152
+ cleanup();
153
+ reject(new Error("Cannot start Docker. Install and start Docker Desktop or Docker Engine", {
154
+ cause
155
+ }));
156
+ });
157
+ child.once("exit", (code, signal) => {
158
+ cleanup();
159
+ if (code === 0) {
160
+ accept();
161
+ } else {
162
+ reject(new Error(failure(signal ?? code)));
163
+ }
164
+ });
165
+ });
166
+ var bindMount = (source, target, readonly = false) => `type=bind,"source=${source.replaceAll('"', '""')}",target=${target}${readonly ? ",readonly" : ""}`;
167
+ async function mountLibraries(libraries, directory) {
168
+ if (!libraries?.length)
169
+ return;
170
+ await mkdir2(resolve4(directory, "libraries"), { recursive: true });
171
+ return Promise.all(libraries.map(async (library) => {
172
+ const data = await readFile2(library.path);
173
+ if (createHash("sha256").update(data).digest("hex") !== library.sha256)
174
+ throw new Error(`Library changed after installation: ${library.title}`);
175
+ const filename = `${library.sha256}-${basename(library.path)}`;
176
+ await writeFile3(resolve4(directory, "libraries", filename), data);
177
+ return { ...library, path: `/input/libraries/${filename}` };
178
+ }));
179
+ }
180
+ var prepareContainerRun = async (config, requestDirectory, options) => {
181
+ await writeFile3(resolve4(requestDirectory, "config.json"), JSON.stringify({
182
+ ...config,
183
+ libraries: options.sources ? await mountLibraries(config.libraries, requestDirectory) : [],
184
+ configFile: undefined,
185
+ docker: undefined,
186
+ codesys: undefined,
187
+ sourceDir: "/sources",
188
+ entry: posix.resolve("/sources", relative(config.sourceDir, config.entry).split(sep).join("/")),
189
+ template: "/template.project",
190
+ outDir: "/output",
191
+ runtime: undefined
192
+ }));
193
+ return [
194
+ "--init",
195
+ ...config.docker?.cpuSet ? ["--cpuset-cpus", config.docker.cpuSet] : [],
196
+ "--network",
197
+ options.network,
198
+ ...process.getuid && process.getgid ? ["--user", `${process.getuid()}:${process.getgid()}`] : [],
199
+ ...(options.environment ?? []).flatMap((name) => ["--env", name]),
200
+ "--mount",
201
+ bindMount(requestDirectory, "/input", true),
202
+ ...options.sources ? ["--mount", bindMount(config.sourceDir, "/sources", true)] : [],
203
+ "--mount",
204
+ bindMount(config.template, "/template.project", true),
205
+ "--mount",
206
+ bindMount(config.outDir, "/output")
207
+ ];
208
+ };
209
+
210
+ // packages/build/src/docker-build.ts
211
+ var execute = promisify(execFile);
212
+ var configureDocker = async () => {
213
+ const image = dockerImage;
214
+ const { stdout } = await execute("docker", ["info", "--format", "{{.OSType}}/{{.Architecture}}"], { timeout: 30000 }).catch((cause) => {
215
+ throw new Error("Cannot connect to Docker. Install and start Docker Desktop or Docker Engine", {
216
+ cause
217
+ });
218
+ });
219
+ const architecture = stdout.trim();
220
+ const arm = architecture === "linux/aarch64" || architecture === "linux/arm64";
221
+ if (!arm && architecture !== "linux/x86_64" && architecture !== "linux/amd64") {
222
+ throw new Error("The builder requires a Linux Docker server on ARM64 or AMD64");
223
+ }
224
+ const context = await mkdtemp2(resolve5(tmpdir2(), "codesys-installer-"));
225
+ let buildContext;
226
+ try {
227
+ buildContext = await prepareDockerContext();
228
+ const cache = resolve5(context, "winetricks");
229
+ await mkdir3(cache);
230
+ if (process.env["CODESYS_WINETRICKS_CACHE"]) {
231
+ await cp3(process.env["CODESYS_WINETRICKS_CACHE"], cache, {
232
+ recursive: true
233
+ });
234
+ }
235
+ console.log(`Preparing Docker builder (${image}). Downloading and installing CODESYS 3.5.22.30 and prerequisites.`);
236
+ await runDocker([
237
+ "build",
238
+ "--platform",
239
+ "linux/amd64",
240
+ "--build-arg",
241
+ `SETUP_BASE=${arm ? "arm-setup" : "base"}`,
242
+ "--build-arg",
243
+ `SETUP_PLATFORM=${arm ? "linux/arm64" : "linux/amd64"}`,
244
+ "--build-arg",
245
+ "INSTALLER_SOURCE=installer-download",
246
+ "--progress",
247
+ "plain",
248
+ "--build-context",
249
+ `installer=${context}`,
250
+ "-f",
251
+ resolve5(buildContext, "docker/Dockerfile"),
252
+ "-t",
253
+ image,
254
+ buildContext
255
+ ], (status) => `Docker configuration failed (${status}). Review the image-build output and run tsplc configure again`);
256
+ return image;
257
+ } finally {
258
+ await Promise.all([
259
+ rm3(context, { recursive: true, force: true }),
260
+ ...buildContext ? [rm3(buildContext, { recursive: true, force: true })] : []
261
+ ]);
262
+ }
263
+ };
264
+ var runtimeImage = "tsplc-control-linux-sl:4.22.0.0";
265
+ var configureRuntime = async () => {
266
+ const context = resolve5(packageDirectory, "docker/control-linux-sl");
267
+ console.log(`Preparing the local PLC runtime (${runtimeImage}).`);
268
+ await runDocker(["build", "--platform", "linux/amd64", "--progress", "plain", "-t", runtimeImage, context], (status) => `Runtime image build failed (${status}). Review the image-build output`);
269
+ return runtimeImage;
270
+ };
271
+ var gatewayImage = "tsplc-edge-gateway:4.22.0.0";
272
+ var configureGateway = async () => {
273
+ const context = resolve5(packageDirectory, "docker/edge-gateway");
274
+ console.log(`Preparing the CODESYS gateway (${gatewayImage}).`);
275
+ await runDocker(["build", "--platform", "linux/amd64", "--progress", "plain", "-t", gatewayImage, context], (status) => `Gateway image build failed (${status}). Review the image-build output`);
276
+ return gatewayImage;
277
+ };
278
+ var buildInDocker = async (config, image) => {
279
+ const requestDirectory = await mkdtemp2(resolve5(tmpdir2(), "codesys-docker-"));
280
+ try {
281
+ const operationId = randomUUID();
282
+ await mkdir3(config.outDir, { recursive: true });
283
+ const failurePath = resolve5(config.outDir, buildFailureFile);
284
+ await rm3(failurePath, { force: true });
285
+ const args = [
286
+ "run",
287
+ "--rm",
288
+ ...await prepareContainerRun(config, requestDirectory, { network: "none", sources: true }),
289
+ image,
290
+ "--request",
291
+ "/input/config.json",
292
+ "--operation-id",
293
+ operationId
294
+ ];
295
+ console.log(`Building with Docker (${image})`);
296
+ try {
297
+ await runDocker(args, (status) => `Docker build failed (${status}). Run tsplc configure if the image is missing; inspect ${config.outDir}/.codesys/docker for compiler logs`);
298
+ } catch (cause) {
299
+ const message = cause instanceof Error ? cause.message : String(cause);
300
+ const failure = await readFile3(failurePath, "utf8").then((text) => buildFailureSchema.parse(JSON.parse(text))).catch((error) => {
301
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
302
+ return;
303
+ throw new AggregateError([cause, error], `${message}. Invalid CODESYS failure metadata (${failurePath})`, { cause: error });
304
+ });
305
+ if (!failure)
306
+ throw cause;
307
+ if (failure.operationId !== operationId)
308
+ throw new Error(`${message}. CODESYS failure metadata belongs to a different Docker build`, {
309
+ cause
310
+ });
311
+ if (cause instanceof Error) {
312
+ cause.message = `${message}
313
+ ${failure.message}`;
314
+ if (failure.diagnostics.length)
315
+ cause.cause = new CodesysCompilerError(failure.message, failure.diagnostics);
316
+ }
317
+ throw cause;
318
+ }
319
+ return await readBuild(config.outDir);
320
+ } finally {
321
+ await rm3(requestDirectory, { recursive: true, force: true });
322
+ }
323
+ };
324
+
325
+ // packages/build/src/host/codesys-worker.ts
326
+ import {
327
+ access,
328
+ copyFile as copyFile2,
329
+ mkdir as mkdir4,
330
+ open,
331
+ readFile as readFile4,
332
+ rename as rename2,
333
+ rm as rm4,
334
+ unlink,
335
+ writeFile as writeFile4
336
+ } from "node:fs/promises";
337
+ import { resolve as resolve6 } from "node:path";
338
+ import { setTimeout as delay } from "node:timers/promises";
339
+ import { z as z4 } from "zod";
340
+
341
+ // packages/build/src/host/codesys-command.ts
342
+ import { commandArgumentSchema } from "@tsplc/config/build";
343
+ var codesysCommand = [
344
+ "@echo off",
345
+ "setlocal DisableDelayedExpansion",
346
+ '"%CODESYS_BUILD_EXE%" --profile="%CODESYS_BUILD_PROFILE%" --noUI --runscript="%CODESYS_BUILD_SCRIPT%"',
347
+ 'set "CODESYS_BUILD_STATUS=%errorlevel%"',
348
+ 'if not "%CODESYS_BUILD_STATUS%"=="0" echo CODESYS Windows exit status: %CODESYS_BUILD_STATUS%',
349
+ "exit /b %CODESYS_BUILD_STATUS%",
350
+ ""
351
+ ].join(`\r
352
+ `);
353
+
354
+ // packages/build/src/host/codesys-host.ts
355
+ import { execFile as execFile3, spawn as spawn2 } from "node:child_process";
356
+ import { promisify as promisify3 } from "node:util";
357
+ import { z as z3 } from "zod";
358
+
359
+ // packages/build/src/host/windows-path.ts
360
+ import { execFile as execFile2 } from "node:child_process";
361
+ import { promisify as promisify2 } from "node:util";
362
+ import { codesysTimeouts } from "@tsplc/config/build";
363
+ var execute2 = promisify2(execFile2);
364
+
365
+ // packages/build/src/host/codesys-host.ts
366
+ var execute3 = promisify3(execFile3);
367
+ var exitSchema = z3.object({
368
+ pid: z3.int().positive(),
369
+ code: z3.int().nullable(),
370
+ signal: z3.string().nullable()
371
+ });
372
+
373
+ // packages/build/src/host/codesys-worker.ts
374
+ var pythonDirectory = resolve6(packageDirectory, "python");
375
+ var sessionSchema = z4.object({
376
+ directory: z4.string(),
377
+ fingerprint: z4.string(),
378
+ pid: z4.int().positive()
379
+ });
380
+ var hasCode = (error, code) => error instanceof Error && ("code" in error) && error.code === code;
381
+ var isProcessAlive = (pid) => {
382
+ try {
383
+ process.kill(pid, 0);
384
+ return true;
385
+ } catch (error) {
386
+ if (hasCode(error, "ESRCH")) {
387
+ return false;
388
+ }
389
+ throw error;
390
+ }
391
+ };
392
+ var withWorkerLock = async (config, run) => {
393
+ await mkdir4(resolve6(config.outDir, ".codesys"), { recursive: true });
394
+ const path = resolve6(config.outDir, ".codesys/command.lock");
395
+ const deadline = Date.now() + config.timeouts.build + config.timeouts.startup;
396
+ while (true) {
397
+ const lock = await open(path, "wx").catch((error) => {
398
+ if (hasCode(error, "EEXIST")) {
399
+ return;
400
+ }
401
+ throw error;
402
+ });
403
+ if (lock) {
404
+ try {
405
+ await lock.writeFile(String(process.pid));
406
+ return await run();
407
+ } finally {
408
+ await lock.close();
409
+ await unlink(path);
410
+ }
411
+ }
412
+ const owner = Number(await readFile4(path, "utf8").catch((error) => {
413
+ if (hasCode(error, "ENOENT")) {
414
+ return "";
415
+ }
416
+ throw error;
417
+ }));
418
+ if (owner > 0 && !isProcessAlive(owner)) {
419
+ throw new Error(`An interrupted command left ${path}. Remove that lock file and retry.`);
420
+ }
421
+ if (Date.now() >= deadline) {
422
+ throw new Error(`Timed out waiting for another CODESYS command (${path})`);
423
+ }
424
+ await delay(200);
425
+ }
426
+ };
427
+
428
+ // packages/build/src/sources.ts
429
+ import { readdir, readFile as readFile5 } from "node:fs/promises";
430
+ import { basename as basename3, resolve as resolve7 } from "node:path";
431
+
432
+ // packages/build/src/st/parse.ts
433
+ import { basename as basename2 } from "node:path";
434
+
435
+ // packages/build/src/st/declarations.ts
436
+ var objectKinds = {
437
+ PROGRAM: "program",
438
+ FUNCTION_BLOCK: "function_block",
439
+ FUNCTIONBLOCK: "function_block",
440
+ FUNCTION: "function",
441
+ METHOD: "method",
442
+ INTERFACE: "interface",
443
+ PROPERTY: "property"
444
+ };
445
+ var modifiers = new Set([
446
+ "PUBLIC",
447
+ "PRIVATE",
448
+ "PROTECTED",
449
+ "INTERNAL",
450
+ "FINAL",
451
+ "ABSTRACT",
452
+ "OVERRIDE"
453
+ ]);
454
+ var globalSections = new Set(["VAR_GLOBAL", "VAR_CONFIG"]);
455
+ var variableSections = new Set([
456
+ ...globalSections,
457
+ "VAR",
458
+ "VAR_INPUT",
459
+ "VAR_OUTPUT",
460
+ "VAR_IN_OUT",
461
+ "VAR_TEMP",
462
+ "VAR_STAT",
463
+ "VAR_INST",
464
+ "VAR_EXTERNAL",
465
+ "VAR_ACCESS",
466
+ "VAR_GENERIC"
467
+ ]);
468
+ var closingObjects = new Set(Object.keys(objectKinds).map((kind) => `END_${kind}`));
469
+ var declarationKeywords = new Set([
470
+ ...Object.keys(objectKinds),
471
+ ...closingObjects,
472
+ ...variableSections,
473
+ "END_VAR",
474
+ "TYPE",
475
+ "END_TYPE",
476
+ "STRUCT",
477
+ "END_STRUCT",
478
+ "UNION",
479
+ "END_UNION",
480
+ "ACTION",
481
+ "END_ACTION",
482
+ "CONFIGURATION",
483
+ "END_CONFIGURATION"
484
+ ]);
485
+ var conditionalDirective = (token) => {
486
+ if (token?.kind === "pragma") {
487
+ return /^\{\s*(IF|ELSIF|ELSE|END_IF)\b/i.exec(token.value)?.[1]?.toUpperCase();
488
+ }
489
+ };
490
+ var isDeclarationPragma = (token) => token?.kind === "pragma" && !conditionalDirective(token);
491
+ var readDeclarationPragmas = (reader) => {
492
+ while (isDeclarationPragma(reader.peek())) {
493
+ reader.take();
494
+ }
495
+ };
496
+ var readDeclarationSemicolon = (reader) => {
497
+ const token = reader.peek();
498
+ if (token?.value === ";" && !/[\r\n]/.test(reader.source.slice(reader.end, token.start))) {
499
+ reader.take();
500
+ }
501
+ };
502
+ var readIdentifier = (reader) => {
503
+ const token = reader.take();
504
+ if (token.kind !== "word" || declarationKeywords.has(token.value)) {
505
+ reader.fail("Expected an IEC identifier", token);
506
+ }
507
+ return reader.source.slice(token.start, token.end);
508
+ };
509
+ var isOverrideModifier = (reader) => {
510
+ const name = reader.peek(1);
511
+ return name?.kind === "word" && !declarationKeywords.has(name.value) && !["(", "[", ":=", ".", "^"].includes(reader.peek(2)?.value ?? "") && !/[\r\n]/.test(reader.source.slice(reader.peek()?.end, name.start));
512
+ };
513
+ var readModifiers = (reader) => {
514
+ while (modifiers.has(reader.peek()?.value ?? "")) {
515
+ if (reader.peek()?.value === "OVERRIDE" && !isOverrideModifier(reader)) {
516
+ return;
517
+ }
518
+ reader.take();
519
+ }
520
+ };
521
+ var typeDelimiters = { "(": ")", "[": "]", "<": ">" };
522
+ var readDelimitedType = (reader, open) => {
523
+ reader.expect(open);
524
+ const closings = [typeDelimiters[open]];
525
+ while (closings.length) {
526
+ const token = reader.take();
527
+ if (token.kind === "string" || token.kind === "pragma") {
528
+ continue;
529
+ }
530
+ if (token.value === "(" || token.value === "[") {
531
+ closings.push(typeDelimiters[token.value]);
532
+ } else if (token.value === ")" || token.value === "]") {
533
+ if (closings.pop() !== token.value) {
534
+ reader.fail(`Mismatched delimiter ${token.value}`, token);
535
+ }
536
+ } else if (open === "<" && closings.at(-1) === ">") {
537
+ if (token.value === "<") {
538
+ closings.push(">");
539
+ }
540
+ if (token.value === ">") {
541
+ closings.pop();
542
+ }
543
+ }
544
+ if (declarationKeywords.has(token.value) || token.value === ";") {
545
+ reader.fail(`Unclosed ${open}`, token);
546
+ }
547
+ }
548
+ };
549
+ var readType = (reader) => {
550
+ const start = reader.peek()?.start ?? reader.source.length;
551
+ while (true) {
552
+ if (["POINTER", "REFERENCE", "REF_TO"].includes(reader.peek()?.value ?? "")) {
553
+ if (reader.take().value !== "REF_TO") {
554
+ reader.expect("TO");
555
+ }
556
+ continue;
557
+ }
558
+ if (reader.consume("ARRAY")) {
559
+ readDelimitedType(reader, "[");
560
+ reader.expect("OF");
561
+ continue;
562
+ }
563
+ break;
564
+ }
565
+ readIdentifier(reader);
566
+ while (reader.consume(".")) {
567
+ readIdentifier(reader);
568
+ }
569
+ for (const open of ["(", "[", "<"]) {
570
+ if (reader.peek()?.value === open) {
571
+ readDelimitedType(reader, open);
572
+ }
573
+ }
574
+ return reader.source.slice(start, reader.end);
575
+ };
576
+ var readVariableSection = (reader) => {
577
+ reader.take();
578
+ const conditionals = [];
579
+ while (reader.peek()?.value !== "END_VAR") {
580
+ const token = reader.take();
581
+ if (variableSections.has(token.value) || closingObjects.has(token.value)) {
582
+ reader.fail("Missing END_VAR", token);
583
+ }
584
+ const directive = conditionalDirective(token);
585
+ if (!directive) {
586
+ continue;
587
+ }
588
+ if (directive === "IF") {
589
+ conditionals.push({ hasElse: false });
590
+ continue;
591
+ }
592
+ const current = conditionals.at(-1);
593
+ if (!current) {
594
+ return reader.fail("Unmatched conditional pragma", token);
595
+ }
596
+ if (directive === "END_IF") {
597
+ conditionals.pop();
598
+ continue;
599
+ }
600
+ if (current.hasElse) {
601
+ reader.fail("Conditional branch after ELSE", token);
602
+ }
603
+ current.hasElse = directive === "ELSE";
604
+ }
605
+ if (conditionals.length) {
606
+ reader.fail("Conditional pragmas must remain inside one variable section");
607
+ }
608
+ reader.take();
609
+ readDeclarationSemicolon(reader);
610
+ };
611
+ var readDeclarationSections = (reader) => {
612
+ while (reader.peek()) {
613
+ let ahead = 0;
614
+ while (isDeclarationPragma(reader.peek(ahead))) {
615
+ ahead++;
616
+ }
617
+ const next = reader.peek(ahead)?.value ?? "";
618
+ if (variableSections.has(next)) {
619
+ readDeclarationPragmas(reader);
620
+ readVariableSection(reader);
621
+ continue;
622
+ }
623
+ if (next !== "EXTENDS" && next !== "IMPLEMENTS") {
624
+ return;
625
+ }
626
+ readDeclarationPragmas(reader);
627
+ reader.take();
628
+ readType(reader);
629
+ while (reader.consume(",")) {
630
+ readType(reader);
631
+ }
632
+ }
633
+ };
634
+
635
+ // packages/build/src/st/lexer.ts
636
+ class StSyntaxError extends Error {
637
+ constructor(source, offset, filename, message) {
638
+ const lines = source.slice(0, offset).split(/\r\n|\r|\n/);
639
+ super(`${filename}:${lines.length}:${(lines.at(-1)?.length ?? 0) + 1}: ${message}`);
640
+ this.name = "StSyntaxError";
641
+ }
642
+ }
643
+ var tokenize = (source, filename = "<source>") => {
644
+ const tokens = [];
645
+ let offset = 0;
646
+ const fail = (start, message) => {
647
+ throw new StSyntaxError(source, start, filename, message);
648
+ };
649
+ const quoted = () => {
650
+ const start = offset;
651
+ const quote = source[offset++];
652
+ while (offset < source.length) {
653
+ if (source[offset] === "$") {
654
+ offset += 2;
655
+ } else if (source[offset++] === quote) {
656
+ return;
657
+ }
658
+ }
659
+ fail(start, "Unterminated string");
660
+ };
661
+ while (offset < source.length) {
662
+ const start = offset;
663
+ const character = source[offset] ?? "";
664
+ if (/\s|\uFEFF/.test(character)) {
665
+ offset++;
666
+ continue;
667
+ }
668
+ if (source.startsWith("//", offset)) {
669
+ while (offset < source.length && !/[\r\n]/.test(source[offset] ?? "")) {
670
+ offset++;
671
+ }
672
+ continue;
673
+ }
674
+ if (source.startsWith("(*", offset)) {
675
+ offset += 2;
676
+ let depth = 1;
677
+ while (offset < source.length && depth > 0) {
678
+ if (source.startsWith("(*", offset)) {
679
+ depth++;
680
+ offset += 2;
681
+ } else if (source.startsWith("*)", offset)) {
682
+ depth--;
683
+ offset += 2;
684
+ } else {
685
+ offset++;
686
+ }
687
+ }
688
+ if (depth) {
689
+ fail(start, "Unterminated block comment");
690
+ }
691
+ continue;
692
+ }
693
+ if (character === "'" || character === '"') {
694
+ quoted();
695
+ tokens.push({
696
+ kind: "string",
697
+ value: source.slice(start, offset),
698
+ start,
699
+ end: offset
700
+ });
701
+ continue;
702
+ }
703
+ if (character === "{") {
704
+ offset++;
705
+ while (offset < source.length && source[offset] !== "}") {
706
+ if (source[offset] === "'" || source[offset] === '"') {
707
+ quoted();
708
+ } else {
709
+ offset++;
710
+ }
711
+ }
712
+ if (offset === source.length) {
713
+ fail(start, "Unterminated pragma");
714
+ }
715
+ offset++;
716
+ tokens.push({
717
+ kind: "pragma",
718
+ value: source.slice(start, offset),
719
+ start,
720
+ end: offset
721
+ });
722
+ continue;
723
+ }
724
+ if (/[A-Za-z_]/.test(character)) {
725
+ offset++;
726
+ while (/[A-Za-z_0-9]/.test(source[offset] ?? "")) {
727
+ offset++;
728
+ }
729
+ tokens.push({
730
+ kind: "word",
731
+ value: source.slice(start, offset).toUpperCase(),
732
+ start,
733
+ end: offset
734
+ });
735
+ continue;
736
+ }
737
+ offset++;
738
+ const pair = source.slice(start, offset + 1);
739
+ if ([":=", "=>", "..", "<=", ">=", "<>", "**"].includes(pair)) {
740
+ offset++;
741
+ }
742
+ tokens.push({
743
+ kind: "symbol",
744
+ value: source.slice(start, offset),
745
+ start,
746
+ end: offset
747
+ });
748
+ }
749
+ return tokens;
750
+ };
751
+
752
+ // packages/build/src/st/reader.ts
753
+ var createTokenReader = (source, filename) => {
754
+ const tokens = tokenize(source, filename);
755
+ let index = 0;
756
+ const peek = (ahead = 0) => tokens[index + ahead];
757
+ const fail = (message, token = peek()) => {
758
+ throw new StSyntaxError(source, token?.start ?? source.length, filename, message);
759
+ };
760
+ const take = () => {
761
+ const token = peek();
762
+ if (!token) {
763
+ return fail("Unexpected end of declaration");
764
+ }
765
+ index++;
766
+ return token;
767
+ };
768
+ const expect = (value) => {
769
+ if (peek()?.value !== value) {
770
+ fail(`Expected ${value}`);
771
+ }
772
+ return take();
773
+ };
774
+ const consume = (value) => {
775
+ if (peek()?.value === value) {
776
+ return take();
777
+ }
778
+ };
779
+ return {
780
+ source,
781
+ filename,
782
+ tokens,
783
+ peek,
784
+ take,
785
+ expect,
786
+ consume,
787
+ fail,
788
+ get index() {
789
+ return index;
790
+ },
791
+ get end() {
792
+ return tokens[index - 1]?.end ?? 0;
793
+ }
794
+ };
795
+ };
796
+
797
+ // packages/build/src/st/parse.ts
798
+ var dutTypes = {
799
+ STRUCT: "structure",
800
+ UNION: "union",
801
+ "(": "enumeration"
802
+ };
803
+ var parseDut = (reader) => {
804
+ reader.expect("TYPE");
805
+ readModifiers(reader);
806
+ const name = readIdentifier(reader);
807
+ if (reader.consume("EXTENDS")) {
808
+ readType(reader);
809
+ }
810
+ reader.expect(":");
811
+ readDeclarationPragmas(reader);
812
+ const dutType = dutTypes[reader.peek()?.value ?? ""] ?? "alias";
813
+ while (reader.peek()?.value !== "END_TYPE") {
814
+ if (reader.peek()?.value === "TYPE") {
815
+ reader.fail("Only one TYPE declaration is allowed per file");
816
+ }
817
+ reader.take();
818
+ }
819
+ reader.take();
820
+ reader.consume(";");
821
+ if (reader.peek()) {
822
+ reader.fail("Unexpected content after END_TYPE");
823
+ }
824
+ return { kind: "dut", name, declaration: reader.source, dutType };
825
+ };
826
+ var parseGlobals = (reader, options) => {
827
+ const header = reader.peek();
828
+ let persistent = false;
829
+ while (reader.peek()) {
830
+ readDeclarationPragmas(reader);
831
+ if (!reader.peek()) {
832
+ break;
833
+ }
834
+ if (!globalSections.has(reader.peek()?.value ?? "")) {
835
+ reader.fail("Expected a global variable section");
836
+ }
837
+ const start = reader.index;
838
+ readVariableSection(reader);
839
+ for (const token of reader.tokens.slice(start + 1, reader.index)) {
840
+ if (token.kind !== "word" || !["CONSTANT", "RETAIN", "PERSISTENT", "NON_RETAIN"].includes(token.value)) {
841
+ break;
842
+ }
843
+ persistent ||= token.value === "PERSISTENT";
844
+ }
845
+ }
846
+ const name = options.name ?? basename2(reader.filename, ".st");
847
+ if (!/^[A-Za-z_][A-Za-z_0-9]*$/.test(name)) {
848
+ reader.fail("Global list requires an IEC filename or an explicit name", header);
849
+ }
850
+ return {
851
+ kind: "gvl",
852
+ name,
853
+ declaration: reader.source,
854
+ ...persistent && { persistent: true }
855
+ };
856
+ };
857
+ var validateImplementation = (reader, tokens) => {
858
+ for (const token of tokens) {
859
+ if (token.kind !== "word") {
860
+ continue;
861
+ }
862
+ if (variableSections.has(token.value) || token.value === "END_VAR") {
863
+ reader.fail("Variable declaration outside the declaration section; conditional declaration envelopes are not supported", token);
864
+ }
865
+ if (declarationKeywords.has(token.value)) {
866
+ reader.fail("Nested objects must be separate child files, or the object terminator is mismatched", token);
867
+ }
868
+ }
869
+ };
870
+ var splitObject = (reader, header, kind, options) => {
871
+ const terminator = header.value === "FUNCTIONBLOCK" ? "END_FUNCTION_BLOCK" : `END_${header.value}`;
872
+ const remaining = reader.tokens.slice(reader.index);
873
+ const terminatorIndex = remaining.findIndex((token) => token.kind === "word" && token.value === terminator);
874
+ if (terminatorIndex < 0 && !options.declarationOnly) {
875
+ reader.fail(`Missing ${terminator}`);
876
+ }
877
+ const bodyTokens = terminatorIndex < 0 ? remaining : remaining.slice(0, terminatorIndex);
878
+ const [endToken, next, afterSemicolon] = terminatorIndex < 0 ? [] : remaining.slice(terminatorIndex);
879
+ const semicolon = next?.value === ";" ? next : undefined;
880
+ const unexpected = semicolon ? afterSemicolon : next;
881
+ if (unexpected) {
882
+ reader.fail(`Unexpected content after ${terminator}`, unexpected);
883
+ }
884
+ validateImplementation(reader, bodyTokens);
885
+ const declarationOnly = options.declarationOnly || kind === "interface" || kind === "property";
886
+ if (declarationOnly && bodyTokens.length) {
887
+ reader.fail("Unexpected implementation in a declaration-only object", bodyTokens[0]);
888
+ }
889
+ const declaration = reader.source.slice(0, reader.end);
890
+ const body = reader.source.slice(reader.end, endToken?.start) + (endToken ? reader.source.slice((semicolon ?? endToken).end) : "");
891
+ if (declarationOnly) {
892
+ return { declaration: declaration + body };
893
+ }
894
+ return { declaration, implementation: body };
895
+ };
896
+ var parsePou = (reader, options) => {
897
+ const header = reader.take();
898
+ if (!(header.value in objectKinds)) {
899
+ reader.fail(`Unsupported object header ${header.value}`, header);
900
+ }
901
+ const kind = objectKinds[header.value];
902
+ readModifiers(reader);
903
+ const name = readIdentifier(reader);
904
+ const returnType = reader.consume(":") ? readType(reader) : undefined;
905
+ if (returnType === undefined && kind === "property") {
906
+ reader.fail(`${header.value} requires a return type`);
907
+ }
908
+ readDeclarationSemicolon(reader);
909
+ readDeclarationSections(reader);
910
+ return {
911
+ kind,
912
+ name,
913
+ ...returnType === undefined ? {} : { returnType },
914
+ ...splitObject(reader, header, kind, options)
915
+ };
916
+ };
917
+ var parseSource = (source, options = {}) => {
918
+ const reader = createTokenReader(source, options.filename ?? "<source>");
919
+ readDeclarationPragmas(reader);
920
+ const header = reader.peek();
921
+ if (header?.value === "TYPE") {
922
+ return parseDut(reader);
923
+ }
924
+ if (header && globalSections.has(header.value)) {
925
+ return parseGlobals(reader, options);
926
+ }
927
+ return parsePou(reader, options);
928
+ };
929
+
930
+ // packages/build/src/io.ts
931
+ var arrayType = /^ARRAY\s*\[\s*(-?\d+)\s*\.\.\s*(-?\d+)\s*\]\s*OF\s+(\w+)$/;
932
+ var layout = (type) => {
933
+ const array = arrayType.exec(type);
934
+ return array ? { element: array[3], length: Number(array[2]) - Number(array[1]) + 1 } : { element: type, length: 1 };
935
+ };
936
+ var widths = {
937
+ BOOL: "X",
938
+ BYTE: "B",
939
+ SINT: "B",
940
+ USINT: "B",
941
+ WORD: "W",
942
+ INT: "W",
943
+ UINT: "W",
944
+ DWORD: "D",
945
+ DINT: "D",
946
+ UDINT: "D",
947
+ REAL: "D"
948
+ };
949
+ function bindStructuredTextIo(objects, bindings) {
950
+ const declarations = [];
951
+ const visit = (object) => {
952
+ const tokens = tokenize(object.declaration, object.source);
953
+ let section = false, start = 0;
954
+ for (let i = 0;i < tokens.length; i++) {
955
+ const token = tokens[i];
956
+ if (variableSections.has(token.value)) {
957
+ section = true;
958
+ start = i + 1;
959
+ continue;
960
+ }
961
+ if (token.value === "END_VAR") {
962
+ section = false;
963
+ continue;
964
+ }
965
+ if (!section || token.value !== ";")
966
+ continue;
967
+ const part = tokens.slice(start, i).filter((t) => t.kind !== "pragma");
968
+ start = i + 1;
969
+ const colonIndex = part.findIndex((t) => t.value === ":");
970
+ if (colonIndex < 1)
971
+ continue;
972
+ const names = part.slice(0, colonIndex).filter((t) => t.kind === "word" && !["CONSTANT", "RETAIN", "PERSISTENT", "AT"].includes(t.value));
973
+ const at = part.findIndex((t) => t.value === "AT");
974
+ const variables = at < 0 ? names : names.filter((t) => t.start < part[at].start);
975
+ const initializer = part.findIndex((t, index) => index > colonIndex && t.value === ":=");
976
+ const typeTokens = part.slice(colonIndex + 1, initializer < 0 ? undefined : initializer);
977
+ const type = typeTokens.length ? object.declaration.slice(typeTokens[0].start, typeTokens.at(-1).end).replace(/\s+/g, " ").toUpperCase() : "";
978
+ for (const name of variables)
979
+ declarations.push({
980
+ object,
981
+ name: name.value,
982
+ names: variables,
983
+ colon: part[colonIndex],
984
+ type
985
+ });
986
+ }
987
+ object.children.forEach(visit);
988
+ };
989
+ objects.forEach(visit);
990
+ const edits = new Map;
991
+ for (const [symbol, binding] of Object.entries(bindings)) {
992
+ const parts = symbol.toUpperCase().split(".");
993
+ if (parts.length > 2)
994
+ throw new Error(`${symbol}: direct IO maps a scalar declaration, not a nested field.`);
995
+ const matches = declarations.filter((d) => d.name === parts.at(-1) && (parts.length === 1 || d.object.name.toUpperCase() === parts[0]));
996
+ if (matches.length !== 1)
997
+ throw new Error(`${symbol}: expected one ST variable declaration, found ${matches.length}. Use Object.Variable to disambiguate.`);
998
+ const declaration = matches[0];
999
+ if (declaration.names.length !== 1)
1000
+ throw new Error(`${symbol}: declare mapped IO variables separately.`);
1001
+ const declared = layout(declaration.type);
1002
+ if (!widths[declared.element] || declared.length > 1 && declared.element === "BOOL" || binding.address[2] !== widths[declared.element])
1003
+ throw new Error(`${symbol}: ${declaration.type} is incompatible with ${binding.address}.`);
1004
+ const expected = binding.type && layout(binding.type.replace(/\s+/g, " ").toUpperCase());
1005
+ if (expected && (expected.element !== declared.element || expected.length !== declared.length))
1006
+ throw new Error(`${symbol}: expected ${binding.type}, found ${declaration.type}.`);
1007
+ const list = edits.get(declaration.object) ?? [];
1008
+ const start = declaration.names[0].end;
1009
+ if (list.some((e) => e.start === start))
1010
+ throw new Error(`${symbol}: the variable has more than one IO binding.`);
1011
+ list.push({ start, end: declaration.colon.start, text: ` AT ${binding.address} ` });
1012
+ edits.set(declaration.object, list);
1013
+ }
1014
+ for (const [object, list] of edits)
1015
+ for (const edit of list.sort((a, b) => b.start - a.start))
1016
+ object.declaration = object.declaration.slice(0, edit.start) + edit.text + object.declaration.slice(edit.end);
1017
+ }
1018
+
1019
+ // packages/build/src/sources.ts
1020
+ var prefixes = {
1021
+ FB: "function_block",
1022
+ PRG: "program",
1023
+ FUN: "function",
1024
+ F: "function",
1025
+ I: "interface",
1026
+ ST: "dut",
1027
+ E: "dut",
1028
+ T: "dut",
1029
+ U: "dut",
1030
+ GVL: "gvl"
1031
+ };
1032
+ var childFolders = {
1033
+ methods: "method",
1034
+ properties: "property",
1035
+ actions: "action"
1036
+ };
1037
+ var canOwn = (kind) => ["program", "function_block", "interface", "property"].includes(kind);
1038
+ var assertUnique = (objects, scope) => {
1039
+ const seen = new Map;
1040
+ for (const object of objects) {
1041
+ const key = object.name.toLowerCase();
1042
+ const previous = seen.get(key);
1043
+ if (previous) {
1044
+ throw new Error(`Duplicate IEC name ${object.name} in ${scope}: ${previous} and ${object.source}`);
1045
+ }
1046
+ seen.set(key, object.source);
1047
+ assertUnique(object.children, `${scope}.${object.name}`);
1048
+ }
1049
+ };
1050
+ var readUnit = async (name, files, role, interfaceMember = false) => {
1051
+ if (!/^[A-Za-z_][A-Za-z_0-9]*$/.test(name)) {
1052
+ throw new Error(`Invalid IEC object name: ${name}`);
1053
+ }
1054
+ for (const [part, candidates] of Object.entries(files)) {
1055
+ if (candidates.length > 1) {
1056
+ throw new Error(`Ambiguous ${part} for ${name}: ${candidates.join(", ")}`);
1057
+ }
1058
+ }
1059
+ const combined = files.combined[0];
1060
+ const declaration = files.declaration[0];
1061
+ const implementation = files.implementation[0];
1062
+ if (combined && (declaration || implementation)) {
1063
+ throw new Error(`Both combined and split sources exist for ${name}`);
1064
+ }
1065
+ const path = combined ?? declaration ?? implementation;
1066
+ if (!path) {
1067
+ throw new Error(`Missing source for ${name}`);
1068
+ }
1069
+ if (role === "action") {
1070
+ if (!combined) {
1071
+ throw new Error(`Actions use one implementation-only .st file: ${path}`);
1072
+ }
1073
+ return {
1074
+ kind: "action",
1075
+ name,
1076
+ declaration: "",
1077
+ implementation: await readFile5(combined, "utf8"),
1078
+ source: combined
1079
+ };
1080
+ }
1081
+ if (role === "get" || role === "set") {
1082
+ if (!combined) {
1083
+ throw new Error(`Property accessors use one .st file: ${path}`);
1084
+ }
1085
+ const body = await readFile5(combined, "utf8");
1086
+ const header = `METHOD ${name}
1087
+ `;
1088
+ const parsed = parseSource(`${header}${body}
1089
+ END_METHOD`, {
1090
+ filename: combined
1091
+ });
1092
+ return {
1093
+ ...parsed,
1094
+ kind: role,
1095
+ name,
1096
+ declaration: parsed.declaration.slice(header.length),
1097
+ source: combined
1098
+ };
1099
+ }
1100
+ if (!combined && !declaration) {
1101
+ throw new Error(`Missing declaration for ${path}`);
1102
+ }
1103
+ const sourcePath = combined ?? declaration ?? path;
1104
+ const parsed = parseSource(await readFile5(sourcePath, "utf8"), {
1105
+ filename: sourcePath,
1106
+ name,
1107
+ declarationOnly: !combined || interfaceMember
1108
+ });
1109
+ if (parsed.name.toLowerCase() !== name.toLowerCase()) {
1110
+ throw new Error(`Source name ${name} does not match declaration ${parsed.name}: ${sourcePath}`);
1111
+ }
1112
+ if (role && parsed.kind !== role) {
1113
+ throw new Error(`Expected ${role}, found ${parsed.kind}: ${sourcePath}`);
1114
+ }
1115
+ if (!role && ["method", "property"].includes(parsed.kind)) {
1116
+ throw new Error(`Child object has no enclosing POU: ${sourcePath}`);
1117
+ }
1118
+ const prefix = /^([A-Z]+)_/.exec(name.toUpperCase())?.[1];
1119
+ if (!role && prefix && prefixes[prefix] && prefixes[prefix] !== parsed.kind) {
1120
+ throw new Error(`Prefix ${prefix}_ conflicts with ${parsed.kind}: ${sourcePath}`);
1121
+ }
1122
+ const declarationOnly = ["dut", "gvl", "interface", "property"].includes(parsed.kind) || interfaceMember;
1123
+ if (declarationOnly && implementation) {
1124
+ throw new Error(`This object has no implementation: ${implementation}`);
1125
+ }
1126
+ if (!combined && !declarationOnly && !implementation) {
1127
+ throw new Error(`Missing implementation for ${sourcePath}`);
1128
+ }
1129
+ return {
1130
+ ...parsed,
1131
+ ...implementation ? { implementation: await readFile5(implementation, "utf8") } : {},
1132
+ source: sourcePath
1133
+ };
1134
+ };
1135
+ var loadSources = async (config) => {
1136
+ const objects = [];
1137
+ const visit = async (directory, folder, parent, role) => {
1138
+ const entries = (await readdir(directory, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name, "en"));
1139
+ const groups = new Map;
1140
+ for (const entry of entries) {
1141
+ if (entry.isSymbolicLink()) {
1142
+ throw new Error(`Source symlinks are not supported: ${resolve7(directory, entry.name)}`);
1143
+ }
1144
+ if (!entry.isFile() || !/\.st$/i.test(entry.name)) {
1145
+ continue;
1146
+ }
1147
+ const stem = entry.name.slice(0, -3);
1148
+ const suffix = /^(.*)\.(declaration|implementation)$/.exec(stem);
1149
+ const generic = ["source", "declaration", "implementation"].includes(stem);
1150
+ const name = generic ? basename3(directory) : suffix?.[1] ?? stem;
1151
+ const section = suffix?.[2] ?? stem;
1152
+ const part = section === "declaration" || section === "implementation" ? section : "combined";
1153
+ const key = name.toLowerCase();
1154
+ const group = groups.get(key) ?? {
1155
+ name,
1156
+ files: { combined: [], declaration: [], implementation: [] }
1157
+ };
1158
+ group.files[part].push(resolve7(directory, entry.name));
1159
+ groups.set(key, group);
1160
+ }
1161
+ const units = [];
1162
+ const ownKey = basename3(directory).toLowerCase();
1163
+ const ordered = [...groups].sort(([a], [b]) => Number(b === ownKey) - Number(a === ownKey));
1164
+ for (const [key, { name, files }] of ordered) {
1165
+ const ownProperty = units.find((unit) => unit.kind === "property" && unit.name.toLowerCase() === ownKey);
1166
+ const accessor = ownProperty && (key === "get" || key === "set") ? key : undefined;
1167
+ const unit = {
1168
+ ...await readUnit(accessor ? `${accessor[0]?.toUpperCase()}${accessor.slice(1)}` : name, files, accessor ?? role, parent?.kind === "interface"),
1169
+ folder: parent ? [] : folder,
1170
+ children: []
1171
+ };
1172
+ if (accessor && ownProperty) {
1173
+ if (parent?.kind === "interface") {
1174
+ throw new Error(`Interface properties cannot implement accessors: ${unit.source}`);
1175
+ }
1176
+ ownProperty.children.push(unit);
1177
+ } else {
1178
+ units.push(unit);
1179
+ if (parent) {
1180
+ parent.children.push(unit);
1181
+ } else {
1182
+ objects.push(unit);
1183
+ }
1184
+ }
1185
+ }
1186
+ const own = units.find((unit) => unit.name.toLowerCase() === ownKey && canOwn(unit.kind));
1187
+ if (own && !parent) {
1188
+ own.folder = folder.slice(0, -1);
1189
+ }
1190
+ for (const entry of entries.filter((entry) => entry.isDirectory())) {
1191
+ const childRole = childFolders[entry.name];
1192
+ if (own && childRole) {
1193
+ if (own.kind === "property" || own.kind === "interface" && childRole === "action") {
1194
+ throw new Error(`Unsupported ${entry.name} under ${own.name}`);
1195
+ }
1196
+ await visit(resolve7(directory, entry.name), [], own, childRole);
1197
+ } else if (parent) {
1198
+ await visit(resolve7(directory, entry.name), [], parent, role);
1199
+ } else {
1200
+ await visit(resolve7(directory, entry.name), [...folder, entry.name]);
1201
+ }
1202
+ }
1203
+ };
1204
+ await visit(resolve7(config.sourceDir), []);
1205
+ assertUnique(objects, "Application");
1206
+ if (config.io)
1207
+ bindStructuredTextIo(objects, config.io);
1208
+ const entry = objects.find((object) => object.source === resolve7(config.entry));
1209
+ if (!entry) {
1210
+ throw new Error(`Entry is not a discovered source: ${config.entry}`);
1211
+ }
1212
+ if (entry.kind !== "program") {
1213
+ throw new Error(`Entry must be a PROGRAM: ${config.entry}`);
1214
+ }
1215
+ if (config.tasks) {
1216
+ const programs = new Set(objects.filter((object) => object.kind === "program").map((object) => object.name));
1217
+ for (const task of config.tasks)
1218
+ for (const program of task.programs)
1219
+ if (!programs.has(program))
1220
+ throw new Error(`Task ${task.name} references an undiscovered PROGRAM ${program}`);
1221
+ }
1222
+ return { entry: entry.name, objects };
1223
+ };
1224
+
1225
+ // packages/build/src/build.ts
1226
+ var buildProject = async (config) => {
1227
+ if (!config.docker)
1228
+ throw new Error("Builds require a Docker builder image.");
1229
+ return buildInDocker(config, dockerImage);
1230
+ };
1231
+ // packages/build/src/controller-online.ts
1232
+ import { execFile as execFile4 } from "node:child_process";
1233
+ import { existsSync } from "node:fs";
1234
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
1235
+ import { mkdir as mkdir5, mkdtemp as mkdtemp3, readFile as readFile6, rm as rm5 } from "node:fs/promises";
1236
+ import { tmpdir as tmpdir3 } from "node:os";
1237
+ import { resolve as resolve8 } from "node:path";
1238
+ import { setTimeout as delay2 } from "node:timers/promises";
1239
+ import { promisify as promisify4 } from "node:util";
1240
+ import { z as z5 } from "zod";
1241
+ var execute4 = promisify4(execFile4);
1242
+ var imageIdSchema = z5.string().regex(/^sha256:[a-f0-9]{64}$/);
1243
+ var endpointSchema = z5.strictObject({
1244
+ host: z5.string().nonempty(),
1245
+ port: z5.number().int().min(1).max(65535)
1246
+ });
1247
+ var controllerResultSchema = z5.strictObject({
1248
+ schemaVersion: z5.literal(1),
1249
+ operationId: z5.uuid(),
1250
+ operation: z5.enum(["download", "online-change", "inspect"]),
1251
+ imageId: imageIdSchema,
1252
+ templateHash: z5.string().regex(/^[a-f0-9]{64}$/),
1253
+ target: z5.strictObject({
1254
+ network: z5.string().nonempty(),
1255
+ gateway: endpointSchema,
1256
+ device: endpointSchema,
1257
+ controller: controllerSchema.optional()
1258
+ }),
1259
+ generation: z5.string().nonempty(),
1260
+ application: z5.string().nonempty(),
1261
+ applicationGuid: z5.string().nonempty(),
1262
+ controllerIds: z5.strictObject({
1263
+ codeId: z5.string().nonempty(),
1264
+ dataId: z5.string().nonempty()
1265
+ }),
1266
+ state: z5.string().nonempty(),
1267
+ values: z5.record(z5.string(), z5.string())
1268
+ });
1269
+ var controllerConnection = async (config) => {
1270
+ if (!config.docker || !config.online) {
1271
+ throw new Error("Docker image and online gateway/device configuration are required for controller operations");
1272
+ }
1273
+ const environmentNames = config.online.credentials ? [config.online.credentials.usernameEnv, config.online.credentials.passwordEnv] : [];
1274
+ for (const name of environmentNames) {
1275
+ if (process.env[name] === undefined) {
1276
+ throw new Error(`Controller credential environment variable ${name} is not set`);
1277
+ }
1278
+ }
1279
+ const { stdout } = await execute4("docker", ["image", "inspect", "--format", "{{.Id}}", dockerImage], { timeout: config.timeouts.hostCommand }).catch((cause) => {
1280
+ throw new Error(`Cannot inspect Docker builder image ${dockerImage}. Install and start Docker, then run tsplc-build configure if the image is missing`, { cause });
1281
+ });
1282
+ return {
1283
+ environmentNames,
1284
+ imageId: imageIdSchema.parse(stdout.trim()),
1285
+ templateHash: createHash2("sha256").update(await readFile6(config.template)).digest("hex"),
1286
+ target: {
1287
+ network: config.online.network,
1288
+ gateway: config.online.gateway,
1289
+ device: config.online.device,
1290
+ controller: config.controller
1291
+ }
1292
+ };
1293
+ };
1294
+ var controllerOperation = async (config, operation, options) => {
1295
+ const { environmentNames, imageId, templateHash, target } = await controllerConnection(config);
1296
+ const operationId = randomUUID2();
1297
+ const requestDirectory = await mkdtemp3(resolve8(tmpdir3(), "codesys-controller-"));
1298
+ const diagnostics = resolve8(config.outDir, ".codesys/controller");
1299
+ const failure = resolve8(config.outDir, controllerFailureFile);
1300
+ try {
1301
+ await mkdir5(config.outDir, { recursive: true });
1302
+ await rm5(failure, { force: true });
1303
+ await runDocker([
1304
+ "run",
1305
+ "--rm",
1306
+ ...await prepareContainerRun(config, requestDirectory, {
1307
+ network: target.network,
1308
+ environment: environmentNames,
1309
+ sources: operation !== "inspect"
1310
+ }),
1311
+ imageId,
1312
+ operation,
1313
+ "--request",
1314
+ "/input/config.json",
1315
+ "--operation-id",
1316
+ operationId,
1317
+ "--image-id",
1318
+ imageId,
1319
+ ...options.initializeUser ? ["--initialize-user"] : [],
1320
+ ...options.read ? ["--read", JSON.stringify(options.read)] : []
1321
+ ], (status) => `Docker controller operation failed (${status}). Inspect ${diagnostics} for diagnostics`).catch(async (cause) => {
1322
+ if (!existsSync(failure))
1323
+ throw cause;
1324
+ const reason = (await readFile6(failure, "utf8")).trim().split(`
1325
+ `).at(-1);
1326
+ throw new Error(`${reason}. Inspect ${diagnostics} for diagnostics`, { cause });
1327
+ });
1328
+ const result = await readFile6(resolve8(diagnostics, "result.json"), "utf8").then((content) => controllerResultSchema.parse(JSON.parse(content))).catch((cause) => {
1329
+ throw new Error(`Docker did not produce a valid controller receipt. Inspect ${diagnostics} for diagnostics`, { cause });
1330
+ });
1331
+ if (result.operationId !== operationId || result.operation !== operation || result.imageId !== imageId || result.templateHash !== templateHash || JSON.stringify(result.target) !== JSON.stringify(target)) {
1332
+ throw new Error(`Controller receipt does not match this operation's image, template or target. Inspect ${diagnostics} for diagnostics`);
1333
+ }
1334
+ return result;
1335
+ } finally {
1336
+ await rm5(requestDirectory, { recursive: true, force: true });
1337
+ }
1338
+ };
1339
+ var updateController = (config, operation, options = {}) => controllerOperation(config, operation, options);
1340
+ var inspectController = (config, read) => controllerOperation(config, "inspect", { read });
1341
+ var explicitDownload = "use an explicit download";
1342
+ var loginCommandSchema = z5.discriminatedUnion("command", [
1343
+ z5.strictObject({ command: z5.literal("state") }),
1344
+ z5.strictObject({
1345
+ command: z5.literal("read"),
1346
+ expressions: z5.array(z5.string().nonempty()).nonempty()
1347
+ }),
1348
+ z5.strictObject({
1349
+ command: z5.enum(["write", "force"]),
1350
+ values: z5.record(z5.string().nonempty(), z5.string().nonempty())
1351
+ }),
1352
+ z5.strictObject({
1353
+ command: z5.literal("unforce"),
1354
+ expressions: z5.array(z5.string().nonempty()),
1355
+ restore: z5.boolean()
1356
+ }),
1357
+ z5.strictObject({ command: z5.enum(["start", "stop"]) }),
1358
+ z5.strictObject({ command: z5.literal("reset"), kind: z5.enum(["Warm", "Cold"]) })
1359
+ ]);
1360
+ var loginResultSchema = z5.union([
1361
+ z5.strictObject({ values: z5.record(z5.string(), z5.string()) }),
1362
+ z5.strictObject({ state: z5.string(), forced: z5.array(z5.string()) })
1363
+ ]);
1364
+ var loginReplySchema = z5.discriminatedUnion("ok", [
1365
+ z5.strictObject({ ok: z5.literal(true), result: z5.unknown() }),
1366
+ z5.strictObject({ ok: z5.literal(false), error: z5.string() })
1367
+ ]);
1368
+ var loginPort = 4851;
1369
+ var docker = (args, timeout) => execute4("docker", args, { timeout }).then(({ stdout }) => stdout.trim());
1370
+ var isRunning = async (name, timeout) => await docker(["container", "ls", "--quiet", "--filter", `name=^${name}$`], timeout) !== "";
1371
+ var attachLogin = async (name, timeout) => {
1372
+ const published = await docker(["port", name, String(loginPort)], timeout);
1373
+ const url = `http://${published.split(`
1374
+ `)[0]}`;
1375
+ const call = async (init) => {
1376
+ const reply = loginReplySchema.parse(await (await fetch(url, init)).json());
1377
+ if (!reply.ok)
1378
+ throw new Error(reply.error);
1379
+ return reply.result;
1380
+ };
1381
+ return {
1382
+ name,
1383
+ ready: () => fetch(url).then((response) => response.ok).catch(() => false),
1384
+ send: async (command) => loginResultSchema.parse(await call({
1385
+ method: "POST",
1386
+ headers: { "Content-Type": "application/json" },
1387
+ body: JSON.stringify(command)
1388
+ })),
1389
+ close: async () => {
1390
+ await call({ method: "DELETE" });
1391
+ await docker(["wait", name], timeout);
1392
+ await docker(["rm", name], timeout);
1393
+ }
1394
+ };
1395
+ };
1396
+ var findControllerLogin = async (name) => await isRunning(name, 15000) ? attachLogin(name, 15000) : undefined;
1397
+ var openControllerLogin = async (config, name) => {
1398
+ const { environmentNames, imageId, target } = await controllerConnection(config);
1399
+ const timeout = config.timeouts.hostCommand;
1400
+ await docker(["rm", "--force", name], timeout);
1401
+ const requestDirectory = resolve8(config.outDir, ".codesys/login-request");
1402
+ await mkdir5(requestDirectory, { recursive: true });
1403
+ await docker([
1404
+ "run",
1405
+ "--detach",
1406
+ "--name",
1407
+ name,
1408
+ "--publish",
1409
+ `127.0.0.1::${loginPort}`,
1410
+ ...await prepareContainerRun(config, requestDirectory, {
1411
+ network: target.network,
1412
+ environment: environmentNames,
1413
+ sources: false
1414
+ }),
1415
+ imageId,
1416
+ "login",
1417
+ "--request",
1418
+ "/input/config.json",
1419
+ "--image-id",
1420
+ imageId,
1421
+ "--port",
1422
+ String(loginPort)
1423
+ ], timeout);
1424
+ const deadline = Date.now() + config.timeouts.startup + config.timeouts.build;
1425
+ while (Date.now() < deadline) {
1426
+ if (!await isRunning(name, timeout)) {
1427
+ const logs = await execute4("docker", ["logs", "--tail", "40", name], { timeout });
1428
+ throw new Error(`The controller login failed:
1429
+ ${logs.stdout}${logs.stderr}`);
1430
+ }
1431
+ const login = await attachLogin(name, timeout);
1432
+ if (await login.ready())
1433
+ return login;
1434
+ await delay2(2000);
1435
+ }
1436
+ await docker(["rm", "--force", name], timeout);
1437
+ throw new Error("The controller login did not open in time");
1438
+ };
1439
+ // packages/build/src/run.ts
1440
+ import { execFile as execFile5 } from "node:child_process";
1441
+ import { mkdtemp as mkdtemp4, readFile as readFile7, rm as rm6, writeFile as writeFile5 } from "node:fs/promises";
1442
+ import { tmpdir as tmpdir4 } from "node:os";
1443
+ import { basename as basename4, dirname as dirname2, join } from "node:path";
1444
+ import { setTimeout as delay3 } from "node:timers/promises";
1445
+ import { promisify as promisify5 } from "node:util";
1446
+ import { z as z6 } from "zod";
1447
+
1448
+ // packages/build/src/runtime-status.ts
1449
+ var getApplicationStatus = (log, application, startedAt) => {
1450
+ const event = log.split(/\r?\n/).reverse().find((line) => {
1451
+ const timestamp = Date.parse(line.split(",")[0] ?? "");
1452
+ if (!Number.isFinite(timestamp) || timestamp < startedAt) {
1453
+ return false;
1454
+ }
1455
+ return line.includes(`<app>${application}</app>`) && /\b(started|stopped|exception|corrupt|failed|denied)\b/i.test(line) && !line.includes("load started");
1456
+ });
1457
+ if (!event) {
1458
+ return "pending";
1459
+ }
1460
+ return event.includes(`Application [<app>${application}</app>] started`) ? "running" : "failed";
1461
+ };
1462
+
1463
+ // packages/build/src/run.ts
1464
+ var execute5 = promisify5(execFile5);
1465
+ var stateSchema = z6.object({
1466
+ Running: z6.boolean(),
1467
+ StartedAt: z6.iso.datetime({ offset: true })
1468
+ });
1469
+ var service = "plc";
1470
+ var runtimeProject = (name) => name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
1471
+ var runtimeDefinition = (name) => ({
1472
+ services: {
1473
+ [service]: {
1474
+ image: runtimeImage,
1475
+ platform: "linux/amd64",
1476
+ hostname: `${runtimeProject(name)}-${service}`,
1477
+ init: true,
1478
+ ports: ["127.0.0.1:4840:4840", "127.0.0.1:11740:11740"],
1479
+ cap_add: ["SYS_NICE", "IPC_LOCK"],
1480
+ volumes: ["control-conf:/conf/codesyscontrol", "control-data:/data/codesyscontrol"],
1481
+ restart: "no"
1482
+ }
1483
+ },
1484
+ volumes: { "control-conf": {}, "control-data": {} }
1485
+ });
1486
+ var runProject = async (config, name) => withWorkerLock(config, async () => {
1487
+ const build = await readBuild(config.outDir).catch((cause) => {
1488
+ throw new Error(`No complete build in ${config.outDir}. Run tsplc-build build first.`, {
1489
+ cause
1490
+ });
1491
+ });
1492
+ const definitionDirectory = await mkdtemp4(join(tmpdir4(), "tsplc-runtime-"));
1493
+ try {
1494
+ const composeFile = join(definitionDirectory, "compose.yaml");
1495
+ await writeFile5(composeFile, JSON.stringify(runtimeDefinition(name)));
1496
+ const docker = (...args) => execute5("docker", args, {
1497
+ timeout: config.timeouts.runtime,
1498
+ maxBuffer: 8 * 1024 * 1024
1499
+ });
1500
+ const compose = (...args) => docker("compose", "--project-name", runtimeProject(name), "--file", composeFile, ...args);
1501
+ const installer = await readFile7(new URL("../runtime/install.sh", import.meta.url), "utf8");
1502
+ console.log(`Installing ${build.application} in Docker service ${service}`);
1503
+ await compose("stop", service);
1504
+ await compose("run", "--rm", "--no-deps", "-T", "--volume", `${dirname2(build.output)}:/codesys-build:ro`, "--entrypoint", "/bin/sh", service, "-c", installer, "codesys-build-install", build.application, basename4(build.output, ".app"));
1505
+ await compose("up", "--detach", "--no-deps", service);
1506
+ const containers = (await compose("ps", "--all", "--quiet", service)).stdout.trim().split(/\s+/).filter(Boolean);
1507
+ if (containers.length !== 1 || !containers[0]) {
1508
+ throw new Error(`Expected one container for Compose service ${service}`);
1509
+ }
1510
+ const container = containers[0];
1511
+ const deadline = Date.now() + config.timeouts.runtime;
1512
+ while (true) {
1513
+ const state = stateSchema.parse(JSON.parse((await docker("inspect", "--format", "{{json .State}}", container)).stdout));
1514
+ if (!state.Running) {
1515
+ throw new Error(`Docker service ${service} exited before the application started. Check docker logs ${container}.`);
1516
+ }
1517
+ const { stdout: log } = await docker("exec", container, "/bin/sh", "-c", "if [ -f /data/codesyscontrol/codesyscontrol.log ]; then cat /data/codesyscontrol/codesyscontrol.log; fi");
1518
+ const status = getApplicationStatus(log, build.application, Date.parse(state.StartedAt));
1519
+ if (status === "running") {
1520
+ console.log(`${build.application} is running in ${service}`);
1521
+ return { ...build, container };
1522
+ }
1523
+ if (status === "failed" || Date.now() >= deadline) {
1524
+ throw new Error(`CODESYS did not start ${build.application}. Runtime log:
1525
+ ${log.split(`
1526
+ `).slice(-25).join(`
1527
+ `)}`);
1528
+ }
1529
+ await delay3(250);
1530
+ }
1531
+ } finally {
1532
+ await rm6(definitionDirectory, { force: true, recursive: true });
1533
+ }
1534
+ });
1535
+ // packages/build/src/libraries.ts
1536
+ import { createHash as createHash3, randomUUID as randomUUID3 } from "node:crypto";
1537
+ import { mkdir as mkdir6, readFile as readFile8, rename as rename3, writeFile as writeFile6 } from "node:fs/promises";
1538
+ import { basename as basename5, relative as relative2, resolve as resolve9 } from "node:path";
1539
+ var libraryRegistry = "https://store-archive.codesys.com/CODESYSLibs";
1540
+ var libraryFile = /\.(?:compiled-library(?:-v\d+)?|library)$/i;
1541
+ var hash = (data) => createHash3("sha256").update(data).digest("hex");
1542
+ var component = (name) => {
1543
+ if (!name.trim() || /[/\\\0\r\n]/.test(name) || name === "." || name === "..")
1544
+ throw new Error(`Invalid library registry component: ${name}.`);
1545
+ return encodeURIComponent(name);
1546
+ };
1547
+ async function response(url, fetcher) {
1548
+ const result = await fetcher(url, { signal: AbortSignal.timeout(30000) });
1549
+ if (!result.ok)
1550
+ throw new Error(`Library registry ${result.status}: ${url}`);
1551
+ return result;
1552
+ }
1553
+ async function libraryIndex(path = ["System"], fetcher = fetch) {
1554
+ const url = `${libraryRegistry}/${path.map(component).join("/")}/index`;
1555
+ const result = await response(url, fetcher);
1556
+ const text = await result.text();
1557
+ if (text.length > 4000000 || /<html/i.test(text))
1558
+ throw new Error(`Invalid library registry index: ${url}`);
1559
+ return text.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
1560
+ }
1561
+ async function validateLibrary(title, version, company = "System", fetcher = fetch) {
1562
+ if (!(await libraryIndex([company], fetcher)).includes(title))
1563
+ throw new Error(`Unknown CODESYS library ${company}/${title}. Names are case sensitive.`);
1564
+ const versions = await libraryIndex([company, title], fetcher);
1565
+ if (!versions.includes(version))
1566
+ throw new Error(`Unknown version ${version} for ${title}. Available: ${versions.join(", ")}`);
1567
+ const filenames = (await libraryIndex([company, title, version], fetcher)).filter((name) => libraryFile.test(name));
1568
+ if (filenames.length !== 1)
1569
+ throw new Error(`Expected one library payload for ${title} ${version}, received ${filenames.length}.`);
1570
+ return {
1571
+ filename: filenames[0],
1572
+ url: `${libraryRegistry}/${[company, title, version, filenames[0]].map(component).join("/")}`
1573
+ };
1574
+ }
1575
+ async function installLibraries(dependencies, root, options = {}) {
1576
+ const file = resolve9(root, "tsplc.lock.json"), store = resolve9(root, ".tsplc/libraries");
1577
+ let previous = { version: 1, dependencies: {} };
1578
+ try {
1579
+ previous = JSON.parse(await readFile8(file, "utf8"));
1580
+ } catch (cause) {
1581
+ if (!(cause && typeof cause === "object" && ("code" in cause) && cause.code === "ENOENT"))
1582
+ throw cause;
1583
+ }
1584
+ if (previous.version !== 1 || !previous.dependencies || typeof previous.dependencies !== "object")
1585
+ throw new Error("Invalid tsplc.lock.json.");
1586
+ const installed = {};
1587
+ const fetcher = options.fetcher ?? fetch;
1588
+ await mkdir6(store, { recursive: true });
1589
+ for (const [title, dependency] of Object.entries(dependencies)) {
1590
+ component(title);
1591
+ const settings = typeof dependency === "string" ? { version: dependency } : dependency;
1592
+ const locked = previous.dependencies[title];
1593
+ const prior = locked ? { ...locked, path: resolve9(root, locked.path) } : undefined;
1594
+ const same = prior !== undefined && prior.namespace === settings.namespace && ("path" in settings ? prior.url === undefined && prior.path === resolve9(root, settings.path) : prior.version === settings.version && prior.company === (settings.company ?? "System"));
1595
+ if (same) {
1596
+ try {
1597
+ if (hash(await readFile8(prior.path)) === prior.sha256) {
1598
+ installed[title] = prior;
1599
+ continue;
1600
+ }
1601
+ } catch {}
1602
+ if (options.frozen)
1603
+ throw new Error(`Locked library ${title} is missing or changed.`);
1604
+ } else if (options.frozen)
1605
+ throw new Error(`Dependency ${title} differs from tsplc.lock.json.`);
1606
+ if ("path" in settings) {
1607
+ const path = resolve9(root, settings.path);
1608
+ if (!libraryFile.test(path))
1609
+ throw new Error(`${title}: expected a .library or .compiled-library file.`);
1610
+ installed[title] = {
1611
+ title,
1612
+ path,
1613
+ sha256: hash(await readFile8(path)),
1614
+ ...settings.namespace ? { namespace: settings.namespace } : {}
1615
+ };
1616
+ } else {
1617
+ const company = settings.company ?? "System";
1618
+ const payload = await validateLibrary(title, settings.version, company, fetcher);
1619
+ const result = await response(payload.url, fetcher);
1620
+ const data = new Uint8Array(await result.arrayBuffer());
1621
+ if (!data.length || data.length > 256 * 1024 * 1024)
1622
+ throw new Error(`${title}: invalid library payload size.`);
1623
+ const sha256 = hash(data);
1624
+ if (same && prior.sha256 !== sha256)
1625
+ throw new Error(`Registry payload changed for locked library ${title} ${settings.version}.`);
1626
+ const path = resolve9(store, `${sha256}-${basename5(payload.filename)}`), temp = `${path}.${randomUUID3()}.tmp`;
1627
+ await writeFile6(temp, data, { flag: "wx" });
1628
+ await rename3(temp, path);
1629
+ installed[title] = {
1630
+ title,
1631
+ company,
1632
+ version: settings.version,
1633
+ path,
1634
+ sha256,
1635
+ url: payload.url,
1636
+ ...settings.namespace ? { namespace: settings.namespace } : {}
1637
+ };
1638
+ }
1639
+ }
1640
+ if (options.frozen && Object.keys(previous.dependencies).some((name) => !Object.hasOwn(dependencies, name)))
1641
+ throw new Error("Removed dependencies differ from tsplc.lock.json.");
1642
+ if (!options.frozen) {
1643
+ const temp = `${file}.${randomUUID3()}.tmp`;
1644
+ const portable = Object.fromEntries(Object.entries(installed).map(([title, library]) => [
1645
+ title,
1646
+ { ...library, path: relative2(root, library.path).replaceAll("\\", "/") }
1647
+ ]));
1648
+ await writeFile6(temp, JSON.stringify({ version: 1, dependencies: portable }, null, 2) + `
1649
+ `);
1650
+ await rename3(temp, file);
1651
+ }
1652
+ return Object.values(installed);
1653
+ }
1654
+ export {
1655
+ CodesysCompilerError,
1656
+ StSyntaxError,
1657
+ buildProject,
1658
+ configSchema,
1659
+ configureDocker,
1660
+ configureGateway,
1661
+ configureRuntime,
1662
+ defineBuildConfig as defineConfig,
1663
+ explicitDownload,
1664
+ findControllerLogin,
1665
+ gatewayImage,
1666
+ inspectController,
1667
+ installLibraries,
1668
+ libraryIndex,
1669
+ loadConfig,
1670
+ loadSources,
1671
+ loginCommandSchema,
1672
+ loginResultSchema,
1673
+ openControllerLogin,
1674
+ parseSource,
1675
+ readBuild,
1676
+ resolveConfig,
1677
+ runProject,
1678
+ updateController,
1679
+ validateLibrary
1680
+ };
1681
+
1682
+ //# debugId=B75F268E384DC61264756E2164756E21
1683
+ //# sourceMappingURL=index.js.map