nodalis-compiler 1.0.19 → 1.0.21

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,7 +1,9 @@
1
1
  # Changelog
2
2
 
3
- ## [1.0.19] 2026-03-03
3
+ ## [1.0.21] 2026-03-03
4
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
+ - Fixed syntax errors with repeat.
5
7
 
6
8
  ## [1.0.17] 2026-02-25
7
9
  - Added support for compilation of multiple ST files as a single project.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodalis-compiler",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
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
  }
@@ -127,7 +127,7 @@ export function convertExpression(expr, isjsfb = false, jsfbVars = [], isjs=fals
127
127
  if (/^%[IQM][XBWDL]?\d+(\.\d+)?$/i.test(e)) return getReadAddressExpression(e);
128
128
 
129
129
  // Don't wrap literals or operators
130
- if (/^(true|false|null|[+-]?\d+|[+-]?(?:(?:\d+\.\d*|\d*\.\d+)(?:[eE][+-]?\d+)?|\d+[eE][+-]?\d+)|0[bB][01]+|0[oO][0-7]+|0[xX][0-9a-f]+|!|&&|\|\||==|!=|[<>=+\-*/(),&|^])$/i.test(e)) return e;
130
+ if (/^(true|false|null|[+-]?\d+|[+-]?(?:(?:\d+\.\d*|\d*\.\d+)(?:[eE][+-]?\d+)?|\d+[eE][+-]?\d+)|0[bB][01]+|0[oO][0-7]+|0[xX][0-9a-f]+|!|&&|\|\||==|!=|>=|<=|>|<|=|\+|-|\*|\/|%|\(|\)|,|&|\||\^)$/i.test(e)) return e;
131
131
 
132
132
  // Don't wrap known function expressions (e.g., getBit)
133
133
  if (/^getBit\(/.test(e)) return e;
@@ -292,9 +292,10 @@ function parseStatementsUntil(endTokens) {
292
292
  const body = parseStatements('UNTIL');
293
293
  expect('UNTIL');
294
294
  const condition = [];
295
- while (peek() && peek().value !== ';') {
295
+ while (peek() && peek().value.toUpperCase() !== 'END_REPEAT') {
296
296
  condition.push(consume().value);
297
297
  }
298
+ expect('END_REPEAT');
298
299
  if (peek()?.value === ';') consume();
299
300
  return { type: 'REPEAT', condition, body };
300
301
  }
@@ -72,20 +72,25 @@ while ((match = regex.exec(code)) !== null) {
72
72
  }
73
73
 
74
74
  function normalizeNumericLiteral(value) {
75
- const boolMatch = String(value).match(new RegExp(`^${BOOL_TYPE_PATTERN}#(TRUE|FALSE|1|0)$`, 'i'));
75
+ const literal = String(value);
76
+ const boolMatch = literal.match(new RegExp(`^${BOOL_TYPE_PATTERN}#(TRUE|FALSE|1|0)$`, 'i'));
76
77
  if (boolMatch) {
77
78
  const boolLiteral = boolMatch[1].toUpperCase();
78
79
  if (boolLiteral === 'TRUE' || boolLiteral === '1') return 'TRUE';
79
80
  return 'FALSE';
80
81
  }
81
82
 
82
- const typedValue = String(value).replace(
83
+ const typedValue = literal.replace(
83
84
  new RegExp(`^(?:${INTEGER_TYPE_PATTERN}|${REAL_TYPE_PATTERN}|${BOOL_TYPE_PATTERN})#`, 'i'),
84
85
  ''
85
86
  );
86
87
 
87
88
  const match = typedValue.match(/^(2|8|16)#([0-9a-f_]+)$/i);
88
- if (!match) return typedValue.replace(/_/g, '');
89
+ if (!match) {
90
+ const normalized = typedValue.replace(/_/g, '');
91
+ if (/^[+-]?\d+$/.test(normalized)) return normalizeDecimalIntegerLiteral(normalized);
92
+ return normalized;
93
+ }
89
94
  const radix = match[1];
90
95
  const digits = match[2].replace(/_/g, '');
91
96
  if (radix === '2') return `0b${digits}`;
@@ -93,6 +98,12 @@ function normalizeNumericLiteral(value) {
93
98
  return `0x${digits}`;
94
99
  }
95
100
 
101
+ function normalizeDecimalIntegerLiteral(value) {
102
+ const sign = value.startsWith('-') ? '-' : '';
103
+ const digits = value.replace(/^[+-]?/, '').replace(/^0+(?=\d)/, '');
104
+ return `${sign}${digits || '0'}`;
105
+ }
106
+
96
107
  function getTokenType(value) {
97
108
  const keywords = new Set([
98
109
  'PROGRAM', 'FUNCTION_BLOCK', 'FUNCTION', 'VAR_INPUT', 'VAR_OUTPUT', 'VAR', 'END_VAR',
@@ -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) {