@pgpmjs/core 7.0.0 → 7.2.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.
Files changed (102) hide show
  1. package/bundle/apply.d.ts +93 -0
  2. package/bundle/apply.js +137 -0
  3. package/bundle/index.d.ts +12 -0
  4. package/bundle/index.js +31 -0
  5. package/core/class/pgpm.d.ts +2 -2
  6. package/core/class/pgpm.js +6 -6
  7. package/esm/bundle/apply.js +131 -0
  8. package/esm/bundle/index.js +15 -0
  9. package/esm/core/class/pgpm.js +6 -6
  10. package/esm/index.js +7 -3
  11. package/esm/migrate/client.js +1 -1
  12. package/esm/migrate/utils/hash.js +3 -6
  13. package/esm/modules/modules.js +1 -1
  14. package/esm/packaging/package.js +40 -24
  15. package/esm/rebundle/index.js +4 -0
  16. package/esm/rebundle/module.js +111 -0
  17. package/esm/rebundle/rebundle.js +170 -0
  18. package/esm/rebundle/workspace.js +170 -0
  19. package/esm/refactor/rename.js +5 -27
  20. package/esm/resolution/deps.js +2 -2
  21. package/esm/resolution/resolve.js +2 -2
  22. package/esm/slice/slice.js +1 -1
  23. package/esm/workspace/minimal.js +73 -0
  24. package/index.d.ts +6 -2
  25. package/index.js +8 -4
  26. package/migrate/client.js +1 -1
  27. package/migrate/utils/hash.d.ts +2 -4
  28. package/migrate/utils/hash.js +4 -8
  29. package/modules/modules.d.ts +1 -1
  30. package/modules/modules.js +1 -1
  31. package/package.json +4 -2
  32. package/packaging/package.d.ts +23 -1
  33. package/packaging/package.js +42 -25
  34. package/rebundle/index.d.ts +4 -0
  35. package/{files/sql-scripts → rebundle}/index.js +4 -2
  36. package/rebundle/module.d.ts +23 -0
  37. package/rebundle/module.js +116 -0
  38. package/rebundle/rebundle.d.ts +26 -0
  39. package/rebundle/rebundle.js +175 -0
  40. package/rebundle/types.d.ts +185 -0
  41. package/rebundle/workspace.d.ts +15 -0
  42. package/rebundle/workspace.js +173 -0
  43. package/refactor/rename.d.ts +2 -8
  44. package/refactor/rename.js +6 -29
  45. package/resolution/deps.d.ts +1 -1
  46. package/resolution/deps.js +2 -2
  47. package/resolution/resolve.js +2 -2
  48. package/slice/slice.d.ts +1 -1
  49. package/slice/slice.js +1 -1
  50. package/slice/types.d.ts +1 -1
  51. package/workspace/minimal.d.ts +64 -0
  52. package/workspace/minimal.js +82 -0
  53. package/esm/files/extension/index.js +0 -3
  54. package/esm/files/extension/reader.js +0 -79
  55. package/esm/files/extension/writer.js +0 -99
  56. package/esm/files/index.js +0 -6
  57. package/esm/files/plan/generator.js +0 -82
  58. package/esm/files/plan/index.js +0 -5
  59. package/esm/files/plan/parser.js +0 -297
  60. package/esm/files/plan/validators.js +0 -181
  61. package/esm/files/plan/writer.js +0 -142
  62. package/esm/files/sql/header.js +0 -259
  63. package/esm/files/sql/index.js +0 -2
  64. package/esm/files/sql/writer.js +0 -107
  65. package/esm/files/sql-scripts/index.js +0 -2
  66. package/esm/files/sql-scripts/reader.js +0 -19
  67. package/esm/files/types/index.js +0 -1
  68. package/esm/utils/author.js +0 -11
  69. package/files/extension/index.d.ts +0 -2
  70. package/files/extension/index.js +0 -19
  71. package/files/extension/reader.d.ts +0 -24
  72. package/files/extension/reader.js +0 -86
  73. package/files/extension/writer.d.ts +0 -48
  74. package/files/extension/writer.js +0 -107
  75. package/files/index.d.ts +0 -5
  76. package/files/index.js +0 -22
  77. package/files/plan/generator.d.ts +0 -23
  78. package/files/plan/generator.js +0 -90
  79. package/files/plan/index.d.ts +0 -4
  80. package/files/plan/index.js +0 -21
  81. package/files/plan/parser.d.ts +0 -28
  82. package/files/plan/parser.js +0 -304
  83. package/files/plan/validators.d.ts +0 -52
  84. package/files/plan/validators.js +0 -187
  85. package/files/plan/writer.d.ts +0 -27
  86. package/files/plan/writer.js +0 -152
  87. package/files/sql/header.d.ts +0 -114
  88. package/files/sql/header.js +0 -266
  89. package/files/sql/index.d.ts +0 -2
  90. package/files/sql/index.js +0 -18
  91. package/files/sql/writer.d.ts +0 -12
  92. package/files/sql/writer.js +0 -114
  93. package/files/sql-scripts/index.d.ts +0 -1
  94. package/files/sql-scripts/reader.d.ts +0 -8
  95. package/files/sql-scripts/reader.js +0 -23
  96. package/files/types/index.d.ts +0 -46
  97. package/files/types/index.js +0 -17
  98. package/files/types/package.d.ts +0 -20
  99. package/utils/author.d.ts +0 -5
  100. package/utils/author.js +0 -14
  101. /package/esm/{files/types/package.js → rebundle/types.js} +0 -0
  102. /package/{files/types/package.js → rebundle/types.js} +0 -0
@@ -0,0 +1,111 @@
1
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { parsePlanFile } from '@pgpmjs/ast/files/plan/parser';
4
+ import { mergeSqlStatements, packageModule } from '../packaging/package';
5
+ import { generatePlanContent } from '../slice/slice';
6
+ import { assembleChunkSql, rebundlePlan } from './rebundle';
7
+ export const SCRIPT_DIRS = ['deploy', 'revert', 'verify'];
8
+ /**
9
+ * Wrap a merged chunk body in a single transaction. Deploy/revert commit;
10
+ * verify rolls back (a verify script must leave no trace). The individual
11
+ * member files' own BEGIN/COMMIT were stripped during the merge, so each
12
+ * rebundled chunk deploys as exactly one transaction.
13
+ */
14
+ function wrapTransaction(body, scriptType) {
15
+ const trimmed = body.trim();
16
+ const tail = scriptType === 'verify' ? 'ROLLBACK;' : 'COMMIT;';
17
+ return `BEGIN;\n\n${trimmed}\n\n${tail}\n`;
18
+ }
19
+ /**
20
+ * Merge one chunk's member scripts (in the correct order per direction) into a
21
+ * single deparsed, single-transaction migration string. Shared by the
22
+ * single-module (`rebundleModule`) and workspace (`rebundleWorkspace`) emitters.
23
+ */
24
+ export async function mergeChunkScript(sourceDir, chunk, scriptType, opts = {}) {
25
+ const merged = await mergeSqlStatements(assembleChunkSql(sourceDir, chunk, scriptType), {
26
+ stripTransactions: true,
27
+ pretty: opts.pretty ?? true,
28
+ functionDelimiter: opts.functionDelimiter ?? '$EOFCODE$',
29
+ });
30
+ return wrapTransaction(merged.sql, scriptType);
31
+ }
32
+ /**
33
+ * Copy any top-level files (control, Makefile, package.json, README, ...) from
34
+ * the source module, excluding the plan and the per-change script directories
35
+ * which are regenerated. The rebundled module stays the same extension — same
36
+ * name, same control `requires` — only its internal change set collapses.
37
+ */
38
+ function copyModuleMetadata(sourceDir, outputDir) {
39
+ for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
40
+ if (entry.isDirectory())
41
+ continue;
42
+ if (entry.name === 'pgpm.plan')
43
+ continue;
44
+ copyFileSync(join(sourceDir, entry.name), join(outputDir, entry.name));
45
+ }
46
+ }
47
+ /**
48
+ * Materialize a rebundled module: merge each chunk's member scripts into a
49
+ * single deparsed migration (reusing `packageModule`'s merge engine), write the
50
+ * chunk-quotient plan, and verify the result is byte-identical when packaged.
51
+ *
52
+ * Reuses existing plumbing throughout — `rebundlePlan` for the chunk model,
53
+ * `mergeSqlStatements`/`packageModule` for statement merging and the gate,
54
+ * `generatePlanContent` for the chunk plan, and the source control/Makefile
55
+ * verbatim.
56
+ */
57
+ export async function rebundleModule(sourceDir, options) {
58
+ const { outputDir, overwrite = false, pretty = true, functionDelimiter = '$EOFCODE$', ...strategy } = options;
59
+ if (existsSync(outputDir) && readdirSync(outputDir).length > 0 && !overwrite) {
60
+ throw new Error(`Output directory is not empty: ${outputDir}. Pass overwrite: true to replace.`);
61
+ }
62
+ const plan = rebundlePlan(sourceDir, strategy);
63
+ // Tags anchor on the last member of the chunk that sealed at them; remap each
64
+ // to its chunk name so `deploy --to @tag` still lands on a real change.
65
+ const parsed = parsePlanFile(join(sourceDir, 'pgpm.plan'));
66
+ const sourceTags = parsed.data?.tags ?? [];
67
+ const memberToChunk = new Map();
68
+ for (const chunk of plan.chunks) {
69
+ for (const member of chunk.deploy)
70
+ memberToChunk.set(member, chunk.name);
71
+ }
72
+ const remappedTags = sourceTags.map(tag => ({
73
+ ...tag,
74
+ change: memberToChunk.get(tag.change) ?? tag.change,
75
+ }));
76
+ mkdirSync(outputDir, { recursive: true });
77
+ for (const dir of SCRIPT_DIRS)
78
+ mkdirSync(join(outputDir, dir), { recursive: true });
79
+ copyModuleMetadata(sourceDir, outputDir);
80
+ const written = [];
81
+ for (const chunk of plan.chunks) {
82
+ const paths = { chunk: chunk.name };
83
+ for (const dir of SCRIPT_DIRS) {
84
+ const merged = await mergeSqlStatements(assembleChunkSql(sourceDir, chunk, dir), {
85
+ stripTransactions: true,
86
+ pretty,
87
+ functionDelimiter,
88
+ });
89
+ const rel = join(dir, `${chunk.name}.sql`);
90
+ writeFileSync(join(outputDir, rel), wrapTransaction(merged.sql, dir));
91
+ paths[dir] = rel;
92
+ }
93
+ written.push(paths);
94
+ }
95
+ // Chunk-quotient plan: one change per chunk, deps are earlier chunk names.
96
+ const planEntries = plan.chunks.map(chunk => ({
97
+ name: chunk.name,
98
+ dependencies: chunk.dependencies,
99
+ }));
100
+ writeFileSync(join(outputDir, 'pgpm.plan'), generatePlanContent(plan.project, planEntries, remappedTags));
101
+ // Byte-identical gate: packaging the source and the rebundled module must
102
+ // produce the same consolidated extension SQL.
103
+ const sourcePkg = await packageModule(sourceDir, { pretty, functionDelimiter });
104
+ const outputPkg = await packageModule(outputDir, { pretty, functionDelimiter });
105
+ const invariant = {
106
+ ok: sourcePkg.sql === outputPkg.sql,
107
+ sourceDiff: !!sourcePkg.diff,
108
+ outputDiff: !!outputPkg.diff,
109
+ };
110
+ return { ...plan, outputDir, written, invariant };
111
+ }
@@ -0,0 +1,170 @@
1
+ import { readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { getChanges } from '@pgpmjs/ast/files/plan/parser';
4
+ import { resolveWithPlan } from '../resolution/resolve';
5
+ import { buildDependencyGraph, buildPackageDependencies, computeDeployOrder, detectPackageCycle, extractPackageFromPath, } from '../slice/slice';
6
+ import { parsePlanFile } from '@pgpmjs/ast/files/plan/parser';
7
+ /**
8
+ * Derive the boundary key for a change under the given strategy.
9
+ * Consecutive changes sharing a key may be merged into the same chunk.
10
+ */
11
+ function boundaryKey(changeName, strategy) {
12
+ if (strategy.boundary === 'none')
13
+ return '';
14
+ const folderKey = () => extractPackageFromPath(changeName, strategy.depth ?? 1, strategy.prefixToStrip ?? 'schemas');
15
+ if (strategy.boundary === 'category') {
16
+ // Classifier/facts-driven seam: the caller decides each change's category;
17
+ // fall back to the folder key for changes it doesn't classify.
18
+ const category = strategy.categoryOf?.(changeName);
19
+ return category ?? folderKey();
20
+ }
21
+ return folderKey();
22
+ }
23
+ /**
24
+ * Assign changes to chunks by walking the source plan in order and greedily
25
+ * accumulating consecutive changes that share a boundary key, sealing a chunk
26
+ * when the key changes, the size cap is reached, or a member carries a tag
27
+ * (tags mark points you can `deploy --to @tag`, so they must stay chunk seams).
28
+ *
29
+ * Because chunks are contiguous runs of the plan, member and chunk order both
30
+ * follow the source, which is what guarantees byte-identical output.
31
+ */
32
+ function assignChunks(orderedChanges, graph, strategy) {
33
+ const chunks = [];
34
+ const seen = new Map();
35
+ const maxSize = strategy.maxChunkSize && strategy.maxChunkSize > 0 ? strategy.maxChunkSize : Infinity;
36
+ let current = null;
37
+ const seal = () => {
38
+ current = null;
39
+ };
40
+ for (const change of orderedChanges) {
41
+ const key = boundaryKey(change, strategy);
42
+ const changeHasTag = (graph.tags.get(change)?.length ?? 0) > 0;
43
+ if (current &&
44
+ current.key === key &&
45
+ current.members.length < maxSize) {
46
+ current.members.push(change);
47
+ }
48
+ else {
49
+ // Deterministic, collision-free chunk name: boundary key (or 'chunk'),
50
+ // suffixed with an ordinal when the same key splits across size caps.
51
+ const base = key || 'chunk';
52
+ const ordinal = (seen.get(base) ?? 0) + 1;
53
+ seen.set(base, ordinal);
54
+ const name = ordinal === 1 ? base : `${base}~${ordinal}`;
55
+ current = { name, members: [change], key };
56
+ chunks.push(current);
57
+ }
58
+ // A member that is a tag anchor seals the chunk after inclusion.
59
+ if (changeHasTag)
60
+ seal();
61
+ }
62
+ return chunks.map(({ name, members }) => ({ name, members }));
63
+ }
64
+ /**
65
+ * Compute a rebundle plan for a pgpm module directory.
66
+ *
67
+ * Uses the existing plan parser, dependency graph, and package-dependency /
68
+ * deploy-order primitives from the slicer — chunks are treated exactly like
69
+ * packages for dependency and ordering purposes. No plan re-parsing or path
70
+ * logic is reinvented here.
71
+ */
72
+ export function rebundlePlan(moduleDir, strategy = {}) {
73
+ const planPath = join(moduleDir, 'pgpm.plan');
74
+ const planResult = parsePlanFile(planPath);
75
+ if (!planResult.data) {
76
+ const msg = planResult.errors?.map(e => `Line ${e.line}: ${e.message}`).join('\n') || 'Unknown error';
77
+ throw new Error(`Failed to parse plan file: ${msg}`);
78
+ }
79
+ const graph = buildDependencyGraph(planResult.data);
80
+ const orderedChanges = planResult.data.changes.map(c => c.name);
81
+ const warnings = [];
82
+ const rawChunks = assignChunks(orderedChanges, graph, strategy);
83
+ // Reverse lookup change -> chunk, reusing slice's package-dep machinery.
84
+ const assignments = new Map();
85
+ const changeToChunk = new Map();
86
+ for (const chunk of rawChunks) {
87
+ assignments.set(chunk.name, new Set(chunk.members));
88
+ for (const member of chunk.members)
89
+ changeToChunk.set(member, chunk.name);
90
+ }
91
+ const chunkDeps = buildPackageDependencies(graph, assignments);
92
+ const cycle = detectPackageCycle(chunkDeps);
93
+ if (cycle) {
94
+ warnings.push({
95
+ type: 'cycle_detected',
96
+ message: `Chunk cycle detected: ${cycle.join(' -> ')}`,
97
+ suggestedAction: 'Widen the boundary or raise maxChunkSize so cyclic changes share a chunk',
98
+ });
99
+ }
100
+ const deployOrder = computeDeployOrder(chunkDeps);
101
+ // Because chunks are contiguous runs of the source plan, the greedy creation
102
+ // order is already a valid deploy order. If the dependency-derived order
103
+ // disagrees, the boundary split a back-edge — surface it rather than silently
104
+ // reordering (that would break byte-identical output).
105
+ const creationOrder = rawChunks.map(c => c.name);
106
+ const orderMatches = deployOrder.length === creationOrder.length &&
107
+ deployOrder.every((name, i) => name === creationOrder[i]);
108
+ if (!orderMatches) {
109
+ warnings.push({
110
+ type: 'heavy_cross_deps',
111
+ message: 'Chunk deploy order differs from source order; a boundary split a dependency edge',
112
+ suggestedAction: 'Adjust the boundary/maxChunkSize so dependent changes stay contiguous',
113
+ });
114
+ }
115
+ const chunkByName = new Map(rawChunks.map(c => [c.name, c.members]));
116
+ const chunks = creationOrder.map(name => {
117
+ const members = chunkByName.get(name);
118
+ return {
119
+ name,
120
+ deploy: members,
121
+ verify: members,
122
+ revert: [...members].reverse(),
123
+ dependencies: [...(chunkDeps.get(name) ?? new Set())].sort(),
124
+ };
125
+ });
126
+ return {
127
+ chunks,
128
+ deployOrder: creationOrder,
129
+ warnings,
130
+ sourceChanges: planResult.data.changes,
131
+ project: planResult.data.package,
132
+ };
133
+ }
134
+ /**
135
+ * Assemble the merged SQL for one chunk in the given direction by concatenating
136
+ * its member script files — the same file-concatenation `resolveWithPlan` uses,
137
+ * just scoped to the chunk. Members are already correctly ordered per direction.
138
+ */
139
+ export function assembleChunkSql(moduleDir, chunk, scriptType = 'deploy') {
140
+ const members = scriptType === 'revert' ? chunk.revert : scriptType === 'verify' ? chunk.verify : chunk.deploy;
141
+ const parts = [];
142
+ for (const change of members) {
143
+ parts.push(readFileSync(join(moduleDir, scriptType, `${change}.sql`), 'utf-8'));
144
+ }
145
+ return parts.join('\n');
146
+ }
147
+ /**
148
+ * Byte-identical gate: assembling all chunks in deploy order must reproduce the
149
+ * module's original resolved output in every direction the engine cares about
150
+ * (deploy topological, verify in deploy order, revert reversed). This is the
151
+ * invariant that makes any granularity-dial position safe.
152
+ */
153
+ export function verifyRebundleInvariant(moduleDir, result) {
154
+ const mismatches = [];
155
+ const assembled = (scriptType) => {
156
+ const chunks = scriptType === 'revert' ? [...result.chunks].reverse() : result.chunks;
157
+ return chunks.map(chunk => assembleChunkSql(moduleDir, chunk, scriptType)).join('\n');
158
+ };
159
+ // deploy / revert have canonical resolvers; verify concatenates in deploy order.
160
+ const originalVerify = getChanges(join(moduleDir, 'pgpm.plan'))
161
+ .map(change => readFileSync(join(moduleDir, 'verify', `${change}.sql`), 'utf-8'))
162
+ .join('\n');
163
+ if (assembled('deploy') !== resolveWithPlan(moduleDir, 'deploy'))
164
+ mismatches.push('deploy');
165
+ if (assembled('revert') !== resolveWithPlan(moduleDir, 'revert'))
166
+ mismatches.push('revert');
167
+ if (assembled('verify') !== originalVerify)
168
+ mismatches.push('verify');
169
+ return { ok: mismatches.length === 0, mismatches };
170
+ }
@@ -0,0 +1,170 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'fs';
2
+ import { basename, join } from 'path';
3
+ import { DEFAULT_TEMPLATE_REPO, scaffoldTemplate } from '../core/template-scaffold';
4
+ import { parsePlanFile } from '@pgpmjs/ast/files/plan/parser';
5
+ import { mergeSqlStatements } from '../packaging/package';
6
+ import { resolveWithPlan } from '../resolution/resolve';
7
+ import { writeMinimalModule, writeMinimalWorkspace } from '../workspace/minimal';
8
+ import { mergeChunkScript } from './module';
9
+ import { rebundlePlan } from './rebundle';
10
+ /**
11
+ * Materialize a rebundled workspace: emit each chunk as its own tiny module
12
+ * (one merged change) under `packages/<chunk>/`, carrying cross-chunk deps via
13
+ * the control-file `requires` (and optionally a plan cross-reference).
14
+ *
15
+ * This is the "modular workspace" carve — the same monolith sliced into
16
+ * independently publishable module forks (e.g. a users module, an admin module).
17
+ * The emitted directory is a real, discoverable pgpm workspace: it reuses
18
+ * `writeMinimalWorkspace`/`writeMinimalModule` (the minimal deployable file set
19
+ * pgpm recognizes — `pgpm.json`, `pgpm.plan`, `<name>.control`, `Makefile`,
20
+ * `package.json`), `rebundlePlan` for the chunk model, and `mergeChunkScript`
21
+ * for the per-chunk merge.
22
+ */
23
+ export async function rebundleWorkspace(sourceDir, options) {
24
+ const { outputDir, overwrite = false, pretty = true, functionDelimiter = '$EOFCODE$', crossChunkDepMode = 'control-only', initScaffold = false, templateRepo = DEFAULT_TEMPLATE_REPO, templateBranch, license = 'CONSTRUCTIVE', ...strategy } = options;
25
+ if (existsSync(outputDir) && readdirSync(outputDir).length > 0 && !overwrite) {
26
+ throw new Error(`Output directory is not empty: ${outputDir}. Pass overwrite: true to replace.`);
27
+ }
28
+ const plan = rebundlePlan(sourceDir, strategy);
29
+ // Remap tags onto their sealing chunk (each tag anchored a chunk boundary).
30
+ const parsed = parsePlanFile(join(sourceDir, 'pgpm.plan'));
31
+ const sourceTags = parsed.data?.tags ?? [];
32
+ const memberToChunk = new Map();
33
+ for (const chunk of plan.chunks) {
34
+ for (const member of chunk.deploy)
35
+ memberToChunk.set(member, chunk.name);
36
+ }
37
+ const remappedTags = sourceTags.map(tag => ({
38
+ ...tag,
39
+ change: memberToChunk.get(tag.change) ?? tag.change,
40
+ }));
41
+ if (initScaffold) {
42
+ // Lay down the full `pgpm init` workspace shell (pgpm.json, pnpm-workspace,
43
+ // lerna.json, tsconfig, lint/format configs, CI); the merged pgpm files are
44
+ // written on top per module below.
45
+ await scaffoldTemplate({
46
+ fromPath: 'workspace',
47
+ outputDir,
48
+ templateRepo,
49
+ branch: templateBranch,
50
+ answers: workspaceAnswers(outputDir, license),
51
+ noTty: true,
52
+ });
53
+ }
54
+ else {
55
+ writeMinimalWorkspace(outputDir, { packages: ['packages/*'] });
56
+ }
57
+ const byDeployOrder = [...plan.chunks].sort((a, b) => plan.deployOrder.indexOf(a.name) - plan.deployOrder.indexOf(b.name));
58
+ const packages = [];
59
+ for (const chunk of byDeployOrder) {
60
+ const pkgRel = join('packages', chunk.name);
61
+ const pkgDir = join(outputDir, pkgRel);
62
+ if (initScaffold) {
63
+ // Scaffold the module's developer shell (package.json, jest.config.js,
64
+ // README, LICENSE, __tests__); writeMinimalModule overwrites the pgpm
65
+ // files (pgpm.plan, .control, Makefile, deploy/revert/verify) below.
66
+ await scaffoldTemplate({
67
+ fromPath: 'module',
68
+ outputDir: pkgDir,
69
+ templateRepo,
70
+ branch: templateBranch,
71
+ answers: moduleAnswers(chunk.name, license),
72
+ noTty: true,
73
+ });
74
+ }
75
+ const scripts = {
76
+ [chunk.name]: {
77
+ deploy: await mergeChunkScript(sourceDir, chunk, 'deploy', { pretty, functionDelimiter }),
78
+ revert: await mergeChunkScript(sourceDir, chunk, 'revert', { pretty, functionDelimiter }),
79
+ verify: await mergeChunkScript(sourceDir, chunk, 'verify', { pretty, functionDelimiter }),
80
+ },
81
+ };
82
+ // Control `requires` always carries the module-level dependency; in
83
+ // 'change' mode the plan additionally records the fine-grained cross-ref.
84
+ const deps = chunk.dependencies;
85
+ const planChange = {
86
+ name: chunk.name,
87
+ dependencies: planChangeDeps(deps, crossChunkDepMode),
88
+ };
89
+ writeMinimalModule(pkgDir, {
90
+ name: chunk.name,
91
+ changes: [planChange],
92
+ scripts,
93
+ tags: remappedTags.filter(t => t.change === chunk.name),
94
+ requires: deps,
95
+ license,
96
+ overwrite: true,
97
+ // When scaffolded, keep the boilerplate's richer package.json.
98
+ writePackageJson: !initScaffold,
99
+ });
100
+ packages.push({ name: chunk.name, dir: pkgRel, dependencies: [...deps] });
101
+ }
102
+ const invariant = { ok: await checkWorkspaceInvariant(sourceDir, outputDir, packages, { pretty, functionDelimiter }) };
103
+ return { ...plan, outputDir, packages, crossChunkDepMode, invariant };
104
+ }
105
+ /**
106
+ * In 'change' mode, express each cross-chunk dep as a plan cross-reference
107
+ * (`<dep>:<dep>`, since each chunk-module has a single change named after it);
108
+ * in 'control-only' mode the plan carries no deps — the control `requires`
109
+ * governs ordering.
110
+ */
111
+ function planChangeDeps(deps, mode) {
112
+ if (mode === 'control-only')
113
+ return [];
114
+ return deps.map(dep => `${dep}:${dep}`);
115
+ }
116
+ /**
117
+ * Non-interactive answers for the `pgpm init` workspace template. Values are
118
+ * deterministic placeholders (the generated pgpm files are the real payload);
119
+ * the workspace name derives from the output directory.
120
+ */
121
+ function workspaceAnswers(outputDir, license) {
122
+ const name = basename(outputDir) || 'rebundled-workspace';
123
+ return {
124
+ name,
125
+ fullName: 'Constructive',
126
+ email: 'noreply@constructive.io',
127
+ moduleName: name,
128
+ username: 'constructive-io',
129
+ repoName: name,
130
+ license,
131
+ };
132
+ }
133
+ /** Non-interactive answers for the `pgpm init` module template. */
134
+ function moduleAnswers(name, license) {
135
+ return {
136
+ name,
137
+ moduleName: name,
138
+ moduleDesc: `${name} module`,
139
+ description: `${name} module`,
140
+ repoName: name,
141
+ fullName: 'Constructive',
142
+ email: 'noreply@constructive.io',
143
+ username: 'constructive-io',
144
+ access: 'public',
145
+ license,
146
+ };
147
+ }
148
+ /**
149
+ * Byte-identical gate for a workspace: concatenate every package's deploy
150
+ * script in workspace deploy order, merge it, and compare against the merged
151
+ * original module deploy output. Both go through the same `mergeSqlStatements`
152
+ * pipeline, so equality proves the carve preserved the deployed statements and
153
+ * their order.
154
+ */
155
+ async function checkWorkspaceInvariant(sourceDir, outputDir, packages, opts) {
156
+ const concatenated = packages
157
+ .map(p => readFileSync(join(outputDir, p.dir, 'deploy', `${p.name}.sql`), 'utf-8'))
158
+ .join('\n');
159
+ const workspaceMerged = await mergeSqlStatements(concatenated, {
160
+ stripTransactions: true,
161
+ pretty: opts.pretty,
162
+ functionDelimiter: opts.functionDelimiter,
163
+ });
164
+ const sourceMerged = await mergeSqlStatements(resolveWithPlan(sourceDir, 'deploy'), {
165
+ stripTransactions: true,
166
+ pretty: opts.pretty,
167
+ functionDelimiter: opts.functionDelimiter,
168
+ });
169
+ return workspaceMerged.sql === sourceMerged.sql;
170
+ }
@@ -1,7 +1,10 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmdirSync, statSync, writeFileSync } from 'fs';
2
2
  import { dirname, join, relative, sep } from 'path';
3
- import { parsePlanFileSimple } from '../files/plan/parser';
4
- import { parsePgpmHeader, renameInHeader, writePgpmScript } from '../files/sql/header';
3
+ import { parsePlanFileSimple } from '@pgpmjs/ast/files/plan/parser';
4
+ import { parsePgpmHeader, renameInHeader, writePgpmScript } from '@pgpmjs/ast/files/sql/header';
5
+ import { renameInPlanContent } from '@pgpmjs/ast/plan-rename';
6
+ // Re-exported from @pgpmjs/ast so existing `@pgpmjs/core` consumers keep importing it here.
7
+ export { renameInPlanContent };
5
8
  const SCRIPT_DIRS = ['deploy', 'revert', 'verify'];
6
9
  function escapeRe(str) {
7
10
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -59,31 +62,6 @@ export function validateRenames(moduleDir, renames) {
59
62
  targets.add(to);
60
63
  }
61
64
  }
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
65
  /**
88
66
  * Atomically rename/move changes within a pgpm module: the coupled 4-way
89
67
  * rewrite of (1) deploy/revert/verify script files on disk, (2) the moved
@@ -2,8 +2,8 @@ import { readFileSync } from 'fs';
2
2
  import { sync as glob } from 'glob';
3
3
  import { join, relative } from 'path';
4
4
  import { PgpmPackage } from '../core/class/pgpm';
5
- import { parsePlanFile } from '../files/plan/parser';
6
- import { scanDeployScript } from '../files/sql/header';
5
+ import { parsePlanFile } from '@pgpmjs/ast/files/plan/parser';
6
+ import { scanDeployScript } from '@pgpmjs/ast/files/sql/header';
7
7
  import { errors } from '@pgpmjs/types';
8
8
  /**
9
9
  * Core dependency resolution algorithm that handles circular dependency detection
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from 'fs';
2
- import { getChanges, getExtensionName } from '../files';
3
- import { parsePlanFile } from '../files/plan/parser';
2
+ import { getChanges, getExtensionName } from '@pgpmjs/ast/files';
3
+ import { parsePlanFile } from '@pgpmjs/ast/files/plan/parser';
4
4
  import { resolveDependencies } from './deps';
5
5
  import { errors } from '@pgpmjs/types';
6
6
  /**
@@ -1,4 +1,4 @@
1
- import { parsePlanFile } from '../files/plan/parser';
1
+ import { parsePlanFile } from '@pgpmjs/ast/files/plan/parser';
2
2
  import { minimatch } from 'minimatch';
3
3
  /**
4
4
  * Build a dependency graph from a parsed plan file
@@ -0,0 +1,73 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { generatePlanContent } from '../slice/slice';
4
+ const SCRIPT_DIRS = ['deploy', 'revert', 'verify'];
5
+ /**
6
+ * The minimal set of files pgpm needs to recognize and deploy a module:
7
+ * `pgpm.plan` (module marker + change order), `<name>.control` (extension
8
+ * metadata), `Makefile` (PGXS packaging), `package.json`, and the
9
+ * deploy/revert/verify scripts. Mirrors what `pgpm init` scaffolds, minus the
10
+ * network-fetched boilerplate (README/LICENSE/jest/tests/lint configs).
11
+ */
12
+ export function writeMinimalModule(moduleDir, options) {
13
+ const { name, changes, scripts, tags = [], requires = [], version = '0.0.1', license = 'CONSTRUCTIVE', overwrite = false, writePackageJson = true, } = options;
14
+ if (fs.existsSync(moduleDir) && fs.readdirSync(moduleDir).length > 0 && !overwrite) {
15
+ throw new Error(`Module directory is not empty: ${moduleDir}. Pass overwrite: true to replace.`);
16
+ }
17
+ for (const dir of SCRIPT_DIRS) {
18
+ fs.mkdirSync(path.join(moduleDir, dir), { recursive: true });
19
+ }
20
+ for (const change of changes) {
21
+ const changeScripts = scripts[change.name];
22
+ if (!changeScripts) {
23
+ throw new Error(`Missing scripts for change "${change.name}" in module "${name}"`);
24
+ }
25
+ fs.writeFileSync(path.join(moduleDir, 'deploy', `${change.name}.sql`), changeScripts.deploy);
26
+ fs.writeFileSync(path.join(moduleDir, 'revert', `${change.name}.sql`), changeScripts.revert);
27
+ fs.writeFileSync(path.join(moduleDir, 'verify', `${change.name}.sql`), changeScripts.verify ?? `-- Verify ${change.name}\n\nBEGIN;\n\nSELECT 1;\n\nROLLBACK;\n`);
28
+ }
29
+ fs.writeFileSync(path.join(moduleDir, 'pgpm.plan'), generatePlanContent(name, changes, tags));
30
+ fs.writeFileSync(path.join(moduleDir, `${name}.control`), generateModuleControl(name, requires, version));
31
+ fs.writeFileSync(path.join(moduleDir, 'Makefile'), generateModuleMakefile(name, version));
32
+ if (writePackageJson) {
33
+ fs.writeFileSync(path.join(moduleDir, 'package.json'), JSON.stringify({ name, version, description: `${name} module`, license }, null, 2) + '\n');
34
+ }
35
+ }
36
+ /**
37
+ * Write the minimal workspace marker (`pgpm.json`) so pgpm discovers the
38
+ * directory as a workspace. This is what `getWorkspacePath` walks up to find.
39
+ */
40
+ export function writeMinimalWorkspace(workspaceDir, options = {}) {
41
+ const { packages = ['packages/*'], overwrite = true } = options;
42
+ const configPath = path.join(workspaceDir, 'pgpm.json');
43
+ if (fs.existsSync(configPath) && !overwrite)
44
+ return;
45
+ fs.mkdirSync(workspaceDir, { recursive: true });
46
+ fs.writeFileSync(configPath, JSON.stringify({ packages }, null, 2) + '\n');
47
+ }
48
+ /**
49
+ * Full extension control file, matching the shape `pgpm init` emits (comment,
50
+ * default_version, module_pathname, requires, relocatable, superuser).
51
+ */
52
+ export function generateModuleControl(name, requires = [], version = '0.0.1') {
53
+ let content = `# ${name} extension\n`;
54
+ content += `comment = '${name} extension'\n`;
55
+ content += `default_version = '${version}'\n`;
56
+ content += `module_pathname = '$libdir/${name}'\n`;
57
+ if (requires.length > 0) {
58
+ content += `requires = '${requires.join(',')}'\n`;
59
+ }
60
+ content += `relocatable = false\n`;
61
+ content += `superuser = false\n`;
62
+ return content;
63
+ }
64
+ /**
65
+ * PGXS Makefile for `pgpm package`/`make install`.
66
+ */
67
+ export function generateModuleMakefile(name, version = '0.0.1') {
68
+ return (`EXTENSION = ${name}\n` +
69
+ `DATA = sql/${name}--${version}.sql\n\n` +
70
+ `PG_CONFIG = pg_config\n` +
71
+ `PGXS := $(shell $(PG_CONFIG) --pgxs)\n` +
72
+ `include $(PGXS)\n`);
73
+ }
package/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './core/class/pgpm';
2
2
  export * from './slice';
3
+ export * from './rebundle';
3
4
  export * from './extensions/extensions';
4
5
  export * from './modules/modules';
5
6
  export * from './packaging/package';
@@ -8,16 +9,19 @@ export * from './resolution/deps';
8
9
  export * from './resolution/resolve';
9
10
  export * from './workspace/paths';
10
11
  export * from './workspace/utils';
12
+ export * from './workspace/minimal';
11
13
  export * from './core/template-scaffold';
12
14
  export * from './core/boilerplate-types';
13
15
  export * from './core/boilerplate-scanner';
14
- export * from './files';
16
+ export * from '@pgpmjs/ast/files';
17
+ export * from '@pgpmjs/ast/module';
18
+ export * from './bundle';
15
19
  export * from './refactor';
16
20
  export { cleanSql } from './migrate/clean';
17
21
  export { PgpmMigrate } from './migrate/client';
18
22
  export { PgpmInit } from './init/client';
19
23
  export * from './roles';
20
- export { parseAuthor } from './utils/author';
24
+ export { parseAuthor } from '@pgpmjs/ast';
21
25
  export { DeployOptions, DeployResult, MigrateChange, MigratePlanFile, RevertOptions, RevertResult, StatusResult, VerifyOptions, VerifyResult } from './migrate/types';
22
26
  export { hashFile, hashString } from './migrate/utils/hash';
23
27
  export { executeQuery, TransactionContext, TransactionOptions, withTransaction } from './migrate/utils/transaction';
package/index.js CHANGED
@@ -17,6 +17,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.withTransaction = exports.executeQuery = exports.hashString = exports.hashFile = exports.parseAuthor = exports.PgpmInit = exports.PgpmMigrate = exports.cleanSql = void 0;
18
18
  __exportStar(require("./core/class/pgpm"), exports);
19
19
  __exportStar(require("./slice"), exports);
20
+ __exportStar(require("./rebundle"), exports);
20
21
  __exportStar(require("./extensions/extensions"), exports);
21
22
  __exportStar(require("./modules/modules"), exports);
22
23
  __exportStar(require("./packaging/package"), exports);
@@ -25,11 +26,14 @@ __exportStar(require("./resolution/deps"), exports);
25
26
  __exportStar(require("./resolution/resolve"), exports);
26
27
  __exportStar(require("./workspace/paths"), exports);
27
28
  __exportStar(require("./workspace/utils"), exports);
29
+ __exportStar(require("./workspace/minimal"), exports);
28
30
  __exportStar(require("./core/template-scaffold"), exports);
29
31
  __exportStar(require("./core/boilerplate-types"), exports);
30
32
  __exportStar(require("./core/boilerplate-scanner"), exports);
31
- // Export package-files functionality (now integrated into core)
32
- __exportStar(require("./files"), exports);
33
+ // Re-export the module AST layer (moved to the @pgpmjs/ast leaf package)
34
+ __exportStar(require("@pgpmjs/ast/files"), exports);
35
+ __exportStar(require("@pgpmjs/ast/module"), exports);
36
+ __exportStar(require("./bundle"), exports);
33
37
  __exportStar(require("./refactor"), exports);
34
38
  var clean_1 = require("./migrate/clean");
35
39
  Object.defineProperty(exports, "cleanSql", { enumerable: true, get: function () { return clean_1.cleanSql; } });
@@ -38,8 +42,8 @@ Object.defineProperty(exports, "PgpmMigrate", { enumerable: true, get: function
38
42
  var client_2 = require("./init/client");
39
43
  Object.defineProperty(exports, "PgpmInit", { enumerable: true, get: function () { return client_2.PgpmInit; } });
40
44
  __exportStar(require("./roles"), exports);
41
- var author_1 = require("./utils/author");
42
- Object.defineProperty(exports, "parseAuthor", { enumerable: true, get: function () { return author_1.parseAuthor; } });
45
+ var ast_1 = require("@pgpmjs/ast");
46
+ Object.defineProperty(exports, "parseAuthor", { enumerable: true, get: function () { return ast_1.parseAuthor; } });
43
47
  var hash_1 = require("./migrate/utils/hash");
44
48
  Object.defineProperty(exports, "hashFile", { enumerable: true, get: function () { return hash_1.hashFile; } });
45
49
  Object.defineProperty(exports, "hashString", { enumerable: true, get: function () { return hash_1.hashString; } });