Compare commits

..

2 Commits

Author SHA1 Message Date
Jarvis
e6d5fe5773 fix: retarget updater to @mosaic/mosaic
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
2026-04-04 20:42:18 -05:00
543388e18b fix(mosaic): resolve framework scripts via import.meta.url (#385)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
Fixes #383 — resolveTool now uses fileURLToPath(import.meta.url). Adds package.json/framework subpath exports. Bumps @mosaic/mosaic to 0.0.18.
2026-04-05 01:41:46 +00:00
3 changed files with 79 additions and 6 deletions

View File

@@ -22,12 +22,13 @@
- `pnpm --filter @mosaic/mosaic test -- __tests__/update-checker.test.ts`
- `pnpm exec eslint --no-warn-ignored packages/mosaic/src/runtime/update-checker.ts packages/mosaic/src/cli.ts packages/mosaic/src/index.ts packages/mosaic/__tests__/update-checker.test.ts packages/cli/src/cli.ts`
- `pnpm --filter @mosaic/mosaic lint`
- pre-push hooks: `typecheck`, `lint`, `format:check`
## Review
- Manual review of the updater diff caught and fixed a cache regression where fallback results would lose the resolved package target on subsequent cached checks.
## Blockers / Risks
## Risks / Notes
- `pnpm --filter @mosaic/mosaic typecheck` fails in this worktree on pre-existing unresolved workspace imports such as `@mosaic/quality-rails`, `@mosaic/prdy`, and `@mosaic/types`; this is broader than the updater change.
- `packages/cli` is excluded from `pnpm-workspace.yaml`, so package-script lint/typecheck are not runnable via normal workspace filters in this worktree.
- Direct `pnpm --filter @mosaic/mosaic typecheck` and `pnpm --filter @mosaic/cli ...` checks were not representative in this worktree because `packages/cli` is excluded from `pnpm-workspace.yaml` and the standalone package check lacked the built workspace dependency graph.
- The repo's pre-push hooks provided the authoritative validation path here and passed: root `typecheck`, `lint`, and `format:check`.

View File

@@ -2,14 +2,34 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { semverLt, formatUpdateNotice } from '../src/runtime/update-checker.js';
import type { UpdateCheckResult } from '../src/runtime/update-checker.js';
const { execSyncMock } = vi.hoisted(() => ({
const { execSyncMock, cacheFiles } = vi.hoisted(() => ({
execSyncMock: vi.fn(),
cacheFiles: new Map<string, string>(),
}));
vi.mock('node:child_process', () => ({
execSync: execSyncMock,
}));
vi.mock('node:fs', () => ({
existsSync: vi.fn((path: string) => cacheFiles.has(path)),
mkdirSync: vi.fn(),
readFileSync: vi.fn((path: string) => {
const value = cacheFiles.get(path);
if (value === undefined) {
throw new Error(`ENOENT: ${path}`);
}
return value;
}),
writeFileSync: vi.fn((path: string, content: string) => {
cacheFiles.set(path, content);
}),
}));
vi.mock('node:os', () => ({
homedir: vi.fn(() => '/mock-home'),
}));
async function importUpdateChecker() {
vi.resetModules();
return import('../src/runtime/update-checker.js');
@@ -40,6 +60,7 @@ describe('semverLt', () => {
describe('formatUpdateNotice', () => {
beforeEach(() => {
execSyncMock.mockReset();
cacheFiles.clear();
});
it('returns empty string when up to date', () => {
@@ -124,4 +145,51 @@ describe('formatUpdateNotice', () => {
expect(result.latest).toBe('0.0.17');
expect(notice).toContain('@mosaic/mosaic@latest');
});
it('does not reuse a cached modern-package result for a legacy install', async () => {
let installedPackage = '@mosaic/mosaic';
execSyncMock.mockImplementation((command: string) => {
if (command.includes('ls -g --depth=0 --json')) {
return JSON.stringify({
dependencies:
installedPackage === '@mosaic/mosaic'
? { '@mosaic/mosaic': { version: '0.0.17' } }
: { '@mosaic/cli': { version: '0.0.16' } },
});
}
if (command.includes('view @mosaic/mosaic version')) {
return installedPackage === '@mosaic/mosaic' ? '0.0.18' : '0.0.17';
}
if (command.includes('view @mosaic/cli version')) {
throw new Error('not found');
}
throw new Error(`Unexpected command: ${command}`);
});
const { checkForUpdate } = await importUpdateChecker();
const modernResult = checkForUpdate();
installedPackage = '@mosaic/cli';
const legacyResult = checkForUpdate();
expect(modernResult.currentPackage).toBe('@mosaic/mosaic');
expect(modernResult.targetPackage).toBe('@mosaic/mosaic');
expect(modernResult.latest).toBe('0.0.18');
expect(legacyResult.currentPackage).toBe('@mosaic/cli');
expect(legacyResult.targetPackage).toBe('@mosaic/mosaic');
expect(legacyResult.latest).toBe('0.0.17');
expect(execSyncMock).toHaveBeenCalledWith(
expect.stringContaining('view @mosaic/cli version'),
expect.any(Object),
);
expect(execSyncMock).toHaveBeenCalledWith(
expect.stringContaining('view @mosaic/mosaic version'),
expect.any(Object),
);
});
});

View File

@@ -131,18 +131,20 @@ export function semverLt(a: string, b: string): boolean {
/** Cache stores only the latest registry version (the expensive network call).
* The installed version is always checked fresh — it's a local `npm ls`. */
interface RegistryCache {
currentPackage?: string;
latest: string;
targetPackage?: string;
checkedAt: string;
registry: string;
}
function readCache(): RegistryCache | null {
function readCache(currentPackage: string): RegistryCache | null {
try {
if (!existsSync(CACHE_FILE)) return null;
const raw = JSON.parse(readFileSync(CACHE_FILE, 'utf-8')) as RegistryCache;
const age = Date.now() - new Date(raw.checkedAt).getTime();
if (age > CACHE_TTL_MS) return null;
if ((raw.currentPackage || '') !== currentPackage) return null;
return raw;
} catch {
return null;
@@ -223,7 +225,7 @@ export function checkForUpdate(options?: { skipCache?: boolean }): UpdateCheckRe
let checkedAt: string;
if (!options?.skipCache) {
const cached = readCache();
const cached = readCache(currentInfo.name);
if (cached) {
latestInfo = {
name: cached.targetPackage || MODERN_PKG,
@@ -234,6 +236,7 @@ export function checkForUpdate(options?: { skipCache?: boolean }): UpdateCheckRe
latestInfo = getLatestVersion(currentInfo.name);
checkedAt = new Date().toISOString();
writeCache({
currentPackage: currentInfo.name,
latest: latestInfo.version,
targetPackage: latestInfo.name,
checkedAt,
@@ -244,6 +247,7 @@ export function checkForUpdate(options?: { skipCache?: boolean }): UpdateCheckRe
latestInfo = getLatestVersion(currentInfo.name);
checkedAt = new Date().toISOString();
writeCache({
currentPackage: currentInfo.name,
latest: latestInfo.version,
targetPackage: latestInfo.name,
checkedAt,