Skip to content
Merged
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## 6.2.2 (2026-07-22)

### New Features

* `pos-cli modules push` now includes `pos-module.lock.json` in the release archive (alongside `pos-module.json`, `template-values.json`, and `README.md`) when the module has one.
* `pos-cli deploy` now embeds a resolved `pos-module.lock.json` at the root of the deploy archive — using the project's own lock file when present, or merging any `modules/<name>/pos-module.lock.json` files found on disk otherwise (e.g. a module dropped into a bare `modules/` directory with no root manifest) — so the instance can see the full module dependency tree.

## 6.2.1 (2026-07-22)

### Fixes
Expand Down
15 changes: 12 additions & 3 deletions lib/archive.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@ import { loadSettingsFileForModule } from './settings.js';
import logger from './logger.js';
import dir from './directories.js';
import prepareArchive from './prepareArchive.js';
import { POS_MODULE_LOCK_FILE } from './modules/paths.js';
import { resolveDeployLock } from './deploy/resolveDeployLock.js';

const isEmpty = d => shell.ls(d).length === 0;

const addModulesToArchive = async (archive, withoutAssets) => {
if (!fs.existsSync(dir.MODULES)) {
return Promise.resolve(true);
return [];
}
const modules = await glob('*', { cwd: dir.MODULES, onlyFiles: false, onlyDirectories: true });
return Promise.all(modules.map(module => addModuleToArchive(module, archive, withoutAssets)));
await Promise.all(modules.map(module => addModuleToArchive(module, archive, withoutAssets)));
return modules;
};

const addModuleToArchive = async (module, archive, withoutAssets, pattern = '{public,private}/**') => {
Expand Down Expand Up @@ -82,7 +85,13 @@ const makeArchive = async (env, { withoutAssets }) => {
archive.addFile(path.join(directory, f), `${directory}/${f}`);
}

await addModulesToArchive(archive, withoutAssets);
const moduleNames = await addModulesToArchive(archive, withoutAssets);

const lock = resolveDeployLock(moduleNames);
if (lock) {
archive.addBuffer(Buffer.from(JSON.stringify(lock, null, 2)), POS_MODULE_LOCK_FILE);
}

archive.finalize();

return archive.done;
Expand Down
51 changes: 51 additions & 0 deletions lib/deploy/resolveDeployLock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import fs from 'fs';
import path from 'path';

import files from '../files.js';
import dir from '../directories.js';
import { POS_MODULE_LOCK_FILE } from '../modules/paths.js';

const readLock = (lockPath) => {
const raw = files.readJSON(lockPath, { exit: false }) || {};
return {
dependencies: raw.dependencies || {},
devDependencies: raw.devDependencies || {},
registries: raw.registries || {}
};
};

/**
* Builds the pos-module.lock.json content to embed at the deploy archive root, so the
* instance can install the full module dependency tree itself instead of pos-cli shipping
* per-module manifests or re-resolving anything over the network at deploy time.
*
* Prefers the project's own root pos-module.lock.json when present — the standard case,
* where `pos-cli modules install` already resolved the full tree for this project. Falls
* back to merging per-module modules/<name>/pos-module.lock.json files, which ship inside
* a module's own release.zip (see `pos-cli modules push`) and land on disk when a module
* is dropped into a bare modules/ directory with no root manifest — e.g. a Partner Portal
* single-module deploy.
*
* Returns null when no lock file is available anywhere.
*/
const resolveDeployLock = (moduleNames) => {
if (fs.existsSync(POS_MODULE_LOCK_FILE)) {
return readLock(POS_MODULE_LOCK_FILE);
}

const merged = { dependencies: {}, devDependencies: {}, registries: {} };
let found = false;
for (const name of moduleNames) {
const nestedLockPath = path.join(dir.MODULES, name, POS_MODULE_LOCK_FILE);
if (!fs.existsSync(nestedLockPath)) continue;
found = true;
const lock = readLock(nestedLockPath);
Object.assign(merged.dependencies, lock.dependencies);
Object.assign(merged.devDependencies, lock.devDependencies);
Object.assign(merged.registries, lock.registries);
}

return found ? merged : null;
};

export { resolveDeployLock };
9 changes: 7 additions & 2 deletions lib/modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { uploadFile } from './s3UploadFile.js';
import waitForStatus from './data/waitForStatus.js';
import { readPassword } from './utils/password.js';
import ServerError from './ServerError.js';
import { POS_MODULE_FILE as moduleManifestFileName } from './modules/paths.js';
import { POS_MODULE_FILE as moduleManifestFileName, POS_MODULE_LOCK_FILE as moduleLockFileName } from './modules/paths.js';

let moduleId;
const archiveFileName = 'release.zip';
Expand Down Expand Up @@ -42,7 +42,7 @@ const createArchive = async (moduleName) => {

if (fs.existsSync(moduleManifestFileName) && !fs.existsSync('modules/')) {
logger.Warn(`Cannot find modules/${moduleName}, creating archive with the current directory.`);
const moduleFiles = await glob(['**/**', moduleManifestFileName, moduleConfigFileName], {
const moduleFiles = await glob(['**/**', moduleManifestFileName, moduleConfigFileName, moduleLockFileName], {
ignore: ['**/node_modules/**', '**/tmp/**', 'app/**'],
onlyFiles: true
});
Expand All @@ -63,6 +63,11 @@ const createArchive = async (moduleName) => {
// pos-module.json is required in the archive: the portal reads it to register
// this module's transitive dependencies in the marketplace registry.
archive.addFile(moduleManifestFileName, `${moduleName}/${moduleManifestFileName}`);
// pos-module.lock.json (resolved via `pos-cli modules install` in this repo) ships
// alongside the manifest so it's available wherever the release archive is consumed.
if (fs.existsSync(moduleLockFileName)) {
archive.addFile(moduleLockFileName, `${moduleName}/${moduleLockFileName}`);
}
} else {
throw new Error(
`There is no directory modules/${moduleName} - please double check the machine_name property in ${moduleManifestFileName}`
Expand Down
17 changes: 2 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@platformos/pos-cli",
"version": "6.2.1",
"version": "6.2.2",
"description": "Manage your platformOS application",
"type": "module",
"imports": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"dependencies": {
"core": "2.1.9",
"user": "5.2.11"
},
"devDependencies": {},
"registries": {
"core": "https://partners.platformos.com",
"user": "https://partners.platformos.com"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p>hi</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p>hi</p>
9 changes: 9 additions & 0 deletions test/fixtures/deploy/modules_root_lock/pos-module.lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"dependencies": {
"core": "2.1.9"
},
"devDependencies": {},
"registries": {
"core": "https://partners.platformos.com"
}
}
29 changes: 29 additions & 0 deletions test/unit/archive.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,35 @@ describe('Archive utilities', () => {
expect(entries.some(e => e.includes('/assets/'))).toBe(false);
});

test('includes the project\'s root pos-module.lock.json at the archive root', async () => {
process.chdir(path.join(fixturesPath, 'modules_root_lock'));
const { makeArchive } = await import('#lib/archive.js');
const zipPath = path.join(tmpDir, 'release.zip');

await makeArchive({ TARGET: zipPath }, { withoutAssets: true });

const entries = await listZipEntries(zipPath);
expect(entries).toContain('pos-module.lock.json');

const content = JSON.parse(await readZipEntry(zipPath, 'pos-module.lock.json'));
expect(content.dependencies).toEqual({ core: '2.1.9' });
});

test('falls back to merging nested modules/<name>/pos-module.lock.json files when there is no root lock', async () => {
process.chdir(path.join(fixturesPath, 'modules_nested_lock'));
const { makeArchive } = await import('#lib/archive.js');
const zipPath = path.join(tmpDir, 'release.zip');

await makeArchive({ TARGET: zipPath }, { withoutAssets: true });

const entries = await listZipEntries(zipPath);
expect(entries).toContain('pos-module.lock.json');
expect(entries.some(e => e === 'modules/mod1/pos-module.lock.json')).toBe(false);

const content = JSON.parse(await readZipEntry(zipPath, 'pos-module.lock.json'));
expect(content.dependencies).toEqual({ core: '2.1.9', user: '5.2.11' });
});

test('only includes module files under public/ or private/, not sibling directories', async () => {
// fixture has testModule/generators/crud.js and testModule/react-app/node_modules/.../page.png
// alongside testModule/public/ — neither should appear in the archive
Expand Down
Loading