Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions pgpm/cli/__tests__/init.boilerplate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import fs from 'fs';
import os from 'os';
import path from 'path';

import { DEFAULT_TEMPLATE_REPO, PGLITE_TEMPLATE_REPO } from '@pgpmjs/core';

import {
persistBoilerplateSource,
readBoilerplateSource,
resolveInitTemplateRepo,
} from '../src/commands/init/boilerplate';

describe('resolveInitTemplateRepo', () => {
it('defaults to the pgpm boilerplates repo (not explicit)', () => {
expect(resolveInitTemplateRepo({})).toEqual({
templateRepo: DEFAULT_TEMPLATE_REPO,
repoWasExplicit: false,
});
});

it('maps --pglite to the pglite boilerplates repo (explicit)', () => {
expect(resolveInitTemplateRepo({ pglite: true })).toEqual({
templateRepo: PGLITE_TEMPLATE_REPO,
repoWasExplicit: true,
});
});

it('lets --repo win over --pglite', () => {
const custom = 'https://github.com/acme/my-boilerplates.git';
expect(resolveInitTemplateRepo({ repo: custom, pglite: true })).toEqual({
templateRepo: custom,
repoWasExplicit: true,
});
});
});

describe('persist/read boilerplate source', () => {
let dir: string;

beforeEach(() => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pgpm-boilerplate-'));
});

afterEach(() => {
fs.rmSync(dir, { recursive: true, force: true });
});

it('records the source into an existing pgpm.json and reads it back', () => {
fs.writeFileSync(
path.join(dir, 'pgpm.json'),
`${JSON.stringify({ packages: ['packages/*'] }, null, 2)}\n`
);

persistBoilerplateSource(dir, { repo: PGLITE_TEMPLATE_REPO });

const config = JSON.parse(fs.readFileSync(path.join(dir, 'pgpm.json'), 'utf8'));
expect(config.packages).toEqual(['packages/*']);
expect(config.boilerplates).toEqual({ repo: PGLITE_TEMPLATE_REPO });

expect(readBoilerplateSource(dir)).toEqual({
repo: PGLITE_TEMPLATE_REPO,
branch: undefined,
dir: undefined,
});
});

it('persists optional branch and dir when provided', () => {
fs.writeFileSync(
path.join(dir, 'pgpm.json'),
`${JSON.stringify({ packages: ['packages/*'] }, null, 2)}\n`
);

persistBoilerplateSource(dir, {
repo: PGLITE_TEMPLATE_REPO,
branch: 'main',
dir: 'pglite',
});

expect(readBoilerplateSource(dir)).toEqual({
repo: PGLITE_TEMPLATE_REPO,
branch: 'main',
dir: 'pglite',
});
});

it('is a no-op when there is no pgpm.json (does not create one)', () => {
persistBoilerplateSource(dir, { repo: PGLITE_TEMPLATE_REPO });
expect(fs.existsSync(path.join(dir, 'pgpm.json'))).toBe(false);
});

it('returns undefined when the workspace has no recorded source', () => {
fs.writeFileSync(
path.join(dir, 'pgpm.json'),
`${JSON.stringify({ packages: ['packages/*'] }, null, 2)}\n`
);
expect(readBoilerplateSource(dir)).toBeUndefined();
});
});
76 changes: 74 additions & 2 deletions pgpm/cli/__tests__/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ jest.setTimeout(60000);
process.env.PGPM_SKIP_UPDATE_CHECK = 'true';
process.env.PGPM_SKIP_SKILL_INSTALL = 'true';

import { PgpmPackage } from '@pgpmjs/core';
import { existsSync } from 'fs';
import { PgpmPackage, PGLITE_TEMPLATE_REPO } from '@pgpmjs/core';
import { existsSync, readFileSync } from 'fs';
import { sync as glob } from 'glob';
import { Inquirerer, ParsedArgs } from 'inquirerer';
import * as path from 'path';

import { commands } from '../src/commands';
import {
addInitDefaults,
setupTests,
TestEnvironment,
TestFixture,
Expand Down Expand Up @@ -630,4 +631,75 @@ describe('cmds:init', () => {
120000
);
});

describe('--pglite flag', () => {
const shouldSkipGitHubTests = process.env.CI === 'true' && !process.env.ALLOW_NETWORK_TESTS;

(shouldSkipGitHubTests ? it.skip : it)(
'records the pglite repo on the workspace and inherits it for modules',
async () => {
const { mockInput, mockOutput } = environment;
const prompter = new Inquirerer({
input: mockInput,
output: mockOutput,
noTty: true
});

const wsName = 'ws-pglite';
const wsRoot = path.join(fixture.tempDir, wsName);

// 1) Create a pglite workspace. --pglite must NOT be overridden by a
// default repo, so build argv directly (withInitDefaults injects --repo).
await commands(addInitDefaults({
_: ['init', 'workspace'],
cwd: fixture.tempDir,
name: wsName,
workspace: true,
pglite: true,
fromBranch: 'main'
}), prompter, {
noTty: true,
input: mockInput,
output: mockOutput,
version: '1.0.0',
minimistOpts: {}
});

// Workspace records the pglite boilerplate source so modules inherit it.
const wsConfig = JSON.parse(
readFileSync(path.join(wsRoot, 'pgpm.json'), 'utf8')
);
expect(wsConfig.boilerplates?.repo).toBe(PGLITE_TEMPLATE_REPO);

// 2) A bare `pgpm init` (no --pglite) inside the workspace inherits it.
const modName = 'pglite-mod';
await commands(addInitDefaults({
_: ['init'],
cwd: wsRoot,
moduleName: modName,
name: modName,
extensions: ['plpgsql'],
fromBranch: 'main'
}), prompter, {
noTty: true,
input: mockInput,
output: mockOutput,
version: '1.0.0',
minimistOpts: {}
});

const modDir = path.join(wsRoot, 'packages', modName);
expect(existsSync(path.join(modDir, 'pgpm.plan'))).toBe(true);

// The inherited pglite module template swaps in pglite-test.
const modPkg = JSON.parse(
readFileSync(path.join(modDir, 'package.json'), 'utf8')
);
const devDeps = modPkg.devDependencies ?? {};
expect(devDeps['pglite-test']).toBeDefined();
expect(devDeps['pgsql-test']).toBeUndefined();
},
180000
);
});
});
78 changes: 78 additions & 0 deletions pgpm/cli/src/commands/init/boilerplate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import fs from 'fs';
import path from 'path';

import {
DEFAULT_TEMPLATE_REPO,
PGLITE_TEMPLATE_REPO,
PgpmPackage,
} from '@pgpmjs/core';

/**
* A template/boilerplate source, recorded on a workspace so that modules
* created inside it inherit the same repo without re-specifying flags.
*/
export interface BoilerplateSource {
repo: string;
branch?: string;
dir?: string;
}

/**
* Resolve the template repo for an `init` invocation from its flags.
*
* Precedence: explicit `--repo` wins, then `--pglite` (sugar for the pglite
* boilerplates repo), otherwise the default repo.
*/
export function resolveInitTemplateRepo(flags: {
repo?: unknown;
pglite?: boolean;
}): { templateRepo: string; repoWasExplicit: boolean } {
const pglite = Boolean(flags.pglite);
const repoWasExplicit = typeof flags.repo === 'string' || pglite;
const templateRepo =
typeof flags.repo === 'string'
? flags.repo
: pglite
? PGLITE_TEMPLATE_REPO
: DEFAULT_TEMPLATE_REPO;
return { templateRepo, repoWasExplicit };
}

/**
* Record the boilerplate source on a freshly scaffolded workspace's `pgpm.json`
* so that `pgpm init` (module) inside it inherits the same template repo.
* Only writes into an existing pgpm.json (pgpm.config.js workspaces are skipped).
*/
export function persistBoilerplateSource(
workspaceDir: string,
source: BoilerplateSource
): void {
const configPath = path.join(workspaceDir, 'pgpm.json');
if (!fs.existsSync(configPath)) return;

try {
const raw = fs.readFileSync(configPath, 'utf8');
const config = JSON.parse(raw);
config.boilerplates = {
repo: source.repo,
...(source.branch ? { branch: source.branch } : {}),
...(source.dir ? { dir: source.dir } : {}),
};
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
} catch {
// Non-fatal: inheritance is a convenience, not a correctness requirement.
}
}

/**
* Read a previously recorded boilerplate source from the enclosing workspace's
* config (see {@link persistBoilerplateSource}).
*/
export function readBoilerplateSource(cwd: string): BoilerplateSource | undefined {
const project = new PgpmPackage(cwd);
const recorded = project.config?.boilerplates;
if (recorded?.repo) {
return { repo: recorded.repo, branch: recorded.branch, dir: recorded.dir };
}
return undefined;
}
Loading
Loading