nodalis-compiler 1.0.18 → 1.0.20

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.20] 2026-03-03
4
+ - Changed IEC parser to interpret Function Blocks that are actually standard functions to a formal function call.
5
+ - Fixed nodejs/jint compiles to put executables in a bin folder.
6
+
3
7
  ## [1.0.17] 2026-02-25
4
8
  - Added support for compilation of multiple ST files as a single project.
5
9
  - Added support for formal parameters.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodalis-compiler",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
4
4
  "description": "Compiles IEC-61131-3/10 languages into code that can be used as a PLC on multiple platforms.",
5
5
  "icon": "nodalis.png",
6
6
  "main": "src/nodalis.js",
@@ -255,7 +255,7 @@ void loop() {
255
255
  fs.cpSync(path.join(supportDir, 'gpio.cpp'), path.join(outputPath, 'gpio.cpp'), { force: true });
256
256
  fs.cpSync(path.join(supportDir, 'json.hpp'), path.join(outputPath, 'json.hpp'), { force: true });
257
257
 
258
- if (outputType === 'executable') {
258
+ if (this.isExecutableOutput()) {
259
259
  const arduinoCli = compilerConfig.arduino_cli || compilerConfig.arduinoCli || 'arduino-cli';
260
260
  const arduinoFqbn = this.resolveArduinoFqbn(target, compilerConfig);
261
261
  if (!arduinoFqbn) {
@@ -266,7 +266,7 @@ void loop() {
266
266
  this.ensureArduinoCoreInstalled(arduinoCli, arduinoFqbn);
267
267
 
268
268
  const buildDir = path.join(outputPath, 'build');
269
- const binDir = path.join(outputPath, 'bin');
269
+ const binDir = this.getExecutableOutputPath();
270
270
  fs.mkdirSync(buildDir, { recursive: true });
271
271
  fs.mkdirSync(binDir, { recursive: true });
272
272
  const arduinoCompileCmd = `${arduinoCli} compile --fqbn "${arduinoFqbn}" "${outputPath}" --build-path "${buildDir}" --output-dir "${binDir}" --export-binaries`;
@@ -260,7 +260,7 @@ int main() {
260
260
  const pathTo = name => path.join(outputPath, name);
261
261
  const targetInfo = this.resolveTarget(target);
262
262
 
263
- if (outputType === 'executable') {
263
+ if (this.isExecutableOutput()) {
264
264
  const requestedTarget = target ?? `${targetInfo.os}-${targetInfo.arch}`;
265
265
  const hostOs = this.getHostOS();
266
266
  const hostArch = this.getHostArch();
@@ -271,7 +271,7 @@ int main() {
271
271
  const archFlags = this.getArchFlags(targetInfo.os, targetInfo.arch, compiler);
272
272
  const formatFlags = (flags = []) => (flags.length ? `${flags.join(' ')} ` : '');
273
273
  const isWindowsTarget = targetInfo.os === 'windows';
274
- const binDir = path.join(outputPath, 'bin');
274
+ const binDir = this.getExecutableOutputPath();
275
275
  fs.mkdirSync(binDir, { recursive: true });
276
276
 
277
277
  // Step 2: Compile open62541.c with C compiler
@@ -14,6 +14,8 @@
14
14
  // See the License for the specific language governing permissions and
15
15
  // limitations under the License.
16
16
 
17
+ import path from 'path';
18
+
17
19
  export const IECLanguage = Object.freeze({
18
20
  LADDER_DIAGRAM: 'LD',
19
21
  STRUCTURED_TEXT: 'ST',
@@ -60,6 +62,14 @@ export class Compiler {
60
62
  this.options = options;
61
63
  }
62
64
 
65
+ isExecutableOutput() {
66
+ return this.options?.outputType === OutputType.EXECUTABLE;
67
+ }
68
+
69
+ getExecutableOutputPath() {
70
+ return path.join(this.options.outputPath, 'bin');
71
+ }
72
+
63
73
  /** @returns {string[]} */
64
74
  get supportedLanguages() {
65
75
  throw new Error('supportedLanguages must be implemented by subclass.');
@@ -224,8 +224,8 @@ export function run(){
224
224
  }
225
225
  fs.mkdirSync(outputPath, { recursive: true });
226
226
  fs.writeFileSync(jsFile, jsCode);
227
- const binDir = path.join(outputPath, 'bin');
228
- if (outputType === 'executable') {
227
+ const binDir = this.getExecutableOutputPath();
228
+ if (this.isExecutableOutput()) {
229
229
  fs.mkdirSync(binDir, { recursive: true });
230
230
  }
231
231
  if(sourcePath.toLowerCase().endsWith(".iec") || sourcePath.toLowerCase().endsWith(".xml") || directoryBundleMode){
@@ -243,34 +243,37 @@ export function run(){
243
243
  ];
244
244
 
245
245
  let coreDir = path.resolve(__dirname + '/support/nodejs');
246
+ const runtimeOutputDir = this.isExecutableOutput() ? binDir : outputPath;
246
247
 
247
248
  for (const file of coreFiles) {
248
- fs.copyFileSync(path.join(coreDir, file), path.join(outputPath, file));
249
+ fs.copyFileSync(path.join(coreDir, file), path.join(runtimeOutputDir, file));
249
250
  }
250
- if (outputType === 'executable') {
251
- fs.copyFileSync(jsFile, path.join(binDir, 'nodalisplc.js'));
251
+ if (this.isExecutableOutput()) {
252
+ fs.copyFileSync(jsFile, path.join(runtimeOutputDir, 'nodalisplc.js'));
252
253
  }
253
- writePackageJson(outputPath, plcname);
254
- installDependencies(outputPath);
254
+ writePackageJson(runtimeOutputDir, plcname);
255
+ installDependencies(runtimeOutputDir);
255
256
  }
256
257
 
257
258
 
258
- if (target === "jint" && outputType === "executable") {
259
+ if (target === "jint" && this.isExecutableOutput()) {
259
260
  const supportDir = path.resolve(__dirname, "support/jint/Nodalis");
260
261
  const buildScript = os.platform() === "win32" ? "build.bat" : "build.sh";
262
+ const projectOutputDir = outputPath;
263
+ const runtimeOutputDir = binDir;
261
264
 
262
- // 1. Copy all files from support/jint/nodalis to the output directory
263
- fs.cpSync(supportDir, outputPath, { recursive: true });
265
+ // Keep the Jint project sources in the main output directory.
266
+ fs.cpSync(supportDir, projectOutputDir, { recursive: true, force: true });
264
267
 
265
- // 2. Run the build script inside the output directory
266
- const buildPath = path.resolve(path.join(outputPath, buildScript));
268
+ // Build from the main output directory, then move publish artifacts under bin.
269
+ const buildPath = path.resolve(path.join(projectOutputDir, buildScript));
267
270
  if(buildPath.endsWith(".sh")){
268
271
  fs.chmodSync(buildPath, 0o755); // make executable
269
272
  }
270
- execSync(buildPath, { cwd: path.resolve(outputPath), stdio: "inherit", shell: true });
273
+ execSync(buildPath, { cwd: path.resolve(projectOutputDir), stdio: "inherit", shell: true });
271
274
 
272
275
  // 3. Copy the generated JS file to each publish folder
273
- const publishRoot = path.join(outputPath, "publish");
276
+ const publishRoot = path.join(projectOutputDir, "publish");
274
277
 
275
278
  const platforms = fs.readdirSync(publishRoot, { withFileTypes: true })
276
279
  .filter(d => d.isDirectory())
@@ -301,7 +304,7 @@ export function run(){
301
304
 
302
305
  for (const platformDir of platforms) {
303
306
  const platformName = path.basename(platformDir);
304
- const platformBinDir = path.join(binDir, platformName);
307
+ const platformBinDir = path.join(runtimeOutputDir, platformName);
305
308
  fs.mkdirSync(platformBinDir, { recursive: true });
306
309
  fs.cpSync(platformDir, platformBinDir, { recursive: true, force: true });
307
310
  }
@@ -1845,6 +1845,35 @@ export class Rung extends Serializable {
1845
1845
  return this.Objects.find((v) => v.ID == id);
1846
1846
  }
1847
1847
 
1848
+ /**
1849
+ * Finds the object with the referenced output point ID.
1850
+ * @param {string} refID The ID of the output point to use in searching for the object.
1851
+ * @returns {FbdObject|LdObject} Returns the object with the output, or null if there is no object with that ID.
1852
+ */
1853
+ findObjectWithOutput(refID) {
1854
+ return this.Objects.find(
1855
+ /**
1856
+ *
1857
+ * @param {FbdObject|LdObject|CommonObject} o
1858
+ */
1859
+ (o) => {
1860
+ if (o.Type === "Block") {
1861
+ const v = o.OutputVariables.Variables.find(
1862
+ /**
1863
+ *
1864
+ * @param {OutputVariable} ov
1865
+ */
1866
+ (ov) => {
1867
+ return ov.OutputPoint.ID === refID;
1868
+ });
1869
+ return typeof ov !== "undefined";
1870
+ }
1871
+ else if (o.Type !== "Comment") {
1872
+ return typeof o.Outputs.find(op => op.ID === refID) !== "undefined";
1873
+ }
1874
+ });
1875
+ }
1876
+
1848
1877
  /**
1849
1878
  *
1850
1879
  * @param {LdObject | FbdObject} obj The ladder logic object from which to find connections.
@@ -2084,19 +2113,65 @@ export class Rung extends Serializable {
2084
2113
  if(expression.length > 0){
2085
2114
  expression += " OR ";
2086
2115
  }
2087
- if(con instanceof FbdObject){
2088
- var v = con.OutputVariables.Variables.find(iv => start.hasInput(iv.OutputPoint.ID));
2089
- if(isValid(v)){
2090
- expression += con.toST(v.ParameterName);
2116
+ if (con instanceof FbdObject) {
2117
+ const sb = FbdObject.getStandardBlock(con.TypeName);
2118
+ if (sb && sb.Style === "F") { //this is a function, not a function block.
2119
+ let fcall = con.TypeName + "(";
2120
+ forEachElem(con.InputVariables.Variables,
2121
+ /**
2122
+ *
2123
+ * @param {InputVariable} v
2124
+ */
2125
+ (v) => {
2126
+ if (v.InputPoint.Connections.length > 0) {
2127
+ let assign = "";
2128
+ forEachElem(v.InputPoint.Connections,
2129
+ /**
2130
+ *
2131
+ * @param {Connection} c
2132
+ */
2133
+ (c) => {
2134
+ const o = this.findObjectWithOutput(c.RefID);
2135
+ if (o) {
2136
+ if (assign.length > 0) {
2137
+ assign += " OR ";
2138
+ }
2139
+ assign += this.#buildExpression(o);
2140
+ }
2141
+ }
2142
+ );
2143
+ if (v.Negated === "true") {
2144
+ assign = "NOT " + assign;
2145
+ }
2146
+ if (!fcall.endsWith("(")) {
2147
+ fcall += ", ";
2148
+ }
2149
+ fcall += `${v.ParameterName} := ${assign}`;
2150
+ }
2151
+ });
2152
+ expression += `${fcall})`;
2091
2153
  }
2154
+ else {
2155
+ var v = con.OutputVariables.Variables.find(iv => start.hasInput(iv.OutputPoint.ID));
2156
+ if (isValid(v)) {
2157
+ expression += con.toST(v.ParameterName);
2158
+ }
2159
+ }
2160
+
2092
2161
  }
2093
2162
  else if(con.Type !== "LeftPowerRail"){
2094
2163
  expression += `(${this.#buildExpression(con)})`;
2095
2164
  }
2096
2165
  }
2097
2166
  );
2098
- if(start.Type !== "Coil"){
2099
- expression = `${start.toST()}${expression.length > 0 ? " AND (" + expression + ")" : ""}`;
2167
+ if (start.Type !== "Coil") {
2168
+ if (start.Type === "DataSink") {
2169
+ expression = start.toST("", expression);
2170
+ }
2171
+ else {
2172
+
2173
+ expression = `${start.toST()}${expression.length > 0 ? " AND (" + expression + ")" : ""}`;
2174
+ }
2100
2175
  }
2101
2176
  return expression;
2102
2177
  }
@@ -2124,6 +2199,34 @@ export class Rung extends Serializable {
2124
2199
  (block) => {
2125
2200
  if(done.includes(block.ID)) return;
2126
2201
  done.push(block.ID);
2202
+ const sb = FbdObject.getStandardBlock(block.TypeName);
2203
+ if (typeof sb !== "undefined" && sb.Style === "F") {
2204
+ //if this is a function and connects with standard ladder logic, then we wait and deal with this in the next step.
2205
+ //If it doesn't connect with the rest of the rung, then we need to deal with it here.
2206
+ if (this.findConnections(block, true).find(o => o.Type === "Contact" || o.Type === "CompareContact" || o.Type === "Coil" || o.Type === "RightPowerRail")) {
2207
+ return;
2208
+ }
2209
+ else {
2210
+ forEachElem(block.OutputVariables.Variables,
2211
+ /**
2212
+ *
2213
+ * @param {OutputVariable} outvar
2214
+ */
2215
+ (outvar) => {
2216
+
2217
+
2218
+ var inobj = this.Objects.find(o => o.hasInput(outvar.OutputPoint.ID));
2219
+ if (isValid(inobj)) {
2220
+ if (inobj.Type === "DataSink") {
2221
+ st += this.#buildExpression(inobj) + ";\n";
2222
+ }
2223
+ }
2224
+
2225
+ }
2226
+ );
2227
+ return;
2228
+ }
2229
+ }
2127
2230
  forEachElem(block.InputVariables.Variables,
2128
2231
  /**
2129
2232
  *
@@ -2898,7 +3001,7 @@ export class Connection extends Serializable{
2898
3001
  export class FbdObject extends Serializable {
2899
3002
 
2900
3003
  /**
2901
- * @type {{TypeName: string, InputVariables: string[], OutputVariables: string[]}[]}
3004
+ * @type {{TypeName: string, InputVariables: string[], OutputVariables: string[], Style: string}[]}
2902
3005
  */
2903
3006
  static StandardBlocks = [
2904
3007
  {
package/src/nodalis.js CHANGED
@@ -128,11 +128,7 @@ export class Nodalis {
128
128
  outputPath,
129
129
  resourceName,
130
130
  sourcePath,
131
- language,
132
- codesysCommand,
133
- codesysCommandTemplate,
134
- codesysProjectFile,
135
- codesysPouName
131
+ language
136
132
  }) {
137
133
  validateFileExtension(language, sourcePath, resourceName);
138
134
 
@@ -147,11 +143,7 @@ export class Nodalis {
147
143
  resourceName,
148
144
  target,
149
145
  outputType,
150
- language,
151
- codesysCommand,
152
- codesysCommandTemplate,
153
- codesysProjectFile,
154
- codesysPouName
146
+ language
155
147
  };
156
148
 
157
149
  await compiler.compile();
@@ -203,6 +195,9 @@ Usage:
203
195
  Actions:
204
196
  --action list-compilers
205
197
  Lists all available compilers and their supported targets, languages, protocols, and versions.
198
+
199
+ --action list-programmers
200
+ Lists all available programmers and their supported targets.
206
201
 
207
202
  --action compile
208
203
  Required options:
@@ -212,10 +207,6 @@ Actions:
212
207
  --resourceName Resource name (used for .iec projects)
213
208
  --sourcePath Path to source file (.st or .iec)
214
209
  --language st (Structured Text) or ld (Ladder Diagram)
215
- --codesysCommand Optional CODESYS executable/command for --target codesys
216
- --codesysCommandTemplate Optional shell template for --target codesys executable builds
217
- --codesysProjectFile Optional .project path for CODESYS automation (defaults to <output>/<resource>.project)
218
- --codesysPouName Optional POU name to create/update in CODESYS project
219
210
 
220
211
  --action deploy Programs a device based on a protocol.
221
212
  --target The device/protocol targeted for programming.
@@ -261,6 +252,11 @@ Examples:
261
252
  console.log(JSON.stringify(list, null, 2));
262
253
  break;
263
254
  }
255
+ case 'list-programmers': {
256
+ const list = app.listProgrammers();
257
+ console.log(JSON.stringify(list, null, 2));
258
+ break;
259
+ }
264
260
 
265
261
  case 'compile': {
266
262
  app.compile({
@@ -269,11 +265,7 @@ Examples:
269
265
  outputPath: argMap.outputPath,
270
266
  resourceName: argMap.resourceName,
271
267
  sourcePath: argMap.sourcePath,
272
- language: argMap.language,
273
- codesysCommand: argMap.codesysCommand,
274
- codesysCommandTemplate: argMap.codesysCommandTemplate,
275
- codesysProjectFile: argMap.codesysProjectFile,
276
- codesysPouName: argMap.codesysPouName
268
+ language: argMap.language
277
269
  }).then(() => {
278
270
  console.log('Compilation completed.');
279
271
  }).catch(err => {
@@ -306,7 +298,7 @@ Examples:
306
298
 
307
299
  default: {
308
300
  console.error(`Unknown or missing action: ${argMap.action}`);
309
- console.error(`Valid actions: list-compilers, compile, deploy`);
301
+ console.error(`Valid actions: list-compilers, list-programmers, compile, deploy`);
310
302
  break;
311
303
  }
312
304
  }
@@ -235,24 +235,41 @@ export async function inferEntryPoint(sourcePath, runtime = 'auto', providedEntr
235
235
  }
236
236
 
237
237
  const candidates = ['nodalisplc', 'nodalisplc.exe', 'nodalisplc.js'];
238
- for (const candidate of candidates) {
239
- if (await exists(path.join(sourcePath, candidate))) {
240
- return candidate;
238
+ const candidateDirectories = ['bin', ''];
239
+ for (const directory of candidateDirectories) {
240
+ for (const candidate of candidates) {
241
+ const relativeCandidate = directory ? path.join(directory, candidate) : candidate;
242
+ if (await exists(path.join(sourcePath, relativeCandidate))) {
243
+ return toPosixPath(relativeCandidate);
244
+ }
241
245
  }
242
246
  }
243
247
 
248
+ const inferredRuntime = inferRuntime('', runtime);
249
+ if (inferredRuntime === 'node') {
250
+ const binNodeEntry = path.join('bin', 'nodalisplc.js');
251
+ if (await exists(path.join(sourcePath, binNodeEntry))) {
252
+ return toPosixPath(binNodeEntry);
253
+ }
254
+ return "nodalisplc.js"
255
+ }
256
+
244
257
  const entries = await fs.readdir(sourcePath, { withFileTypes: true });
245
258
  const files = entries.filter(entry => entry.isFile()).map(entry => entry.name);
246
- if (files.length === 0) {
247
- throw new Error(`Could not infer entry point for source directory: ${sourcePath}`);
259
+ if (files.length > 0) {
260
+ return files[0];
248
261
  }
249
262
 
250
- const inferredRuntime = inferRuntime('', runtime);
251
- if (inferredRuntime === 'node') {
252
- return "nodalisplc.js"
263
+ const binEntriesPath = path.join(sourcePath, 'bin');
264
+ if (await exists(binEntriesPath)) {
265
+ const binEntries = await fs.readdir(binEntriesPath, { withFileTypes: true });
266
+ const binFiles = binEntries.filter(entry => entry.isFile()).map(entry => entry.name);
267
+ if (binFiles.length > 0) {
268
+ return toPosixPath(path.join('bin', binFiles[0]));
269
+ }
253
270
  }
254
271
 
255
- return files[0];
272
+ throw new Error(`Could not infer entry point for source directory: ${sourcePath}`);
256
273
  }
257
274
 
258
275
  export function quoteForPosixSingle(value) {