Commit f4b0328

Anton Golub <antongolub@antongolub.com>
2025-03-01 16:01:21
refactor: use ts exts for relative paths (#1111)
* refactor: use ts exts for relative paths * test: fix coverage
1 parent a23261b
.github/workflows/test.yml
@@ -215,7 +215,7 @@ jobs:
       - name: Install deps
         run: npm ci
       - name: Install TypeScript ${{ matrix.ts }}
-        run: npm i --force --no-package-lock typescript@${{ matrix.ts }}
+        run: npm i --force --no-package-lock --no-fund typescript@${{ matrix.ts }}
       - uses: actions/download-artifact@v4
         with:
           name: build
scripts/build-dts.mjs
@@ -110,6 +110,7 @@ for (const dts of await glob(['build/**/*.d.ts', '!build/vendor-*.d.ts'])) {
   const contents =
     (pkgEntries.some((e) => dts.includes(e)) ? prefix : '') +
     (await fs.readFile(dts, 'utf8'))
+      .replaceAll(".ts';", ".js';")
       .split('\n')
       .filter((line) => !line.startsWith('/// <reference types'))
       .join('\n')
src/cli.ts
@@ -28,11 +28,12 @@ import {
   path,
   stdin,
   VERSION,
-} from './index.js'
-import { installDeps, parseDeps } from './deps.js'
-import { startRepl } from './repl.js'
-import { randomId, bufToString } from './util.js'
-import { createRequire, type minimist } from './vendor.js'
+} from './index.ts'
+import { installDeps, parseDeps } from './deps.ts'
+import { startRepl } from './repl.ts'
+import { randomId, bufToString } from './util.ts'
+import { transformMarkdown } from './md.ts'
+import { createRequire, type minimist } from './vendor.ts'
 
 const EXT = '.mjs'
 const EXT_RE = /^\.[mc]?[jt]sx?$/
@@ -206,6 +207,8 @@ async function readScriptFromHttp(remote: string): Promise<string> {
   return res.text()
 }
 
+export { transformMarkdown }
+
 export function injectGlobalRequire(origin: string): void {
   const __filename = path.resolve(origin)
   const __dirname = path.dirname(__filename)
@@ -213,79 +216,6 @@ export function injectGlobalRequire(origin: string): void {
   Object.assign(globalThis, { __filename, __dirname, require })
 }
 
-export function transformMarkdown(buf: Buffer | string): string {
-  const output = []
-  const tabRe = /^(  +|\t)/
-  const codeBlockRe =
-    /^(?<fence>(`{3,20}|~{3,20}))(?:(?<js>(js|javascript|ts|typescript))|(?<bash>(sh|shell|bash))|.*)$/
-  let state = 'root'
-  let codeBlockEnd = ''
-  let prevLineIsEmpty = true
-  for (const line of bufToString(buf).split(/\r?\n/)) {
-    switch (state) {
-      case 'root':
-        if (tabRe.test(line) && prevLineIsEmpty) {
-          output.push(line)
-          state = 'tab'
-          continue
-        }
-        const { fence, js, bash } = line.match(codeBlockRe)?.groups || {}
-        if (!fence) {
-          prevLineIsEmpty = line === ''
-          output.push('// ' + line)
-          continue
-        }
-        codeBlockEnd = fence
-        if (js) {
-          state = 'js'
-          output.push('')
-        } else if (bash) {
-          state = 'bash'
-          output.push('await $`')
-        } else {
-          state = 'other'
-          output.push('')
-        }
-        break
-      case 'tab':
-        if (line === '') {
-          output.push('')
-        } else if (tabRe.test(line)) {
-          output.push(line)
-        } else {
-          output.push('// ' + line)
-          state = 'root'
-        }
-        break
-      case 'js':
-        if (line === codeBlockEnd) {
-          output.push('')
-          state = 'root'
-        } else {
-          output.push(line)
-        }
-        break
-      case 'bash':
-        if (line === codeBlockEnd) {
-          output.push('`')
-          state = 'root'
-        } else {
-          output.push(line)
-        }
-        break
-      case 'other':
-        if (line === codeBlockEnd) {
-          output.push('')
-          state = 'root'
-        } else {
-          output.push('// ' + line)
-        }
-        break
-    }
-  }
-  return output.join('\n')
-}
-
 export function isMain(
   metaurl: string = import.meta.url,
   scriptpath: string = process.argv[1]
src/core.ts
@@ -31,7 +31,7 @@ import {
   formatExitMessage,
   getCallerLocation,
   getExitCodeInfo,
-} from './error.js'
+} from './error.ts'
 import {
   exec,
   buildCmd,
@@ -41,7 +41,7 @@ import {
   VoidStream,
   type ChalkInstance,
   type TSpawnStore,
-} from './vendor-core.js'
+} from './vendor-core.ts'
 import {
   type Duration,
   log,
@@ -60,9 +60,9 @@ import {
   toCamelCase,
   randomId,
   bufArrJoin,
-} from './util.js'
+} from './util.ts'
 
-export { log, type LogEntry } from './util.js'
+export { log, type LogEntry } from './util.ts'
 
 const CWD = Symbol('processCwd')
 const SYNC = Symbol('syncExec')
src/deps.ts
@@ -12,9 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { $ } from './core.js'
-import { spinner } from './goods.js'
-import { depseek } from './vendor.js'
+import { $ } from './core.ts'
+import { spinner } from './goods.ts'
+import { depseek } from './vendor.ts'
 
 /**
  * Install npm dependencies
src/globals.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import * as _ from './index.js'
+import * as _ from './index.ts'
 
 Object.assign(globalThis, _)
 // TODO: global types not working with jsr.io
src/goods.ts
@@ -14,7 +14,7 @@
 
 import assert from 'node:assert'
 import { createInterface } from 'node:readline'
-import { $, within, ProcessOutput } from './core.js'
+import { $, within, ProcessOutput } from './core.ts'
 import {
   type Duration,
   identity,
@@ -22,13 +22,13 @@ import {
   parseBool,
   parseDuration,
   toCamelCase,
-} from './util.js'
+} from './util.ts'
 import {
   type RequestInfo,
   type RequestInit,
   nodeFetch,
   minimist,
-} from './vendor.js'
+} from './vendor.ts'
 
 export { default as path } from 'node:path'
 export * as os from 'node:os'
src/index.ts
@@ -12,11 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { ProcessPromise } from './core.js'
-import { fs } from './vendor.js'
+import { ProcessPromise } from './core.ts'
+import { fs } from './vendor.ts'
 
-export * from './core.js'
-export * from './goods.js'
+export * from './core.ts'
+export * from './goods.ts'
 export {
   minimist,
   chalk,
@@ -27,7 +27,7 @@ export {
   ps,
   glob,
   glob as globby,
-} from './vendor.js'
+} from './vendor.ts'
 
 export const VERSION: string = fs.readJsonSync(
   new URL('../package.json', import.meta.url)
@@ -42,7 +42,7 @@ export {
   tempdir as tmpdir,
   tempfile,
   tempfile as tmpfile,
-} from './util.js'
+} from './util.ts'
 
 /**
  *  @deprecated Use $`cmd`.nothrow() instead.
src/md.ts
@@ -0,0 +1,88 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { bufToString } from './util.ts'
+
+export function transformMarkdown(buf: Buffer | string): string {
+  const output = []
+  const tabRe = /^(  +|\t)/
+  const codeBlockRe =
+    /^(?<fence>(`{3,20}|~{3,20}))(?:(?<js>(js|javascript|ts|typescript))|(?<bash>(sh|shell|bash))|.*)$/
+  let state = 'root'
+  let codeBlockEnd = ''
+  let prevLineIsEmpty = true
+  for (const line of bufToString(buf).split(/\r?\n/)) {
+    switch (state) {
+      case 'root':
+        if (tabRe.test(line) && prevLineIsEmpty) {
+          output.push(line)
+          state = 'tab'
+          continue
+        }
+        const { fence, js, bash } = line.match(codeBlockRe)?.groups || {}
+        if (!fence) {
+          prevLineIsEmpty = line === ''
+          output.push('// ' + line)
+          continue
+        }
+        codeBlockEnd = fence
+        if (js) {
+          state = 'js'
+          output.push('')
+        } else if (bash) {
+          state = 'bash'
+          output.push('await $`')
+        } else {
+          state = 'other'
+          output.push('')
+        }
+        break
+      case 'tab':
+        if (line === '') {
+          output.push('')
+        } else if (tabRe.test(line)) {
+          output.push(line)
+        } else {
+          output.push('// ' + line)
+          state = 'root'
+        }
+        break
+      case 'js':
+        if (line === codeBlockEnd) {
+          output.push('')
+          state = 'root'
+        } else {
+          output.push(line)
+        }
+        break
+      case 'bash':
+        if (line === codeBlockEnd) {
+          output.push('`')
+          state = 'root'
+        } else {
+          output.push(line)
+        }
+        break
+      case 'other':
+        if (line === codeBlockEnd) {
+          output.push('')
+          state = 'root'
+        } else {
+          output.push('// ' + line)
+        }
+        break
+    }
+  }
+  return output.join('\n')
+}
src/repl.ts
@@ -16,8 +16,8 @@ import os from 'node:os'
 import path from 'node:path'
 import repl from 'node:repl'
 import { inspect } from 'node:util'
-import { ProcessOutput, defaults } from './core.js'
-import { chalk } from './vendor-core.js'
+import { ProcessOutput, defaults } from './core.ts'
+import { chalk } from './vendor-core.ts'
 
 const HISTORY =
   process.env.ZX_REPL_HISTORY ?? path.join(os.homedir(), '.zx_repl_history')
src/util.ts
@@ -20,10 +20,10 @@ import {
   type RequestInfo,
   type RequestInit,
   type TSpawnStoreChunks,
-} from './vendor-core.js'
+} from './vendor-core.ts'
 import { inspect } from 'node:util'
 
-export { isStringLiteral } from './vendor-core.js'
+export { isStringLiteral } from './vendor-core.ts'
 
 export function tempdir(
   prefix: string = `zx-${randomId()}`,
src/vendor.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { bus } from './vendor-core.js'
+import { bus } from './vendor-core.ts'
 import {
   depseek as _depseek,
   dotenv as _dotenv,
@@ -22,10 +22,10 @@ import {
   YAML as _YAML,
   glob as _glob,
   nodeFetch as _nodeFetch,
-} from './vendor-extra.js'
+} from './vendor-extra.ts'
 
-export * from './vendor-core.js'
-export { createRequire } from './vendor-extra.js'
+export * from './vendor-core.ts'
+export { createRequire } from './vendor-extra.ts'
 
 const { wrap } = bus
 export const depseek: typeof _depseek = wrap('depseek', _depseek)
test/smoke/tsconfig.json
@@ -0,0 +1,15 @@
+{
+  "compilerOptions": {
+    "target": "ES2021",
+    "lib": ["ES2021"],
+    "moduleResolution": "NodeNext",
+    "module": "NodeNext",
+    "strict": true,
+    "outDir": "./build",
+    "declaration": true,
+    "emitDeclarationOnly": true,
+    "types": []
+  },
+  "include": ["./src/**/*"],
+  "exclude": ["./src/globals.ts"]
+}
test/all.test.js
@@ -20,6 +20,7 @@ import './export.test.js'
 import './global.test.js'
 import './goods.test.js'
 import './index.test.js'
+import './md.test.ts'
 import './package.test.js'
 import './util.test.js'
 import './vendor.test.js'
test/cli.test.js
@@ -18,7 +18,7 @@ import { fileURLToPath } from 'node:url'
 import net from 'node:net'
 import getPort from 'get-port'
 import { $, path, tmpfile, tmpdir, fs } from '../build/index.js'
-import { isMain, normalizeExt, transformMarkdown } from '../build/cli.js'
+import { isMain, normalizeExt } from '../build/cli.js'
 
 const __filename = fileURLToPath(import.meta.url)
 const spawn = $.spawn
@@ -349,43 +349,6 @@ describe('cli', () => {
       }
     })
 
-    test('transformMarkdown()', () => {
-      // prettier-ignore
-      assert.equal(transformMarkdown(`
-# Title
-    
-~~~js
-await $\`echo "js"\`
-~~~
-
-typescript code block
-~~~~~ts
-await $\`echo "ts"\`
-~~~~~
-
-~~~
-unknown code block
-~~~
-
-`), `// 
-// # Title
-//     
-
-await $\`echo "js"\`
-
-// 
-// typescript code block
-
-await $\`echo "ts"\`
-
-// 
-
-// unknown code block
-
-// 
-// `)
-    })
-
     test('normalizeExt()', () => {
       assert.equal(normalizeExt('.ts'), '.ts')
       assert.equal(normalizeExt('ts'), '.ts')
test/md.test.ts
@@ -0,0 +1,72 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { test, describe } from 'node:test'
+import assert from 'node:assert'
+import { transformMarkdown } from '../src/md.ts'
+
+describe('md', () => {
+  test('transformMarkdown()', () => {
+    assert.equal(transformMarkdown('\n'), '// \n// ')
+    assert.equal(transformMarkdown('  \n    '), '  \n    ')
+    assert.equal(
+      transformMarkdown(`
+\t~~~js
+console.log('js')`),
+      `// \n\t~~~js\n// console.log('js')`
+    )
+    // prettier-ignore
+    assert.equal(transformMarkdown(`
+# Title
+    
+~~~js
+await $\`echo "js"\`
+~~~
+
+typescript code block
+~~~~~ts
+await $\`echo "ts"\`
+~~~~~
+
+~~~
+unknown code block
+~~~
+
+~~~sh
+echo foo
+~~~
+
+`), `// 
+// # Title
+//     
+
+await $\`echo "js"\`
+
+// 
+// typescript code block
+
+await $\`echo "ts"\`
+
+// 
+
+// unknown code block
+
+// 
+await $\`
+echo foo
+\`
+// 
+// `)
+  })
+})
test/package.test.js
@@ -62,6 +62,7 @@ describe('package', () => {
         'build/index.cjs',
         'build/index.d.ts',
         'build/index.js',
+        'build/md.d.ts',
         'build/util.cjs',
         'build/util.d.ts',
         'build/util.js',
.nycrc
@@ -2,5 +2,14 @@
   "reporter": ["html", "text"],
   "lines": 98,
   "branches": "90",
-  "statements": "98"
+  "statements": "98",
+  "exclude": [
+    "build/deno.js",
+    "build/vendor-extra.cjs",
+    "build/vendor-core.cjs",
+    "build/esblib.cjs",
+    "test/**",
+    "scripts",
+    "src/util.ts"
+  ]
 }
.size-limit.json
@@ -30,7 +30,7 @@
   {
     "name": "all",
     "path": "build/*",
-    "limit": "850.2 kB",
+    "limit": "850.25 kB",
     "brotli": false,
     "gzip": false
   }
package.json
@@ -63,7 +63,7 @@
     "fmt": "prettier --write .",
     "fmt:check": "prettier --check .",
     "build": "npm run build:js && npm run build:dts && npm run build:tests",
-    "build:js": "node scripts/build-js.mjs --format=cjs --hybrid --entry=src/*.ts:!src/error.ts:!src/repl.ts && npm run build:vendor",
+    "build:js": "node scripts/build-js.mjs --format=cjs --hybrid --entry=src/*.ts:!src/error.ts:!src/repl.ts:!src/md.ts && npm run build:vendor",
     "build:vendor": "node scripts/build-js.mjs --format=cjs --entry=src/vendor-*.ts --bundle=all",
     "build:tests": "node scripts/build-tests.mjs",
     "build:dts": "tsc --project tsconfig.json && rm build/error.d.ts build/repl.d.ts && node scripts/build-dts.mjs",
@@ -75,7 +75,7 @@
     "test:it": "node ./test/it/build.test.js",
     "test:jsr": "node ./test/it/build-jsr.test.js",
     "test:unit": "node --experimental-strip-types ./test/all.test.js",
-    "test:coverage": "c8 -x build/deno.js -x build/vendor-extra.cjs -x build/vendor-core.cjs -x build/esblib.cjs -x 'test/**' -x scripts --check-coverage npm run test:unit",
+    "test:coverage": "c8 -c .nycrc --check-coverage npm run test:unit",
     "test:circular": "madge --circular src/*",
     "test:types": "tsd",
     "test:license": "node ./test/extra.test.js",
@@ -84,7 +84,7 @@
     "test:smoke:strip-types": "node --experimental-strip-types test/smoke/ts.test.ts",
     "test:smoke:tsx": "tsx test/smoke/ts.test.ts",
     "test:smoke:tsc": "cd test/smoke && mkdir -p node_modules && ln -s ../../../  ./node_modules/zx; ../../node_modules/typescript/bin/tsc -v && ../../node_modules/typescript/bin/tsc --esModuleInterop --module node16 --rootDir . --outdir ./temp ts.test.ts && node ./temp/ts.test.js",
-    "test:smoke:ts-node": "node --loader ts-node/esm test/smoke/ts.test.ts",
+    "test:smoke:ts-node": "cd test/smoke && node --loader ts-node/esm ts.test.ts",
     "test:smoke:bun": "bun test ./test/smoke/bun.test.js && bun ./test/smoke/node.test.mjs",
     "test:smoke:win32": "node ./test/smoke/win32.test.js",
     "test:smoke:cjs": "node ./test/smoke/node.test.cjs",
tsconfig.json
@@ -8,6 +8,7 @@
     "outDir": "./build",
     "declaration": true,
     "emitDeclarationOnly": true,
+    "allowImportingTsExtensions": true,
     "types": []
   },
   "include": ["./src/**/*"],