diff --git a/CHANGELOG.md b/CHANGELOG.md index e1385ee5..06c3b2a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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//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 diff --git a/lib/archive.js b/lib/archive.js index fc36e93b..048416c6 100644 --- a/lib/archive.js +++ b/lib/archive.js @@ -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}/**') => { @@ -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; diff --git a/lib/deploy/resolveDeployLock.js b/lib/deploy/resolveDeployLock.js new file mode 100644 index 00000000..5208e8d9 --- /dev/null +++ b/lib/deploy/resolveDeployLock.js @@ -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//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 }; diff --git a/lib/modules.js b/lib/modules.js index a9531397..e20562fd 100644 --- a/lib/modules.js +++ b/lib/modules.js @@ -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'; @@ -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 }); @@ -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}` diff --git a/package-lock.json b/package-lock.json index ef16fbdc..08147df7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@platformos/pos-cli", - "version": "6.2.1", + "version": "6.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@platformos/pos-cli", - "version": "6.2.1", + "version": "6.2.2", "bundleDependencies": [ "commander", "degit", @@ -188,7 +188,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" @@ -201,7 +200,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -1053,7 +1051,6 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.6.tgz", "integrity": "sha512-kIU8SLQkYWGp3pVKiYzA5OSaNF5EE03P/R8zEmmrG6XwOg5oBjXyQVVIauQ0dgau4zYhpZEhJrvIYt6oM+zZZA==", "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^5.0.0", "@octokit/graphql": "^8.2.2", @@ -1911,7 +1908,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~8.3.0" } @@ -1944,7 +1940,6 @@ "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.10", @@ -2672,7 +2667,6 @@ "resolved": "https://registry.npmjs.org/@yeoman/types/-/types-1.11.1.tgz", "integrity": "sha512-27CI5hHQAHfq8ohYILmLNzClbdzBJzu+ny9AzUVV6naJO0l4/+t+67QDKlwQvt+TW3oE5j74I/Mh4Kn14rsVXA==", "license": "MIT", - "peer": true, "engines": { "node": "^16.13.0 || >=18.12.0" }, @@ -4068,7 +4062,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -4750,7 +4743,6 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz", "integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==", "license": "MIT", - "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -4851,7 +4843,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.31.tgz", "integrity": "sha512-zJIHFrl6bq3RDd2YusFNCDlM8qUprxKswyi/OPzPyzKDdyBXDqWx8bZlZ7R+saTdSTatUmb3O7K4SspGPaEOQg==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -6018,7 +6009,6 @@ "resolved": "https://registry.npmjs.org/mem-fs/-/mem-fs-4.1.5.tgz", "integrity": "sha512-6aWRo1jLo82ngo44tPQXjzINN7gvSNWhMU96I+O5nsJWdPi5vbUJWMTDGZDb/AVpCJRpaLGcFgrIBIpyk0DuUA==", "license": "MIT", - "peer": true, "dependencies": { "vinyl": "^3.0.1", "vinyl-file": "^5.0.0" @@ -8630,7 +8620,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -9044,7 +9033,6 @@ "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.1.10", "@vitest/mocker": "4.1.10", @@ -9725,7 +9713,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index a2092569..827be55c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@platformos/pos-cli", - "version": "6.2.1", + "version": "6.2.2", "description": "Manage your platformOS application", "type": "module", "imports": { diff --git a/test/fixtures/deploy/modules_nested_lock/modules/mod1/pos-module.lock.json b/test/fixtures/deploy/modules_nested_lock/modules/mod1/pos-module.lock.json new file mode 100644 index 00000000..0ecad043 --- /dev/null +++ b/test/fixtures/deploy/modules_nested_lock/modules/mod1/pos-module.lock.json @@ -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" + } +} diff --git a/test/fixtures/deploy/modules_nested_lock/modules/mod1/public/views/hello.liquid b/test/fixtures/deploy/modules_nested_lock/modules/mod1/public/views/hello.liquid new file mode 100644 index 00000000..d07225ee --- /dev/null +++ b/test/fixtures/deploy/modules_nested_lock/modules/mod1/public/views/hello.liquid @@ -0,0 +1 @@ +

hi

diff --git a/test/fixtures/deploy/modules_root_lock/modules/mod1/public/views/hello.liquid b/test/fixtures/deploy/modules_root_lock/modules/mod1/public/views/hello.liquid new file mode 100644 index 00000000..d07225ee --- /dev/null +++ b/test/fixtures/deploy/modules_root_lock/modules/mod1/public/views/hello.liquid @@ -0,0 +1 @@ +

hi

diff --git a/test/fixtures/deploy/modules_root_lock/pos-module.lock.json b/test/fixtures/deploy/modules_root_lock/pos-module.lock.json new file mode 100644 index 00000000..1db4d66c --- /dev/null +++ b/test/fixtures/deploy/modules_root_lock/pos-module.lock.json @@ -0,0 +1,9 @@ +{ + "dependencies": { + "core": "2.1.9" + }, + "devDependencies": {}, + "registries": { + "core": "https://partners.platformos.com" + } +} diff --git a/test/unit/archive.test.js b/test/unit/archive.test.js index eee5ae4a..22242324 100644 --- a/test/unit/archive.test.js +++ b/test/unit/archive.test.js @@ -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//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