@pgpmjs/transform 0.17.0 → 0.19.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/bundle-driver.d.ts +1 -1
- package/catalog-check.d.ts +47 -0
- package/catalog-check.js +130 -0
- package/esm/catalog-check.js +124 -0
- package/esm/fixture-closure.js +1 -1
- package/esm/index.js +11 -6
- package/esm/module-emit.js +65 -0
- package/esm/module-source.js +54 -0
- package/esm/partition.js +89 -0
- package/esm/restructure.js +62 -0
- package/esm/semantic-diff-driver.js +72 -26
- package/fixture-closure.d.ts +1 -1
- package/fixture-closure.js +1 -1
- package/index.d.ts +20 -10
- package/index.js +34 -16
- package/module-emit.d.ts +39 -0
- package/module-emit.js +104 -0
- package/module-source.d.ts +33 -0
- package/module-source.js +92 -0
- package/package.json +7 -6
- package/partition.d.ts +32 -0
- package/partition.js +127 -0
- package/restructure.d.ts +38 -0
- package/restructure.js +67 -0
- package/semantic-diff-driver.d.ts +7 -4
- package/semantic-diff-driver.js +71 -25
package/partition.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.partitionExportRows = exports.parsePartitionConfig = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const plpgsql_parser_1 = require("plpgsql-parser");
|
|
39
|
+
const partition_driver_1 = require("./partition-driver");
|
|
40
|
+
const regen_1 = require("./regen");
|
|
41
|
+
const PATH_STYLES = ['directory', 'flat'];
|
|
42
|
+
/**
|
|
43
|
+
* Parse and validate a partition config file (JSON matching
|
|
44
|
+
* `PartitionConfig`: rules, defaultPackage, optional style/splitRiders).
|
|
45
|
+
* Throws with a message naming the offending field.
|
|
46
|
+
*/
|
|
47
|
+
const parsePartitionConfig = (filePath) => {
|
|
48
|
+
let raw;
|
|
49
|
+
try {
|
|
50
|
+
raw = fs.readFileSync(filePath, 'utf-8');
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
throw new Error(`Partition config not found: ${filePath}`);
|
|
54
|
+
}
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(raw);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
throw new Error(`Partition config is not valid JSON (${filePath}): ${err?.message ?? err}`);
|
|
61
|
+
}
|
|
62
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
63
|
+
throw new Error(`Partition config must be a JSON object (${filePath})`);
|
|
64
|
+
}
|
|
65
|
+
if (typeof parsed.defaultPackage !== 'string' || !parsed.defaultPackage) {
|
|
66
|
+
throw new Error('Partition config: "defaultPackage" must be a non-empty string');
|
|
67
|
+
}
|
|
68
|
+
if (!Array.isArray(parsed.rules)) {
|
|
69
|
+
throw new Error('Partition config: "rules" must be an array');
|
|
70
|
+
}
|
|
71
|
+
parsed.rules.forEach((rule, i) => {
|
|
72
|
+
if (typeof rule !== 'object' || rule === null) {
|
|
73
|
+
throw new Error(`Partition config: rules[${i}] must be an object`);
|
|
74
|
+
}
|
|
75
|
+
if (typeof rule.package !== 'string' || !rule.package) {
|
|
76
|
+
throw new Error(`Partition config: rules[${i}].package must be a non-empty string`);
|
|
77
|
+
}
|
|
78
|
+
if (!Array.isArray(rule.select) || rule.select.length === 0) {
|
|
79
|
+
throw new Error(`Partition config: rules[${i}].select must be a non-empty array of selectors`);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
if (parsed.style !== undefined && !PATH_STYLES.includes(parsed.style)) {
|
|
83
|
+
throw new Error(`Partition config: "style" must be one of: ${PATH_STYLES.join(', ')}`);
|
|
84
|
+
}
|
|
85
|
+
if (parsed.splitRiders !== undefined && !Array.isArray(parsed.splitRiders)) {
|
|
86
|
+
throw new Error('Partition config: "splitRiders" must be an array of statement kinds');
|
|
87
|
+
}
|
|
88
|
+
return parsed;
|
|
89
|
+
};
|
|
90
|
+
exports.parsePartitionConfig = parsePartitionConfig;
|
|
91
|
+
/**
|
|
92
|
+
* Partition changes into packages and generate revert/verify for every
|
|
93
|
+
* emitted change. Cross-package dependencies keep the `<pkg>:<path>`
|
|
94
|
+
* convention in `deps`. May throw `PartitionCycleError`.
|
|
95
|
+
*/
|
|
96
|
+
const partitionExportRows = async (rows, config) => {
|
|
97
|
+
await (0, plpgsql_parser_1.loadModule)();
|
|
98
|
+
const input = rows.map(row => ({
|
|
99
|
+
name: row.deploy,
|
|
100
|
+
dependencies: row.deps ?? [],
|
|
101
|
+
deploy: row.content
|
|
102
|
+
}));
|
|
103
|
+
const { packages, warnings } = (0, partition_driver_1.partitionUnits)(input, config);
|
|
104
|
+
const result = packages.map(pkg => ({
|
|
105
|
+
name: pkg.name,
|
|
106
|
+
requires: pkg.requires,
|
|
107
|
+
rows: pkg.changes.map(change => {
|
|
108
|
+
const scripts = (0, regen_1.regenerateScripts)(change.deploy);
|
|
109
|
+
for (const warning of scripts.revert.warnings) {
|
|
110
|
+
warnings.push(`${pkg.name}:${change.name}: ${warning}`);
|
|
111
|
+
}
|
|
112
|
+
for (const warning of scripts.verify.warnings) {
|
|
113
|
+
warnings.push(`${pkg.name}:${change.name}: ${warning}`);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
name: change.name,
|
|
117
|
+
deploy: change.name,
|
|
118
|
+
deps: change.dependencies,
|
|
119
|
+
content: change.deploy,
|
|
120
|
+
revert: scripts.revert.sql,
|
|
121
|
+
verify: scripts.verify.sql
|
|
122
|
+
};
|
|
123
|
+
})
|
|
124
|
+
}));
|
|
125
|
+
return { packages: result, warnings };
|
|
126
|
+
};
|
|
127
|
+
exports.partitionExportRows = partitionExportRows;
|
package/restructure.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Granularity dial for `pgpm export`: route exported sql_actions rows through
|
|
3
|
+
* the dials pipeline (`restructureChanges` in `@pgpmjs/transform`) so change
|
|
4
|
+
* paths are derived from the naming spec (`identityOf` + `pathFor`) and
|
|
5
|
+
* `requires` come from the statement dependency graph instead of the
|
|
6
|
+
* hand-chained deps recorded at action time.
|
|
7
|
+
*/
|
|
8
|
+
import { PgpmRow } from '@pgpmjs/ast';
|
|
9
|
+
import { PathStyle } from '@pgpmjs/naming-spec';
|
|
10
|
+
import { Granularity } from '@pgsql/transform';
|
|
11
|
+
export type ExportGranularity = Granularity;
|
|
12
|
+
export declare const EXPORT_GRANULARITIES: readonly ExportGranularity[];
|
|
13
|
+
export declare const isExportGranularity: (value: unknown) => value is ExportGranularity;
|
|
14
|
+
export interface RestructureExportRowsResult {
|
|
15
|
+
rows: PgpmRow[];
|
|
16
|
+
warnings: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface RestructureExportRowsOptions {
|
|
19
|
+
/** Naming-spec rendering style for derived paths (default `directory`). */
|
|
20
|
+
naming?: PathStyle;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Restructure exported migration rows to the target granularity.
|
|
24
|
+
*
|
|
25
|
+
* The rows' deploy SQL is flattened in export order, restructured, and
|
|
26
|
+
* re-sliced into per-object changes whose paths come from the naming spec and
|
|
27
|
+
* whose deps come from the statement graph. When two changes derive the same
|
|
28
|
+
* path (the same object altered again), later occurrences get the spec's
|
|
29
|
+
* alteration convention via `alterationPathFor` (monotonic per-parent
|
|
30
|
+
* counter), with the parent added to their deps.
|
|
31
|
+
*
|
|
32
|
+
* Revert/verify scripts are not carried through from the original rows (the
|
|
33
|
+
* restructured grouping no longer matches them one-to-one); instead each
|
|
34
|
+
* change gets scripts generated from its own statements via
|
|
35
|
+
* `revertFor`/`verifyFor` (drops in reverse topological order, one existence
|
|
36
|
+
* check per created object).
|
|
37
|
+
*/
|
|
38
|
+
export declare const restructureExportRows: (rows: PgpmRow[], granularity: ExportGranularity, options?: RestructureExportRowsOptions) => Promise<RestructureExportRowsResult>;
|
package/restructure.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.restructureExportRows = exports.isExportGranularity = exports.EXPORT_GRANULARITIES = void 0;
|
|
4
|
+
const naming_spec_1 = require("@pgpmjs/naming-spec");
|
|
5
|
+
const transform_1 = require("@pgsql/transform");
|
|
6
|
+
const plpgsql_parser_1 = require("plpgsql-parser");
|
|
7
|
+
const granularity_driver_1 = require("./granularity-driver");
|
|
8
|
+
exports.EXPORT_GRANULARITIES = ['atomic', 'object', 'consolidated'];
|
|
9
|
+
const isExportGranularity = (value) => typeof value === 'string' && exports.EXPORT_GRANULARITIES.includes(value);
|
|
10
|
+
exports.isExportGranularity = isExportGranularity;
|
|
11
|
+
/**
|
|
12
|
+
* Restructure exported migration rows to the target granularity.
|
|
13
|
+
*
|
|
14
|
+
* The rows' deploy SQL is flattened in export order, restructured, and
|
|
15
|
+
* re-sliced into per-object changes whose paths come from the naming spec and
|
|
16
|
+
* whose deps come from the statement graph. When two changes derive the same
|
|
17
|
+
* path (the same object altered again), later occurrences get the spec's
|
|
18
|
+
* alteration convention via `alterationPathFor` (monotonic per-parent
|
|
19
|
+
* counter), with the parent added to their deps.
|
|
20
|
+
*
|
|
21
|
+
* Revert/verify scripts are not carried through from the original rows (the
|
|
22
|
+
* restructured grouping no longer matches them one-to-one); instead each
|
|
23
|
+
* change gets scripts generated from its own statements via
|
|
24
|
+
* `revertFor`/`verifyFor` (drops in reverse topological order, one existence
|
|
25
|
+
* check per created object).
|
|
26
|
+
*/
|
|
27
|
+
const restructureExportRows = async (rows, granularity, options = {}) => {
|
|
28
|
+
await (0, plpgsql_parser_1.loadModule)();
|
|
29
|
+
const style = options.naming ?? 'directory';
|
|
30
|
+
const changeName = (facts) => {
|
|
31
|
+
const identity = (0, transform_1.identityOf)(facts);
|
|
32
|
+
if (identity)
|
|
33
|
+
return (0, naming_spec_1.pathFor)(identity, { style });
|
|
34
|
+
return 'misc/statements';
|
|
35
|
+
};
|
|
36
|
+
const { changes, warnings } = (0, granularity_driver_1.restructureChanges)(rows.map(row => ({
|
|
37
|
+
name: row.deploy,
|
|
38
|
+
dependencies: row.deps ?? [],
|
|
39
|
+
deploy: row.content
|
|
40
|
+
})), { granularity, changeName });
|
|
41
|
+
const counters = new Map();
|
|
42
|
+
const taken = new Set();
|
|
43
|
+
const restructured = changes.map(change => {
|
|
44
|
+
let deploy = change.name;
|
|
45
|
+
const deps = [...change.dependencies];
|
|
46
|
+
if (taken.has(deploy)) {
|
|
47
|
+
let n = (counters.get(change.name) ?? 0) + 1;
|
|
48
|
+
while (taken.has((0, naming_spec_1.alterationPathFor)(change.name, n)))
|
|
49
|
+
n++;
|
|
50
|
+
counters.set(change.name, n);
|
|
51
|
+
deploy = (0, naming_spec_1.alterationPathFor)(change.name, n);
|
|
52
|
+
if (!deps.includes(change.name))
|
|
53
|
+
deps.push(change.name);
|
|
54
|
+
}
|
|
55
|
+
taken.add(deploy);
|
|
56
|
+
return {
|
|
57
|
+
name: deploy,
|
|
58
|
+
deploy,
|
|
59
|
+
deps,
|
|
60
|
+
content: change.deploy,
|
|
61
|
+
revert: change.revert,
|
|
62
|
+
verify: change.verify
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
return { rows: restructured, warnings };
|
|
66
|
+
};
|
|
67
|
+
exports.restructureExportRows = restructureExportRows;
|
|
@@ -16,10 +16,13 @@
|
|
|
16
16
|
* modified objects routed through the granularity pipeline
|
|
17
17
|
* (`restructureChanges`), so names come from the naming spec and `requires`
|
|
18
18
|
* from the statement graph — the same derivation every other projection uses.
|
|
19
|
-
* Every emitted change carries a full deploy/revert/verify triple:
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* Every emitted change carries a full deploy/revert/verify triple: inverses
|
|
20
|
+
* and existence checks are composed at the AST level via `@pgsql/scripts`'
|
|
21
|
+
* node-level API (`invertStatement`/`existenceCheck`) — never hand-templated
|
|
22
|
+
* and never round-tripped through deparsed text — deparsing only once at the
|
|
23
|
+
* end. Statements the node layer cannot derive fall back to the text layer
|
|
24
|
+
* (`revertFor`/`verifyFor`) solely to surface its not-derivable comment and
|
|
25
|
+
* warning.
|
|
23
26
|
*
|
|
24
27
|
* Requires `loadModule()` from `plpgsql-parser` before use.
|
|
25
28
|
*/
|
package/semantic-diff-driver.js
CHANGED
|
@@ -20,10 +20,13 @@ exports.diffChangeSets = diffChangeSets;
|
|
|
20
20
|
* modified objects routed through the granularity pipeline
|
|
21
21
|
* (`restructureChanges`), so names come from the naming spec and `requires`
|
|
22
22
|
* from the statement graph — the same derivation every other projection uses.
|
|
23
|
-
* Every emitted change carries a full deploy/revert/verify triple:
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* Every emitted change carries a full deploy/revert/verify triple: inverses
|
|
24
|
+
* and existence checks are composed at the AST level via `@pgsql/scripts`'
|
|
25
|
+
* node-level API (`invertStatement`/`existenceCheck`) — never hand-templated
|
|
26
|
+
* and never round-tripped through deparsed text — deparsing only once at the
|
|
27
|
+
* end. Statements the node layer cannot derive fall back to the text layer
|
|
28
|
+
* (`revertFor`/`verifyFor`) solely to surface its not-derivable comment and
|
|
29
|
+
* warning.
|
|
27
30
|
*
|
|
28
31
|
* Requires `loadModule()` from `plpgsql-parser` before use.
|
|
29
32
|
*/
|
|
@@ -211,32 +214,46 @@ function diffTable(from, to, diff, warnings) {
|
|
|
211
214
|
stmts.push(alter({ subtype: 'AT_DropColumn', name }));
|
|
212
215
|
}
|
|
213
216
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
217
|
+
// Self-inverse ALTER TABLE subtypes: a removed extra with one of these
|
|
218
|
+
// undoes with the paired subtype rather than a DROP CONSTRAINT.
|
|
219
|
+
const INVERSE_SUBTYPE = {
|
|
220
|
+
AT_EnableRowSecurity: 'AT_DisableRowSecurity',
|
|
221
|
+
AT_DisableRowSecurity: 'AT_EnableRowSecurity',
|
|
222
|
+
AT_ForceRowSecurity: 'AT_NoForceRowSecurity',
|
|
223
|
+
AT_NoForceRowSecurity: 'AT_ForceRowSecurity'
|
|
224
|
+
};
|
|
225
|
+
// Removed extras drop before added ones: a changed constraint that keeps
|
|
226
|
+
// its name must DROP CONSTRAINT before the replacement ADD CONSTRAINT.
|
|
227
227
|
for (const [print, extra] of from.extras) {
|
|
228
228
|
if (to.extras.has(print))
|
|
229
229
|
continue;
|
|
230
230
|
const conname = extra.Constraint?.conname
|
|
231
231
|
?? extra
|
|
232
232
|
.AlterTableCmd?.def?.Constraint?.conname;
|
|
233
|
+
const subtype = extra.AlterTableCmd?.subtype;
|
|
233
234
|
if (conname) {
|
|
234
235
|
stmts.push(alter({ subtype: 'AT_DropConstraint', name: conname }));
|
|
235
236
|
}
|
|
237
|
+
else if (subtype && INVERSE_SUBTYPE[subtype]) {
|
|
238
|
+
stmts.push(alter({ subtype: INVERSE_SUBTYPE[subtype] }));
|
|
239
|
+
}
|
|
236
240
|
else {
|
|
237
241
|
warnings.push(`${diff.path}: removed table element has no name to drop; emit a manual ALTER`);
|
|
238
242
|
}
|
|
239
243
|
}
|
|
244
|
+
for (const [print, extra] of to.extras) {
|
|
245
|
+
if (!from.extras.has(print)) {
|
|
246
|
+
const at = extra.AlterTableCmd;
|
|
247
|
+
if (at)
|
|
248
|
+
stmts.push(alter(at));
|
|
249
|
+
else if (extra.Constraint) {
|
|
250
|
+
stmts.push(alter({ subtype: 'AT_AddConstraint', def: extra }));
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
warnings.push(`${diff.path}: unrecognized added table element; emit a manual ALTER`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
240
257
|
return stmts;
|
|
241
258
|
}
|
|
242
259
|
/**
|
|
@@ -256,17 +273,46 @@ function diffSchemas(from, to, options = {}) {
|
|
|
256
273
|
const forwardSql = [];
|
|
257
274
|
const modifiedChanges = [];
|
|
258
275
|
const dropUnits = [];
|
|
259
|
-
/**
|
|
276
|
+
/**
|
|
277
|
+
* `@pgsql/scripts` inverse of a deploy script (drops in reverse topo
|
|
278
|
+
* order), composed at the AST level via `invertStatement` and deparsed
|
|
279
|
+
* once. Underivable statements fall back to `revertFor` on the single
|
|
280
|
+
* statement to surface its not-derivable comment and warning.
|
|
281
|
+
*/
|
|
260
282
|
const inverseOf = (deploy, context) => {
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
283
|
+
const facts = (0, transform_1.classifyStatements)(deploy);
|
|
284
|
+
const graph = (0, transform_1.buildStatementGraph)(facts);
|
|
285
|
+
const pieces = [];
|
|
286
|
+
for (const i of [...graph.order].reverse()) {
|
|
287
|
+
const nodes = (0, scripts_1.invertStatement)(facts[i]);
|
|
288
|
+
if (nodes === null) {
|
|
289
|
+
const { sql, warnings: w } = (0, scripts_1.revertFor)([facts[i]]);
|
|
290
|
+
warnings.push(...w.map(x => `${context}: ${x}`));
|
|
291
|
+
if (sql.trim())
|
|
292
|
+
pieces.push(sql);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
pieces.push(...nodes.map(n => deparseStmt(n)));
|
|
296
|
+
}
|
|
297
|
+
return pieces.join('\n');
|
|
264
298
|
};
|
|
265
|
-
/**
|
|
299
|
+
/**
|
|
300
|
+
* `@pgsql/scripts` existence checks for a deploy script, composed from
|
|
301
|
+
* `existenceCheck`'s SelectStmt nodes and deparsed once. Underivable
|
|
302
|
+
* statements fall back to `verifyFor` to surface the warning.
|
|
303
|
+
*/
|
|
266
304
|
const verifyOf = (deploy, context) => {
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
305
|
+
const pieces = [];
|
|
306
|
+
for (const fact of (0, transform_1.classifyStatements)(deploy)) {
|
|
307
|
+
const nodes = (0, scripts_1.existenceCheck)(fact);
|
|
308
|
+
if (nodes === null) {
|
|
309
|
+
const { warnings: w } = (0, scripts_1.verifyFor)([fact]);
|
|
310
|
+
warnings.push(...w.map(x => `${context}: ${x}`));
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
pieces.push(...nodes.map(n => deparseStmt(n)));
|
|
314
|
+
}
|
|
315
|
+
return pieces.join('\n\n');
|
|
270
316
|
};
|
|
271
317
|
for (const key of b.order) {
|
|
272
318
|
const unit = b.units.get(key);
|