preserveFieldNames
Setting Name | preserveFieldNames |
|---|---|
Location | config.js (root of a Profound.js instance) |
Type | boolean |
Available Since | Profound.js 7.18.0 |
Affects | Profound.js runtime and the Converter (single source of truth) |
Runtime Impact | Yes — controls how RPG/CL identifiers containing # and @ are resolved at runtime |
Overview
In RPG, the characters # and @ are valid inside variable, field, file, record-format, subroutine, procedure, and program names. JavaScript does not allow these characters in identifiers.
preserveFieldNames controls how Profound.js handles that mismatch:
Setting | Behavior |
|---|---|
false (legacy behavior) | The Converter rewrites # and @ to . A field named myfield@ becomes myfield everywhere it is generated and referenced. The runtime resolves the renamed identifier directly. |
true (new behavior introduced in 7.18.0) | The Converter keeps the original name and emits this["..."] style accessors. The runtime resolves names through the same indirection. myfield@ stays myfield@ and is accessed as this["myfield@"]. |
Because the same flag drives both the Converter and the runtime, the generated JS files and the framework that runs them always agree on the symbol shape — provided the flag is not changed without reconverting (see warning below).
Default Value
Definition. The "default value" of a config setting is the value Profound.js resolves to when the setting is not present in config.js at all.
Default: false (resolves as undefined → falsy → legacy behavior).
This default exists for backward compatibility. Customer instances that were created before 7.18.0 already have JS files generated with the underscore-replacement rule. Their config.js contains no preserveFieldNames entry, the runtime treats it as falsy, and both halves remain consistent with the JS already on disk.
Behavior by Instance Type
Brand-new instances created on Profound.js 7.18.0 or later
The scaffold writes preserveFieldNames: true into the generated config.js. New instances opt into the new behavior from day one.
Existing instances upgraded to 7.18.0 or later
Upgrading the Profound.js runtime does not modify a customer's existing config.js. The setting stays absent (or stays at whatever value it had), and the runtime continues to use the legacy behavior. No customer action is required to keep working after an upgrade.
Changing preserveFieldNames on an existing instance
Reconversion required: preserveFieldNames controls the shape of the generated JS. Flipping the flag without reconverting will break the application, because the JS on disk references symbols in one shape while the runtime resolves them in the other.
If a customer changes preserveFieldNames on an existing instance, they MUST re-run the Converter against all RPG/CL source so the generated JS matches the new setting.
This applies in both directions (false → true and true → false).
Scope of the new behavior in 7.18.0
When preserveFieldNames: true, all three originally planned phases are active:
Phase | Description | |
|---|---|---|
| 1 | Fields and data structures | RPG/CL field definitions and their uses, including database table fields, display file fields, printer file fields, and any externally discovered fields. |
| 2 | File names and record format names | Database tables, display files, printer files. Accessed via quoted / this[] syntax (e.g. display["myscreen@"].execute()). |
| 3 | Function, subroutine, procedure, and program-derived names | Exported and invoked via this["..."] / exports["..."] |
Examples
Each example shows:
Aspect | Description | |
|---|---|---|
| 1 | The RPG source | Original RPG code snippet. |
| 2 | Converter output without preserveFieldNames (legacy) | Output where # and @ are rewritten to _. |
| 3 | Converter output with preserveFieldNames: true | Output where names are preserved via this[...] / exports[...]. |
| 4 | Fields | Demonstrates field naming conventions. |
Fields
RPG:
dcl-s @output varchar(50);
@output = 'PASS';
dsply @output;Without preserveFieldNames (legacy):
_output = 'PASS';
console.log(_output);With preserveFieldNames: true:
this["@output"] = 'PASS';
console.log(this["@output"]);Data structure — QUALIFIED
RPG:
Dcl-Ds @someDS QUALIFIED;
@Field1 Char(10) inz('1');
#Field2 Char(10);
End-Ds;
if @someDS.@Field1 = '1';
output = 'PASS';
endif;Without preserveFieldNames (legacy):
if (_someDS._Field1.rtrim() === '1') {
output = 'PASS';
}With preserveFieldNames: true:
if (this["@someDS"]["@Field1"].rtrim() === '1') {
output = 'PASS';
}Data structure — NON-QUALIFIED
RPG:
Dcl-Ds @someDS;
@Field1 Char(10) inz('1');
#Field2 Char(10);
End-Ds;
if @Field1 = '1';
output = 'PASS';
endif;Without preserveFieldNames (legacy):
if (_Field1.rtrim() === '1') {
output = 'PASS';
}With preserveFieldNames: true:
if (this["@Field1"].rtrim() === '1') {
output = 'PASS';
}Note — why the new behavior helps even when collisions don't break things
Under the legacy behavior, the converter prevents JS-level clashes for RPG-defined symbols by appending a numeric suffix. @FIELD1 and #FIELD1 would emit as _FIELD1 and _FIELD12 respectively — the two fields stay distinct, but the suffixed names no longer resemble the RPG source, and the suffix order can shift if you add or remove fields. With preserveFieldNames: true the names stay readable and stable as this["@FIELD1"] and this["#FIELD1"].
Record format
RPG (printer file with a record format named REC#):
dcl-f printer9p printer Usage(*Output) UsrOpn;
open printer9p;
move testdata rec#;
write(e) DETAIL;
close printer9p;Without preserveFieldNames (legacy):
printer9p.open();
pjs.move(testdata, rec_);
try { printer9p.detail.write(); } catch (err) { pjs.setError(err); }
printer9p.close();With preserveFieldNames: true:
printer9p.open();
pjs.move(testdata, this["rec#"]);
try { printer9p.detail.write(); } catch (err) { pjs.setError(err); }
printer9p.close();Files (display / printer with @ or # in the file or format name)
RPG (display file MYSCRN@ with record format FMT#1):
dcl-f myscrn@ workstn;
exfmt fmt#1;Without preserveFieldNames (legacy):
// File and record-format names rewritten in-place.
// In some positions (e.g. file prefixes) the legacy rule cannot
// safely rename the symbol and the Converter raised an error instead.
myscrn_.fmt_1.execute();With preserveFieldNames: true:
display["myscrn@"]["fmt#1"].execute();Functions (program entrypoint with @ / # in the program name)
RPG (program named MY@PGM):
ctl-opt main(MY@PGM);
dcl-proc MY@PGM;
// ...
end-proc;Without preserveFieldNames (legacy):
function my_pgm() {
// ...
}
exports.default = my_pgm;With preserveFieldNames: true:
function my_pgm() { // JS function identifier itself stays sanitized
// ...
}
exports["my@pgm"] = my_pgm; // export key preserves the original program name
exports.default = my_pgm;Procedures
RPG (three procedures whose names differ only by _ / @ / # — a classic legacy collision):
DCALL_1 PR
D prParm_1 2p 0
D prParm_2 2p 0
DCALL@1 PR
D prParm@1 2p 0
D prParm@2 2p 0
DCALL#1 PR
D prParm#1 2p 0
D prParm#2 2p 0
PROC = %Paddr(CALL#1);
Callb PROC;Without preserveFieldNames (legacy):
// CALL_1, CALL@1, CALL#1 all sanitize to call_1 — the three
// distinct procedures collide into a single export.
exports["call_1"] = function () { /* which one? */ };
proc = pjs.procAddr("call_1");
this[proc](pjs.refParm("parm1"), pjs.refParm("parm2"));With preserveFieldNames: true:
exports["call_1"] = function () { /* CALL_1 */ };
exports["call@1"] = function () { /* CALL@1 */ };
exports["call#1"] = function () { /* CALL#1 */ };
proc = pjs.procAddr("call#1");
this[proc](pjs.refParm("parm1"), pjs.refParm("parm2"));Subroutines
RPG:
ExSr New@Rate;
BegSr New@Rate;
rate = 50;
EndSr;Without preserveFieldNames (legacy):
new_Rate();
function new_Rate() {
rate = 50;
}With preserveFieldNames: true:
this["new@Rate"]();
exports["new@Rate"] = function () {
rate = 50;
};CL Fields
Per the design, CL variable definitions and uses follow the same rule as RPG fields: under the legacy behavior the converter rewrites #/@ to _; with preserveFieldNames: true the original name is kept and accessed via this[…].
CL Source
DCL VAR(&MY@VAR) TYPE(*CHAR) LEN(10) VALUE('PASS')
DCL VAR(&CNT#) TYPE(*DEC) LEN(3 0)
CHGVAR VAR(&CNT#) VALUE(1)
SNDPGMMSG MSG(&MY@VAR)Without preserveFieldNames (legacy):
my_var = "PASS";
cnt_ = 0;
cnt_ = 1;
pjs.sndpgmmsg(my_var);With preserveFieldNames: true:
this["my@var"] = "PASS";
this["cnt#"] = 0;
this["cnt#"] = 1;
pjs.sndpgmmsg(this["my@var"]);SQL Host-Variable Binding
Host variables that contain # or @ are bound through the same mechanism as any other field — the difference is only in how the symbol is resolved when the binding array is constructed and when the result is fetched back.
RPG:
dcl-s @acct Char(15) inz(' 42110001111');
dcl-s actyp@ Char(2);
Exec Sql
Select Actyp
Into :actyp@
From gelmas
Where comno = 1
and gltyp = 1
and glano = :@acct;
if actyp@ = 'EX';
output = 'PASS';
endif;Without preserveFieldNames (legacy):
sql_statement1 = pjs.prepare(
"Select Actyp From gelmas Where comno = 1 and gltyp = 1 and glano = ?"
);
sql_statement1.bindParameters([
[acct, SQL_PARAM_INPUT]
]);
sql_statement1.execute();
pjs.fetch(sql_statement1, actyp);
if (actyp_ === 'EX') {
output = 'PASS';
}With preserveFieldNames: true:
sql_statement1 = pjs.prepare(
"Select Actyp From gelmas Where comno = 1 and gltyp = 1 and glano = ?"
);
sql_statement1.bindParameters([
[this["@acct"], SQL_PARAM_INPUT]
]);
sql_statement1.execute();
pjs.fetch(sql_statement1, this["actyp@"]);
if (this["actyp@"] === 'EX') {
output = 'PASS';
}The same applies to all SQL forms — EXEC SQL FETCH NEXT … INTO :hostVar, multi-row fetch into an array DS, SELECT INTO, etc. The host-variable expression is just another field reference.
Copybooks
Copybook contents are inlined at the /copy site. The same rule applies to anything declared inside the copybook — under the legacy behavior #/@ are rewritten in-place; with preserveFieldNames: true they are preserved at both the definition (inside the copybook) and at the call site (inside the including program).
RPG: (Main Program)
ctl-opt dftactgrp(*no);
/copy specchars_copy
exsr do@calc;
dsply result;
*InLr = *On;RPG: (specchars_copy Copybook)
dcl-s @result packed(7:0);
begsr do@calc;
@result = pow(2 : 8);
result = @result;
endsr;Without preserveFieldNames (legacy):
function spec_pgm() {
pjs.import("./*.js");
do_calc();
console.log(result);
flags["LR"] = true;
return result;
pjs.include("specchars_copy.js");
}
exports.default = spec_pgm;
exports["do_calc"] = function () {
result = Math.pow(2, 8); // collides with any other field named result
result = _result;
};With preserveFieldNames: true:
function spec_pgm() {
pjs.import("./*.js");
this["do@calc"];
console.log(result);
flags["LR"] = true;
return result;
pjs.include("specchars_copy.js");
}
exports.default = spec_pgm;
exports["do@calc"] = function () {
this["@result"] = Math.pow(2, 8);
result = this["@result"];
};Prefix-Derived Field Names
When an externally described file is declared with prefix(@) or prefix(#), every field in the record format gets that character prepended. Under the legacy behavior the prefix character was sanitized — and in some prefix positions the rename could not be performed safely and the Converter raised an error. With preserveFieldNames: true the prefix is preserved and the prefixed fields are accessed through this[…].
RPG:
dcl-f productsp keyed prefix(@);
dcl-s data varchar(50);
Chain (111) productsp;
If %Found(productsp);
data = %Char(@PRID);
EndIf;
dsply data;Without preserveFieldNames (legacy):
productsp.getRecord([111]);
if (productsp.found()) {
data = pjs.char(_prid); // prefix char dropped; in some prefix positions the Converter would have failed with a conversion error.
}
console.log(data);With preserveFieldNames: true:
productsp.getRecord([111]);
if (productsp.found()) {
data = pjs.char(this["@prid"]);
}
console.log(data);This category is one of the strongest reasons the new behavior exists: prefix-derived fields are the case where the legacy rule was least able to produce correct output, and where customers most often saw unavoidable conversion errors before 7.18.0.
Why the new behavior exists
The legacy output no longer matches the RPG source, which hurts readability and debugging.
Underscore replacement produced unreadable, unstable names: myfield@ and myfield# both sanitize to myfield_, and the converter then disambiguates by appending a numeric suffix (myfield_, myfield_2). The fields stay distinct, but the names no longer resemble the RPG source and can shift order if fields are added or removed.
Some positions (e.g. file prefix characters) cannot be replaced safely and previously produced conversion errors.
Every new framework integration (display files, printer files, tables, SQL) had to re-implement the rename rule. this["..."] indirection removes that burden.
Quick reference
Scenario | preserveFieldNames in config.js | Behavior | Reconversion needed? |
|---|---|---|---|
Pre-7.18.0 customer, upgraded to 7.18.0, no edits | absent | Legacy (underscore replacement) | No |
Brand-new 7.18.0+ instance | true (written by scaffold) | New (names preserved via this[]) | No |
Existing customer opts in | flipped from absent / false to true | New | Yes — reconvert all RPG/CL |
Existing customer reverts | flipped from true to false / absent | Legacy | Yes — reconvert all RPG/CL |