diff --git a/packages/ztd-cli/tests/publishWorkflow.unit.test.ts b/packages/ztd-cli/tests/publishWorkflow.unit.test.ts index 1adb61754..e2ef902de 100644 --- a/packages/ztd-cli/tests/publishWorkflow.unit.test.ts +++ b/packages/ztd-cli/tests/publishWorkflow.unit.test.ts @@ -82,6 +82,18 @@ test('published-package mode includes the rawsql-ts getting-started smoke path', expect(publishedPackageModeScript).toContain('const formatter = new SqlFormatter();'); }); +test('published-package mode only runs the rawsql-ts getting-started smoke when its tarball is present', () => { + const coreSection = publishedPackageModeScript.slice( + publishedPackageModeScript.indexOf('function verifyCoreGettingStarted(packages) {'), + publishedPackageModeScript.indexOf('function verifyNpmPrimaryPath(packages) {'), + ); + + expect(coreSection).toContain('const tarballDependencies = createTarballDependencyMap(packages);'); + expect(coreSection).toContain('if (!hasTarballDependency(tarballDependencies, "rawsql-ts")) {'); + expect(coreSection).toContain('return null;'); + expect(coreSection).toContain('"rawsql-ts": tarballDependencies["rawsql-ts"],'); +}); + test('published-package mode expects local-source guard scripts from npm consumer scaffolds', () => { const npmSection = publishedPackageModeScript.slice( publishedPackageModeScript.indexOf('function verifyNpmPrimaryPath(packages) {'), @@ -131,9 +143,15 @@ test('packed tarball install smoke only runs commands for tarballs included in t }); test('published-package smoke rebinds scaffold runtime dependencies to packed tarballs', () => { - expect(publishedPackageModeScript).toContain('for (const sectionName of ["dependencies", "optionalDependencies", "peerDependencies"])'); + expect(publishedPackageModeScript).toContain('function createPublishedDependencyRangeMap(packages) {'); + expect(publishedPackageModeScript).toContain('dependencyName === "rawsql-ts" || dependencyName.startsWith("@rawsql-ts/")'); + expect(publishedPackageModeScript).toContain('for (const sectionName of ["dependencies", "optionalDependencies", "peerDependencies", "devDependencies"])'); expect(publishedPackageModeScript).toContain('section[dependencyName] = tarballDependencies[dependencyName];'); - expect(publishedPackageModeScript).toContain('restoreTarballDependencies(appDir, tarballDependencies);'); + expect(publishedPackageModeScript).toContain('currentRange.startsWith("file:")'); + expect(publishedPackageModeScript).toContain('section[dependencyName] = publishedDependencyRanges.get(dependencyName);'); + expect(publishedPackageModeScript).toContain('fs.rmSync(path.join(directory, "package-lock.json"), { force: true });'); + expect(publishedPackageModeScript).toContain('fs.rmSync(path.join(directory, "node_modules"), { force: true, recursive: true });'); + expect(publishedPackageModeScript).toContain('restorePublishedDependencyRanges(appDir, packages);'); }); test('publish plan can include unpublished workspace dependencies required by published scaffolds', () => { diff --git a/scripts/verify-published-package-mode.mjs b/scripts/verify-published-package-mode.mjs index c40dc3609..26b33d3b1 100644 --- a/scripts/verify-published-package-mode.mjs +++ b/scripts/verify-published-package-mode.mjs @@ -237,6 +237,30 @@ function hasTarballDependency(tarballDependencies, packageName) { return typeof tarballDependencies[packageName] === "string"; } +function createPublishedDependencyRangeMap(packages) { + const dependencyRanges = new Map(); + + for (const pkg of packages) { + for (const sectionName of ["dependencies", "optionalDependencies", "peerDependencies", "devDependencies"]) { + const section = pkg.manifest[sectionName]; + if (!section || typeof section !== "object" || Array.isArray(section)) { + continue; + } + + for (const [dependencyName, dependencyRange] of Object.entries(section)) { + if ( + typeof dependencyRange === "string" + && (dependencyName === "rawsql-ts" || dependencyName.startsWith("@rawsql-ts/")) + ) { + dependencyRanges.set(dependencyName, dependencyRange); + } + } + } + } + + return dependencyRanges; +} + function setPackageTypeModule(directory) { const packageJsonPath = path.join(directory, "package.json"); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); @@ -248,25 +272,45 @@ function readPackageJson(directory) { return JSON.parse(fs.readFileSync(path.join(directory, "package.json"), "utf8")); } -function restoreTarballDependencies(directory, tarballDependencies) { +function restorePublishedDependencyRanges(directory, packages) { const packageJsonPath = path.join(directory, "package.json"); const packageJson = readPackageJson(directory); - for (const sectionName of ["dependencies", "optionalDependencies", "peerDependencies"]) { + const tarballDependencies = createTarballDependencyMap(packages); + const publishedDependencyRanges = createPublishedDependencyRangeMap(packages); + let changed = false; + + for (const sectionName of ["dependencies", "optionalDependencies", "peerDependencies", "devDependencies"]) { const section = packageJson[sectionName]; if (!section || typeof section !== "object" || Array.isArray(section)) { continue; } - for (const dependencyName of Object.keys(section)) { + + for (const [dependencyName, currentRange] of Object.entries(section)) { if (typeof tarballDependencies[dependencyName] === "string") { - section[dependencyName] = tarballDependencies[dependencyName]; + if (section[dependencyName] !== tarballDependencies[dependencyName]) { + section[dependencyName] = tarballDependencies[dependencyName]; + changed = true; + } + continue; + } + + if ( + typeof currentRange === "string" + && currentRange.startsWith("file:") + && publishedDependencyRanges.has(dependencyName) + ) { + section[dependencyName] = publishedDependencyRanges.get(dependencyName); + changed = true; } } } - packageJson.devDependencies = { - ...(packageJson.devDependencies ?? {}), - ...tarballDependencies, - }; + fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf8"); + + if (changed) { + fs.rmSync(path.join(directory, "package-lock.json"), { force: true }); + fs.rmSync(path.join(directory, "node_modules"), { force: true, recursive: true }); + } } function writeNode16Tsconfig(directory) { @@ -443,6 +487,12 @@ function verifyPackedTarballInstall(packages) { function verifyCoreGettingStarted(packages) { const appDir = path.join(packageRoot, "rawsql-ts-getting-started"); + const tarballDependencies = createTarballDependencyMap(packages); + + if (!hasTarballDependency(tarballDependencies, "rawsql-ts")) { + return null; + } + ensureCleanDir(appDir); writePackageJson(appDir, { @@ -451,7 +501,7 @@ function verifyCoreGettingStarted(packages) { version: "0.0.0", type: "module", devDependencies: { - "rawsql-ts": createTarballDependencyMap(packages)["rawsql-ts"], + "rawsql-ts": tarballDependencies["rawsql-ts"], }, }); @@ -525,7 +575,7 @@ function verifyNpmPrimaryPath(packages) { assertFileMissing(appDir, path.join("src", "features", "smoke", "queries", "smoke", "tests", "smoke.boundary.ztd.test.ts"), "phase-a default-scaffold-shape"); assertFileMissing(appDir, path.join(".ztd", "agents", "manifest.json"), "phase-a default-scaffold-shape"); - restoreTarballDependencies(appDir, tarballDependencies); + restorePublishedDependencyRanges(appDir, packages); runIn(appDir, NPM, ["install"]); return { appDir };