diff --git a/README.md b/README.md index 1b3af2c..8da1472 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ - ๐Ÿ“ฆ **Bundler-free** โ€” No bundler or bundler configs involved - ๐ŸŸฆ **No config file** โ€” Reads from your `package.json` and `tsconfig.json` - ๐Ÿ“ **Declarative entrypoint map** โ€” Specify your TypeScript entrypoints in `package.json#/zshy` -- ๐Ÿค– **Auto-generated `"exports"`** โ€” Writes `"exports"` map directly into your `package.json` +- ๐Ÿค– **Auto-generated `"exports"`** โ€” Writes `"exports"` map directly into your `package.json` and `jsr.json` - ๐Ÿงฑ **Dual-module builds** โ€” Builds ESM and CJS outputs from a single TypeScript source file - ๐Ÿ“‚ **Unopinionated** โ€” Use any file structure or import extension syntax you like - ๐Ÿ“ฆ **Asset handling** โ€” Non-JS assets are copied to the output directory @@ -461,6 +461,12 @@ With this addition, `zshy` will add the `"my-source"` condition to the generated } ``` +### JSR + +For packages that also have a `jsr.json` file for publishing to [JSR](https://jsr.io/), `zshy` will copy your configured exports to `jsr.json/#exports`, making your `zshy` configuration the single source of truth for exports. + +This will copy over the paths of the source code entrypoints, not the paths to the transpiled code, since JSR supports and encourages publishing TypeScript source code rather than pairs of `.js` + `.d.ts` files. +


@@ -686,7 +692,7 @@ To learn more, read the ["Masquerading as CJS"](https://github.com/arethetypeswr ```ts function hello() { - console.log('hello'); + console.log("hello"); } export default hello; @@ -696,7 +702,7 @@ export default hello; ```ts function hello() { - console.log('hello'); + console.log("hello"); } exports.default = hello; module.exports = exports.default; @@ -750,9 +756,9 @@ With this setup, your build outputs (`index.js`, etc) will be written to the pac
-### Can I prevent `zshy` from modifying my `package.json`? +### Can I prevent `zshy` from modifying my `package.json`/`jsr.json`? -Yes. If you prefer to manage your `package.json` fields manually, you can prevent `zshy` from making any changes by setting the `noEdit` option to `true` in your `package.json#/zshy` config. +Yes. If you prefer to manage your export fields manually, you can prevent `zshy` from making any changes by setting the `noEdit` option to `true` in your `package.json#/zshy` config. ```jsonc { @@ -763,7 +769,7 @@ Yes. If you prefer to manage your `package.json` fields manually, you can preven } ``` -When `noEdit` is enabled, `zshy` will build your files but will not write to `package.json`. You will be responsible for populating the `"exports"`, `"bin"`, `"main"`, `"module"`, and `"types"` fields yourself. +When `noEdit` is enabled, `zshy` will build your files but will not write to `package.json` or `jsr.json`. You will be responsible for populating the `"exports"`, `"bin"`, `"main"`, `"module"`, and `"types"` fields yourself.
diff --git a/src/main.ts b/src/main.ts index 0d97d65..b2e7f80 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,6 +6,8 @@ import { table } from "table"; import * as ts from "typescript"; import { type BuildContext, compileProject } from "./compile.js"; import { + detectConfigIndentation, + findConfigPath, formatForLog, isSourceFile, isTestFile, @@ -158,24 +160,15 @@ Examples: log.info("Build will fail only on errors (default)"); } } + /////////////////////////////////// /// find and read pkg json /// /////////////////////////////////// // Find package.json by scanning up the file system - let packageJsonPath = "./package.json"; - let currentDir = process.cwd(); - - while (currentDir !== path.dirname(currentDir)) { - const candidatePath = path.join(currentDir, "package.json"); - if (fs.existsSync(candidatePath)) { - packageJsonPath = candidatePath; - break; - } - currentDir = path.dirname(currentDir); - } + const packageJsonPath = findConfigPath("package.json"); - if (!fs.existsSync(packageJsonPath)) { + if (!packageJsonPath) { log.error("โŒ package.json not found in current directory or any parent directories"); process.exit(1); } @@ -186,13 +179,7 @@ Examples: const pkgJson = JSON.parse(pkgJsonRaw); // Detect indentation from package.json to preserve it. - let indent: string | number = 2; // Default to 2 spaces - const indentMatch = pkgJsonRaw.match(/^([ \t]+)/m); - if (indentMatch?.[1]) { - indent = indentMatch[1]; - } else if (!pkgJsonRaw.includes("\n")) { - indent = 0; // minified - } + const pkgJsonIndent = detectConfigIndentation(pkgJsonRaw); const pkgJsonDir = path.dirname(packageJsonPath); const pkgJsonRelPath = relativePosix(pkgJsonDir, packageJsonPath); @@ -285,11 +272,13 @@ Examples: const config = { ...rawConfig } as NormalizedConfig; + // Normalize boolean options + config.noEdit ??= false; + // Normalize cjs property if (config.cjs === undefined) { config.cjs = true; // Default to true if not specified } - config.noEdit ??= false; // Validate that if cjs is disabled, no conditions are set to "cjs" if (config.cjs === false && config.conditions) { @@ -1045,7 +1034,54 @@ Examples: /////////////////////////////// log.info("[dryrun] Skipping package.json modification"); } else { - fs.writeFileSync(packageJsonPath, JSON.stringify(pkgJson, null, indent) + "\n"); + fs.writeFileSync(packageJsonPath, JSON.stringify(pkgJson, null, pkgJsonIndent) + "\n"); + } + } + + ////////////////////////////////// + /// write jsr exports /// + ////////////////////////////////// + + // Check if jsr.json exists in the project + const jsrJsonPath = findConfigPath("jsr.json"); + + if (jsrJsonPath) { + if (config.noEdit) { + if (!isSilent) { + log.info("[noedit] Skipping modification of jsr.json"); + } + } else { + if (!isSilent) { + log.info(`${prefix}Updating jsr.json...`); + } + + // read jsr.json + const jsrJsonRaw = fs.readFileSync(jsrJsonPath, "utf-8"); + const jsrJson = JSON.parse(jsrJsonRaw); + + // Detect indentation from jsr.json to preserve it. + const jsrJsonIndent = detectConfigIndentation(jsrJsonRaw); + + const jsrJsonDir = path.dirname(jsrJsonPath); + const jsrJsonRelPath = relativePosix(jsrJsonDir, jsrJsonPath); + + if (!isSilent) { + log.info(`Reading jsr.json from ./${jsrJsonRelPath}`); + } + + // Copy exports from zshy config to jsr.json exports + const jsrExports = config.exports; + jsrJson.exports = jsrExports; + if (isVerbose) { + log.info(`Setting "exports": ${formatForLog(jsrExports)}`); + } + + // Write jsr json + if (isDryRun) { + log.info("[dryrun] Skipping jsr.json modification"); + } else { + fs.writeFileSync(jsrJsonPath, JSON.stringify(jsrJson, null, jsrJsonIndent) + "\n"); + } } } diff --git a/src/utils.ts b/src/utils.ts index c043399..dea16f3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,4 @@ +import * as fs from "node:fs"; import * as path from "node:path"; import * as ts from "typescript"; @@ -132,3 +133,32 @@ export function isTestFile(filePath: string): boolean { return false; } + +export function findConfigPath(fileName: string): string | null { + let resultPath = `./${fileName}`; + let currentDir = process.cwd(); + + while (currentDir !== path.dirname(currentDir)) { + const candidatePath = path.join(currentDir, fileName); + if (fs.existsSync(candidatePath)) { + resultPath = candidatePath; + break; + } + currentDir = path.dirname(currentDir); + } + + return fs.existsSync(resultPath) ? resultPath : null; +} + +export function detectConfigIndentation(fileContents: string): string | number { + let indent: string | number = 2; // Default to 2 spaces + const indentMatch = fileContents.match(/^([ \t]+)/m); + + if (indentMatch?.[1]) { + indent = indentMatch[1]; + } else if (!fileContents.includes("\n")) { + indent = 0; // minified + } + + return indent; +} diff --git a/test/__snapshots__/zshy.test.ts.snap b/test/__snapshots__/zshy.test.ts.snap index 83901ee..1d1def2 100644 --- a/test/__snapshots__/zshy.test.ts.snap +++ b/test/__snapshots__/zshy.test.ts.snap @@ -1,5 +1,108 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`zshy with different tsconfig configurations > should copy exports to jsr.json when jsr.json exists 1`] = ` +{ + "exitCode": 0, + "stderr": "", + "stdout": "โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— + โ•‘ zshy ยป the bundler-free TypeScript build tool โ•‘ + โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +ยป Starting build... +ยป Verbose mode enabled +ยป Detected package manager: +ยป Build will fail only on errors (default) +ยป Detected project root: /test/jsr +ยป Reading package.json from ./package.json +ยป Parsed zshy config: { + "exports": { + ".": "./src/index.ts" + } + } +ยป Reading tsconfig from ./tsconfig.json +ยป Determining entrypoints... + โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•คโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— + โ•‘ Subpath โ”‚ Entrypoint โ•‘ + โ•Ÿโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ข + โ•‘ "my-pkg" โ”‚ ./src/index.ts โ•‘ + โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•งโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +ยป Resolved build paths: + โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•คโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— + โ•‘ Location โ”‚ Resolved path โ•‘ + โ•Ÿโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ข + โ•‘ rootDir โ”‚ ./src โ•‘ + โ•‘ outDir โ”‚ ./dist โ•‘ + โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•งโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +ยป Package is an ES module (package.json#/type is "module") +ยป Cleaning up outDir... +ยป Cleaning up declarationDir... +ยป Resolved entrypoints: [ + "./src/index.ts" + ] +ยป Resolved compilerOptions: { + "lib": [ + "lib.esnext.d.ts" + ], + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Bundler", + "moduleDetection": 2, + "allowJs": true, + "declaration": true, + "jsx": 4, + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true, + "verbatimModuleSyntax": false, + "noEmit": false, + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "noUnusedLocals": true, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + "sourceMap": true, + "declarationMap": true, + "resolveJsonModule": true, + "noImplicitOverride": true, + "noImplicitThis": true, + "outDir": "/test/jsr/dist", + "emitDeclarationOnly": false, + "composite": false + } +ยป Building CJS... (rewriting .ts -> .cjs/.d.cts) +ยป Enabling CJS interop transform... +ยป Building ESM... +ยป Writing files (8 total)... + ./dist/index.cjs + ./dist/index.cjs.map + ./dist/index.d.cts + ./dist/index.d.cts.map + ./dist/index.d.ts + ./dist/index.d.ts.map + ./dist/index.js + ./dist/index.js.map +ยป Updating package.json... +ยป Setting "main": "./dist/index.cjs" +ยป Setting "module": "./dist/index.js" +ยป Setting "types": "./dist/index.d.cts" +ยป Setting "exports": { + ".": { + "types": "./dist/index.d.cts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + } +ยป Updating jsr.json... +ยป Reading jsr.json from ./jsr.json +ยป Setting "exports": { + ".": "./src/index.ts" + } +ยป Build complete!", +} +`; + exports[`zshy with different tsconfig configurations > should not edit package.json when noEdit is true 1`] = ` { "exitCode": 0, diff --git a/test/jsr/dist/index.cjs b/test/jsr/dist/index.cjs new file mode 100644 index 0000000..1f51044 --- /dev/null +++ b/test/jsr/dist/index.cjs @@ -0,0 +1,10 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.hi = hi; +/** + * Main entry point for the test library + */ +function hi() { + console.log("hi"); +} +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/test/jsr/dist/index.cjs.map b/test/jsr/dist/index.cjs.map new file mode 100644 index 0000000..3e34d49 --- /dev/null +++ b/test/jsr/dist/index.cjs.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;AAGA,gBAEC;AALD;;GAEG;AACH,SAAgB,EAAE;IAChB,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACpB,CAAC"} \ No newline at end of file diff --git a/test/jsr/dist/index.d.cts b/test/jsr/dist/index.d.cts new file mode 100644 index 0000000..24f2c4a --- /dev/null +++ b/test/jsr/dist/index.d.cts @@ -0,0 +1,5 @@ +/** + * Main entry point for the test library + */ +export declare function hi(): void; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/test/jsr/dist/index.d.cts.map b/test/jsr/dist/index.d.cts.map new file mode 100644 index 0000000..a9ea779 --- /dev/null +++ b/test/jsr/dist/index.d.cts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,EAAE,SAEjB"} \ No newline at end of file diff --git a/test/jsr/dist/index.d.ts b/test/jsr/dist/index.d.ts new file mode 100644 index 0000000..24f2c4a --- /dev/null +++ b/test/jsr/dist/index.d.ts @@ -0,0 +1,5 @@ +/** + * Main entry point for the test library + */ +export declare function hi(): void; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/test/jsr/dist/index.d.ts.map b/test/jsr/dist/index.d.ts.map new file mode 100644 index 0000000..a9ea779 --- /dev/null +++ b/test/jsr/dist/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,EAAE,SAEjB"} \ No newline at end of file diff --git a/test/jsr/dist/index.js b/test/jsr/dist/index.js new file mode 100644 index 0000000..f635caf --- /dev/null +++ b/test/jsr/dist/index.js @@ -0,0 +1,7 @@ +/** + * Main entry point for the test library + */ +export function hi() { + console.log("hi"); +} +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/test/jsr/dist/index.js.map b/test/jsr/dist/index.js.map new file mode 100644 index 0000000..a2a8c25 --- /dev/null +++ b/test/jsr/dist/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,UAAU,EAAE;IAChB,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACpB,CAAC"} \ No newline at end of file diff --git a/test/jsr/jsr.json b/test/jsr/jsr.json new file mode 100644 index 0000000..330b91d --- /dev/null +++ b/test/jsr/jsr.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://jsr.io/schema/config-file.v1.json", + "name": "@jsr/my-pkg", + "version": "1.0.0", + "exports": { + ".": "./src/index.ts" + } +} diff --git a/test/jsr/package.json b/test/jsr/package.json new file mode 100644 index 0000000..2fa37a5 --- /dev/null +++ b/test/jsr/package.json @@ -0,0 +1,30 @@ +{ + "name": "my-pkg", + "version": "1.0.0", + "description": "Test fixture for zshy - represents a typical TypeScript library", + "type": "module", + "scripts": { + "build": "tsx ../../src/index.ts --project tsconfig.json" + }, + "devDependencies": { + "typescript": "^5.8.3" + }, + "zshy": { + "exports": { + ".": "./src/index.ts" + } + }, + "files": [ + "dist" + ], + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.cts", + "exports": { + ".": { + "types": "./dist/index.d.cts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + } +} diff --git a/test/jsr/src/index.ts b/test/jsr/src/index.ts new file mode 100644 index 0000000..422d0a8 --- /dev/null +++ b/test/jsr/src/index.ts @@ -0,0 +1,6 @@ +/** + * Main entry point for the test library + */ +export function hi() { + console.log("hi"); +} diff --git a/test/jsr/tsconfig.json b/test/jsr/tsconfig.json new file mode 100644 index 0000000..4e4f2c3 --- /dev/null +++ b/test/jsr/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.default.json", + "compilerOptions": { + "outDir": "./dist" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file diff --git a/test/zshy.test.ts b/test/zshy.test.ts index abb2519..467cb38 100644 --- a/test/zshy.test.ts +++ b/test/zshy.test.ts @@ -173,6 +173,14 @@ describe("zshy with different tsconfig configurations", () => { expect(snapshot).toMatchSnapshot(); }); + it("should copy exports to jsr.json when jsr.json exists", () => { + const snapshot = runZshyWithTsconfig("tsconfig.json", { + dryRun: false, + cwd: process.cwd() + "/test/jsr", + }); + expect(snapshot).toMatchSnapshot(); + }); + it("should support multiple bin entries", () => { const snapshot = runZshyWithTsconfig("tsconfig.json", { dryRun: false,