|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +/** |
| 4 | + * `os init` and `os create` (standalone) pin every `@objectstack/*` dependency |
| 5 | + * of the project they emit to the CLI's own version, EXACTLY — no `^`, no `~`. |
| 6 | + * On the v18 line a breaking change may ship in a minor or a patch, and the |
| 7 | + * docs tell a project to pin exact versions; a caret would let the next |
| 8 | + * re-resolve move a fresh project across such a release unasked. |
| 9 | + * |
| 10 | + * `init.test.ts` and `create.test.ts` pin that against the version this |
| 11 | + * checkout carries. This file pins the shape the NEXT version pass produces |
| 12 | + * and no other test sees until it lands: a `cut-rc` pass in pre mode makes |
| 13 | + * the CLI `X.Y.Z-rc.N`, and that whole string — prerelease tail included — is |
| 14 | + * the pin. The CLI reads its version from its own `package.json` once and |
| 15 | + * caches it, so the module graph is re-imported under a planted manifest read. |
| 16 | + * The in-repo placement is the control: it keeps `workspace:*`. |
| 17 | + */ |
| 18 | + |
| 19 | +import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest'; |
| 20 | +import fs from 'fs'; |
| 21 | +import path from 'path'; |
| 22 | +import { fileURLToPath } from 'url'; |
| 23 | + |
| 24 | +const HERE = path.dirname(fileURLToPath(import.meta.url)); |
| 25 | +const CLI_MANIFEST = path.resolve(HERE, '..', 'package.json'); |
| 26 | +const PRERELEASE = '18.1.0-rc.2'; |
| 27 | + |
| 28 | +type InitModule = typeof import('../src/commands/init.js'); |
| 29 | +type CreateModule = typeof import('../src/commands/create.js'); |
| 30 | + |
| 31 | +let init: InitModule; |
| 32 | +let create: CreateModule; |
| 33 | + |
| 34 | +/** Every `@objectstack/*` entry of a manifest, both dependency sections. */ |
| 35 | +function objectstackDeps(pkg: { |
| 36 | + dependencies?: Record<string, string>; |
| 37 | + devDependencies?: Record<string, string>; |
| 38 | +}): [string, string][] { |
| 39 | + return Object.entries({ ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) }).filter( |
| 40 | + ([name]) => name.startsWith('@objectstack/'), |
| 41 | + ); |
| 42 | +} |
| 43 | + |
| 44 | +beforeAll(async () => { |
| 45 | + const realRead = fs.readFileSync; |
| 46 | + const spy = vi.spyOn(fs, 'readFileSync').mockImplementation(((file: unknown, ...rest: unknown[]) => |
| 47 | + typeof file === 'string' && path.resolve(file) === CLI_MANIFEST |
| 48 | + ? JSON.stringify({ name: '@objectstack/cli', version: PRERELEASE }) |
| 49 | + : (realRead as (...args: unknown[]) => unknown)(file, ...rest)) as typeof fs.readFileSync); |
| 50 | + try { |
| 51 | + vi.resetModules(); |
| 52 | + init = await import('../src/commands/init.js'); |
| 53 | + create = await import('../src/commands/create.js'); |
| 54 | + // Read while the planted manifest is in place; the value is cached after. |
| 55 | + expect(init.getCliVersion()).toBe(PRERELEASE); |
| 56 | + } finally { |
| 57 | + spy.mockRestore(); |
| 58 | + } |
| 59 | +}); |
| 60 | + |
| 61 | +afterAll(() => { |
| 62 | + vi.resetModules(); |
| 63 | +}); |
| 64 | + |
| 65 | +describe('a prerelease CLI pins scaffolded @objectstack/* deps exactly', () => { |
| 66 | + it('os init: every template, every @objectstack/* dep', () => { |
| 67 | + const keys = Object.keys(init.TEMPLATES); |
| 68 | + expect(keys.length).toBeGreaterThan(0); |
| 69 | + for (const key of keys) { |
| 70 | + const deps = objectstackDeps(init.TEMPLATES[key]); |
| 71 | + expect(deps.length, key).toBeGreaterThan(0); |
| 72 | + for (const [name, spec] of deps) expect(spec, `${key}: ${name}`).toBe(PRERELEASE); |
| 73 | + } |
| 74 | + }); |
| 75 | + |
| 76 | + it('os create (standalone): every template, every @objectstack/* dep', () => { |
| 77 | + expect(create.objectstackDependencySpec('standalone')).toBe(PRERELEASE); |
| 78 | + const keys = Object.keys(create.templates); |
| 79 | + expect(keys.length).toBeGreaterThan(0); |
| 80 | + for (const key of keys) { |
| 81 | + const pkg = create.templates[key].filesFor('standalone')['package.json']('my-thing') as { |
| 82 | + dependencies?: Record<string, string>; |
| 83 | + devDependencies?: Record<string, string>; |
| 84 | + }; |
| 85 | + const deps = objectstackDeps(pkg); |
| 86 | + expect(deps.length, key).toBeGreaterThan(0); |
| 87 | + for (const [name, spec] of deps) expect(spec, `${key}: ${name}`).toBe(PRERELEASE); |
| 88 | + } |
| 89 | + }); |
| 90 | + |
| 91 | + it('os create --in-repo keeps workspace:* (the control)', () => { |
| 92 | + expect(create.objectstackDependencySpec('in-repo')).toBe('workspace:*'); |
| 93 | + for (const key of Object.keys(create.templates)) { |
| 94 | + const pkg = create.templates[key].filesFor('in-repo')['package.json']('my-thing') as { |
| 95 | + dependencies?: Record<string, string>; |
| 96 | + devDependencies?: Record<string, string>; |
| 97 | + }; |
| 98 | + for (const [name, spec] of objectstackDeps(pkg)) { |
| 99 | + expect(spec, `${key}: ${name}`).toBe('workspace:*'); |
| 100 | + } |
| 101 | + } |
| 102 | + }); |
| 103 | +}); |
0 commit comments