@tsplc/compiler 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.
- package/LICENSE +21 -0
- package/README.md +16 -2
- package/backends/codesys/config.ts +54 -0
- package/backends/codesys/emit.ts +1063 -0
- package/backends/codesys/engineering.ts +88 -0
- package/backends/codesys/http.ts +598 -0
- package/backends/codesys/index.ts +12 -0
- package/backends/codesys/io-paths.ts +103 -0
- package/backends/codesys/names.ts +76 -0
- package/backends/codesys/numeric.ts +356 -0
- package/backends/codesys/plan.ts +512 -0
- package/backends/codesys/publication.ts +59 -0
- package/backends/codesys/storage.ts +63 -0
- package/backends/codesys/task-access.ts +118 -0
- package/backends/codesys/types.ts +178 -0
- package/backends/codesys/vendor.ts +108 -0
- package/dist/cli.js +1396 -0
- package/dist/cli.js.map +16 -0
- package/dist/clock.js +8 -0
- package/dist/clock.js.map +10 -0
- package/dist/codesys.js +2587 -0
- package/dist/codesys.js.map +22 -0
- package/dist/elaborate-capture.js +118 -0
- package/dist/elaborate-capture.js.map +10 -0
- package/dist/index.js +6859 -0
- package/dist/index.js.map +25 -0
- package/dist/ir.js +1888 -0
- package/dist/ir.js.map +22 -0
- package/dist/math.js +24 -0
- package/dist/math.js.map +10 -0
- package/dist/project.js +429 -0
- package/dist/project.js.map +11 -0
- package/ir/aggregates.ts +333 -0
- package/ir/allocation.ts +74 -0
- package/ir/core-model.ts +116 -0
- package/ir/core.ts +63 -0
- package/ir/declarations.ts +201 -0
- package/ir/diagnostics.ts +43 -0
- package/ir/domains.ts +96 -0
- package/ir/index.ts +7 -0
- package/ir/link.ts +176 -0
- package/ir/model.ts +268 -0
- package/ir/normalization-error.ts +10 -0
- package/ir/routine.ts +826 -0
- package/ir/scalars.ts +675 -0
- package/ir/types.ts +70 -0
- package/package.json +106 -4
- package/project/artifacts.ts +277 -0
- package/project/cli.ts +305 -0
- package/project/index.ts +1 -0
- package/project/plant.ts +437 -0
- package/project/project.ts +312 -0
- package/project/server.ts +83 -0
- package/src/frontend/async.ts +1060 -0
- package/src/frontend/blocks.ts +419 -0
- package/src/frontend/configuration.ts +167 -0
- package/src/frontend/const-functions.ts +153 -0
- package/src/frontend/cooperative.ts +75 -0
- package/src/frontend/elaborate/capture.ts +138 -0
- package/src/frontend/elaborate/graph.ts +94 -0
- package/src/frontend/elaborate/index.ts +302 -0
- package/src/frontend/elaborate/layouts.ts +390 -0
- package/src/frontend/elaborate/model.ts +177 -0
- package/src/frontend/elaborate/specialize.ts +2280 -0
- package/src/frontend/exposure.ts +147 -0
- package/src/frontend/module-graph.ts +63 -0
- package/src/frontend/promises.ts +751 -0
- package/src/frontend/services.ts +435 -0
- package/src/frontend/types.ts +578 -0
- package/src/frontend/typescript.ts +2442 -0
- package/src/index.ts +7 -0
- package/stdlib/clock.ts +2 -0
- package/stdlib/math.ts +19 -0
- package/types/async.d.ts +81 -0
- package/types/clock.d.ts +4 -0
- package/types/config.d.ts +2 -0
- package/types/data-types.d.ts +31 -0
- package/types/globals.d.ts +40 -0
- package/types/index.d.ts +111 -0
- package/types/plc.d.ts +5 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1396 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// packages/compiler/project/server.ts
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { mkdtemp, mkdir, rm, rename } from "node:fs/promises";
|
|
7
|
+
import { dirname, resolve, join } from "node:path";
|
|
8
|
+
import { createRequire } from "node:module";
|
|
9
|
+
import ts from "typescript";
|
|
10
|
+
import { typeCheckOnly } from "@tsplc/compiler";
|
|
11
|
+
var resolveServerModule = (root, name) => name.startsWith(".") || name.startsWith("/") || existsSync(resolve(root, name)) ? resolve(root, name) : createRequire(join(root, "package.json")).resolve(name);
|
|
12
|
+
var kebabCase = (name) => name.replace(/([a-z0-9])([A-Z])|([A-Z])([A-Z][a-z])/g, "$1$3-$2$4").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase();
|
|
13
|
+
var buildServer = async (config, root) => {
|
|
14
|
+
const settings = config.server;
|
|
15
|
+
if (!settings?.runtime)
|
|
16
|
+
throw new Error("Configure server.runtime to build an executable.");
|
|
17
|
+
const runtime = settings.runtime;
|
|
18
|
+
const entry = resolveServerModule(root, runtime.entry);
|
|
19
|
+
const windows = runtime.target ? runtime.target.includes("windows") : process.platform === "win32";
|
|
20
|
+
const outFile = resolve(root, "build/server", `${kebabCase(config.name)}-services${windows ? ".exe" : ""}`);
|
|
21
|
+
const tsconfig = ts.findConfigFile(dirname(entry), ts.sys.fileExists);
|
|
22
|
+
if (!tsconfig)
|
|
23
|
+
throw new Error("Server entry requires a tsconfig.json.");
|
|
24
|
+
const read = ts.readConfigFile(tsconfig, ts.sys.readFile);
|
|
25
|
+
if (read.error)
|
|
26
|
+
throw new Error(ts.flattenDiagnosticMessageText(read.error.messageText, `
|
|
27
|
+
`));
|
|
28
|
+
const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, dirname(tsconfig));
|
|
29
|
+
const program = ts.createProgram({
|
|
30
|
+
rootNames: [entry, ...parsed.fileNames.filter((file) => /\.d\.[cm]?ts$/.test(file))],
|
|
31
|
+
options: { ...parsed.options, ...typeCheckOnly },
|
|
32
|
+
projectReferences: parsed.projectReferences
|
|
33
|
+
});
|
|
34
|
+
const diagnostics = [...parsed.errors, ...ts.getPreEmitDiagnostics(program)];
|
|
35
|
+
if (diagnostics.length)
|
|
36
|
+
throw new Error(ts.formatDiagnosticsWithColorAndContext(diagnostics, {
|
|
37
|
+
getCanonicalFileName: (path) => path,
|
|
38
|
+
getCurrentDirectory: () => root,
|
|
39
|
+
getNewLine: () => `
|
|
40
|
+
`
|
|
41
|
+
}));
|
|
42
|
+
await mkdir(dirname(outFile), { recursive: true });
|
|
43
|
+
const staging = await mkdtemp(join(dirname(outFile), ".tsplc-server-"));
|
|
44
|
+
try {
|
|
45
|
+
const binary = join(staging, "server");
|
|
46
|
+
const result = await Bun.build({
|
|
47
|
+
entrypoints: [entry],
|
|
48
|
+
env: "disable",
|
|
49
|
+
compile: {
|
|
50
|
+
outfile: binary,
|
|
51
|
+
...runtime.target ? { target: runtime.target } : {},
|
|
52
|
+
autoloadDotenv: false,
|
|
53
|
+
autoloadBunfig: false,
|
|
54
|
+
autoloadTsconfig: false,
|
|
55
|
+
autoloadPackageJson: false
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
if (!result.success)
|
|
59
|
+
throw new AggregateError(result.logs, "Server executable build failed.");
|
|
60
|
+
await rename(windows ? `${binary}.exe` : binary, outFile);
|
|
61
|
+
return {
|
|
62
|
+
server: {
|
|
63
|
+
executable: outFile,
|
|
64
|
+
target: runtime.target ?? `bun-${process.platform}-${process.arch}`
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
} finally {
|
|
68
|
+
await rm(staging, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// packages/compiler/project/cli.ts
|
|
73
|
+
import { parseArgs } from "util";
|
|
74
|
+
import { resolve as resolve6 } from "path";
|
|
75
|
+
import {
|
|
76
|
+
buildTargetFor,
|
|
77
|
+
controllerFor as controllerFor2,
|
|
78
|
+
loadTsplcConfig as loadTsplcConfig3,
|
|
79
|
+
rawBuildConfig,
|
|
80
|
+
simulationConfig
|
|
81
|
+
} from "@tsplc/config";
|
|
82
|
+
import {
|
|
83
|
+
buildProject as buildSt,
|
|
84
|
+
configureDocker,
|
|
85
|
+
configureRuntime,
|
|
86
|
+
inspectController,
|
|
87
|
+
installLibraries as installLibraries3,
|
|
88
|
+
loadSources,
|
|
89
|
+
resolveConfig,
|
|
90
|
+
runProject,
|
|
91
|
+
updateController
|
|
92
|
+
} from "@tsplc/build";
|
|
93
|
+
import { checkHostConfig as checkHostConfig3 } from "@tsplc/compiler";
|
|
94
|
+
import { formatDiagnostic, hasErrors as hasErrors4 } from "@tsplc/compiler/ir";
|
|
95
|
+
import { deviceTree as deviceTree3 } from "@tsplc/typegen";
|
|
96
|
+
import { loadHardware as loadHardware3, resolveIo as resolveIo3 } from "@tsplc/typegen/hardware";
|
|
97
|
+
import { generateProject as generateProject2 } from "@tsplc/typegen";
|
|
98
|
+
|
|
99
|
+
// packages/compiler/project/plant.ts
|
|
100
|
+
import { basename, relative, resolve as resolve4, sep } from "node:path";
|
|
101
|
+
import { createInterface } from "node:readline/promises";
|
|
102
|
+
import { pathToFileURL } from "node:url";
|
|
103
|
+
import {
|
|
104
|
+
AgentOperationError,
|
|
105
|
+
createAgentClient
|
|
106
|
+
} from "@tsplc/agent/client";
|
|
107
|
+
import { controllerFor } from "@tsplc/config";
|
|
108
|
+
import { hasErrors as hasErrors2 } from "@tsplc/compiler/ir";
|
|
109
|
+
import { generateProject } from "@tsplc/typegen";
|
|
110
|
+
import { z } from "zod";
|
|
111
|
+
|
|
112
|
+
// packages/compiler/project/project.ts
|
|
113
|
+
import { copyFile, mkdir as mkdir3, mkdtemp as mkdtemp2, realpath, rm as rm3, writeFile as writeFile2 } from "node:fs/promises";
|
|
114
|
+
import { dirname as dirname3, join as join3, resolve as resolve2 } from "node:path";
|
|
115
|
+
import {
|
|
116
|
+
buildCodesys,
|
|
117
|
+
codesysBuildConfig,
|
|
118
|
+
diagnoseCodesys,
|
|
119
|
+
emitCodesys,
|
|
120
|
+
ioTargets,
|
|
121
|
+
publicationSelectors,
|
|
122
|
+
planCodesys
|
|
123
|
+
} from "@tsplc/compiler/codesys";
|
|
124
|
+
import { analyze, checkHostConfig } from "@tsplc/compiler";
|
|
125
|
+
import {
|
|
126
|
+
compilerConfig,
|
|
127
|
+
loadTsplcConfig
|
|
128
|
+
} from "@tsplc/config";
|
|
129
|
+
import { deviceTree, loadHardware, resolveIo } from "@tsplc/typegen";
|
|
130
|
+
import { installLibraries } from "@tsplc/build";
|
|
131
|
+
import { hasErrors, normalizeModule } from "@tsplc/compiler/ir";
|
|
132
|
+
|
|
133
|
+
// packages/compiler/project/artifacts.ts
|
|
134
|
+
import { createHash } from "node:crypto";
|
|
135
|
+
import { lstat, mkdir as mkdir2, readdir, readFile, rename as rename2, rm as rm2, writeFile } from "node:fs/promises";
|
|
136
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
137
|
+
var missing = (cause) => cause instanceof Error && ("code" in cause) && cause.code === "ENOENT";
|
|
138
|
+
var digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
139
|
+
function manifest(value, configFile) {
|
|
140
|
+
if (typeof value !== "object" || value === null || !("kind" in value) || value.kind !== "tsplc-artifacts" || !("version" in value) || value.version !== 1 || !("configFile" in value) || value.configFile !== configFile || !("files" in value) || !Array.isArray(value.files)) {
|
|
141
|
+
throw new Error("Output directory has no matching tsPLC ownership manifest.");
|
|
142
|
+
}
|
|
143
|
+
const files = value.files.map((item) => {
|
|
144
|
+
if (typeof item !== "object" || item === null || !("path" in item) || typeof item.path !== "string" || !/^[A-Za-z0-9_ .-]+(?:\/[A-Za-z0-9_ .-]+)*$/.test(item.path) || item.path.split("/").some((part) => part === "." || part === "..") || ["manifest.json", "receipt.json"].includes(item.path) || !("sha256" in item) || typeof item.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(item.sha256)) {
|
|
145
|
+
throw new Error("Invalid generated file ownership entry.");
|
|
146
|
+
}
|
|
147
|
+
return { path: item.path, sha256: item.sha256 };
|
|
148
|
+
});
|
|
149
|
+
if (new Set(files.map((item) => item.path)).size !== files.length) {
|
|
150
|
+
throw new Error("Duplicate generated file ownership entry.");
|
|
151
|
+
}
|
|
152
|
+
return { kind: "tsplc-artifacts", version: 1, configFile, files };
|
|
153
|
+
}
|
|
154
|
+
async function directory(path) {
|
|
155
|
+
try {
|
|
156
|
+
const entry = await lstat(path);
|
|
157
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) {
|
|
158
|
+
throw new Error(`Generated directory must be a real directory: ${path}`);
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
} catch (cause) {
|
|
162
|
+
if (missing(cause)) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
throw cause;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function filesIn(path, prefix = "") {
|
|
169
|
+
const result = [];
|
|
170
|
+
for (const entry of await readdir(path, { withFileTypes: true })) {
|
|
171
|
+
const name = prefix + entry.name;
|
|
172
|
+
if (entry.isDirectory() && !entry.isSymbolicLink()) {
|
|
173
|
+
result.push(...await filesIn(join2(path, entry.name), `${name}/`));
|
|
174
|
+
} else {
|
|
175
|
+
result.push(name);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
async function engineeringDirectory(path, configFile) {
|
|
181
|
+
const marker = join2(path, "ownership.json");
|
|
182
|
+
const exists = await directory(path);
|
|
183
|
+
if (!exists || !(await readdir(path)).length) {
|
|
184
|
+
await mkdir2(path, { recursive: true });
|
|
185
|
+
await writeFile(marker, `${JSON.stringify({ kind: "tsplc-engineering", version: 1, configFile }, null, 2)}
|
|
186
|
+
`, { flag: "wx" });
|
|
187
|
+
} else {
|
|
188
|
+
const entry = await lstat(marker).catch((cause) => {
|
|
189
|
+
if (missing(cause)) {
|
|
190
|
+
throw new Error(`Engineering cache has no matching tsPLC ownership marker: ${path}`);
|
|
191
|
+
}
|
|
192
|
+
throw cause;
|
|
193
|
+
});
|
|
194
|
+
if (!entry.isFile() || entry.isSymbolicLink()) {
|
|
195
|
+
throw new Error("Engineering ownership marker must be a real file.");
|
|
196
|
+
}
|
|
197
|
+
const value = JSON.parse(await readFile(marker, "utf8"));
|
|
198
|
+
if (value?.kind !== "tsplc-engineering" || value.version !== 1 || value.configFile !== configFile) {
|
|
199
|
+
throw new Error(`Engineering cache has no matching tsPLC ownership marker: ${path}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const inspect = async (directory) => {
|
|
203
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
204
|
+
const child = join2(directory, entry.name);
|
|
205
|
+
if (entry.isSymbolicLink()) {
|
|
206
|
+
throw new Error(`Engineering cache must not contain symlinks: ${child}`);
|
|
207
|
+
}
|
|
208
|
+
if (entry.isDirectory()) {
|
|
209
|
+
await inspect(child);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
await inspect(path);
|
|
214
|
+
}
|
|
215
|
+
async function owned(configFile, withdrawReceipt = false) {
|
|
216
|
+
const root = join2(dirname2(configFile), ".tsplc");
|
|
217
|
+
const output = join2(root, "artifacts");
|
|
218
|
+
if (!await directory(root) || !await directory(output)) {
|
|
219
|
+
return { root, output, unknown: [] };
|
|
220
|
+
}
|
|
221
|
+
const manifestPath = join2(output, "manifest.json");
|
|
222
|
+
const manifestEntry = await lstat(manifestPath);
|
|
223
|
+
if (!manifestEntry.isFile() || manifestEntry.isSymbolicLink()) {
|
|
224
|
+
throw new Error("Ownership manifest must be a real file.");
|
|
225
|
+
}
|
|
226
|
+
const ownership = manifest(JSON.parse(await readFile(manifestPath, "utf8")), configFile);
|
|
227
|
+
const receipt = join2(output, "receipt.json");
|
|
228
|
+
try {
|
|
229
|
+
const entry = await lstat(receipt);
|
|
230
|
+
if (!entry.isFile() || entry.isSymbolicLink()) {
|
|
231
|
+
throw new Error("Build receipt must be a real file.");
|
|
232
|
+
}
|
|
233
|
+
const value = JSON.parse(await readFile(receipt, "utf8"));
|
|
234
|
+
if (value?.kind !== "tsplc-build" || value.version !== 1 || value.configFile !== configFile || !["emitted", "vendor-compiled"].includes(value.status)) {
|
|
235
|
+
throw new Error("Build receipt is not owned by this configuration.");
|
|
236
|
+
}
|
|
237
|
+
if (withdrawReceipt) {
|
|
238
|
+
await rm2(receipt);
|
|
239
|
+
}
|
|
240
|
+
} catch (cause) {
|
|
241
|
+
if (!missing(cause)) {
|
|
242
|
+
throw cause;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const names = new Set([
|
|
246
|
+
...ownership.files.map((file) => file.path),
|
|
247
|
+
"manifest.json",
|
|
248
|
+
"receipt.json"
|
|
249
|
+
]);
|
|
250
|
+
for (const file of ownership.files) {
|
|
251
|
+
let parent = output;
|
|
252
|
+
for (const part of file.path.split("/").slice(0, -1)) {
|
|
253
|
+
parent = join2(parent, part);
|
|
254
|
+
if (!await directory(parent)) {
|
|
255
|
+
throw new Error(`Missing generated directory: ${parent}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const path = join2(output, file.path);
|
|
259
|
+
const entry = await lstat(path);
|
|
260
|
+
if (!entry.isFile() || entry.isSymbolicLink() || digest(await readFile(path)) !== file.sha256) {
|
|
261
|
+
throw new Error(`Generated file was replaced or edited; preserve it before rebuilding/cleaning: ${path}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
root,
|
|
266
|
+
output,
|
|
267
|
+
ownership,
|
|
268
|
+
unknown: (await filesIn(output)).filter((name) => !names.has(name))
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
async function publishArtifacts(staging, previous, receipt) {
|
|
272
|
+
const configFile = receipt.configFile;
|
|
273
|
+
const generated = (await filesIn(staging)).sort();
|
|
274
|
+
const ownership = { kind: "tsplc-artifacts", version: 1, configFile, files: [] };
|
|
275
|
+
for (const path of generated) {
|
|
276
|
+
ownership.files.push({ path, sha256: digest(await readFile(join2(staging, path))) });
|
|
277
|
+
}
|
|
278
|
+
await writeFile(join2(staging, "manifest.json"), `${JSON.stringify(ownership, null, 2)}
|
|
279
|
+
`);
|
|
280
|
+
await writeFile(join2(staging, "receipt.json"), `${JSON.stringify({ kind: "tsplc-build", version: 1, generatedAt: new Date().toISOString(), ...receipt }, null, 2)}
|
|
281
|
+
`);
|
|
282
|
+
const current = await owned(configFile);
|
|
283
|
+
if (JSON.stringify(current.ownership) !== JSON.stringify(previous.ownership)) {
|
|
284
|
+
throw new Error("Output ownership changed during the build; preserve it before rebuilding.");
|
|
285
|
+
}
|
|
286
|
+
if (current.unknown.length) {
|
|
287
|
+
throw new Error(`Output contains unrelated files: ${current.unknown.join(", ")}`);
|
|
288
|
+
}
|
|
289
|
+
const backup = `${staging}-previous`;
|
|
290
|
+
if (previous.ownership) {
|
|
291
|
+
await rename2(previous.output, backup);
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
await rename2(staging, previous.output);
|
|
295
|
+
} catch (cause) {
|
|
296
|
+
if (previous.ownership) {
|
|
297
|
+
await rename2(backup, previous.output);
|
|
298
|
+
}
|
|
299
|
+
throw cause;
|
|
300
|
+
}
|
|
301
|
+
if (previous.ownership) {
|
|
302
|
+
await rm2(backup, { recursive: true });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async function cleanArtifacts(configFile) {
|
|
306
|
+
const generated = await owned(configFile);
|
|
307
|
+
if (!generated.ownership) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
for (const entry of generated.ownership.files) {
|
|
311
|
+
await rm2(join2(generated.output, entry.path));
|
|
312
|
+
}
|
|
313
|
+
await rm2(join2(generated.output, "receipt.json"), { force: true });
|
|
314
|
+
await rm2(join2(generated.output, "manifest.json"));
|
|
315
|
+
const removeEmpty = async (path) => {
|
|
316
|
+
for (const entry of await readdir(path, { withFileTypes: true })) {
|
|
317
|
+
if (entry.isDirectory() && !entry.isSymbolicLink()) {
|
|
318
|
+
await removeEmpty(join2(path, entry.name));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (!(await readdir(path)).length) {
|
|
322
|
+
await rm2(path, { recursive: true });
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
await removeEmpty(generated.output);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// packages/compiler/project/project.ts
|
|
329
|
+
var failure = (origin, code, cause) => ({
|
|
330
|
+
origin,
|
|
331
|
+
code,
|
|
332
|
+
severity: "error",
|
|
333
|
+
message: cause instanceof Error ? cause.message : String(cause)
|
|
334
|
+
});
|
|
335
|
+
async function loadProject(file, loaded, buildTarget) {
|
|
336
|
+
let configFile = resolve2(file);
|
|
337
|
+
try {
|
|
338
|
+
configFile = await realpath(configFile);
|
|
339
|
+
const diagnostics = checkHostConfig(configFile);
|
|
340
|
+
if (hasErrors(diagnostics))
|
|
341
|
+
return { configFile, diagnostics };
|
|
342
|
+
const shared = (loaded ?? await loadTsplcConfig(configFile)).config;
|
|
343
|
+
const root = dirname3(configFile);
|
|
344
|
+
const config = compilerConfig(shared, resolveIo(shared, await loadHardware(shared, root)), buildTarget);
|
|
345
|
+
return {
|
|
346
|
+
configFile,
|
|
347
|
+
shared,
|
|
348
|
+
config: { ...config, ...sourceInput(config, root) },
|
|
349
|
+
diagnostics
|
|
350
|
+
};
|
|
351
|
+
} catch (cause) {
|
|
352
|
+
return { configFile, diagnostics: [failure("config", "PLC5003", cause)] };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function sourceInput(compiler, root) {
|
|
356
|
+
return {
|
|
357
|
+
tsconfig: resolve2(root, compiler.tsconfig),
|
|
358
|
+
programs: Object.fromEntries(Object.entries(compiler.programs).map(([name, entry]) => [
|
|
359
|
+
name,
|
|
360
|
+
{ ...entry, module: resolve2(root, entry.module) }
|
|
361
|
+
]))
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
async function projectTargets(file = "tsplc.config.ts", options = {}) {
|
|
365
|
+
const configFile = await realpath(resolve2(file));
|
|
366
|
+
const { config } = options.loaded ?? await loadTsplcConfig(configFile);
|
|
367
|
+
if (!config.compiler)
|
|
368
|
+
return;
|
|
369
|
+
const source = analyze({
|
|
370
|
+
...sourceInput(config.compiler, dirname3(configFile)),
|
|
371
|
+
services: config.server
|
|
372
|
+
});
|
|
373
|
+
const core = source.module && normalizeModule(source.module).core;
|
|
374
|
+
return core ? { io: ioTargets(core), publication: publicationSelectors(core) } : undefined;
|
|
375
|
+
}
|
|
376
|
+
async function checkProject(file = "tsplc.config.ts", options = {}) {
|
|
377
|
+
const loaded = await loadProject(file, options.loaded, options.buildTarget);
|
|
378
|
+
if (!loaded.config) {
|
|
379
|
+
return loaded;
|
|
380
|
+
}
|
|
381
|
+
const config = loaded.config;
|
|
382
|
+
if (config.target.kind !== "codesys") {
|
|
383
|
+
return {
|
|
384
|
+
...loaded,
|
|
385
|
+
diagnostics: [failure("backend", "PLC7001", `Unsupported target: ${config.target.kind}`)]
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
const source = analyze({
|
|
389
|
+
tsconfig: config.tsconfig,
|
|
390
|
+
programs: config.programs,
|
|
391
|
+
services: loaded.shared?.server
|
|
392
|
+
});
|
|
393
|
+
if (!source.module) {
|
|
394
|
+
return { ...loaded, diagnostics: source.diagnostics };
|
|
395
|
+
}
|
|
396
|
+
const normalized = normalizeModule(source.module);
|
|
397
|
+
if (!normalized.core) {
|
|
398
|
+
return { ...loaded, diagnostics: normalized.diagnostics };
|
|
399
|
+
}
|
|
400
|
+
const tasks = config.tasks.map((task) => ({
|
|
401
|
+
name: task.name,
|
|
402
|
+
intervalNanoseconds: task.schedule.interval.nanoseconds,
|
|
403
|
+
priority: task.priority,
|
|
404
|
+
programs: task.programs
|
|
405
|
+
}));
|
|
406
|
+
const planned = planCodesys(normalized.core, { tasks }, config.target.options);
|
|
407
|
+
return {
|
|
408
|
+
...loaded,
|
|
409
|
+
...planned,
|
|
410
|
+
diagnostics: [
|
|
411
|
+
...loaded.diagnostics,
|
|
412
|
+
...source.diagnostics,
|
|
413
|
+
...normalized.diagnostics,
|
|
414
|
+
...planned.diagnostics
|
|
415
|
+
]
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
async function engineeringInputs(shared, configFile, bundle, frozen) {
|
|
419
|
+
const root = dirname3(configFile);
|
|
420
|
+
const tree = await deviceTree(shared, await loadHardware(shared, root));
|
|
421
|
+
return {
|
|
422
|
+
libraries: await installLibraries(shared.dependencies, root, { frozen }),
|
|
423
|
+
deviceTree: tree && {
|
|
424
|
+
...tree,
|
|
425
|
+
mappings: bundle.ioMappings.map(({ address, variable }) => ({ address, variable }))
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
async function buildProject(file = "tsplc.config.ts", options = {}) {
|
|
430
|
+
let staging;
|
|
431
|
+
try {
|
|
432
|
+
const configFile = await realpath(resolve2(file));
|
|
433
|
+
const previous = await owned(configFile, true);
|
|
434
|
+
if (previous.unknown.length) {
|
|
435
|
+
throw new Error(`Output contains unrelated files: ${previous.unknown.join(", ")}`);
|
|
436
|
+
}
|
|
437
|
+
const checked = await checkProject(configFile, {
|
|
438
|
+
loaded: options.loaded,
|
|
439
|
+
buildTarget: options.buildTarget
|
|
440
|
+
});
|
|
441
|
+
if (!checked.plan || !checked.config) {
|
|
442
|
+
return { diagnostics: checked.diagnostics };
|
|
443
|
+
}
|
|
444
|
+
const bundle = emitCodesys(checked.plan);
|
|
445
|
+
if (hasErrors(bundle.diagnostics)) {
|
|
446
|
+
return { diagnostics: bundle.diagnostics };
|
|
447
|
+
}
|
|
448
|
+
const shared = checked.shared;
|
|
449
|
+
const engineering = shared && (!options.emitOnly || options.forController) ? await engineeringInputs(shared, configFile, bundle, options.frozenLockfile) : undefined;
|
|
450
|
+
const controllerConfig = shared?.build ? codesysBuildConfig(checked.plan, bundle, {
|
|
451
|
+
configFile,
|
|
452
|
+
sourceDir: join3(previous.output, "st"),
|
|
453
|
+
outDir: resolve2(dirname3(configFile), shared.build.outDir),
|
|
454
|
+
controller: options.controller,
|
|
455
|
+
...engineering
|
|
456
|
+
}) : undefined;
|
|
457
|
+
await mkdir3(previous.root, { recursive: true });
|
|
458
|
+
staging = await mkdtemp2(join3(previous.root, "stage-"));
|
|
459
|
+
const artifacts = [
|
|
460
|
+
...bundle.artifacts,
|
|
461
|
+
{ path: "mappings.json", content: `${JSON.stringify(bundle.mappings, null, 2)}
|
|
462
|
+
` },
|
|
463
|
+
{ path: "io-symbols.json", content: `${JSON.stringify(bundle.ioSymbols, null, 2)}
|
|
464
|
+
` },
|
|
465
|
+
{ path: "publication.json", content: `${JSON.stringify(bundle.publication, null, 2)}
|
|
466
|
+
` },
|
|
467
|
+
{ path: "config.json", content: `${JSON.stringify(checked.config, null, 2)}
|
|
468
|
+
` }
|
|
469
|
+
];
|
|
470
|
+
for (const artifact of artifacts) {
|
|
471
|
+
const path = join3(staging, "st", artifact.path);
|
|
472
|
+
await mkdir3(dirname3(path), { recursive: true });
|
|
473
|
+
await writeFile2(path, artifact.content);
|
|
474
|
+
}
|
|
475
|
+
let vendor;
|
|
476
|
+
if (!options.emitOnly) {
|
|
477
|
+
const outDir = shared?.build ? resolve2(dirname3(configFile), shared.build.outDir ?? "build") : join3(previous.root, "engineering");
|
|
478
|
+
await engineeringDirectory(outDir, configFile);
|
|
479
|
+
try {
|
|
480
|
+
vendor = await buildCodesys(checked.plan, bundle, {
|
|
481
|
+
configFile,
|
|
482
|
+
sourceDir: join3(staging, "st"),
|
|
483
|
+
outDir,
|
|
484
|
+
controller: options.controller,
|
|
485
|
+
...engineering
|
|
486
|
+
});
|
|
487
|
+
} catch (cause) {
|
|
488
|
+
return { diagnostics: diagnoseCodesys(cause, bundle) };
|
|
489
|
+
}
|
|
490
|
+
const vendorArtifacts = [
|
|
491
|
+
["Application.project", vendor.project],
|
|
492
|
+
["Application.app", vendor.output],
|
|
493
|
+
["Application.crc", vendor.checksum]
|
|
494
|
+
];
|
|
495
|
+
for (const [name, path] of vendorArtifacts) {
|
|
496
|
+
if (!path)
|
|
497
|
+
continue;
|
|
498
|
+
const destination = join3(staging, "vendor", name);
|
|
499
|
+
await mkdir3(dirname3(destination), { recursive: true });
|
|
500
|
+
await copyFile(path, destination);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const status = vendor ? "vendor-compiled" : "emitted";
|
|
504
|
+
await publishArtifacts(staging, previous, {
|
|
505
|
+
configFile,
|
|
506
|
+
status,
|
|
507
|
+
...vendor && { vendor: { application: vendor.application, tasks: vendor.tasks } }
|
|
508
|
+
});
|
|
509
|
+
staging = undefined;
|
|
510
|
+
return {
|
|
511
|
+
directory: previous.output,
|
|
512
|
+
status,
|
|
513
|
+
...controllerConfig && { controllerConfig },
|
|
514
|
+
diagnostics: [...checked.diagnostics, ...bundle.diagnostics]
|
|
515
|
+
};
|
|
516
|
+
} catch (cause) {
|
|
517
|
+
return { diagnostics: [failure("config", "PLC5004", cause)] };
|
|
518
|
+
} finally {
|
|
519
|
+
if (staging) {
|
|
520
|
+
await rm3(staging, { recursive: true, force: true });
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// packages/compiler/project/server.ts
|
|
526
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
527
|
+
import { mkdtemp as mkdtemp3, mkdir as mkdir4, rm as rm4, rename as rename3 } from "node:fs/promises";
|
|
528
|
+
import { dirname as dirname4, resolve as resolve3, join as join4 } from "node:path";
|
|
529
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
530
|
+
import ts2 from "typescript";
|
|
531
|
+
import { typeCheckOnly as typeCheckOnly2 } from "@tsplc/compiler";
|
|
532
|
+
var resolveServerModule2 = (root, name) => name.startsWith(".") || name.startsWith("/") || existsSync2(resolve3(root, name)) ? resolve3(root, name) : createRequire2(join4(root, "package.json")).resolve(name);
|
|
533
|
+
var kebabCase2 = (name) => name.replace(/([a-z0-9])([A-Z])|([A-Z])([A-Z][a-z])/g, "$1$3-$2$4").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase();
|
|
534
|
+
var buildServer2 = async (config, root) => {
|
|
535
|
+
const settings = config.server;
|
|
536
|
+
if (!settings?.runtime)
|
|
537
|
+
throw new Error("Configure server.runtime to build an executable.");
|
|
538
|
+
const runtime = settings.runtime;
|
|
539
|
+
const entry = resolveServerModule2(root, runtime.entry);
|
|
540
|
+
const windows = runtime.target ? runtime.target.includes("windows") : process.platform === "win32";
|
|
541
|
+
const outFile = resolve3(root, "build/server", `${kebabCase2(config.name)}-services${windows ? ".exe" : ""}`);
|
|
542
|
+
const tsconfig = ts2.findConfigFile(dirname4(entry), ts2.sys.fileExists);
|
|
543
|
+
if (!tsconfig)
|
|
544
|
+
throw new Error("Server entry requires a tsconfig.json.");
|
|
545
|
+
const read = ts2.readConfigFile(tsconfig, ts2.sys.readFile);
|
|
546
|
+
if (read.error)
|
|
547
|
+
throw new Error(ts2.flattenDiagnosticMessageText(read.error.messageText, `
|
|
548
|
+
`));
|
|
549
|
+
const parsed = ts2.parseJsonConfigFileContent(read.config, ts2.sys, dirname4(tsconfig));
|
|
550
|
+
const program = ts2.createProgram({
|
|
551
|
+
rootNames: [entry, ...parsed.fileNames.filter((file) => /\.d\.[cm]?ts$/.test(file))],
|
|
552
|
+
options: { ...parsed.options, ...typeCheckOnly2 },
|
|
553
|
+
projectReferences: parsed.projectReferences
|
|
554
|
+
});
|
|
555
|
+
const diagnostics = [...parsed.errors, ...ts2.getPreEmitDiagnostics(program)];
|
|
556
|
+
if (diagnostics.length)
|
|
557
|
+
throw new Error(ts2.formatDiagnosticsWithColorAndContext(diagnostics, {
|
|
558
|
+
getCanonicalFileName: (path) => path,
|
|
559
|
+
getCurrentDirectory: () => root,
|
|
560
|
+
getNewLine: () => `
|
|
561
|
+
`
|
|
562
|
+
}));
|
|
563
|
+
await mkdir4(dirname4(outFile), { recursive: true });
|
|
564
|
+
const staging = await mkdtemp3(join4(dirname4(outFile), ".tsplc-server-"));
|
|
565
|
+
try {
|
|
566
|
+
const binary = join4(staging, "server");
|
|
567
|
+
const result = await Bun.build({
|
|
568
|
+
entrypoints: [entry],
|
|
569
|
+
env: "disable",
|
|
570
|
+
compile: {
|
|
571
|
+
outfile: binary,
|
|
572
|
+
...runtime.target ? { target: runtime.target } : {},
|
|
573
|
+
autoloadDotenv: false,
|
|
574
|
+
autoloadBunfig: false,
|
|
575
|
+
autoloadTsconfig: false,
|
|
576
|
+
autoloadPackageJson: false
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
if (!result.success)
|
|
580
|
+
throw new AggregateError(result.logs, "Server executable build failed.");
|
|
581
|
+
await rename3(windows ? `${binary}.exe` : binary, outFile);
|
|
582
|
+
return {
|
|
583
|
+
server: {
|
|
584
|
+
executable: outFile,
|
|
585
|
+
target: runtime.target ?? `bun-${process.platform}-${process.arch}`
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
} finally {
|
|
589
|
+
await rm4(staging, { recursive: true, force: true });
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
// packages/compiler/project/plant.ts
|
|
594
|
+
var plantCommands = new Set([
|
|
595
|
+
"deploy",
|
|
596
|
+
"target",
|
|
597
|
+
"status",
|
|
598
|
+
"login",
|
|
599
|
+
"logout",
|
|
600
|
+
"vars",
|
|
601
|
+
"read",
|
|
602
|
+
"write",
|
|
603
|
+
"force",
|
|
604
|
+
"unforce",
|
|
605
|
+
"start",
|
|
606
|
+
"stop",
|
|
607
|
+
"reset"
|
|
608
|
+
]);
|
|
609
|
+
var ask = async (question) => {
|
|
610
|
+
const lines = createInterface({ input: process.stdin, output: process.stdout });
|
|
611
|
+
try {
|
|
612
|
+
return (await lines.question(question)).trim();
|
|
613
|
+
} finally {
|
|
614
|
+
lines.close();
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
var askSecret = (question) => new Promise((accept, reject) => {
|
|
618
|
+
if (!process.stdin.isTTY) {
|
|
619
|
+
reject(new Error("The controller password needs an interactive terminal."));
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
process.stdin.setRawMode(true);
|
|
623
|
+
process.stdin.setEncoding("utf8");
|
|
624
|
+
process.stdin.resume();
|
|
625
|
+
process.stdout.write(question);
|
|
626
|
+
let secret = "";
|
|
627
|
+
const finish = () => {
|
|
628
|
+
process.stdin.off("data", read);
|
|
629
|
+
process.stdin.setRawMode(false);
|
|
630
|
+
process.stdin.pause();
|
|
631
|
+
process.stdout.write(`
|
|
632
|
+
`);
|
|
633
|
+
};
|
|
634
|
+
const read = (text) => {
|
|
635
|
+
for (const character of text) {
|
|
636
|
+
if (character === "\r" || character === `
|
|
637
|
+
`) {
|
|
638
|
+
finish();
|
|
639
|
+
accept(secret);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (character === "\x03") {
|
|
643
|
+
finish();
|
|
644
|
+
reject(new Error("Cancelled."));
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
secret = character === "" ? secret.slice(0, -1) : secret + character;
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
process.stdin.on("data", read);
|
|
651
|
+
});
|
|
652
|
+
var choose = async (question, items, label) => {
|
|
653
|
+
items.forEach((item, index) => console.log(` ${index + 1}. ${label(item)}`));
|
|
654
|
+
while (true) {
|
|
655
|
+
const item = items[Number(await ask(`${question} [1-${items.length}]: `)) - 1];
|
|
656
|
+
if (item !== undefined)
|
|
657
|
+
return item;
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
var confirm = async (question, flags) => flags.yes || /^y(es)?$/i.test(await ask(`${question} [y/N]: `));
|
|
661
|
+
var askCredentials = async () => ({
|
|
662
|
+
username: await ask("Controller username: "),
|
|
663
|
+
password: await askSecret("Controller password: ")
|
|
664
|
+
});
|
|
665
|
+
var progress = (message) => console.log(` ${message}`);
|
|
666
|
+
var connect = () => {
|
|
667
|
+
const environment = z.object({
|
|
668
|
+
TSPLC_AGENT_URL: z.url({
|
|
669
|
+
error: "Set TSPLC_AGENT_URL to the plant agent, such as http://line-pc:4850"
|
|
670
|
+
}),
|
|
671
|
+
TSPLC_AGENT_TOKEN: z.string({ error: "Set TSPLC_AGENT_TOKEN to the plant agent's token" })
|
|
672
|
+
}).parse(process.env);
|
|
673
|
+
return createAgentClient({
|
|
674
|
+
url: environment.TSPLC_AGENT_URL,
|
|
675
|
+
token: environment.TSPLC_AGENT_TOKEN
|
|
676
|
+
});
|
|
677
|
+
};
|
|
678
|
+
var selectTarget = async (agent) => {
|
|
679
|
+
const interfaces = await agent.interfaces();
|
|
680
|
+
if (!interfaces.length)
|
|
681
|
+
throw new Error("The plant PC has no IPv4 network.");
|
|
682
|
+
const network = interfaces.length === 1 && interfaces[0] ? interfaces[0] : await choose("Which network is the controller on?", interfaces, (item) => `${item.name} ${item.address}/${item.prefixLength}`);
|
|
683
|
+
console.log(`Scanning ${network.address}/${network.prefixLength} for CODESYS controllers…`);
|
|
684
|
+
const found = await agent.discover(network.name);
|
|
685
|
+
const manual = { address: "", port: 11740, hostname: null };
|
|
686
|
+
const choice = await choose("Which controller?", [...found, manual], (item) => item === manual ? "Enter an address" : `${item.address}${item.hostname ? ` (${item.hostname})` : ""}`);
|
|
687
|
+
const host = choice === manual ? await ask("Controller address: ") : choice.address;
|
|
688
|
+
return agent.setTarget({ host, port: choice.port });
|
|
689
|
+
};
|
|
690
|
+
var contractVariables = async (config, root) => {
|
|
691
|
+
if (!config.typegen)
|
|
692
|
+
return [];
|
|
693
|
+
const contract = z.object({
|
|
694
|
+
symbols: z.array(z.object({ name: z.string(), gvl: z.string(), type: z.string() })),
|
|
695
|
+
typeFields: z.record(z.string(), z.array(z.object({ field: z.string(), type: z.string() })))
|
|
696
|
+
}).parse(await import(pathToFileURL(resolve4(root, config.typegen.dir, "plc.ts")).href));
|
|
697
|
+
return contract.symbols.flatMap(({ name, gvl, type }) => {
|
|
698
|
+
const path = `${gvl}.${name}`;
|
|
699
|
+
const fields = contract.typeFields[type];
|
|
700
|
+
if (!fields)
|
|
701
|
+
return [{ path, type }];
|
|
702
|
+
return fields.map((field) => ({
|
|
703
|
+
path: field.field === "" ? path : `${path}${field.field.startsWith("[") ? "" : "."}${field.field}`,
|
|
704
|
+
type: field.type
|
|
705
|
+
}));
|
|
706
|
+
});
|
|
707
|
+
};
|
|
708
|
+
var prepareRelease = async (file, loaded, flags) => {
|
|
709
|
+
const { config, root } = loaded;
|
|
710
|
+
if (!config.compiler)
|
|
711
|
+
throw new Error("tsplc deploy deploys tsPLC programs; configure compiler.");
|
|
712
|
+
const emitted = await buildProject(file, {
|
|
713
|
+
emitOnly: true,
|
|
714
|
+
forController: true,
|
|
715
|
+
frozenLockfile: flags["frozen-lockfile"],
|
|
716
|
+
loaded,
|
|
717
|
+
buildTarget: "production",
|
|
718
|
+
controller: controllerFor(config, "deploy")
|
|
719
|
+
});
|
|
720
|
+
if (hasErrors2(emitted.diagnostics))
|
|
721
|
+
return { diagnostics: emitted.diagnostics };
|
|
722
|
+
const settings = emitted.controllerConfig;
|
|
723
|
+
if (!settings)
|
|
724
|
+
throw new Error("Configure build settings for deployment.");
|
|
725
|
+
await generateProject(file, { loaded, targets: await projectTargets(file, { loaded }) });
|
|
726
|
+
const portable = (path) => path.split(sep).join("/");
|
|
727
|
+
const libraries = (settings.libraries ?? []).map((library) => ({
|
|
728
|
+
...library,
|
|
729
|
+
release: `libraries/${library.sha256}-${basename(library.path)}`
|
|
730
|
+
}));
|
|
731
|
+
const files = [
|
|
732
|
+
{ path: "template.project", content: Bun.file(settings.template) },
|
|
733
|
+
...libraries.map((library) => ({ path: library.release, content: Bun.file(library.path) }))
|
|
734
|
+
];
|
|
735
|
+
for await (const source of new Bun.Glob("**/*").scan({ cwd: settings.sourceDir, dot: true }))
|
|
736
|
+
files.push({
|
|
737
|
+
path: `st/${portable(source)}`,
|
|
738
|
+
content: Bun.file(resolve4(settings.sourceDir, source))
|
|
739
|
+
});
|
|
740
|
+
let server;
|
|
741
|
+
if (config.server?.runtime) {
|
|
742
|
+
const { executable } = (await buildServer2(config, root)).server;
|
|
743
|
+
server = `server/${basename(executable)}`;
|
|
744
|
+
files.push({ path: server, content: Bun.file(executable) });
|
|
745
|
+
}
|
|
746
|
+
const manifest = {
|
|
747
|
+
config: {
|
|
748
|
+
io: {},
|
|
749
|
+
deviceTree: settings.deviceTree,
|
|
750
|
+
controller: settings.controller,
|
|
751
|
+
libraryReferences: settings.libraryReferences,
|
|
752
|
+
libraries: libraries.map(({ release, ...library }) => ({ ...library, path: release })),
|
|
753
|
+
task: settings.task,
|
|
754
|
+
tasks: settings.tasks,
|
|
755
|
+
removeObjects: settings.removeObjects,
|
|
756
|
+
symbols: settings.symbols,
|
|
757
|
+
timeouts: settings.timeouts,
|
|
758
|
+
template: "template.project",
|
|
759
|
+
sourceDir: "st",
|
|
760
|
+
entry: `st/${portable(relative(settings.sourceDir, settings.entry))}`
|
|
761
|
+
},
|
|
762
|
+
variables: await contractVariables(config, root),
|
|
763
|
+
server
|
|
764
|
+
};
|
|
765
|
+
return { release: { manifest, files } };
|
|
766
|
+
};
|
|
767
|
+
var deploy = async (agent, file, loaded, flags) => {
|
|
768
|
+
const status = await agent.status();
|
|
769
|
+
if (status.login)
|
|
770
|
+
throw new Error("A login is open; log out before deploying: tsplc logout");
|
|
771
|
+
const target = !status.target || flags.select ? await selectTarget(agent) : status.target;
|
|
772
|
+
console.log(`Building the release for ${target.host}…`);
|
|
773
|
+
const { release, diagnostics } = await prepareRelease(file, loaded, flags);
|
|
774
|
+
if (!release)
|
|
775
|
+
return { diagnostics };
|
|
776
|
+
if (flags.download && !await confirm("A full download stops the running application while it loads. Continue?", flags))
|
|
777
|
+
throw new Error("Cancelled.");
|
|
778
|
+
const credentials = await askCredentials();
|
|
779
|
+
const settings = {
|
|
780
|
+
credentials,
|
|
781
|
+
download: flags.download ?? false,
|
|
782
|
+
initializeUser: flags["initialize-user"] ?? false
|
|
783
|
+
};
|
|
784
|
+
console.log(release.manifest.server ? "Deploying the server and the program…" : "Deploying the program…");
|
|
785
|
+
try {
|
|
786
|
+
return await agent.deploy(release, settings, progress);
|
|
787
|
+
} catch (error) {
|
|
788
|
+
if (!(error instanceof AgentOperationError && error.downloadRequired) || !await confirm(`${error.message}. A full download stops the running application while it loads. Download now?`, flags))
|
|
789
|
+
throw error;
|
|
790
|
+
return agent.deploy(release, { ...settings, download: true }, progress);
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
var ensureLogin = async (agent) => {
|
|
794
|
+
if ((await agent.status()).login)
|
|
795
|
+
return;
|
|
796
|
+
const credentials = await askCredentials();
|
|
797
|
+
console.log("Logging in. CODESYS takes a few minutes to start; the login stays open until tsplc logout.");
|
|
798
|
+
await agent.login(credentials, progress);
|
|
799
|
+
};
|
|
800
|
+
var variableChildren = (variables, prefix) => {
|
|
801
|
+
const children = new Map;
|
|
802
|
+
const start = prefix.toLowerCase();
|
|
803
|
+
for (const { path, type } of variables) {
|
|
804
|
+
const lower = path.toLowerCase();
|
|
805
|
+
if (prefix && !(lower === start || lower.startsWith(`${start}.`) || lower.startsWith(`${start}[`)))
|
|
806
|
+
continue;
|
|
807
|
+
const rest = path.slice(prefix.length).replace(/^\./, "");
|
|
808
|
+
if (rest === "")
|
|
809
|
+
return [{ name: path, type, leaves: 1 }];
|
|
810
|
+
const name = /^(\[[^\]]*\]|[^.[]+)/.exec(rest)?.[0] ?? rest;
|
|
811
|
+
const child = children.get(name) ?? { leaves: 0 };
|
|
812
|
+
children.set(name, { type: rest === name ? type : undefined, leaves: child.leaves + 1 });
|
|
813
|
+
}
|
|
814
|
+
const indexes = [...children.keys()].filter((name) => /^\[\d+\]$/.test(name)).map((name) => Number(name.slice(1, -1)));
|
|
815
|
+
const listed = [...children].filter(([name]) => !/^\[\d+\]$/.test(name));
|
|
816
|
+
if (indexes.length) {
|
|
817
|
+
const first = children.get(`[${Math.min(...indexes)}]`);
|
|
818
|
+
listed.unshift([`[${Math.min(...indexes)}..${Math.max(...indexes)}]`, first ?? { leaves: 0 }]);
|
|
819
|
+
}
|
|
820
|
+
return listed.map(([name, child]) => ({ name, ...child }));
|
|
821
|
+
};
|
|
822
|
+
var printLogin = (result) => {
|
|
823
|
+
if ("values" in result) {
|
|
824
|
+
for (const [expression, value] of Object.entries(result.values))
|
|
825
|
+
console.log(`${expression} = ${value}`);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
console.log(`Application ${result.state}`);
|
|
829
|
+
if (result.forced.length)
|
|
830
|
+
console.log(`Forced: ${result.forced.join(", ")}`);
|
|
831
|
+
};
|
|
832
|
+
var runPlantCommand = async (command, args, flags, project) => {
|
|
833
|
+
const agent = connect();
|
|
834
|
+
const expect = (count, usage) => {
|
|
835
|
+
if (args.length !== count)
|
|
836
|
+
throw new Error(`Usage: tsplc ${command} ${usage}`);
|
|
837
|
+
};
|
|
838
|
+
const send = async (loginCommand) => {
|
|
839
|
+
await ensureLogin(agent);
|
|
840
|
+
printLogin(await agent.command(loginCommand));
|
|
841
|
+
};
|
|
842
|
+
switch (command) {
|
|
843
|
+
case "deploy": {
|
|
844
|
+
expect(0, "[--download] [--select] [--initialize-user]");
|
|
845
|
+
const deployed = await deploy(agent, project.file, await project.load(), flags);
|
|
846
|
+
if ("diagnostics" in deployed)
|
|
847
|
+
return deployed;
|
|
848
|
+
console.log(`Deployed release ${deployed.id} (${deployed.operation}).`);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
case "target": {
|
|
852
|
+
expect(0, "");
|
|
853
|
+
const target = await selectTarget(agent);
|
|
854
|
+
console.log(`Deployments now go to ${target.host}:${target.port}.`);
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
case "status": {
|
|
858
|
+
expect(0, "");
|
|
859
|
+
const status = await agent.status();
|
|
860
|
+
console.log(`Controller ${status.target ? `${status.target.host}:${status.target.port}` : "not selected (tsplc target)"}`);
|
|
861
|
+
console.log(`Release ${status.release ? `${status.release.id}, ${status.release.operation} at ${status.release.deployedAt}` : "none"}`);
|
|
862
|
+
console.log(`Server ${status.server ? `${status.server.running ? "running" : "restarting"} (${status.server.executable})` : "none"}`);
|
|
863
|
+
console.log(`Login ${status.login ? `open; application ${status.login.state}${status.login.forced.length ? `; forced: ${status.login.forced.join(", ")}` : ""}` : "closed"}`);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
case "login":
|
|
867
|
+
expect(0, "");
|
|
868
|
+
await ensureLogin(agent);
|
|
869
|
+
printLogin(await agent.command({ command: "state" }));
|
|
870
|
+
return;
|
|
871
|
+
case "logout": {
|
|
872
|
+
expect(0, "");
|
|
873
|
+
const forced = (await agent.status()).login?.forced ?? [];
|
|
874
|
+
if (forced.length && !await confirm(`Forced: ${forced.join(", ")}. Forces stay on the controller after logout. Log out anyway?`, flags))
|
|
875
|
+
throw new Error("Cancelled.");
|
|
876
|
+
await agent.logout();
|
|
877
|
+
console.log("Logged out.");
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
case "vars": {
|
|
881
|
+
if (args.length > 1)
|
|
882
|
+
throw new Error("Usage: tsplc vars [path]");
|
|
883
|
+
const variables = await agent.variables();
|
|
884
|
+
const children = variableChildren(variables, args[0] ?? "");
|
|
885
|
+
if (!children.length)
|
|
886
|
+
throw new Error(`No published variable ${args[0]}.`);
|
|
887
|
+
for (const child of children)
|
|
888
|
+
console.log(`${child.name.padEnd(32)} ${child.type ?? `${child.leaves} variables`}`);
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
case "read": {
|
|
892
|
+
const [first, ...rest] = args;
|
|
893
|
+
if (!first)
|
|
894
|
+
throw new Error("Usage: tsplc read <expression>...");
|
|
895
|
+
return send({ command: "read", expressions: [first, ...rest] });
|
|
896
|
+
}
|
|
897
|
+
case "write":
|
|
898
|
+
case "force": {
|
|
899
|
+
const [expression, value, ...extra] = args;
|
|
900
|
+
if (!expression || !value || extra.length)
|
|
901
|
+
throw new Error(`Usage: tsplc ${command} <expression> <value>`);
|
|
902
|
+
return send({ command, values: { [expression]: value } });
|
|
903
|
+
}
|
|
904
|
+
case "unforce":
|
|
905
|
+
return send({ command: "unforce", expressions: args, restore: flags.restore ?? false });
|
|
906
|
+
case "start":
|
|
907
|
+
expect(0, "");
|
|
908
|
+
return send({ command: "start" });
|
|
909
|
+
case "stop":
|
|
910
|
+
expect(0, "");
|
|
911
|
+
if (!await confirm("Stop the application on the controller?", flags))
|
|
912
|
+
throw new Error("Cancelled.");
|
|
913
|
+
return send({ command: "stop" });
|
|
914
|
+
case "reset": {
|
|
915
|
+
expect(1, "warm|cold");
|
|
916
|
+
const kind = z.enum(["warm", "cold"]).parse(args[0]);
|
|
917
|
+
if (!await confirm(`Reset the application (${kind})? It stops and reinitializes its variables.`, flags))
|
|
918
|
+
throw new Error("Cancelled.");
|
|
919
|
+
return send({ command: "reset", kind: kind === "warm" ? "Warm" : "Cold" });
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
// packages/compiler/project/project.ts
|
|
925
|
+
import { copyFile as copyFile2, mkdir as mkdir5, mkdtemp as mkdtemp4, realpath as realpath2, rm as rm5, writeFile as writeFile3 } from "node:fs/promises";
|
|
926
|
+
import { dirname as dirname5, join as join5, resolve as resolve5 } from "node:path";
|
|
927
|
+
import {
|
|
928
|
+
buildCodesys as buildCodesys2,
|
|
929
|
+
codesysBuildConfig as codesysBuildConfig2,
|
|
930
|
+
diagnoseCodesys as diagnoseCodesys2,
|
|
931
|
+
emitCodesys as emitCodesys2,
|
|
932
|
+
ioTargets as ioTargets2,
|
|
933
|
+
publicationSelectors as publicationSelectors2,
|
|
934
|
+
planCodesys as planCodesys2
|
|
935
|
+
} from "@tsplc/compiler/codesys";
|
|
936
|
+
import { analyze as analyze2, checkHostConfig as checkHostConfig2 } from "@tsplc/compiler";
|
|
937
|
+
import {
|
|
938
|
+
compilerConfig as compilerConfig2,
|
|
939
|
+
loadTsplcConfig as loadTsplcConfig2
|
|
940
|
+
} from "@tsplc/config";
|
|
941
|
+
import { deviceTree as deviceTree2, loadHardware as loadHardware2, resolveIo as resolveIo2 } from "@tsplc/typegen";
|
|
942
|
+
import { installLibraries as installLibraries2 } from "@tsplc/build";
|
|
943
|
+
import { hasErrors as hasErrors3, normalizeModule as normalizeModule2 } from "@tsplc/compiler/ir";
|
|
944
|
+
var failure2 = (origin, code, cause) => ({
|
|
945
|
+
origin,
|
|
946
|
+
code,
|
|
947
|
+
severity: "error",
|
|
948
|
+
message: cause instanceof Error ? cause.message : String(cause)
|
|
949
|
+
});
|
|
950
|
+
async function loadProject2(file, loaded, buildTarget) {
|
|
951
|
+
let configFile = resolve5(file);
|
|
952
|
+
try {
|
|
953
|
+
configFile = await realpath2(configFile);
|
|
954
|
+
const diagnostics = checkHostConfig2(configFile);
|
|
955
|
+
if (hasErrors3(diagnostics))
|
|
956
|
+
return { configFile, diagnostics };
|
|
957
|
+
const shared = (loaded ?? await loadTsplcConfig2(configFile)).config;
|
|
958
|
+
const root = dirname5(configFile);
|
|
959
|
+
const config = compilerConfig2(shared, resolveIo2(shared, await loadHardware2(shared, root)), buildTarget);
|
|
960
|
+
return {
|
|
961
|
+
configFile,
|
|
962
|
+
shared,
|
|
963
|
+
config: { ...config, ...sourceInput2(config, root) },
|
|
964
|
+
diagnostics
|
|
965
|
+
};
|
|
966
|
+
} catch (cause) {
|
|
967
|
+
return { configFile, diagnostics: [failure2("config", "PLC5003", cause)] };
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
function sourceInput2(compiler, root) {
|
|
971
|
+
return {
|
|
972
|
+
tsconfig: resolve5(root, compiler.tsconfig),
|
|
973
|
+
programs: Object.fromEntries(Object.entries(compiler.programs).map(([name, entry]) => [
|
|
974
|
+
name,
|
|
975
|
+
{ ...entry, module: resolve5(root, entry.module) }
|
|
976
|
+
]))
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
async function projectTargets2(file = "tsplc.config.ts", options = {}) {
|
|
980
|
+
const configFile = await realpath2(resolve5(file));
|
|
981
|
+
const { config } = options.loaded ?? await loadTsplcConfig2(configFile);
|
|
982
|
+
if (!config.compiler)
|
|
983
|
+
return;
|
|
984
|
+
const source = analyze2({
|
|
985
|
+
...sourceInput2(config.compiler, dirname5(configFile)),
|
|
986
|
+
services: config.server
|
|
987
|
+
});
|
|
988
|
+
const core = source.module && normalizeModule2(source.module).core;
|
|
989
|
+
return core ? { io: ioTargets2(core), publication: publicationSelectors2(core) } : undefined;
|
|
990
|
+
}
|
|
991
|
+
async function checkProject2(file = "tsplc.config.ts", options = {}) {
|
|
992
|
+
const loaded = await loadProject2(file, options.loaded, options.buildTarget);
|
|
993
|
+
if (!loaded.config) {
|
|
994
|
+
return loaded;
|
|
995
|
+
}
|
|
996
|
+
const config = loaded.config;
|
|
997
|
+
if (config.target.kind !== "codesys") {
|
|
998
|
+
return {
|
|
999
|
+
...loaded,
|
|
1000
|
+
diagnostics: [failure2("backend", "PLC7001", `Unsupported target: ${config.target.kind}`)]
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
const source = analyze2({
|
|
1004
|
+
tsconfig: config.tsconfig,
|
|
1005
|
+
programs: config.programs,
|
|
1006
|
+
services: loaded.shared?.server
|
|
1007
|
+
});
|
|
1008
|
+
if (!source.module) {
|
|
1009
|
+
return { ...loaded, diagnostics: source.diagnostics };
|
|
1010
|
+
}
|
|
1011
|
+
const normalized = normalizeModule2(source.module);
|
|
1012
|
+
if (!normalized.core) {
|
|
1013
|
+
return { ...loaded, diagnostics: normalized.diagnostics };
|
|
1014
|
+
}
|
|
1015
|
+
const tasks = config.tasks.map((task) => ({
|
|
1016
|
+
name: task.name,
|
|
1017
|
+
intervalNanoseconds: task.schedule.interval.nanoseconds,
|
|
1018
|
+
priority: task.priority,
|
|
1019
|
+
programs: task.programs
|
|
1020
|
+
}));
|
|
1021
|
+
const planned = planCodesys2(normalized.core, { tasks }, config.target.options);
|
|
1022
|
+
return {
|
|
1023
|
+
...loaded,
|
|
1024
|
+
...planned,
|
|
1025
|
+
diagnostics: [
|
|
1026
|
+
...loaded.diagnostics,
|
|
1027
|
+
...source.diagnostics,
|
|
1028
|
+
...normalized.diagnostics,
|
|
1029
|
+
...planned.diagnostics
|
|
1030
|
+
]
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
async function engineeringInputs2(shared, configFile, bundle, frozen) {
|
|
1034
|
+
const root = dirname5(configFile);
|
|
1035
|
+
const tree = await deviceTree2(shared, await loadHardware2(shared, root));
|
|
1036
|
+
return {
|
|
1037
|
+
libraries: await installLibraries2(shared.dependencies, root, { frozen }),
|
|
1038
|
+
deviceTree: tree && {
|
|
1039
|
+
...tree,
|
|
1040
|
+
mappings: bundle.ioMappings.map(({ address, variable }) => ({ address, variable }))
|
|
1041
|
+
}
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
async function buildProject2(file = "tsplc.config.ts", options = {}) {
|
|
1045
|
+
let staging;
|
|
1046
|
+
try {
|
|
1047
|
+
const configFile = await realpath2(resolve5(file));
|
|
1048
|
+
const previous = await owned(configFile, true);
|
|
1049
|
+
if (previous.unknown.length) {
|
|
1050
|
+
throw new Error(`Output contains unrelated files: ${previous.unknown.join(", ")}`);
|
|
1051
|
+
}
|
|
1052
|
+
const checked = await checkProject2(configFile, {
|
|
1053
|
+
loaded: options.loaded,
|
|
1054
|
+
buildTarget: options.buildTarget
|
|
1055
|
+
});
|
|
1056
|
+
if (!checked.plan || !checked.config) {
|
|
1057
|
+
return { diagnostics: checked.diagnostics };
|
|
1058
|
+
}
|
|
1059
|
+
const bundle = emitCodesys2(checked.plan);
|
|
1060
|
+
if (hasErrors3(bundle.diagnostics)) {
|
|
1061
|
+
return { diagnostics: bundle.diagnostics };
|
|
1062
|
+
}
|
|
1063
|
+
const shared = checked.shared;
|
|
1064
|
+
const engineering = shared && (!options.emitOnly || options.forController) ? await engineeringInputs2(shared, configFile, bundle, options.frozenLockfile) : undefined;
|
|
1065
|
+
const controllerConfig = shared?.build ? codesysBuildConfig2(checked.plan, bundle, {
|
|
1066
|
+
configFile,
|
|
1067
|
+
sourceDir: join5(previous.output, "st"),
|
|
1068
|
+
outDir: resolve5(dirname5(configFile), shared.build.outDir),
|
|
1069
|
+
controller: options.controller,
|
|
1070
|
+
...engineering
|
|
1071
|
+
}) : undefined;
|
|
1072
|
+
await mkdir5(previous.root, { recursive: true });
|
|
1073
|
+
staging = await mkdtemp4(join5(previous.root, "stage-"));
|
|
1074
|
+
const artifacts = [
|
|
1075
|
+
...bundle.artifacts,
|
|
1076
|
+
{ path: "mappings.json", content: `${JSON.stringify(bundle.mappings, null, 2)}
|
|
1077
|
+
` },
|
|
1078
|
+
{ path: "io-symbols.json", content: `${JSON.stringify(bundle.ioSymbols, null, 2)}
|
|
1079
|
+
` },
|
|
1080
|
+
{ path: "publication.json", content: `${JSON.stringify(bundle.publication, null, 2)}
|
|
1081
|
+
` },
|
|
1082
|
+
{ path: "config.json", content: `${JSON.stringify(checked.config, null, 2)}
|
|
1083
|
+
` }
|
|
1084
|
+
];
|
|
1085
|
+
for (const artifact of artifacts) {
|
|
1086
|
+
const path = join5(staging, "st", artifact.path);
|
|
1087
|
+
await mkdir5(dirname5(path), { recursive: true });
|
|
1088
|
+
await writeFile3(path, artifact.content);
|
|
1089
|
+
}
|
|
1090
|
+
let vendor;
|
|
1091
|
+
if (!options.emitOnly) {
|
|
1092
|
+
const outDir = shared?.build ? resolve5(dirname5(configFile), shared.build.outDir ?? "build") : join5(previous.root, "engineering");
|
|
1093
|
+
await engineeringDirectory(outDir, configFile);
|
|
1094
|
+
try {
|
|
1095
|
+
vendor = await buildCodesys2(checked.plan, bundle, {
|
|
1096
|
+
configFile,
|
|
1097
|
+
sourceDir: join5(staging, "st"),
|
|
1098
|
+
outDir,
|
|
1099
|
+
controller: options.controller,
|
|
1100
|
+
...engineering
|
|
1101
|
+
});
|
|
1102
|
+
} catch (cause) {
|
|
1103
|
+
return { diagnostics: diagnoseCodesys2(cause, bundle) };
|
|
1104
|
+
}
|
|
1105
|
+
const vendorArtifacts = [
|
|
1106
|
+
["Application.project", vendor.project],
|
|
1107
|
+
["Application.app", vendor.output],
|
|
1108
|
+
["Application.crc", vendor.checksum]
|
|
1109
|
+
];
|
|
1110
|
+
for (const [name, path] of vendorArtifacts) {
|
|
1111
|
+
if (!path)
|
|
1112
|
+
continue;
|
|
1113
|
+
const destination = join5(staging, "vendor", name);
|
|
1114
|
+
await mkdir5(dirname5(destination), { recursive: true });
|
|
1115
|
+
await copyFile2(path, destination);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
const status = vendor ? "vendor-compiled" : "emitted";
|
|
1119
|
+
await publishArtifacts(staging, previous, {
|
|
1120
|
+
configFile,
|
|
1121
|
+
status,
|
|
1122
|
+
...vendor && { vendor: { application: vendor.application, tasks: vendor.tasks } }
|
|
1123
|
+
});
|
|
1124
|
+
staging = undefined;
|
|
1125
|
+
return {
|
|
1126
|
+
directory: previous.output,
|
|
1127
|
+
status,
|
|
1128
|
+
...controllerConfig && { controllerConfig },
|
|
1129
|
+
diagnostics: [...checked.diagnostics, ...bundle.diagnostics]
|
|
1130
|
+
};
|
|
1131
|
+
} catch (cause) {
|
|
1132
|
+
return { diagnostics: [failure2("config", "PLC5004", cause)] };
|
|
1133
|
+
} finally {
|
|
1134
|
+
if (staging) {
|
|
1135
|
+
await rm5(staging, { recursive: true, force: true });
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
async function cleanProject(file = "tsplc.config.ts") {
|
|
1140
|
+
try {
|
|
1141
|
+
await cleanArtifacts(await realpath2(resolve5(file)));
|
|
1142
|
+
return { diagnostics: [] };
|
|
1143
|
+
} catch (cause) {
|
|
1144
|
+
return { diagnostics: [failure2("config", "PLC5004", cause)] };
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// packages/compiler/project/cli.ts
|
|
1149
|
+
async function main() {
|
|
1150
|
+
const { values, positionals } = parseArgs({
|
|
1151
|
+
allowPositionals: true,
|
|
1152
|
+
options: {
|
|
1153
|
+
config: { type: "string" },
|
|
1154
|
+
help: { type: "boolean", short: "h" },
|
|
1155
|
+
"emit-only": { type: "boolean" },
|
|
1156
|
+
format: { type: "string" },
|
|
1157
|
+
read: { type: "string", multiple: true },
|
|
1158
|
+
"initialize-user": { type: "boolean" },
|
|
1159
|
+
"frozen-lockfile": { type: "boolean" },
|
|
1160
|
+
check: { type: "boolean" },
|
|
1161
|
+
plc: { type: "boolean" },
|
|
1162
|
+
server: { type: "boolean" },
|
|
1163
|
+
simulate: { type: "boolean" },
|
|
1164
|
+
runtime: { type: "boolean" },
|
|
1165
|
+
download: { type: "boolean" },
|
|
1166
|
+
restore: { type: "boolean" },
|
|
1167
|
+
select: { type: "boolean" },
|
|
1168
|
+
yes: { type: "boolean", short: "y" }
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
const command = positionals[0], file = values.config ?? "tsplc.config.ts";
|
|
1172
|
+
if (!command || values.help) {
|
|
1173
|
+
console.log(`tsplc install|typegen|check|build|run|download|online-change|inspect|clean|configure [--config tsplc.config.ts] [--plc|--server] [--simulate] [--runtime] [--emit-only] [--format json] [--read symbol]
|
|
1174
|
+
|
|
1175
|
+
Plant agent (TSPLC_AGENT_URL, TSPLC_AGENT_TOKEN):
|
|
1176
|
+
tsplc deploy [--download] [--select] [--initialize-user]
|
|
1177
|
+
tsplc target | status | login | logout
|
|
1178
|
+
tsplc vars [path] | read <expression>... | write <expression> <value>
|
|
1179
|
+
tsplc force <expression> <value> | unforce [expression]... [--restore]
|
|
1180
|
+
tsplc start | stop | reset warm|cold (-y skips confirmations)`);
|
|
1181
|
+
return 0;
|
|
1182
|
+
}
|
|
1183
|
+
if (plantCommands.has(command)) {
|
|
1184
|
+
const failed = await runPlantCommand(command, positionals.slice(1), values, {
|
|
1185
|
+
file,
|
|
1186
|
+
load: () => loadTsplcConfig3(file)
|
|
1187
|
+
});
|
|
1188
|
+
return failed ? report(failed, values.format) : 0;
|
|
1189
|
+
}
|
|
1190
|
+
if (positionals.length > 1)
|
|
1191
|
+
throw new Error("Specify one command.");
|
|
1192
|
+
if (![
|
|
1193
|
+
"install",
|
|
1194
|
+
"typegen",
|
|
1195
|
+
"check",
|
|
1196
|
+
"build",
|
|
1197
|
+
"run",
|
|
1198
|
+
"download",
|
|
1199
|
+
"online-change",
|
|
1200
|
+
"inspect",
|
|
1201
|
+
"clean",
|
|
1202
|
+
"configure"
|
|
1203
|
+
].includes(command))
|
|
1204
|
+
throw new Error(`Unknown command ${command}.`);
|
|
1205
|
+
if (values.format && !["json", "text"].includes(values.format))
|
|
1206
|
+
throw new Error("--format must be json or text.");
|
|
1207
|
+
if (values["emit-only"] && command !== "build")
|
|
1208
|
+
throw new Error("--emit-only is supported by build only.");
|
|
1209
|
+
if (values.runtime && command !== "configure")
|
|
1210
|
+
throw new Error("--runtime applies only to configure.");
|
|
1211
|
+
if ((values.plc || values.server) && command !== "build")
|
|
1212
|
+
throw new Error("--plc and --server apply only to build.");
|
|
1213
|
+
if (values.simulate && !["run", "download", "online-change"].includes(command))
|
|
1214
|
+
throw new Error("--simulate applies only to run, download and online-change.");
|
|
1215
|
+
if (values.plc && values.server)
|
|
1216
|
+
throw new Error("Choose --plc or --server, or omit both for a full build.");
|
|
1217
|
+
if (values.server && values["emit-only"])
|
|
1218
|
+
throw new Error("--emit-only applies to PLC emission only.");
|
|
1219
|
+
const log = console.log, previousLogTarget = process.env["TSPLC_LOG_STDERR"];
|
|
1220
|
+
if (values.format === "json") {
|
|
1221
|
+
console.log = console.error;
|
|
1222
|
+
process.env["TSPLC_LOG_STDERR"] = "1";
|
|
1223
|
+
}
|
|
1224
|
+
let result;
|
|
1225
|
+
try {
|
|
1226
|
+
if (command === "configure") {
|
|
1227
|
+
await (values.runtime ? configureRuntime() : configureDocker());
|
|
1228
|
+
result = { ok: true };
|
|
1229
|
+
} else if (command === "clean") {
|
|
1230
|
+
result = await cleanProject(file);
|
|
1231
|
+
} else {
|
|
1232
|
+
if (["check", "build", "run", "download", "online-change"].includes(command)) {
|
|
1233
|
+
const diagnostics = checkHostConfig3(resolve6(file));
|
|
1234
|
+
if (hasErrors4(diagnostics))
|
|
1235
|
+
throw new DiagnosticFailure({ diagnostics });
|
|
1236
|
+
}
|
|
1237
|
+
const loaded = await loadTsplcConfig3(file);
|
|
1238
|
+
const { config, configFile, root } = loaded;
|
|
1239
|
+
const target = values.simulate ? { ...loaded, config: simulationConfig(config) } : loaded;
|
|
1240
|
+
if (command === "install")
|
|
1241
|
+
result = {
|
|
1242
|
+
libraries: await installLibraries3(config.dependencies, root, {
|
|
1243
|
+
frozen: values["frozen-lockfile"]
|
|
1244
|
+
})
|
|
1245
|
+
};
|
|
1246
|
+
else if (command === "typegen") {
|
|
1247
|
+
const targets = await projectTargets2(file, { loaded });
|
|
1248
|
+
if (config.compiler && !values.check) {
|
|
1249
|
+
await generateProject2(file, { hardwareOnly: true, loaded, targets });
|
|
1250
|
+
const emitted = await buildProject2(file, { emitOnly: true, loaded });
|
|
1251
|
+
if (hasErrors4(emitted.diagnostics))
|
|
1252
|
+
throw new DiagnosticFailure(emitted);
|
|
1253
|
+
}
|
|
1254
|
+
result = await generateProject2(file, { check: values.check, loaded, targets });
|
|
1255
|
+
} else if (command === "check") {
|
|
1256
|
+
result = config.compiler ? await checkProject2(file, { loaded }) : {
|
|
1257
|
+
objects: (await loadSources(resolveConfig({
|
|
1258
|
+
...rawBuildConfig(config),
|
|
1259
|
+
io: resolveIo3(config, await loadHardware3(config, root))
|
|
1260
|
+
}, configFile))).objects.length
|
|
1261
|
+
};
|
|
1262
|
+
} else if (command === "build" && values.server) {
|
|
1263
|
+
result = await buildServer(config, root);
|
|
1264
|
+
} else if (command === "build" || command === "run") {
|
|
1265
|
+
if (config.compiler) {
|
|
1266
|
+
const built = await buildProject2(file, {
|
|
1267
|
+
emitOnly: values["emit-only"],
|
|
1268
|
+
frozenLockfile: values["frozen-lockfile"],
|
|
1269
|
+
loaded: target,
|
|
1270
|
+
controller: controllerFor2(config, command),
|
|
1271
|
+
buildTarget: buildTargetFor(command)
|
|
1272
|
+
});
|
|
1273
|
+
if (hasErrors4(built.diagnostics))
|
|
1274
|
+
throw new DiagnosticFailure(built);
|
|
1275
|
+
await generateProject2(file, {
|
|
1276
|
+
loaded,
|
|
1277
|
+
targets: await projectTargets2(file, { loaded })
|
|
1278
|
+
});
|
|
1279
|
+
result = built;
|
|
1280
|
+
} else {
|
|
1281
|
+
if (values["emit-only"])
|
|
1282
|
+
throw new Error("Raw Structured Text is already emitted.");
|
|
1283
|
+
if (!config.build)
|
|
1284
|
+
throw new Error("Configure build settings.");
|
|
1285
|
+
const build = rawBuildConfig(config);
|
|
1286
|
+
const libraries = await installLibraries3(config.dependencies, root, {
|
|
1287
|
+
frozen: values["frozen-lockfile"]
|
|
1288
|
+
});
|
|
1289
|
+
const hardware = await loadHardware3(target.config, root);
|
|
1290
|
+
result = await buildSt(resolveConfig({
|
|
1291
|
+
...build,
|
|
1292
|
+
libraries,
|
|
1293
|
+
io: resolveIo3(target.config, hardware),
|
|
1294
|
+
deviceTree: await deviceTree3(target.config, hardware),
|
|
1295
|
+
controller: controllerFor2(config, command)
|
|
1296
|
+
}, configFile));
|
|
1297
|
+
await generateProject2(file, { hardwareOnly: !config.typegen, loaded });
|
|
1298
|
+
}
|
|
1299
|
+
if (command === "build" && !values.plc && !values["emit-only"] && config.server?.runtime) {
|
|
1300
|
+
const backend = await buildServer(config, root);
|
|
1301
|
+
result = { ...typeof result === "object" && result !== null ? result : {}, ...backend };
|
|
1302
|
+
}
|
|
1303
|
+
if (command === "run") {
|
|
1304
|
+
if (!config.build)
|
|
1305
|
+
throw new Error("Configure build to run this project.");
|
|
1306
|
+
await runProject(resolveConfig(config.compiler ? config.build : rawBuildConfig(config), configFile), config.name);
|
|
1307
|
+
}
|
|
1308
|
+
} else if (command === "inspect") {
|
|
1309
|
+
if (!config.build)
|
|
1310
|
+
throw new Error("Configure build settings and controller connection.");
|
|
1311
|
+
result = await inspectController(resolveConfig({ ...config.build, controller: controllerFor2(config, command) }, configFile), values.read);
|
|
1312
|
+
} else if (["download", "online-change"].includes(command)) {
|
|
1313
|
+
if (!config.build)
|
|
1314
|
+
throw new Error("Configure build settings and controller connection.");
|
|
1315
|
+
let settings;
|
|
1316
|
+
if (config.compiler) {
|
|
1317
|
+
const emitted = await buildProject2(file, {
|
|
1318
|
+
emitOnly: true,
|
|
1319
|
+
forController: true,
|
|
1320
|
+
frozenLockfile: values["frozen-lockfile"],
|
|
1321
|
+
loaded: target,
|
|
1322
|
+
buildTarget: buildTargetFor(command)
|
|
1323
|
+
});
|
|
1324
|
+
if (hasErrors4(emitted.diagnostics))
|
|
1325
|
+
throw new DiagnosticFailure(emitted);
|
|
1326
|
+
if (!emitted.controllerConfig)
|
|
1327
|
+
throw new Error("Configure build settings for deployment.");
|
|
1328
|
+
settings = { ...emitted.controllerConfig, io: {} };
|
|
1329
|
+
} else
|
|
1330
|
+
settings = {
|
|
1331
|
+
...resolveConfig({
|
|
1332
|
+
...rawBuildConfig(config),
|
|
1333
|
+
io: resolveIo3(target.config, await loadHardware3(target.config, root))
|
|
1334
|
+
}, configFile),
|
|
1335
|
+
libraries: await installLibraries3(config.dependencies, root, {
|
|
1336
|
+
frozen: values["frozen-lockfile"]
|
|
1337
|
+
})
|
|
1338
|
+
};
|
|
1339
|
+
result = await updateController({ ...settings, controller: controllerFor2(config, command) }, command, {
|
|
1340
|
+
initializeUser: values["initialize-user"],
|
|
1341
|
+
read: values.read
|
|
1342
|
+
});
|
|
1343
|
+
} else
|
|
1344
|
+
throw new Error(`Unknown command ${command}.`);
|
|
1345
|
+
}
|
|
1346
|
+
} finally {
|
|
1347
|
+
console.log = log;
|
|
1348
|
+
if (previousLogTarget === undefined)
|
|
1349
|
+
delete process.env["TSPLC_LOG_STDERR"];
|
|
1350
|
+
else
|
|
1351
|
+
process.env["TSPLC_LOG_STDERR"] = previousLogTarget;
|
|
1352
|
+
}
|
|
1353
|
+
return report(result, values.format);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
class DiagnosticFailure extends Error {
|
|
1357
|
+
result;
|
|
1358
|
+
constructor(result) {
|
|
1359
|
+
super("PLC validation failed.");
|
|
1360
|
+
this.result = result;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
function report(result, format) {
|
|
1364
|
+
const diagnostics = result && typeof result === "object" && "diagnostics" in result ? result.diagnostics : [];
|
|
1365
|
+
const ok = !hasErrors4(diagnostics);
|
|
1366
|
+
if (format === "json")
|
|
1367
|
+
console.log(JSON.stringify({ ok, ...result && typeof result === "object" ? result : { result } }));
|
|
1368
|
+
else {
|
|
1369
|
+
for (const diagnostic of diagnostics)
|
|
1370
|
+
console.error(formatDiagnostic(diagnostic));
|
|
1371
|
+
if (ok)
|
|
1372
|
+
console.log("tsPLC completed.");
|
|
1373
|
+
}
|
|
1374
|
+
return ok ? 0 : 1;
|
|
1375
|
+
}
|
|
1376
|
+
try {
|
|
1377
|
+
process.exitCode = await main();
|
|
1378
|
+
} catch (cause) {
|
|
1379
|
+
const json = process.argv.includes("json") || process.argv.includes("--format=json");
|
|
1380
|
+
if (cause instanceof DiagnosticFailure) {
|
|
1381
|
+
process.exitCode = report(cause.result, json ? "json" : undefined);
|
|
1382
|
+
} else {
|
|
1383
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
1384
|
+
if (json)
|
|
1385
|
+
console.log(JSON.stringify({
|
|
1386
|
+
ok: false,
|
|
1387
|
+
diagnostics: [{ origin: "config", code: "PLC5003", severity: "error", message }]
|
|
1388
|
+
}));
|
|
1389
|
+
else
|
|
1390
|
+
console.error(message);
|
|
1391
|
+
process.exitCode = 1;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
//# debugId=2A9AAF31224302AE64756E2164756E21
|
|
1396
|
+
//# sourceMappingURL=cli.js.map
|