@pgpmjs/core 6.32.2 → 6.34.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/esm/files/sql/header.js +259 -0
- package/esm/files/sql/index.js +1 -0
- package/esm/index.js +1 -0
- package/esm/refactor/index.js +1 -0
- package/esm/refactor/rename.js +200 -0
- package/esm/resolution/deps.js +26 -63
- package/esm/slice/slice.js +11 -3
- package/files/sql/header.d.ts +114 -0
- package/files/sql/header.js +266 -0
- package/files/sql/index.d.ts +1 -0
- package/files/sql/index.js +1 -0
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/package.json +2 -2
- package/refactor/index.d.ts +1 -0
- package/refactor/index.js +17 -0
- package/refactor/rename.d.ts +59 -0
- package/refactor/rename.js +206 -0
- package/resolution/deps.js +26 -63
- package/slice/slice.d.ts +2 -2
- package/slice/slice.js +11 -3
- package/slice/types.d.ts +15 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { parsePlanFile } from '../plan/parser';
|
|
4
|
+
import { parseReference } from '../plan/validators';
|
|
5
|
+
const HEADER_LINE = /^--\s*(Deploy|Revert|Verify)\b(:)?\s*(.*?)\s*$/;
|
|
6
|
+
const REQUIRES_LINE = /^--\s*requires:\s*(.*?)\s*$/;
|
|
7
|
+
const COMMENT_LINE = /^\s*--/;
|
|
8
|
+
const TO_PG_SUFFIX = /\s+to\s+pg\s*$/;
|
|
9
|
+
/**
|
|
10
|
+
* Parse the header block of a pgpm SQL script into a structured model.
|
|
11
|
+
*
|
|
12
|
+
* Recognizes both header formats:
|
|
13
|
+
* `-- Deploy <project>:<change> to pg` (legacy sqitch-compatible)
|
|
14
|
+
* `-- Deploy <change>` / `-- Deploy: <change>` (new)
|
|
15
|
+
* along with `-- requires: <ref>` lines where `<ref>` may be a plain change
|
|
16
|
+
* name, `{package}:{change}`, `@tag`, or `{package}:@tag`.
|
|
17
|
+
*
|
|
18
|
+
* The parse is lossless: `header.lines` plus `body` reassemble the original
|
|
19
|
+
* content byte-exactly (see {@link writePgpmScript}).
|
|
20
|
+
*/
|
|
21
|
+
export function parsePgpmHeader(content) {
|
|
22
|
+
const header = {
|
|
23
|
+
verb: null,
|
|
24
|
+
project: null,
|
|
25
|
+
change: null,
|
|
26
|
+
toPg: false,
|
|
27
|
+
requires: [],
|
|
28
|
+
lines: []
|
|
29
|
+
};
|
|
30
|
+
const lines = content.split('\n');
|
|
31
|
+
let lastHeaderIdx = -1;
|
|
32
|
+
for (let i = 0; i < lines.length; i++) {
|
|
33
|
+
const line = lines[i];
|
|
34
|
+
if (line.trim() === '') {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (!COMMENT_LINE.test(line)) {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
const headerMatch = line.match(HEADER_LINE);
|
|
41
|
+
if (headerMatch && header.verb === null) {
|
|
42
|
+
header.verb = headerMatch[1];
|
|
43
|
+
let target = headerMatch[3];
|
|
44
|
+
if (TO_PG_SUFFIX.test(target)) {
|
|
45
|
+
header.toPg = true;
|
|
46
|
+
target = target.replace(TO_PG_SUFFIX, '');
|
|
47
|
+
}
|
|
48
|
+
const colonIndex = target.indexOf(':');
|
|
49
|
+
if (colonIndex > 0 && /^[a-zA-Z0-9_-]+$/.test(target.substring(0, colonIndex))) {
|
|
50
|
+
header.project = target.substring(0, colonIndex);
|
|
51
|
+
header.change = target.substring(colonIndex + 1).trim();
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
header.change = target.trim() || null;
|
|
55
|
+
}
|
|
56
|
+
lastHeaderIdx = i;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const requiresMatch = line.match(REQUIRES_LINE);
|
|
60
|
+
if (requiresMatch) {
|
|
61
|
+
const raw = requiresMatch[1];
|
|
62
|
+
header.requires.push({ raw, ref: parseReference(raw) });
|
|
63
|
+
lastHeaderIdx = i;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
// Other comment lines (e.g. attribution) before/between header lines are
|
|
67
|
+
// tolerated but only absorbed into the header when a later header line
|
|
68
|
+
// follows; trailing comments belong to the body.
|
|
69
|
+
}
|
|
70
|
+
header.lines = lines.slice(0, lastHeaderIdx + 1);
|
|
71
|
+
return {
|
|
72
|
+
header,
|
|
73
|
+
body: lines.slice(lastHeaderIdx + 1).join('\n')
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Serialize a header back to its raw lines. When the structured fields have
|
|
78
|
+
* been modified via {@link renameInHeader}, the raw lines already reflect the
|
|
79
|
+
* change; this simply joins header and body losslessly.
|
|
80
|
+
*/
|
|
81
|
+
export function writePgpmScript(script) {
|
|
82
|
+
if (script.header.lines.length === 0)
|
|
83
|
+
return script.body;
|
|
84
|
+
return script.header.lines.join('\n') + '\n' + script.body;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Rewrite change names in a script header according to a rename map
|
|
88
|
+
* (old change name -> new change name). Rewrites both the `-- Deploy` line's
|
|
89
|
+
* change identity and any `-- requires:` lines referencing renamed changes,
|
|
90
|
+
* preserving format (project qualifier, ` to pg` suffix, tag refs untouched).
|
|
91
|
+
*
|
|
92
|
+
* Returns the number of substitutions applied; mutates `script` in place.
|
|
93
|
+
*/
|
|
94
|
+
export function renameInHeader(script, renames) {
|
|
95
|
+
const { header } = script;
|
|
96
|
+
let count = 0;
|
|
97
|
+
const renameRef = (raw) => {
|
|
98
|
+
const ref = parseReference(raw);
|
|
99
|
+
if (!ref) {
|
|
100
|
+
const to = renames.get(raw);
|
|
101
|
+
return to ?? null;
|
|
102
|
+
}
|
|
103
|
+
if (ref.tag || ref.symbolic || ref.relative)
|
|
104
|
+
return null;
|
|
105
|
+
const change = ref.change;
|
|
106
|
+
if (!change)
|
|
107
|
+
return null;
|
|
108
|
+
const to = renames.get(change);
|
|
109
|
+
if (!to)
|
|
110
|
+
return null;
|
|
111
|
+
return ref.package ? `${ref.package}:${to}` : to;
|
|
112
|
+
};
|
|
113
|
+
header.lines = header.lines.map(line => {
|
|
114
|
+
const headerMatch = line.match(HEADER_LINE);
|
|
115
|
+
if (headerMatch && header.change && renames.has(header.change)) {
|
|
116
|
+
const to = renames.get(header.change);
|
|
117
|
+
const oldTarget = header.project ? `${header.project}:${header.change}` : header.change;
|
|
118
|
+
const newTarget = header.project ? `${header.project}:${to}` : to;
|
|
119
|
+
const rewritten = line.replace(oldTarget, newTarget);
|
|
120
|
+
if (rewritten !== line) {
|
|
121
|
+
header.change = to;
|
|
122
|
+
count++;
|
|
123
|
+
return rewritten;
|
|
124
|
+
}
|
|
125
|
+
return line;
|
|
126
|
+
}
|
|
127
|
+
const requiresMatch = line.match(REQUIRES_LINE);
|
|
128
|
+
if (requiresMatch) {
|
|
129
|
+
const raw = requiresMatch[1];
|
|
130
|
+
const renamed = renameRef(raw);
|
|
131
|
+
if (renamed !== null) {
|
|
132
|
+
count++;
|
|
133
|
+
return line.replace(raw, renamed);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return line;
|
|
137
|
+
});
|
|
138
|
+
header.requires = header.requires.map(req => {
|
|
139
|
+
const renamed = renameRef(req.raw);
|
|
140
|
+
if (renamed === null)
|
|
141
|
+
return req;
|
|
142
|
+
return { raw: renamed, ref: parseReference(renamed) };
|
|
143
|
+
});
|
|
144
|
+
return count;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Scan a deploy script the way dependency resolution historically has:
|
|
148
|
+
* line-by-line over the whole file, collecting `-- requires: <ref>` lines
|
|
149
|
+
* verbatim and validating any `-- Deploy <project>:<change>[ to pg]` line
|
|
150
|
+
* against the script's path identity and project name.
|
|
151
|
+
*
|
|
152
|
+
* Kept strictly line-regex-compatible with the original resolver behavior
|
|
153
|
+
* (requires lines are honored anywhere in the file, and the legacy
|
|
154
|
+
* `-- Deploy: <change>` colon form is not identity-validated).
|
|
155
|
+
*
|
|
156
|
+
* @throws when the Deploy line's project or change identity mismatches.
|
|
157
|
+
*/
|
|
158
|
+
export function scanDeployScript(content, options) {
|
|
159
|
+
const { key, extname, makeKey } = options;
|
|
160
|
+
const requires = [];
|
|
161
|
+
for (const line of content.split('\n')) {
|
|
162
|
+
const requiresMatch = line.match(/^-- requires: (.*)/);
|
|
163
|
+
if (requiresMatch) {
|
|
164
|
+
requires.push(requiresMatch[1].trim());
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (/:/.test(line)) {
|
|
168
|
+
const m2 = line.match(/^-- Deploy ([^:]*):([\w\/]+)(?:\s+to\s+pg)?/);
|
|
169
|
+
if (m2) {
|
|
170
|
+
const actualProject = m2[1];
|
|
171
|
+
const keyToTest = m2[2];
|
|
172
|
+
if (extname !== actualProject) {
|
|
173
|
+
throw new Error(`Mismatched project name in deploy file:
|
|
174
|
+
Expected project: ${extname}
|
|
175
|
+
Found in line : ${actualProject}
|
|
176
|
+
Line : ${line}`);
|
|
177
|
+
}
|
|
178
|
+
const expectedKey = makeKey(keyToTest);
|
|
179
|
+
if (key !== expectedKey) {
|
|
180
|
+
throw new Error(`Deployment script path or internal name mismatch:
|
|
181
|
+
Expected key : ${key}
|
|
182
|
+
Found in line : ${expectedKey}
|
|
183
|
+
Line : ${line}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
const m2 = line.match(/^-- Deploy (.*?)(?:\s+to\s+pg)?\s*$/);
|
|
189
|
+
if (m2) {
|
|
190
|
+
const keyToTest = m2[1].trim();
|
|
191
|
+
if (key !== makeKey(keyToTest)) {
|
|
192
|
+
throw new Error('deployment script in wrong place or is named wrong internally\n' + line);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return { requires };
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Verify that every change in a module's pgpm.plan has a deploy script whose
|
|
201
|
+
* header identity matches its path, and report dependency drift between the
|
|
202
|
+
* plan's dep lists and the scripts' `-- requires:` lines.
|
|
203
|
+
*
|
|
204
|
+
* Read-only diagnostic; returns issues rather than throwing.
|
|
205
|
+
*/
|
|
206
|
+
export function verifyPlanMatchesHeaders(moduleDir) {
|
|
207
|
+
const issues = [];
|
|
208
|
+
const planPath = join(moduleDir, 'pgpm.plan');
|
|
209
|
+
const parsed = parsePlanFile(planPath);
|
|
210
|
+
if (!parsed.data) {
|
|
211
|
+
throw new Error(`Failed to parse plan file: ${planPath}`);
|
|
212
|
+
}
|
|
213
|
+
for (const planChange of parsed.data.changes) {
|
|
214
|
+
const change = planChange.name;
|
|
215
|
+
const file = join(moduleDir, 'deploy', `${change}.sql`);
|
|
216
|
+
let content;
|
|
217
|
+
try {
|
|
218
|
+
content = readFileSync(file, 'utf-8');
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
issues.push({
|
|
222
|
+
change,
|
|
223
|
+
file,
|
|
224
|
+
kind: 'missing-file',
|
|
225
|
+
message: `deploy script not found for plan change "${change}"`
|
|
226
|
+
});
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const { header } = parsePgpmHeader(content);
|
|
230
|
+
if (header.change && header.change !== change) {
|
|
231
|
+
issues.push({
|
|
232
|
+
change,
|
|
233
|
+
file,
|
|
234
|
+
kind: 'identity-mismatch',
|
|
235
|
+
message: `header declares "${header.change}" but path identity is "${change}"`
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
const planDeps = new Set(planChange.dependencies || []);
|
|
239
|
+
const headerDeps = new Set(header.requires.map(r => r.raw));
|
|
240
|
+
const missingInHeader = [...planDeps].filter(d => !headerDeps.has(d));
|
|
241
|
+
const missingInPlan = [...headerDeps].filter(d => !planDeps.has(d));
|
|
242
|
+
if (missingInHeader.length > 0 || missingInPlan.length > 0) {
|
|
243
|
+
const parts = [];
|
|
244
|
+
if (missingInHeader.length > 0) {
|
|
245
|
+
parts.push(`in plan but not in header requires: ${missingInHeader.join(', ')}`);
|
|
246
|
+
}
|
|
247
|
+
if (missingInPlan.length > 0) {
|
|
248
|
+
parts.push(`in header requires but not in plan: ${missingInPlan.join(', ')}`);
|
|
249
|
+
}
|
|
250
|
+
issues.push({
|
|
251
|
+
change,
|
|
252
|
+
file,
|
|
253
|
+
kind: 'requires-drift',
|
|
254
|
+
message: parts.join('; ')
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return issues;
|
|
259
|
+
}
|
package/esm/files/sql/index.js
CHANGED
package/esm/index.js
CHANGED
|
@@ -13,6 +13,7 @@ export * from './core/boilerplate-types';
|
|
|
13
13
|
export * from './core/boilerplate-scanner';
|
|
14
14
|
// Export package-files functionality (now integrated into core)
|
|
15
15
|
export * from './files';
|
|
16
|
+
export * from './refactor';
|
|
16
17
|
export { cleanSql } from './migrate/clean';
|
|
17
18
|
export { PgpmMigrate } from './migrate/client';
|
|
18
19
|
export { PgpmInit } from './init/client';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './rename';
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmdirSync, statSync, writeFileSync } from 'fs';
|
|
2
|
+
import { dirname, join, relative, sep } from 'path';
|
|
3
|
+
import { parsePlanFileSimple } from '../files/plan/parser';
|
|
4
|
+
import { parsePgpmHeader, renameInHeader, writePgpmScript } from '../files/sql/header';
|
|
5
|
+
const SCRIPT_DIRS = ['deploy', 'revert', 'verify'];
|
|
6
|
+
function escapeRe(str) {
|
|
7
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
8
|
+
}
|
|
9
|
+
function findSqlFiles(dir) {
|
|
10
|
+
const files = [];
|
|
11
|
+
if (!existsSync(dir))
|
|
12
|
+
return files;
|
|
13
|
+
const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
const fullPath = join(dir, entry.name);
|
|
16
|
+
if (entry.isDirectory()) {
|
|
17
|
+
files.push(...findSqlFiles(fullPath));
|
|
18
|
+
}
|
|
19
|
+
else if (entry.isFile() && entry.name.endsWith('.sql')) {
|
|
20
|
+
files.push(fullPath);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return files;
|
|
24
|
+
}
|
|
25
|
+
function removeEmptyDirs(dir, stopAt) {
|
|
26
|
+
let current = dir;
|
|
27
|
+
while (current !== stopAt && current.startsWith(stopAt)) {
|
|
28
|
+
if (!existsSync(current) || readdirSync(current).length > 0)
|
|
29
|
+
return;
|
|
30
|
+
rmdirSync(current);
|
|
31
|
+
current = dirname(current);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Validate a rename map against a module directory: every source change must
|
|
36
|
+
* exist in the plan, no target may collide with an existing change, and no
|
|
37
|
+
* two sources may map to the same target.
|
|
38
|
+
*
|
|
39
|
+
* @throws on any violation.
|
|
40
|
+
*/
|
|
41
|
+
export function validateRenames(moduleDir, renames) {
|
|
42
|
+
const planPath = join(moduleDir, 'pgpm.plan');
|
|
43
|
+
const plan = parsePlanFileSimple(planPath);
|
|
44
|
+
const changeNames = new Set(plan.changes.map(c => c.name));
|
|
45
|
+
const targets = new Set();
|
|
46
|
+
for (const [from, to] of renames) {
|
|
47
|
+
if (from === to) {
|
|
48
|
+
throw new Error(`Rename maps change to itself: ${from}`);
|
|
49
|
+
}
|
|
50
|
+
if (!changeNames.has(from)) {
|
|
51
|
+
throw new Error(`Cannot rename unknown change: ${from}`);
|
|
52
|
+
}
|
|
53
|
+
if (changeNames.has(to) && !renames.has(to)) {
|
|
54
|
+
throw new Error(`Rename target already exists in plan: ${to}`);
|
|
55
|
+
}
|
|
56
|
+
if (targets.has(to)) {
|
|
57
|
+
throw new Error(`Multiple changes rename to the same target: ${to}`);
|
|
58
|
+
}
|
|
59
|
+
targets.add(to);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Rewrite change names in a pgpm.plan's content according to a rename map.
|
|
64
|
+
* Textual and format-preserving: only the change-name token at the start of a
|
|
65
|
+
* change line, plain (same-package) dependency refs inside `[...]`, and tag
|
|
66
|
+
* lines' change field are rewritten; timestamps, planners, comments,
|
|
67
|
+
* cross-package refs, and tag refs are untouched.
|
|
68
|
+
*/
|
|
69
|
+
export function renameInPlanContent(content, renames) {
|
|
70
|
+
const froms = Array.from(renames.keys()).sort((a, b) => b.length - a.length);
|
|
71
|
+
return content
|
|
72
|
+
.split('\n')
|
|
73
|
+
.map(line => {
|
|
74
|
+
const trimmed = line.trim();
|
|
75
|
+
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('%'))
|
|
76
|
+
return line;
|
|
77
|
+
let result = line;
|
|
78
|
+
for (const from of froms) {
|
|
79
|
+
const to = renames.get(from);
|
|
80
|
+
const re = new RegExp(`(^|[\\s\\[])${escapeRe(from)}(?=$|[\\s\\]@])`, 'g');
|
|
81
|
+
result = result.replace(re, `$1${to}`);
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
})
|
|
85
|
+
.join('\n');
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Atomically rename/move changes within a pgpm module: the coupled 4-way
|
|
89
|
+
* rewrite of (1) deploy/revert/verify script files on disk, (2) the moved
|
|
90
|
+
* scripts' own header identity, (3) `-- requires:` headers in every script
|
|
91
|
+
* that references a renamed change, and (4) the pgpm.plan.
|
|
92
|
+
*
|
|
93
|
+
* SQL statement bodies are NOT touched: schema-name rewrites inside SQL are a
|
|
94
|
+
* separate AST-level concern for callers to compose (e.g. transform passes).
|
|
95
|
+
*/
|
|
96
|
+
export function renameChanges(moduleDir, renames, options = {}) {
|
|
97
|
+
const dryRun = options.dryRun ?? false;
|
|
98
|
+
const result = {
|
|
99
|
+
filesMoved: [],
|
|
100
|
+
headersRewritten: [],
|
|
101
|
+
planRewritten: false
|
|
102
|
+
};
|
|
103
|
+
if (dryRun)
|
|
104
|
+
result.dryRunReport = [];
|
|
105
|
+
if (renames.size === 0)
|
|
106
|
+
return result;
|
|
107
|
+
validateRenames(moduleDir, renames);
|
|
108
|
+
// 1. Move script files
|
|
109
|
+
for (const scriptDir of SCRIPT_DIRS) {
|
|
110
|
+
for (const [from, to] of renames) {
|
|
111
|
+
const oldPath = join(moduleDir, scriptDir, `${from}.sql`);
|
|
112
|
+
if (!existsSync(oldPath) || !statSync(oldPath).isFile())
|
|
113
|
+
continue;
|
|
114
|
+
const newPath = join(moduleDir, scriptDir, `${to}.sql`);
|
|
115
|
+
if (existsSync(newPath)) {
|
|
116
|
+
throw new Error(`Rename target file already exists: ${relative(moduleDir, newPath)}`);
|
|
117
|
+
}
|
|
118
|
+
if (dryRun) {
|
|
119
|
+
result.dryRunReport.push(`Would move: ${relative(moduleDir, oldPath)} -> ${relative(moduleDir, newPath)}`);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
mkdirSync(dirname(newPath), { recursive: true });
|
|
123
|
+
renameSync(oldPath, newPath);
|
|
124
|
+
removeEmptyDirs(dirname(oldPath), join(moduleDir, scriptDir));
|
|
125
|
+
}
|
|
126
|
+
result.filesMoved.push({
|
|
127
|
+
from: [scriptDir, `${from}.sql`].join(sep),
|
|
128
|
+
to: [scriptDir, `${to}.sql`].join(sep)
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// 2 + 3. Rewrite headers in every script (identity of moved files,
|
|
133
|
+
// requires of dependents)
|
|
134
|
+
for (const scriptDir of SCRIPT_DIRS) {
|
|
135
|
+
const dir = join(moduleDir, scriptDir);
|
|
136
|
+
for (const file of findSqlFiles(dir)) {
|
|
137
|
+
const content = readFileSync(file, 'utf-8');
|
|
138
|
+
const script = parsePgpmHeader(content);
|
|
139
|
+
const count = renameInHeader(script, renames);
|
|
140
|
+
if (count === 0)
|
|
141
|
+
continue;
|
|
142
|
+
const rewritten = writePgpmScript(script);
|
|
143
|
+
if (rewritten === content)
|
|
144
|
+
continue;
|
|
145
|
+
if (dryRun) {
|
|
146
|
+
result.dryRunReport.push(`Would rewrite header: ${relative(moduleDir, file)}`);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
writeFileSync(file, rewritten);
|
|
150
|
+
}
|
|
151
|
+
result.headersRewritten.push(relative(moduleDir, file));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// In dry-run mode files were not moved, so dependents' headers are found at
|
|
155
|
+
// their original paths above; the moved files' own headers are still
|
|
156
|
+
// reported through the same scan.
|
|
157
|
+
// 4. Rewrite pgpm.plan
|
|
158
|
+
const planPath = join(moduleDir, 'pgpm.plan');
|
|
159
|
+
const planContent = readFileSync(planPath, 'utf-8');
|
|
160
|
+
const newPlanContent = renameInPlanContent(planContent, renames);
|
|
161
|
+
if (newPlanContent !== planContent) {
|
|
162
|
+
if (dryRun) {
|
|
163
|
+
result.dryRunReport.push('Would rewrite: pgpm.plan');
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
writeFileSync(planPath, newPlanContent);
|
|
167
|
+
}
|
|
168
|
+
result.planRewritten = true;
|
|
169
|
+
}
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Derive a change rename map from a schema mapping (old schema name -> new
|
|
174
|
+
* schema name) by rewriting the `schemas/<name>/` path segment of every plan
|
|
175
|
+
* change that lives under a renamed schema.
|
|
176
|
+
*
|
|
177
|
+
* This is the module-level counterpart of directory-based schema renames:
|
|
178
|
+
* `schemas/my-app/tables/users` with mapping `my-app -> my_app` yields
|
|
179
|
+
* `schemas/my_app/tables/users`.
|
|
180
|
+
*/
|
|
181
|
+
export function deriveRenamesFromSchemaMapping(moduleDir, schemaMapping) {
|
|
182
|
+
const renames = new Map();
|
|
183
|
+
const planPath = join(moduleDir, 'pgpm.plan');
|
|
184
|
+
const plan = parsePlanFileSimple(planPath);
|
|
185
|
+
const schemas = Array.from(schemaMapping.keys()).sort((a, b) => b.length - a.length);
|
|
186
|
+
for (const change of plan.changes) {
|
|
187
|
+
let renamed = change.name;
|
|
188
|
+
for (const schema of schemas) {
|
|
189
|
+
const to = schemaMapping.get(schema);
|
|
190
|
+
if (!to || to === schema)
|
|
191
|
+
continue;
|
|
192
|
+
const re = new RegExp(`(^|/)(schemas/)${escapeRe(schema)}(/|$)`, 'g');
|
|
193
|
+
renamed = renamed.replace(re, `$1$2${to}$3`);
|
|
194
|
+
}
|
|
195
|
+
if (renamed !== change.name) {
|
|
196
|
+
renames.set(change.name, renamed);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return renames;
|
|
200
|
+
}
|
package/esm/resolution/deps.js
CHANGED
|
@@ -3,6 +3,7 @@ import { sync as glob } from 'glob';
|
|
|
3
3
|
import { join, relative } from 'path';
|
|
4
4
|
import { PgpmPackage } from '../core/class/pgpm';
|
|
5
5
|
import { parsePlanFile } from '../files/plan/parser';
|
|
6
|
+
import { scanDeployScript } from '../files/sql/header';
|
|
6
7
|
import { errors } from '@pgpmjs/types';
|
|
7
8
|
/**
|
|
8
9
|
* Core dependency resolution algorithm that handles circular dependency detection
|
|
@@ -333,84 +334,46 @@ export const resolveDependencies = (packageDir, extname, options = {}) => {
|
|
|
333
334
|
const files = glob(`${packageDir}/deploy/**/*.sql`).sort((a, b) => a.localeCompare(b));
|
|
334
335
|
for (const file of files) {
|
|
335
336
|
const data = readFileSync(file, 'utf-8');
|
|
336
|
-
const lines = data.split('\n');
|
|
337
337
|
const key = '/' + relative(packageDir, file);
|
|
338
338
|
deps[key] = [];
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
// Full resolution: replace tag with actual change
|
|
358
|
-
const resolvedDep = `${projectName}:${taggedChange}`;
|
|
359
|
-
deps[key].push(resolvedDep);
|
|
360
|
-
}
|
|
361
|
-
else if (tagResolution === 'internal') {
|
|
362
|
-
// Internal resolution: keep tag in deps but track mapping
|
|
363
|
-
tagMappings[dep] = `${projectName}:${taggedChange}`;
|
|
364
|
-
deps[key].push(dep);
|
|
365
|
-
}
|
|
339
|
+
const { requires } = scanDeployScript(data, { key, extname, makeKey });
|
|
340
|
+
for (const dep of requires) {
|
|
341
|
+
// For 'preserve' mode, just add the dependency as-is (like original getDeps)
|
|
342
|
+
if (tagResolution === 'preserve') {
|
|
343
|
+
deps[key].push(dep);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
// For other modes, handle tag resolution
|
|
347
|
+
if (dep.includes('@')) {
|
|
348
|
+
const match = dep.match(/^([^:]+):@(.+)$/);
|
|
349
|
+
if (match) {
|
|
350
|
+
const [, projectName, tagName] = match;
|
|
351
|
+
const taggedChange = resolveTagToChange(projectName, tagName);
|
|
352
|
+
if (taggedChange) {
|
|
353
|
+
if (tagResolution === 'resolve') {
|
|
354
|
+
// Full resolution: replace tag with actual change
|
|
355
|
+
const resolvedDep = `${projectName}:${taggedChange}`;
|
|
356
|
+
deps[key].push(resolvedDep);
|
|
366
357
|
}
|
|
367
|
-
else {
|
|
368
|
-
//
|
|
358
|
+
else if (tagResolution === 'internal') {
|
|
359
|
+
// Internal resolution: keep tag in deps but track mapping
|
|
360
|
+
tagMappings[dep] = `${projectName}:${taggedChange}`;
|
|
369
361
|
deps[key].push(dep);
|
|
370
362
|
}
|
|
371
363
|
}
|
|
372
364
|
else {
|
|
373
|
-
//
|
|
365
|
+
// Could not resolve tag, keep it as is
|
|
374
366
|
deps[key].push(dep);
|
|
375
367
|
}
|
|
376
368
|
}
|
|
377
369
|
else {
|
|
378
|
-
//
|
|
370
|
+
// Invalid tag format, keep as is
|
|
379
371
|
deps[key].push(dep);
|
|
380
372
|
}
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
// Handle deploy statements - exactly as in original
|
|
384
|
-
let m2;
|
|
385
|
-
let keyToTest;
|
|
386
|
-
if (/:/.test(line)) {
|
|
387
|
-
m2 = line.match(/^-- Deploy ([^:]*):([\w\/]+)(?:\s+to\s+pg)?/);
|
|
388
|
-
if (m2) {
|
|
389
|
-
const actualProject = m2[1];
|
|
390
|
-
keyToTest = m2[2];
|
|
391
|
-
if (extname !== actualProject) {
|
|
392
|
-
throw new Error(`Mismatched project name in deploy file:
|
|
393
|
-
Expected project: ${extname}
|
|
394
|
-
Found in line : ${actualProject}
|
|
395
|
-
Line : ${line}`);
|
|
396
|
-
}
|
|
397
|
-
const expectedKey = makeKey(keyToTest);
|
|
398
|
-
if (key !== expectedKey) {
|
|
399
|
-
throw new Error(`Deployment script path or internal name mismatch:
|
|
400
|
-
Expected key : ${key}
|
|
401
|
-
Found in line : ${expectedKey}
|
|
402
|
-
Line : ${line}`);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
373
|
}
|
|
406
374
|
else {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
keyToTest = m2[1].trim();
|
|
410
|
-
if (key !== makeKey(keyToTest)) {
|
|
411
|
-
throw new Error('deployment script in wrong place or is named wrong internally\n' + line);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
375
|
+
// Not a tag, keep as is
|
|
376
|
+
deps[key].push(dep);
|
|
414
377
|
}
|
|
415
378
|
}
|
|
416
379
|
}
|
package/esm/slice/slice.js
CHANGED
|
@@ -340,7 +340,9 @@ export function generateControlContent(pkgName, deps) {
|
|
|
340
340
|
/**
|
|
341
341
|
* Generate a single package output
|
|
342
342
|
*/
|
|
343
|
-
export function generateSinglePackage(pkgName, changes, graph, changeToPackage, pkgDeps,
|
|
343
|
+
export function generateSinglePackage(pkgName, changes, graph, changeToPackage, pkgDeps, crossPackageDeps = 'change') {
|
|
344
|
+
// Back-compat: earlier signature took useTagsForCrossPackageDeps: boolean
|
|
345
|
+
const crossPackageDepMode = crossPackageDeps === true ? 'tag' : crossPackageDeps === false ? 'change' : crossPackageDeps;
|
|
344
346
|
// Sort changes in topological order within package
|
|
345
347
|
const sortedChanges = topologicalSortWithinPackage(changes, graph);
|
|
346
348
|
// Build plan entries with updated dependencies
|
|
@@ -361,7 +363,13 @@ export function generateSinglePackage(pkgName, changes, graph, changeToPackage,
|
|
|
361
363
|
}
|
|
362
364
|
else {
|
|
363
365
|
// Cross-package dependency
|
|
364
|
-
if (
|
|
366
|
+
if (crossPackageDepMode === 'control-only') {
|
|
367
|
+
// Dropped from the plan line; carried by the control file's
|
|
368
|
+
// `requires` (extension install ordering deploys the whole
|
|
369
|
+
// dependency package first)
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (crossPackageDepMode === 'tag') {
|
|
365
373
|
// Find latest tag in the dependency package
|
|
366
374
|
const depPkgChanges = [...graph.nodes.keys()]
|
|
367
375
|
.filter(c => changeToPackage.get(c) === depPkg);
|
|
@@ -461,7 +469,7 @@ export function slicePlan(config) {
|
|
|
461
469
|
const changes = assignments.get(pkgName);
|
|
462
470
|
if (!changes || changes.size === 0)
|
|
463
471
|
continue;
|
|
464
|
-
const pkgOutput = generateSinglePackage(pkgName, changes, graph, changeToPackage, packageDeps.get(pkgName) || new Set(), config.useTagsForCrossPackageDeps
|
|
472
|
+
const pkgOutput = generateSinglePackage(pkgName, changes, graph, changeToPackage, packageDeps.get(pkgName) || new Set(), config.crossPackageDepMode ?? (config.useTagsForCrossPackageDeps ? 'tag' : 'change'));
|
|
465
473
|
packages.push(pkgOutput);
|
|
466
474
|
// Check for heavy cross-package dependencies
|
|
467
475
|
let crossDeps = 0;
|