@tsplc/compiler 0.2.9 → 0.2.11

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.
@@ -11,13 +11,12 @@ import type {
11
11
  Variable,
12
12
  } from "@tsplc/compiler/ir";
13
13
  import { defaultValue, instancedBlock, standardBlocks } from "@tsplc/compiler/ir";
14
- import { Names, pascalCase } from "./names.ts";
14
+ import { planGlobalNames } from "./global-names.ts";
15
+ import { Names } from "./names.ts";
15
16
  import { NumericLowering } from "./numeric.ts";
16
17
  import { type CodesysPlan, diagnostic, isCodesysPlan } from "./plan.ts";
17
- import { storageLayout } from "./storage.ts";
18
18
  import { accessedStorage } from "./task-access.ts";
19
19
  import { emitHttp, httpMethods, httpCertificateVerifier, opcuaServiceCapacity } from "./http.ts";
20
- import { CodesysTypes } from "./types.ts";
21
20
  import type { Publication } from "@tsplc/config";
22
21
 
23
22
  export interface SourceMapping {
@@ -59,7 +58,7 @@ export function emitCodesys(plan: CodesysPlan): CodesysBundle {
59
58
  // A bundle owns target names and source mappings. Per-routine declarations and
60
59
  // implementation lines stay local to emitRoutine, so they cannot leak between POUs.
61
60
  class CodesysEmitter {
62
- private readonly names = new Names();
61
+ private readonly names;
63
62
  private networkName: string | undefined;
64
63
  private readonly rpcChannels: {
65
64
  path: string;
@@ -94,9 +93,10 @@ class CodesysEmitter {
94
93
  string,
95
94
  { names: Names; references: Map<string, string> }
96
95
  >();
97
- private readonly targetTypes: CodesysTypes;
96
+ private readonly targetTypes;
98
97
  private readonly numeric: NumericLowering;
99
- private readonly layout;
98
+ private readonly globals;
99
+ private readonly retainedVariables;
100
100
 
101
101
  constructor(private readonly plan: CodesysPlan) {
102
102
  const { core, options } = plan;
@@ -107,22 +107,18 @@ class CodesysEmitter {
107
107
  if (!fn.owner && [...accessedStorage(fn, core)].some((id) => persistent.has(id)))
108
108
  this.programRoutines.add(fn.id);
109
109
  }
110
- for (const name of Object.values(options.programNames ?? {})) this.names.reserve(name);
111
-
112
- this.typeMap = new Map(core.types.map((type) => [type.id, type]));
113
- this.blocks = new Map(core.blocks.map((block) => [block.id, block]));
114
- this.blockNames = new Map(
115
- core.blocks.map((block) => [
116
- block.id,
117
- block.standard
118
- ? standardBlocks[block.standard].native
119
- : this.names.allocateObject("FB", block.name),
120
- ]),
110
+ const { names, typeMap, blockNames, targetTypes, globals, retainedVariables } = planGlobalNames(
111
+ core,
112
+ options.programNames,
121
113
  );
122
- for (const name of this.blockNames.values()) this.names.reserve(name);
123
- this.targetTypes = new CodesysTypes(this.typeMap, this.names, this.blockNames);
114
+ this.names = names;
115
+ this.typeMap = typeMap;
116
+ this.blockNames = blockNames;
117
+ this.targetTypes = targetTypes;
118
+ this.globals = globals;
119
+ this.retainedVariables = retainedVariables;
120
+ this.blocks = new Map(core.blocks.map((block) => [block.id, block]));
124
121
  this.numeric = new NumericLowering(this.typeMap, (label) => this.names.allocate(label));
125
- this.layout = storageLayout(core);
126
122
  this.objects.push(...this.targetTypes.objects);
127
123
  }
128
124
 
@@ -268,7 +264,7 @@ END_VAR
268
264
  let source = object.id.split(":")[0] ?? "";
269
265
  const program = core.programs.find((item) => item.id === object.id);
270
266
  if (program) source = program.entry.split(":")[0] ?? "";
271
- const group = this.layout.globalLists.find((item) => item.id === object.id);
267
+ const group = this.globals.find((item) => item.id === object.id);
272
268
  if (group) {
273
269
  // Variable IDs are relative to tsconfig, unlike their diagnostic spans.
274
270
  source = group.variables[0]?.id.split(":")[0] ?? "";
@@ -290,24 +286,11 @@ END_VAR
290
286
 
291
287
  private emitGlobals(): void {
292
288
  const { options } = this.plan;
293
- const { layout, names, references, mappings, targetTypes, typeMap, objects } = this;
294
- const hasExports = (group: (typeof layout.globalLists)[number]) =>
295
- group.variables.some((variable) => variable.exported && !variable.exposure);
296
- for (const group of layout.globalLists) {
297
- const splitByStorage = layout.globalLists.some(
298
- (other) =>
299
- other.module === group.module && other.storage !== group.storage && hasExports(other),
300
- );
301
- // Exported lists are the program's public interface; private storage stays generated.
302
- const name = hasExports(group)
303
- ? names.allocate(
304
- pascalCase(splitByStorage ? `${group.module}-${group.storage}` : group.module),
305
- )
306
- : names.allocateObject("GVL", `${group.module}_${group.storage}`);
289
+ const { globals, references, mappings, targetTypes, typeMap, objects } = this;
290
+ for (const group of globals) {
291
+ const { name, fields } = group;
307
292
  const published = new Set(this.plan.publication[options.buildTarget]);
308
293
  const publish = group.variables.some((variable) => published.has(variable.id));
309
- const fields = new Names();
310
- for (const name of this.blockNames.values()) fields.reserve(name);
311
294
  const lines = [
312
295
  ...(publish ? ["{attribute 'linkalways'}"] : []),
313
296
  "{attribute 'qualified_only'}",
@@ -318,7 +301,7 @@ END_VAR
318
301
  : "VAR_GLOBAL",
319
302
  ];
320
303
  for (const item of group.variables) {
321
- const field = fields.allocate(item.name);
304
+ const field = required(fields.get(item.id));
322
305
  references.set(item.id, `${name}.${field}`);
323
306
  if (published.has(item.id))
324
307
  lines.push(` {attribute 'symbol' := '${item.exposure ? "read" : "readwrite"}'}`);
@@ -606,7 +589,7 @@ END_VAR`,
606
589
  blocks,
607
590
  blockFields,
608
591
  numeric,
609
- layout,
592
+ retainedVariables,
610
593
  } = this;
611
594
  const block = "inputs" in fn ? fn : undefined;
612
595
  const method = "owner" in fn ? fn.owner : undefined;
@@ -998,11 +981,11 @@ END_VAR`,
998
981
  section("VAR_OUTPUT", block.outputs);
999
982
  section(
1000
983
  "VAR",
1001
- block.state.filter((item) => !layout.retainedVariables.has(item.id)),
984
+ block.state.filter((item) => !retainedVariables.has(item.id)),
1002
985
  );
1003
986
  section(
1004
987
  "VAR RETAIN",
1005
- block.state.filter((item) => layout.retainedVariables.has(item.id)),
988
+ block.state.filter((item) => retainedVariables.has(item.id)),
1006
989
  );
1007
990
  }
1008
991
  if (declarations.length) {
@@ -0,0 +1,49 @@
1
+ import { standardBlocks, type VerifiedCoreModule } from "@tsplc/compiler/ir";
2
+ import { Names, pascalCase } from "./names.ts";
3
+ import { storageLayout } from "./storage.ts";
4
+ import { CodesysTypes } from "./types.ts";
5
+
6
+ export const planGlobalNames = (
7
+ core: VerifiedCoreModule,
8
+ programNames: Readonly<Record<string, string>> = {},
9
+ ) => {
10
+ const names = new Names();
11
+ for (const name of Object.values(programNames)) names.reserve(name);
12
+
13
+ const typeMap = new Map(core.types.map((type) => [type.id, type]));
14
+ const blockNames = new Map(
15
+ core.blocks.map((block) => [
16
+ block.id,
17
+ block.standard
18
+ ? standardBlocks[block.standard].native
19
+ : names.allocateObject("FB", block.name),
20
+ ]),
21
+ );
22
+ for (const name of blockNames.values()) names.reserve(name);
23
+ const targetTypes = new CodesysTypes(typeMap, names, blockNames);
24
+
25
+ const { globalLists, retainedVariables } = storageLayout(core);
26
+ const hasExports = (group: (typeof globalLists)[number]) =>
27
+ group.variables.some((variable) => variable.exported && !variable.exposure);
28
+ const globals = globalLists.map((group) => {
29
+ const splitByStorage = globalLists.some(
30
+ (other) =>
31
+ other.module === group.module && other.storage !== group.storage && hasExports(other),
32
+ );
33
+ // Exported lists are the program's public interface. Private storage stays generated.
34
+ const name = hasExports(group)
35
+ ? names.allocate(
36
+ pascalCase(splitByStorage ? `${group.module}-${group.storage}` : group.module),
37
+ )
38
+ : names.allocateObject("GVL", `${group.module}_${group.storage}`);
39
+ const fields = new Names();
40
+ for (const name of blockNames.values()) fields.reserve(name);
41
+ return {
42
+ ...group,
43
+ name,
44
+ fields: new Map(group.variables.map((item) => [item.id, fields.allocate(item.name)])),
45
+ };
46
+ });
47
+
48
+ return { names, typeMap, blockNames, targetTypes, globals, retainedVariables };
49
+ };
@@ -17,7 +17,7 @@ import { iecSpelling, isStoragePath, resolveStoragePath, type StorageBinding } f
17
17
  import { isIecIdentifier } from "./names.ts";
18
18
  import { storageLayout } from "./storage.ts";
19
19
  import { validateHttpLayout, resolveHttpEndpoint, type HttpEndpoint } from "./http.ts";
20
- import { accessedStorage } from "./task-access.ts";
20
+ import { accessedStorage, schedulePrograms, taskStorageOwnership } from "./task-access.ts";
21
21
  import { resolvePublication } from "./publication.ts";
22
22
  import type { Publication } from "@tsplc/config";
23
23
 
@@ -58,58 +58,12 @@ export function planCodesys(
58
58
  ),
59
59
  };
60
60
 
61
- const backgroundTasks = new Map<string, Deployment["tasks"][number]>();
62
- for (const program of core.programs) {
63
- if (!program.background) continue;
64
- const { priority, intervalNanoseconds } = program.background;
65
- const name = `TsplcBackground_${priority}_${BigInt(intervalNanoseconds) / 1000000n}`;
66
- const existing = backgroundTasks.get(name);
67
- backgroundTasks.set(name, {
68
- name,
69
- priority,
70
- intervalNanoseconds,
71
- programs: [...(existing?.programs ?? []), program.name],
72
- });
73
- }
74
- deployment = { tasks: [...deployment.tasks, ...backgroundTasks.values()] };
75
61
  const options = parsed.data;
76
62
  const diagnostics: Diagnostic[] = [];
77
63
  const report = (message: string, span?: SourceSpan): void => {
78
64
  diagnostics.push(diagnostic(message, span));
79
65
  };
80
- const callerTasks = deployment.tasks;
81
- for (const continuation of core.programs.filter((program) => program.continuation)) {
82
- const scope = continuation.continuation!;
83
- const root = core.variables.find(
84
- (variable) => variable.name === scope.instance && variable.span.file === scope.file,
85
- );
86
- if (!root) {
87
- report(`Missing continuation owner ${scope.instance}.`);
88
- continue;
89
- }
90
- const owners = callerTasks.filter((task) =>
91
- task.programs.some((name) => {
92
- const program = core.programs.find((program) => program.name === name);
93
- if (!program || program.background) return false;
94
- const fn = core.functions.find((fn) => fn.id === program.entry);
95
- return fn && accessedStorage(fn, core).has(root.id);
96
- }),
97
- );
98
- if (owners.length !== 1) {
99
- report(
100
- `Continuation owner ${scope.instance} must belong to exactly one caller task.`,
101
- root.span,
102
- );
103
- continue;
104
- }
105
- deployment = {
106
- tasks: deployment.tasks.map((task) =>
107
- task.name === owners[0]!.name
108
- ? { ...task, programs: [...task.programs, continuation.name] }
109
- : task,
110
- ),
111
- };
112
- }
66
+ deployment = { tasks: schedulePrograms(core, deployment.tasks, report) };
113
67
  const http = new Map<string, HttpEndpoint>();
114
68
  for (const block of core.blocks)
115
69
  if (block.runtime?.kind === "http" || block.runtime?.kind === "rpc") {
@@ -314,21 +268,10 @@ function ioBits(type: PlcType | undefined): number {
314
268
  }
315
269
 
316
270
  function validateTasks(core: VerifiedCoreModule, deployment: Deployment, report: Report): void {
317
- // One cyclic task owns each mutable storage root, including transitive calls
318
- // and borrowed globals. Sharing state within an ordered task remains valid.
319
271
  const programs = new Map(core.programs.map((program) => [program.name, program]));
320
272
  const assignment = new Map<string, string>();
321
273
  const taskNames = new Set<string>();
322
- const stateOwners = new Map<string, string>();
323
- const globals = new Map(core.variables.map((item) => [item.id, item]));
324
- const functions = new Map(core.functions.map((item) => [item.id, item]));
325
- const blockRuntimes = new Map(
326
- core.types.flatMap((type) =>
327
- type.kind === "block"
328
- ? [[type.id, core.blocks.find((block) => block.id === type.block)?.runtime?.kind] as const]
329
- : [],
330
- ),
331
- );
274
+ const checkOwnership = taskStorageOwnership(core, report);
332
275
  for (const task of deployment.tasks) {
333
276
  if (!isIecIdentifier(task.name)) report(`Invalid task name ${task.name}.`);
334
277
  if (taskNames.has(task.name.toUpperCase())) report(`Duplicate task name ${task.name}.`);
@@ -351,35 +294,7 @@ function validateTasks(core: VerifiedCoreModule, deployment: Deployment, report:
351
294
  }
352
295
  if (assignment.has(name)) report(`Program ${name} must belong to exactly one task.`);
353
296
  assignment.set(name, task.name);
354
- const fn = functions.get(program.entry);
355
- if (!fn) throw new Error("Verified core has a missing entry.");
356
- const host =
357
- program.background &&
358
- core.variables.find(
359
- (item) =>
360
- item.name === program.background?.worker &&
361
- item.span.file === fn.span.file &&
362
- blockRuntimes.get(item.type) === "backgroundHost",
363
- );
364
- for (const id of accessedStorage(fn, core, host ? host.id : undefined)) {
365
- const item = globals.get(id);
366
- if (!item || item.storage === "constant") continue;
367
- if (blockRuntimes.get(item.type) === "mailbox") continue;
368
- const ownsWorker =
369
- program.background?.worker === item.name && item.span.file === fn.span.file;
370
- if (program.background && (!ownsWorker || item.id === host?.id))
371
- report(
372
- `Background task ${program.name} accesses scan-owned storage ${item.name}; pass value snapshots through the mailbox.`,
373
- item.span,
374
- );
375
- const owner = stateOwners.get(id);
376
- if (owner && owner !== task.name)
377
- report(
378
- `Storage ${item.name} is shared across cyclic tasks ${owner} and ${task.name}.`,
379
- item.span,
380
- );
381
- stateOwners.set(id, task.name);
382
- }
297
+ checkOwnership(task.name, program);
383
298
  }
384
299
  }
385
300
  for (const name of programs.keys())
@@ -4,9 +4,86 @@ import type {
4
4
  CoreFunction,
5
5
  CorePath,
6
6
  CoreStatement,
7
+ PlcProgram,
8
+ SourceSpan,
9
+ Task,
7
10
  VerifiedCoreModule,
8
11
  } from "@tsplc/compiler/ir";
9
12
 
13
+ type Report = (message: string, span?: SourceSpan) => void;
14
+
15
+ export function schedulePrograms(
16
+ core: VerifiedCoreModule,
17
+ tasks: readonly Task[],
18
+ report: Report,
19
+ ): readonly Task[] {
20
+ const background = new Map<string, Task & { programs: string[] }>();
21
+ for (const program of core.programs) {
22
+ if (!program.background) continue;
23
+ const { priority, intervalNanoseconds } = program.background;
24
+ const name = `TsplcBackground_${priority}_${BigInt(intervalNanoseconds) / 1_000_000n}`;
25
+ const existing = background.get(name);
26
+ if (existing) {
27
+ existing.programs.push(program.name);
28
+ continue;
29
+ }
30
+ background.set(name, {
31
+ name,
32
+ priority,
33
+ intervalNanoseconds,
34
+ programs: [program.name],
35
+ });
36
+ }
37
+ return appendContinuations(core, [...tasks, ...background.values()], report);
38
+ }
39
+
40
+ /** The generated worker may enter its host, and only mailbox roots are shared between tasks. */
41
+ export function taskStorageOwnership(
42
+ core: VerifiedCoreModule,
43
+ report: Report,
44
+ ): (task: string, program: PlcProgram) => void {
45
+ const owners = new Map<string, string>();
46
+ const globals = new Map(core.variables.map((item) => [item.id, item]));
47
+ const functions = new Map(core.functions.map((item) => [item.id, item]));
48
+ const blocks = new Map(core.blocks.map((block) => [block.id, block.runtime?.kind]));
49
+ const runtimes = new Map(
50
+ core.types.flatMap((type) =>
51
+ type.kind === "block" ? [[type.id, blocks.get(type.block)] as const] : [],
52
+ ),
53
+ );
54
+
55
+ return (task, program) => {
56
+ const fn = functions.get(program.entry);
57
+ if (!fn) throw new Error("Verified core has a missing entry.");
58
+ const worker = program.background?.worker;
59
+ const host =
60
+ worker &&
61
+ core.variables.find(
62
+ (item) =>
63
+ item.name === worker &&
64
+ item.span.file === fn.span.file &&
65
+ runtimes.get(item.type) === "backgroundHost",
66
+ );
67
+ for (const id of accessedStorage(fn, core, host ? host.id : undefined)) {
68
+ const item = globals.get(id);
69
+ if (!item || item.storage === "constant" || runtimes.get(item.type) === "mailbox") continue;
70
+ const ownsWorker = worker === item.name && item.span.file === fn.span.file;
71
+ if (program.background && (!ownsWorker || (host && item.id === host.id)))
72
+ report(
73
+ `Background task ${program.name} accesses scan-owned storage ${item.name}; pass value snapshots through the mailbox.`,
74
+ item.span,
75
+ );
76
+ const owner = owners.get(id);
77
+ if (owner && owner !== task)
78
+ report(
79
+ `Storage ${item.name} is shared across cyclic tasks ${owner} and ${task}.`,
80
+ item.span,
81
+ );
82
+ owners.set(id, task);
83
+ }
84
+ };
85
+ }
86
+
10
87
  // Follow both helper calls and instance updates. Borrow addresses count as storage
11
88
  // access, so scheduling cannot accidentally give one mutable record two owners.
12
89
  export function accessedStorage(
@@ -116,3 +193,51 @@ export function accessedStorage(
116
193
  body(fn.body);
117
194
  return result;
118
195
  }
196
+
197
+ /** Detached continuations run after their caller programs in the same cyclic task. */
198
+ function appendContinuations(
199
+ core: VerifiedCoreModule,
200
+ tasks: readonly Task[],
201
+ report: Report,
202
+ ): readonly Task[] {
203
+ const continuations = core.programs.flatMap((program) =>
204
+ program.continuation ? [{ name: program.name, scope: program.continuation }] : [],
205
+ );
206
+ if (!continuations.length) return tasks;
207
+
208
+ const programs = new Map(core.programs.map((program) => [program.name, program]));
209
+ const functions = new Map(core.functions.map((routine) => [routine.id, routine]));
210
+ const placements = tasks.map((task): { task: Task; roots: Set<string>; additions: string[] } => {
211
+ const roots = new Set<string>();
212
+ for (const name of task.programs) {
213
+ const program = programs.get(name);
214
+ if (!program || program.background) continue;
215
+ const entry = functions.get(program.entry);
216
+ if (entry) for (const id of accessedStorage(entry, core)) roots.add(id);
217
+ }
218
+ return { task, roots, additions: [] };
219
+ });
220
+ for (const { name, scope } of continuations) {
221
+ const root = core.variables.find(
222
+ (variable) => variable.name === scope.instance && variable.span.file === scope.file,
223
+ );
224
+ if (!root) {
225
+ report(`Missing continuation owner ${scope.instance}.`);
226
+ continue;
227
+ }
228
+ const owners = placements.filter(({ roots }) => roots.has(root.id));
229
+ const [owner] = owners;
230
+ if (!owner || owners.length !== 1) {
231
+ report(
232
+ `Continuation owner ${scope.instance} must belong to exactly one caller task.`,
233
+ root.span,
234
+ );
235
+ continue;
236
+ }
237
+ owner.additions.push(name);
238
+ }
239
+ return placements.map(({ task, additions }) => ({
240
+ ...task,
241
+ programs: task.programs.concat(additions),
242
+ }));
243
+ }
package/dist/cli.js CHANGED
@@ -10,14 +10,11 @@ import ts from "typescript";
10
10
  import { typeCheckOnly } from "@tsplc/compiler";
11
11
  var resolveServerModule = (root, name) => name.startsWith(".") || name.startsWith("/") || existsSync(resolve(root, name)) ? resolve(root, name) : createRequire(join(root, "package.json")).resolve(name);
12
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;
13
+ var checkedServerEntry = (config, root) => {
14
+ const runtime = config.server?.runtime;
15
+ if (!runtime)
16
+ throw new Error("Configure server.runtime to build the server.");
18
17
  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
18
  const tsconfig = ts.findConfigFile(dirname(entry), ts.sys.fileExists);
22
19
  if (!tsconfig)
23
20
  throw new Error("Server entry requires a tsconfig.json.");
@@ -39,6 +36,12 @@ var buildServer = async (config, root) => {
39
36
  getNewLine: () => `
40
37
  `
41
38
  }));
39
+ return { entry, runtime };
40
+ };
41
+ var buildServer = async (config, root) => {
42
+ const { entry, runtime } = checkedServerEntry(config, root);
43
+ const windows = runtime.target ? runtime.target.includes("windows") : process.platform === "win32";
44
+ const outFile = resolve(root, "build/server", `${kebabCase(config.name)}-services${windows ? ".exe" : ""}`);
42
45
  await mkdir(dirname(outFile), { recursive: true });
43
46
  const staging = await mkdtemp(join(dirname(outFile), ".tsplc-server-"));
44
47
  try {
@@ -531,14 +534,11 @@ import ts2 from "typescript";
531
534
  import { typeCheckOnly as typeCheckOnly2 } from "@tsplc/compiler";
532
535
  var resolveServerModule2 = (root, name) => name.startsWith(".") || name.startsWith("/") || existsSync2(resolve3(root, name)) ? resolve3(root, name) : createRequire2(join4(root, "package.json")).resolve(name);
533
536
  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;
537
+ var checkedServerEntry2 = (config, root) => {
538
+ const runtime = config.server?.runtime;
539
+ if (!runtime)
540
+ throw new Error("Configure server.runtime to build the server.");
539
541
  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
542
  const tsconfig = ts2.findConfigFile(dirname4(entry), ts2.sys.fileExists);
543
543
  if (!tsconfig)
544
544
  throw new Error("Server entry requires a tsconfig.json.");
@@ -560,34 +560,25 @@ var buildServer2 = async (config, root) => {
560
560
  getNewLine: () => `
561
561
  `
562
562
  }));
563
+ return { entry, runtime };
564
+ };
565
+ var bundleServer = async (config, root) => {
566
+ const { entry } = checkedServerEntry2(config, root);
567
+ const outFile = resolve3(root, "build/server", `${kebabCase2(config.name)}-services.js`);
568
+ const result = await Bun.build({
569
+ entrypoints: [entry],
570
+ target: "bun",
571
+ minify: true,
572
+ env: "disable"
573
+ });
574
+ if (!result.success)
575
+ throw new AggregateError(result.logs, "Server bundle build failed.");
576
+ const [script, ...assets] = result.outputs;
577
+ if (!script || assets.length)
578
+ throw new Error(`The server bundle needs files beside its script (${assets.map(({ path }) => path).join(", ")}), which deployments don't carry.`);
563
579
  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
- }
580
+ await Bun.write(outFile, script);
581
+ return outFile;
591
582
  };
592
583
 
593
584
  // packages/compiler/project/plant.ts
@@ -663,6 +654,13 @@ var askCredentials = async () => ({
663
654
  password: await askSecret("Controller password: ")
664
655
  });
665
656
  var progress = (message) => console.log(` ${message}`);
657
+ var timedProgress = () => {
658
+ const start = Date.now();
659
+ return (message) => {
660
+ const seconds = Math.round((Date.now() - start) / 1000);
661
+ progress(`${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")} ${message}`);
662
+ };
663
+ };
666
664
  var connect = () => {
667
665
  const environment = z.object({
668
666
  TSPLC_AGENT_URL: z.url({
@@ -732,16 +730,16 @@ var prepareRelease = async (file, loaded, flags) => {
732
730
  { path: "template.project", content: Bun.file(settings.template) },
733
731
  ...libraries.map((library) => ({ path: library.release, content: Bun.file(library.path) }))
734
732
  ];
735
- for await (const source of new Bun.Glob("**/*").scan({ cwd: settings.sourceDir, dot: true }))
733
+ for await (const source of new Bun.Glob("**/*.st").scan({ cwd: settings.sourceDir, dot: true }))
736
734
  files.push({
737
735
  path: `st/${portable(source)}`,
738
736
  content: Bun.file(resolve4(settings.sourceDir, source))
739
737
  });
740
738
  let server;
741
739
  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) });
740
+ const script = await bundleServer(config, root);
741
+ server = `server/${basename(script)}`;
742
+ files.push({ path: server, content: Bun.file(script) });
745
743
  }
746
744
  const manifest = {
747
745
  config: {
@@ -770,9 +768,11 @@ var deploy = async (agent, file, loaded, flags) => {
770
768
  throw new Error("A login is open; log out before deploying: tsplc logout");
771
769
  const target = !status.target || flags.select ? await selectTarget(agent) : status.target;
772
770
  console.log(`Building the release for ${target.host}…`);
771
+ const building = timedProgress();
773
772
  const { release, diagnostics } = await prepareRelease(file, loaded, flags);
774
773
  if (!release)
775
774
  return { diagnostics };
775
+ building("Built");
776
776
  if (flags.download && !await confirm("A full download stops the running application while it loads. Continue?", flags))
777
777
  throw new Error("Cancelled.");
778
778
  const credentials = await askCredentials();
@@ -782,12 +782,14 @@ var deploy = async (agent, file, loaded, flags) => {
782
782
  initializeUser: flags["initialize-user"] ?? false
783
783
  };
784
784
  console.log(release.manifest.server ? "Deploying the server and the program…" : "Deploying the program…");
785
+ const deploying = timedProgress();
786
+ deploying("Uploading");
785
787
  try {
786
- return await agent.deploy(release, settings, progress);
788
+ return await agent.deploy(release, settings, deploying);
787
789
  } catch (error) {
788
790
  if (!(error instanceof AgentOperationError && error.downloadRequired) || !await confirm(`${error.message}. A full download stops the running application while it loads. Download now?`, flags))
789
791
  throw error;
790
- return agent.deploy(release, { ...settings, download: true }, progress);
792
+ return agent.deploy(release, { ...settings, download: true }, timedProgress());
791
793
  }
792
794
  };
793
795
  var ensureLogin = async (agent) => {
@@ -1392,5 +1394,5 @@ try {
1392
1394
  }
1393
1395
  }
1394
1396
 
1395
- //# debugId=2A9AAF31224302AE64756E2164756E21
1397
+ //# debugId=3D22720924D3B22B64756E2164756E21
1396
1398
  //# sourceMappingURL=cli.js.map