@pgpmjs/core 7.11.1 → 7.14.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.
@@ -0,0 +1,14 @@
1
+ import { ResolvedApplySpec } from './types';
2
+ /** Whether a module directory is an apply-spec proxy module. */
3
+ export declare function hasApplySpec(moduleDir: string): boolean;
4
+ /**
5
+ * Read, validate, and normalize a proxy module's `pgpm.apply.json`.
6
+ *
7
+ * Normalizations: `source` is expanded to its object form and `name` defaults
8
+ * to the proxy directory's name.
9
+ *
10
+ * @throws when the file is missing, unparsable, or structurally invalid.
11
+ */
12
+ export declare function readApplySpec(moduleDir: string): ResolvedApplySpec;
13
+ /** In-memory core of {@link readApplySpec}. */
14
+ export declare function parseApplySpec(content: string, specPath: string): ResolvedApplySpec;
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hasApplySpec = hasApplySpec;
4
+ exports.readApplySpec = readApplySpec;
5
+ exports.parseApplySpec = parseApplySpec;
6
+ const fs_1 = require("fs");
7
+ const path_1 = require("path");
8
+ const types_1 = require("./types");
9
+ /** Whether a module directory is an apply-spec proxy module. */
10
+ function hasApplySpec(moduleDir) {
11
+ return (0, fs_1.existsSync)((0, path_1.join)(moduleDir, types_1.APPLY_SPEC_FILE));
12
+ }
13
+ /**
14
+ * Read, validate, and normalize a proxy module's `pgpm.apply.json`.
15
+ *
16
+ * Normalizations: `source` is expanded to its object form and `name` defaults
17
+ * to the proxy directory's name.
18
+ *
19
+ * @throws when the file is missing, unparsable, or structurally invalid.
20
+ */
21
+ function readApplySpec(moduleDir) {
22
+ const specPath = (0, path_1.join)(moduleDir, types_1.APPLY_SPEC_FILE);
23
+ if (!(0, fs_1.existsSync)(specPath)) {
24
+ throw new Error(`No ${types_1.APPLY_SPEC_FILE} found in ${moduleDir}`);
25
+ }
26
+ return parseApplySpec((0, fs_1.readFileSync)(specPath, 'utf-8'), specPath);
27
+ }
28
+ /** In-memory core of {@link readApplySpec}. */
29
+ function parseApplySpec(content, specPath) {
30
+ let parsed;
31
+ try {
32
+ parsed = JSON.parse(content);
33
+ }
34
+ catch (err) {
35
+ throw new Error(`Failed to parse ${specPath}: ${err instanceof Error ? err.message : err}`);
36
+ }
37
+ const source = typeof parsed?.source === 'string' ? { module: parsed.source } : parsed?.source;
38
+ if (!source || typeof source.module !== 'string' || !source.module) {
39
+ throw new Error(`${specPath}: "source" (source module name, or { "module": ... }) is required`);
40
+ }
41
+ const hasSchemas = parsed.schemas !== undefined;
42
+ if (hasSchemas) {
43
+ if (typeof parsed.schemas !== 'object' ||
44
+ Array.isArray(parsed.schemas) ||
45
+ Object.keys(parsed.schemas).length === 0 ||
46
+ Object.entries(parsed.schemas).some(([from, to]) => typeof from !== 'string' || typeof to !== 'string' || !from || !to)) {
47
+ throw new Error(`${specPath}: "schemas" must be a non-empty string → string map`);
48
+ }
49
+ }
50
+ const ROUTE_KINDS = ['table', 'view', 'function', 'procedure', 'type'];
51
+ const hasRoute = parsed.route !== undefined;
52
+ if (hasRoute) {
53
+ if (!Array.isArray(parsed.route) || parsed.route.length === 0) {
54
+ throw new Error(`${specPath}: "route" must be a non-empty array of route entries`);
55
+ }
56
+ for (const entry of parsed.route) {
57
+ if (!entry ||
58
+ typeof entry !== 'object' ||
59
+ typeof entry.fromSchema !== 'string' ||
60
+ !entry.fromSchema ||
61
+ typeof entry.name !== 'string' ||
62
+ !entry.name ||
63
+ typeof entry.toSchema !== 'string' ||
64
+ !entry.toSchema ||
65
+ !ROUTE_KINDS.includes(entry.kind)) {
66
+ throw new Error(`${specPath}: each "route" entry needs { fromSchema, kind (${ROUTE_KINDS.join('|')}), name, toSchema } as non-empty strings`);
67
+ }
68
+ }
69
+ }
70
+ if (!hasSchemas && !hasRoute) {
71
+ throw new Error(`${specPath}: at least one of "schemas" (string → string map) or "route" (object routes) is required`);
72
+ }
73
+ const name = parsed.name ?? (0, path_1.basename)((0, path_1.dirname)(specPath));
74
+ if (typeof name !== 'string' || !name) {
75
+ throw new Error(`${specPath}: "name" must be a non-empty string`);
76
+ }
77
+ if (name === source.module) {
78
+ throw new Error(`${specPath}: apply module "${name}" cannot apply itself`);
79
+ }
80
+ const requires = parsed.requires;
81
+ if (requires !== undefined &&
82
+ (!Array.isArray(requires) || requires.some((r) => typeof r !== 'string' || !r))) {
83
+ throw new Error(`${specPath}: "requires" must be an array of module names`);
84
+ }
85
+ if (requires?.includes(source.module)) {
86
+ throw new Error(`${specPath}: "requires" must not include the source module "${source.module}" — ` +
87
+ `it is source material, not a runtime dependency`);
88
+ }
89
+ return { ...parsed, name, source, requires };
90
+ }
@@ -0,0 +1,3 @@
1
+ export * from './materialize';
2
+ export * from './apply-spec';
3
+ export * from './types';
package/apply/index.js ADDED
@@ -0,0 +1,19 @@
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./materialize"), exports);
18
+ __exportStar(require("./apply-spec"), exports);
19
+ __exportStar(require("./types"), exports);
@@ -0,0 +1,40 @@
1
+ import { MigrationBundle } from '@pgpmjs/bundle';
2
+ import { ModuleMap } from '../modules/modules';
3
+ import { ResolvedApplySpec } from './types';
4
+ export interface MaterializeApplyOptions {
5
+ /** Directory of the source module being applied. */
6
+ sourceDir: string;
7
+ /** The apply spec (typically read from the proxy via {@link readApplySpec}). */
8
+ spec: ResolvedApplySpec;
9
+ /** Directory to materialize into (default: a fresh temp dir). */
10
+ outDir?: string;
11
+ }
12
+ export interface MaterializeApplyResult {
13
+ /** The transpiled, content-addressed bundle that was materialized. */
14
+ bundle: MigrationBundle;
15
+ /** The deployable module directory the bundle was written to. */
16
+ outDir: string;
17
+ }
18
+ /**
19
+ * Transpile a source module per an apply spec and materialize the result as a
20
+ * deployable module directory carrying the instance's identity.
21
+ *
22
+ * Pure pipeline over existing primitives: `bundleFromModule` →
23
+ * `transpileBundle(makeSchemaTranspiler)` → `verifyBundle` →
24
+ * `materializeBundle`. Deterministic — identical source + spec always produce
25
+ * an identical bundle (and digest), which is what makes verify/revert able to
26
+ * re-derive the exact deployed artifact.
27
+ */
28
+ export declare function materializeApplyModule(options: MaterializeApplyOptions): Promise<MaterializeApplyResult>;
29
+ /**
30
+ * Deploy-path dispatch seam: given a resolved module's directory, return the
31
+ * directory the migration engine should actually run against.
32
+ *
33
+ * Non-proxy modules pass through unchanged. For proxy modules (directories
34
+ * carrying a `pgpm.apply.json`), the source module is located in the workspace
35
+ * module map, transpiled per the spec, and materialized to a cached temp dir
36
+ * carrying the instance's identity.
37
+ */
38
+ export declare function resolveEffectiveModulePath(moduleName: string, modulePath: string, moduleMap: ModuleMap, workspacePath: string): Promise<string>;
39
+ /** Test seam: clear the per-process materialization cache. */
40
+ export declare function clearApplyMaterializationCache(): void;
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.materializeApplyModule = materializeApplyModule;
4
+ exports.resolveEffectiveModulePath = resolveEffectiveModulePath;
5
+ exports.clearApplyMaterializationCache = clearApplyMaterializationCache;
6
+ const fs_1 = require("fs");
7
+ const os_1 = require("os");
8
+ const path_1 = require("path");
9
+ const bundle_1 = require("@pgpmjs/bundle");
10
+ const transform_1 = require("@pgpmjs/transform");
11
+ const apply_spec_1 = require("./apply-spec");
12
+ // Functions that take a bare schema name as a string-literal first argument.
13
+ // The AST pass only remaps dotted literals ('schema.object'); these helpers
14
+ // (ubiquitous in verify scripts) name the schema alone, so a string-level
15
+ // pre-pass remaps them.
16
+ const SCHEMA_NAME_LITERAL_FUNCS = ['verify_schema', 'has_schema_privilege'];
17
+ const schemaNameLiteralPass = (content, schemaMapping) => {
18
+ const pattern = new RegExp(`\\b(${SCHEMA_NAME_LITERAL_FUNCS.join('|')})\\s*\\(\\s*'([^']+)'`, 'gi');
19
+ return content.replace(pattern, (match, _fn, schema) => {
20
+ const mapped = schemaMapping.get(schema);
21
+ return mapped ? match.replace(`'${schema}'`, `'${mapped}'`) : match;
22
+ });
23
+ };
24
+ /**
25
+ * Transpile a source module per an apply spec and materialize the result as a
26
+ * deployable module directory carrying the instance's identity.
27
+ *
28
+ * Pure pipeline over existing primitives: `bundleFromModule` →
29
+ * `transpileBundle(makeSchemaTranspiler)` → `verifyBundle` →
30
+ * `materializeBundle`. Deterministic — identical source + spec always produce
31
+ * an identical bundle (and digest), which is what makes verify/revert able to
32
+ * re-derive the exact deployed artifact.
33
+ */
34
+ async function materializeApplyModule(options) {
35
+ const { sourceDir, spec } = options;
36
+ const instanceName = spec.name;
37
+ // the schema transpiler's SQL/PLpgSQL parser is WASM-backed and needs a
38
+ // one-time async init (idempotent)
39
+ await (0, transform_1.loadModule)();
40
+ const source = (0, bundle_1.bundleFromModule)(sourceDir);
41
+ if (spec.source.bundleDigest && spec.source.bundleDigest !== source.manifest.digest) {
42
+ throw new Error(`Apply spec for "${instanceName}" pins source bundle digest ${spec.source.bundleDigest}, ` +
43
+ `but the installed source "${spec.source.module}" hashes to ${source.manifest.digest}. ` +
44
+ `Reinstall the pinned version or update the spec.`);
45
+ }
46
+ // Every distinct target schema is treated as "may already exist": apply
47
+ // deploys into consumer-chosen namespaces that can pre-exist (a shared schema,
48
+ // another tenant), so `CREATE SCHEMA` is emitted idempotently. This is what
49
+ // lets the same source apply cleanly whether or not the destination exists.
50
+ const targetSchemas = [
51
+ ...new Set([
52
+ ...Object.values(spec.schemas ?? {}),
53
+ ...(spec.route ?? []).map(r => r.toSchema)
54
+ ])
55
+ ];
56
+ const { renameChange, transformScript, result } = (0, transform_1.makeSchemaTranspiler)({
57
+ schemaMap: spec.schemas,
58
+ routes: spec.route,
59
+ transform: {
60
+ prePasses: [schemaNameLiteralPass],
61
+ assumeSchemasExist: targetSchemas
62
+ }
63
+ });
64
+ const transpiled = (0, bundle_1.transpileBundle)(source, {
65
+ renameChange,
66
+ transformScript,
67
+ renameModule: instanceName,
68
+ provenance: {
69
+ appliedFrom: spec.source.module,
70
+ ...(spec.source.package ? { sourcePackage: spec.source.package } : {}),
71
+ ...(spec.source.version ? { sourceVersion: spec.source.version } : {})
72
+ }
73
+ });
74
+ if (result.errors.length > 0) {
75
+ const detail = result.errors.map(e => `${e.file}: ${e.error}`).join('; ');
76
+ throw new Error(`Apply transpile of "${spec.source.module}" as "${instanceName}" failed: ${detail}`);
77
+ }
78
+ const issues = (0, bundle_1.verifyBundle)(transpiled);
79
+ if (issues.length > 0) {
80
+ throw new Error(`Transpiled bundle for "${instanceName}" failed integrity verification: ` +
81
+ issues.map(i => i.kind).join(', '));
82
+ }
83
+ const outDir = options.outDir ?? (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), `pgpm-apply-${instanceName}-`));
84
+ (0, bundle_1.materializeBundle)(transpiled, outDir);
85
+ return { bundle: transpiled, outDir };
86
+ }
87
+ // One materialization per proxy path per process: the pipeline is
88
+ // deterministic, so re-deriving within a process is pure waste.
89
+ const materializedCache = new Map();
90
+ /**
91
+ * Deploy-path dispatch seam: given a resolved module's directory, return the
92
+ * directory the migration engine should actually run against.
93
+ *
94
+ * Non-proxy modules pass through unchanged. For proxy modules (directories
95
+ * carrying a `pgpm.apply.json`), the source module is located in the workspace
96
+ * module map, transpiled per the spec, and materialized to a cached temp dir
97
+ * carrying the instance's identity.
98
+ */
99
+ async function resolveEffectiveModulePath(moduleName, modulePath, moduleMap, workspacePath) {
100
+ if (!(0, apply_spec_1.hasApplySpec)(modulePath))
101
+ return modulePath;
102
+ const cached = materializedCache.get(modulePath);
103
+ if (cached)
104
+ return cached;
105
+ const spec = (0, apply_spec_1.readApplySpec)(modulePath);
106
+ const sourceModule = moduleMap[spec.source.module];
107
+ if (!sourceModule) {
108
+ throw new Error(`Apply spec in "${moduleName}" references source module "${spec.source.module}", ` +
109
+ `which was not found in the workspace. Run \`pgpm install\` to install it` +
110
+ (spec.source.package ? ` (${spec.source.package})` : '') +
111
+ `.`);
112
+ }
113
+ const sourceDir = (0, path_1.resolve)(workspacePath, sourceModule.path);
114
+ const { outDir } = await materializeApplyModule({ sourceDir, spec });
115
+ materializedCache.set(modulePath, outDir);
116
+ return outDir;
117
+ }
118
+ /** Test seam: clear the per-process materialization cache. */
119
+ function clearApplyMaterializationCache() {
120
+ materializedCache.clear();
121
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The apply-spec ("proxy module") types.
3
+ *
4
+ * A proxy module is a directory containing a single `pgpm.apply.json` file:
5
+ * "apply <source module> transpiled with this schema map, under my name". It
6
+ * needs no `.control`, no `pgpm.plan`, and no deploy/revert/verify scripts —
7
+ * workspace discovery synthesizes a module entry from the spec, so the proxy
8
+ * participates in dependency resolution by name exactly like a regular
9
+ * module. At deploy/verify/revert time the engine transpiles the source
10
+ * module in memory and runs the normal migration path against the result;
11
+ * transpiled code is never committed.
12
+ */
13
+ /** File name of the apply spec inside a proxy module directory. */
14
+ export declare const APPLY_SPEC_FILE = "pgpm.apply.json";
15
+ /** Expanded reference to the source module a proxy module applies. */
16
+ export interface ApplySpecSource {
17
+ /**
18
+ * Control/module name of the source module as it appears in the workspace
19
+ * module map (e.g. `pgpm-totp`). The source is typically installed into the
20
+ * workspace extensions directory via `pgpm install`; it is source material,
21
+ * not a runtime dependency, so it must not appear in `requires`.
22
+ */
23
+ module: string;
24
+ /** Optional npm package name the source is installed from (e.g. `@pgpm/totp`). */
25
+ package?: string;
26
+ /** Optional exact version pin for the source package. */
27
+ version?: string;
28
+ /**
29
+ * Optional content-addressed pin: the expected `manifest.digest` of the
30
+ * source bundle. When present, apply refuses to proceed if the installed
31
+ * source does not hash to exactly this digest.
32
+ */
33
+ bundleDigest?: string;
34
+ }
35
+ /**
36
+ * An object-level route in a `pgpm.apply.json` spec: send one named object out
37
+ * of a source schema to a target schema, overriding the whole-schema default
38
+ * for that object only. Kinds are expressed as separate properties (no dotted
39
+ * identity strings), so a table and a function of the same name route
40
+ * independently.
41
+ */
42
+ export interface ApplyRouteEntry {
43
+ /** Source schema the object is defined in (e.g. `users`). */
44
+ fromSchema: string;
45
+ /** Object namespace: `table`/`view` → relation, `procedure` → function. */
46
+ kind: 'table' | 'view' | 'function' | 'procedure' | 'type';
47
+ /** Unqualified object name (e.g. `accounts`). */
48
+ name: string;
49
+ /** Target schema the object is routed to (e.g. `reporting`). */
50
+ toSchema: string;
51
+ }
52
+ /** The parsed, normalized `pgpm.apply.json` spec. */
53
+ export interface PgpmApplySpec {
54
+ /**
55
+ * Module name this instance is deployed as. Defaults to the proxy
56
+ * directory's name.
57
+ */
58
+ name?: string;
59
+ /** Source module reference: a module name, or the expanded object form. */
60
+ source: string | ApplySpecSource;
61
+ /**
62
+ * Whole-schema default: source schema → target schema (drives the AST + plan
63
+ * rewrite). Optional when `route` fully covers the objects being moved.
64
+ */
65
+ schemas?: Record<string, string>;
66
+ /**
67
+ * Object-level routes. Each entry overrides the `schemas` default for its
68
+ * specific object, letting one source schema fan out per object (e.g. a
69
+ * table into a tenant schema and a function into a shared schema).
70
+ */
71
+ route?: ApplyRouteEntry[];
72
+ /**
73
+ * Runtime requires of the transpiled output (native extensions and other
74
+ * modules). Defaults to the source module's own requires.
75
+ */
76
+ requires?: string[];
77
+ /** Version reported for this instance (default: the source's version). */
78
+ version?: string;
79
+ }
80
+ /** {@link PgpmApplySpec} with `source` normalized to the object form. */
81
+ export interface ResolvedApplySpec extends Omit<PgpmApplySpec, 'source'> {
82
+ source: ApplySpecSource;
83
+ }
package/apply/types.js ADDED
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ /**
3
+ * The apply-spec ("proxy module") types.
4
+ *
5
+ * A proxy module is a directory containing a single `pgpm.apply.json` file:
6
+ * "apply <source module> transpiled with this schema map, under my name". It
7
+ * needs no `.control`, no `pgpm.plan`, and no deploy/revert/verify scripts —
8
+ * workspace discovery synthesizes a module entry from the spec, so the proxy
9
+ * participates in dependency resolution by name exactly like a regular
10
+ * module. At deploy/verify/revert time the engine transpiles the source
11
+ * module in memory and runs the normal migration path against the result;
12
+ * transpiled code is never committed.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.APPLY_SPEC_FILE = void 0;
16
+ /** File name of the apply spec inside a proxy module directory. */
17
+ exports.APPLY_SPEC_FILE = 'pgpm.apply.json';
@@ -97,6 +97,14 @@ export declare class PgpmPackage {
97
97
  * Handles naming collisions by preferring the shortest path.
98
98
  */
99
99
  listModules(): ModuleMap;
100
+ /**
101
+ * Discover apply-spec proxy modules (`pgpm.apply.json`) and synthesize
102
+ * module-map entries for them so they participate in dependency resolution
103
+ * by name like any other module. A proxy needs no `.control` or `pgpm.plan`;
104
+ * its `requires` default to the source module's requires. Control-file
105
+ * modules win name collisions.
106
+ */
107
+ private addApplyModules;
100
108
  getModuleMap(): ModuleMap;
101
109
  getAvailableModules(): Promise<string[]>;
102
110
  getModuleProject(name: string): PgpmPackage;
@@ -271,6 +279,18 @@ export declare class PgpmPackage {
271
279
  resolved: string[];
272
280
  external: string[];
273
281
  }>;
282
+ /**
283
+ * Dependency closure for a named module. Proxy (apply-spec) modules have no
284
+ * pgpm.plan, so they resolve straight off the workspace module map instead
285
+ * of instantiating a module-rooted package.
286
+ */
287
+ private getModuleExtensionsFor;
288
+ /**
289
+ * Directory the migration engine should run against for a named module:
290
+ * the module's own directory, or — for proxy (apply-spec) modules — the
291
+ * materialized transpiled instance.
292
+ */
293
+ private getEffectiveModulePathFor;
274
294
  private parsePackageTarget;
275
295
  deploy(opts: PgpmOptions, target?: string, recursive?: boolean): Promise<void>;
276
296
  /**
@@ -48,6 +48,9 @@ const parse_package_name_1 = require("parse-package-name");
48
48
  const path_1 = __importStar(require("path"));
49
49
  const pg_cache_1 = require("pg-cache");
50
50
  const yanse_1 = __importDefault(require("yanse"));
51
+ const materialize_1 = require("../../apply/materialize");
52
+ const apply_spec_1 = require("../../apply/apply-spec");
53
+ const types_2 = require("../../apply/types");
51
54
  const extensions_1 = require("../../extensions/extensions");
52
55
  const files_1 = require("@pgpmjs/ast/files");
53
56
  const files_2 = require("@pgpmjs/ast/files");
@@ -281,11 +284,44 @@ class PgpmPackage {
281
284
  }
282
285
  });
283
286
  // Parse the selected control files
284
- return Array.from(selectedFiles.entries()).reduce((acc, [moduleName, file]) => {
287
+ const moduleMap = Array.from(selectedFiles.entries()).reduce((acc, [moduleName, file]) => {
285
288
  const module = (0, files_2.parseControlFile)(file, this.workspacePath);
286
289
  acc[moduleName] = module;
287
290
  return acc;
288
291
  }, {});
292
+ return this.addApplyModules(moduleMap, packageDirs);
293
+ }
294
+ /**
295
+ * Discover apply-spec proxy modules (`pgpm.apply.json`) and synthesize
296
+ * module-map entries for them so they participate in dependency resolution
297
+ * by name like any other module. A proxy needs no `.control` or `pgpm.plan`;
298
+ * its `requires` default to the source module's requires. Control-file
299
+ * modules win name collisions.
300
+ */
301
+ addApplyModules(moduleMap, packageDirs) {
302
+ const specFiles = [...new Set(packageDirs.flatMap(dir => glob.sync((0, glob_1.globPattern)(dir, `**/${types_2.APPLY_SPEC_FILE}`))))].filter((file) => !/node_modules/.test(file)).sort((a, b) => a.localeCompare(b));
303
+ for (const file of specFiles) {
304
+ let spec;
305
+ try {
306
+ spec = (0, apply_spec_1.readApplySpec)(path_1.default.dirname(file));
307
+ }
308
+ catch (err) {
309
+ console.warn(`Skipping invalid apply spec ${file}: ${err instanceof Error ? err.message : err}`);
310
+ continue;
311
+ }
312
+ const name = spec.name;
313
+ if (moduleMap[name])
314
+ continue;
315
+ const source = moduleMap[spec.source.module];
316
+ moduleMap[name] = {
317
+ path: path_1.default
318
+ .dirname(path_1.default.relative(this.workspacePath, file))
319
+ .replace(/\\/g, '/'),
320
+ requires: spec.requires ?? source?.requires ?? [],
321
+ version: spec.version ?? source?.version ?? '0.0.1'
322
+ };
323
+ }
324
+ return moduleMap;
289
325
  }
290
326
  getModuleMap() {
291
327
  if (!this.workspacePath)
@@ -1235,6 +1271,38 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
1235
1271
  external: external
1236
1272
  };
1237
1273
  }
1274
+ /**
1275
+ * Dependency closure for a named module. Proxy (apply-spec) modules have no
1276
+ * pgpm.plan, so they resolve straight off the workspace module map instead
1277
+ * of instantiating a module-rooted package.
1278
+ */
1279
+ getModuleExtensionsFor(name) {
1280
+ const modules = this.getModuleMap();
1281
+ if (!modules[name]) {
1282
+ throw types_1.errors.MODULE_NOT_FOUND({ name });
1283
+ }
1284
+ const modulePath = (0, path_1.resolve)(this.workspacePath, modules[name].path);
1285
+ if ((0, apply_spec_1.hasApplySpec)(modulePath)) {
1286
+ return (0, deps_1.resolveExtensionDependencies)(name, modules);
1287
+ }
1288
+ return this.getModuleProject(name).getModuleExtensions();
1289
+ }
1290
+ /**
1291
+ * Directory the migration engine should run against for a named module:
1292
+ * the module's own directory, or — for proxy (apply-spec) modules — the
1293
+ * materialized transpiled instance.
1294
+ */
1295
+ async getEffectiveModulePathFor(name) {
1296
+ const modules = this.getModuleMap();
1297
+ if (!modules[name]) {
1298
+ throw types_1.errors.MODULE_NOT_FOUND({ name });
1299
+ }
1300
+ const modulePath = (0, path_1.resolve)(this.workspacePath, modules[name].path);
1301
+ if ((0, apply_spec_1.hasApplySpec)(modulePath)) {
1302
+ return (0, materialize_1.resolveEffectiveModulePath)(name, modulePath, modules, this.workspacePath);
1303
+ }
1304
+ return this.getModuleProject(name).getModulePath();
1305
+ }
1238
1306
  parsePackageTarget(target) {
1239
1307
  let name;
1240
1308
  let toChange;
@@ -1279,8 +1347,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
1279
1347
  extensions = await this.resolveWorkspaceExtensionDependencies();
1280
1348
  }
1281
1349
  else {
1282
- const moduleProject = this.getModuleProject(name);
1283
- extensions = moduleProject.getModuleExtensions();
1350
+ extensions = this.getModuleExtensionsFor(name);
1284
1351
  }
1285
1352
  const pgPool = (0, pg_cache_1.getPgPool)(opts.pg);
1286
1353
  const targetDescription = name === null ? 'all modules' : name;
@@ -1293,10 +1360,9 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
1293
1360
  await pgPool.query(msg);
1294
1361
  }
1295
1362
  else {
1296
- const modulePath = (0, path_1.resolve)(this.workspacePath, modules[extension].path);
1363
+ const modulePath = await (0, materialize_1.resolveEffectiveModulePath)(extension, (0, path_1.resolve)(this.workspacePath, modules[extension].path), modules, this.workspacePath);
1297
1364
  log.info(`📂 Deploying local module: ${extension}`);
1298
1365
  if (opts.deployment.fast) {
1299
- const localProject = this.getModuleProject(extension);
1300
1366
  const cacheKey = getCacheKey(opts.pg, extension, opts.pg.database);
1301
1367
  if (opts.deployment.cache && deployFastCache[cacheKey]) {
1302
1368
  log.warn(`⚡ Using cached pkg for ${extension}.`);
@@ -1305,7 +1371,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
1305
1371
  }
1306
1372
  let pkg;
1307
1373
  try {
1308
- pkg = await (0, package_1.packageModule)(localProject.modulePath, {
1374
+ pkg = await (0, package_1.packageModule)(modulePath, {
1309
1375
  usePlan: opts.deployment.usePlan,
1310
1376
  extension: false
1311
1377
  });
@@ -1374,8 +1440,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
1374
1440
  if (name === null) {
1375
1441
  throw types_1.errors.WORKSPACE_OPERATION_ERROR({ operation: 'deployment' });
1376
1442
  }
1377
- const moduleProject = this.getModuleProject(name);
1378
- const modulePath = moduleProject.getModulePath();
1443
+ const modulePath = await this.getEffectiveModulePathFor(name);
1379
1444
  if (!modulePath) {
1380
1445
  throw types_1.errors.PATH_NOT_FOUND({ path: name, type: 'module' });
1381
1446
  }
@@ -1442,7 +1507,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
1442
1507
  }
1443
1508
  }
1444
1509
  else {
1445
- const modulePath = (0, path_1.resolve)(this.workspacePath, modules[extension].path);
1510
+ const modulePath = await (0, materialize_1.resolveEffectiveModulePath)(extension, (0, path_1.resolve)(this.workspacePath, modules[extension].path), modules, this.workspacePath);
1446
1511
  log.info(`📂 Reverting local module: ${extension}`);
1447
1512
  try {
1448
1513
  const client = new client_1.PgpmMigrate(opts.pg);
@@ -1475,8 +1540,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
1475
1540
  if (name === null) {
1476
1541
  throw types_1.errors.WORKSPACE_OPERATION_ERROR({ operation: 'revert' });
1477
1542
  }
1478
- const moduleProject = this.getModuleProject(name);
1479
- const modulePath = moduleProject.getModulePath();
1543
+ const modulePath = await this.getEffectiveModulePathFor(name);
1480
1544
  if (!modulePath) {
1481
1545
  throw types_1.errors.PATH_NOT_FOUND({ path: name, type: 'module' });
1482
1546
  }
@@ -1503,8 +1567,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
1503
1567
  extensions = await this.resolveWorkspaceExtensionDependencies();
1504
1568
  }
1505
1569
  else {
1506
- const moduleProject = this.getModuleProject(name);
1507
- extensions = moduleProject.getModuleExtensions();
1570
+ extensions = this.getModuleExtensionsFor(name);
1508
1571
  }
1509
1572
  const pgPool = (0, pg_cache_1.getPgPool)(opts.pg);
1510
1573
  const targetDescription = name === null ? 'all modules' : name;
@@ -1517,7 +1580,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
1517
1580
  await pgPool.query(query, [extension]);
1518
1581
  }
1519
1582
  else {
1520
- const modulePath = (0, path_1.resolve)(this.workspacePath, modules[extension].path);
1583
+ const modulePath = await (0, materialize_1.resolveEffectiveModulePath)(extension, (0, path_1.resolve)(this.workspacePath, modules[extension].path), modules, this.workspacePath);
1521
1584
  log.info(`📂 Verifying local module: ${extension}`);
1522
1585
  try {
1523
1586
  const client = new client_1.PgpmMigrate(opts.pg);
@@ -1549,8 +1612,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
1549
1612
  if (name === null) {
1550
1613
  throw types_1.errors.WORKSPACE_OPERATION_ERROR({ operation: 'verification' });
1551
1614
  }
1552
- const moduleProject = this.getModuleProject(name);
1553
- const modulePath = moduleProject.getModulePath();
1615
+ const modulePath = await this.getEffectiveModulePathFor(name);
1554
1616
  if (!modulePath) {
1555
1617
  throw types_1.errors.PATH_NOT_FOUND({ path: name, type: 'module' });
1556
1618
  }