Closed33

google/zx 使ってみる & TypeScriptで動くようにする

Futa HirakobaFuta Hirakoba

さてhelpを見るか

❯ zx --help        
internal/process/esm_loader.js:74
    internalBinding('errors').triggerUncaughtException(
                              ^

Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/Users/korosuke613/ghq/github.com/korosuke613/playground/zx/--help' imported from /Users/korosuke613/.asdf/installs/nodejs/14.16.1/.npm/lib/node_modules/zx/zx.mjs
    at finalizeResolution (internal/modules/esm/resolve.js:276:11)
    at moduleResolve (internal/modules/esm/resolve.js:699:10)
    at Loader.defaultResolve [as _resolve] (internal/modules/esm/resolve.js:810:11)
    at Loader.resolve (internal/modules/esm/loader.js:86:40)
    at Loader.getModuleJob (internal/modules/esm/loader.js:230:28)
    at Loader.import (internal/modules/esm/loader.js:165:28)
    at importModuleDynamically (internal/modules/esm/translators.js:114:35)
    at exports.importModuleDynamicallyCallback (internal/process/esm_loader.js:30:14)
    at file:///Users/korosuke613/.asdf/installs/nodejs/14.16.1/.npm/lib/node_modules/zx/zx.mjs:57:5
    at ModuleJob.run (internal/modules/esm/module_job.js:152:23) {
  code: 'ERR_MODULE_NOT_FOUND'
}

...

--helpっていうファイルを実行しようとしてる???
-hhelpも試してみたが同じような結果となった。

help無さそう...

Futa HirakobaFuta Hirakoba

とりあえずHello Worldする。top-level awaitをするために拡張子は.mjsがおすすめとのこと

helloWorld.mjs
#!usr/bin/env zx

const msg = "Hello World"
await $`echo ${msg}`

実行。zxコマンドにファイルパスを渡す。

❯ zx helloWorld.mjs 
$ echo 'Hello World'
Hello World

おお!とりあえず実行できた。

Futa HirakobaFuta Hirakoba

zx helloWorld.mjsで実行するのでshebangはいらないのでは?とのアドバイスをもらった。

helloWorld.mjs
const msg = "Hello World"
await $`echo ${msg}`

上をzx helloWorld.mjsしたら普通に実行できた。

Futa HirakobaFuta Hirakoba

gitのworking treeがクリーンかどうか確認するコードを書いた。git statusはdirtyでもexitcodeが0になるので文字数でdirtyかどうか判断する。--porcelainはマシンリーダブルな形式にしてくれるオプション。

checkDirtyWorkingTreeForGit.mjs
const status = await $`git status --porcelain`

if(status.length > 0){
  throw new Error("git working tree is dirty.")
}

実行

❯ zx checkDirtyWorkingTreeForGit.mjs 
$ git status --porcelain
A  zx/checkDirtyWorkingTreeForGit.mjs
 M zx/helloWorld.mjs

うまくいかんぞ。git statusの結果って標準エラー出力に出るんだっけ。どうやって取得するんだ

Futa HirakobaFuta Hirakoba

$`command`の出力であるProcessOutputclassはstderrを持ってるらしい
出力。

checkDirtyWorkingTreeForGit.mjs
const status = await $`git status --porcelain`

console.log(`stdout: ${status.stdout}`)
console.log(`stderr: ${status.stderr}`)
console.log(`exitCode: ${status.exitCode}`)
console.log(`status.length: ${status.length}`)
console.log(`status: ${status}`)
console.log(status)

実行。

❯ zx checkDirtyWorkingTreeForGit.mjs
$ git status --porcelain
AM zx/checkDirtyWorkingTreeForGit.mjs
 M zx/helloWorld.mjs
stdout: AM zx/checkDirtyWorkingTreeForGit.mjs
 M zx/helloWorld.mjs

stderr: 
exitCode: 0
status.length: undefined
status: AM zx/checkDirtyWorkingTreeForGit.mjs
 M zx/helloWorld.mjs

ProcessOutput {}

statusの結果はstdoutに入ってた。そうか。そもそもstatus.lengthundefinedになるのか。
それはいいとして、console.log(status)の出力はProcessOutput{}になるのか...
console.log(`status: ${status}`)の出力はstatus.stdoutになるらしい。ここら辺よくわからん

Futa HirakobaFuta Hirakoba

とりあえず俺がやりたかったことは以下でできた。

checkDirtyWorkingTreeForGit.mjs
const status = await $`git status --porcelain`

if(status.stdout.length > 0){
  throw new Error("git working tree is dirty.")
}

❯ zx checkDirtyWorkingTreeForGit.mjs
$ git status --porcelain
AM zx/checkDirtyWorkingTreeForGit.mjs
 M zx/helloWorld.mjs
file:///Users/korosuke613/ghq/github.com/korosuke613/playground/zx/checkDirtyWorkingTreeForGit.mjs:11
  throw new Error("git working tree is dirty.")
        ^

Error: git working tree is dirty.
    at file:///Users/korosuke613/ghq/github.com/korosuke613/playground/zx/checkDirtyWorkingTreeForGit.mjs:11:9
    at processTicksAndRejections (internal/process/task_queues.js:93:5)

まあこの内容なら普通にシェルでいいんだけど

Futa HirakobaFuta Hirakoba

checkDirtyWorkingTreeForGit.tsを作って実行した。

checkDirtyWorkingTreeForGit.ts
import {$} from 'zx'

(async ()=>{
  const status = await $`git status --porcelain`

  if(status.stdout.length > 0){
    throw new Error("git working tree is dirty.")
  }
})()
❯ npx ts-node checkDirtyWorkingTreeForGit.ts
Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/node_modules/zx/index.mjs
    at Module.load (internal/modules/cjs/loader.js:926:11)
    at Function.Module._load (internal/modules/cjs/loader.js:769:14)
    at Module.require (internal/modules/cjs/loader.js:952:19)
    at require (internal/modules/cjs/helpers.js:88:18)
    at Object.<anonymous> (/Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/checkDirtyWorkingTreeForGit.ts:1:1)
    at Module._compile (internal/modules/cjs/loader.js:1063:30)
    at Module.m._compile (/Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/node_modules/ts-node/src/index.ts:1056:23)
    at Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
    at Object.require.extensions.<computed> [as .ts] (/Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/node_modules/ts-node/src/index.ts:1059:12)
    at Module.load (internal/modules/cjs/loader.js:928:32)

ERR_REQUIRE_ESM。あーーーー

Futa HirakobaFuta Hirakoba

色々がんばった結果、nodeの--loaderts-node/esmを指定することにより実行できた。(参考

"type": "module"が必要。

package.json
{
  "type": "module",
  "dependencies": {
    "zx": "^1.2.2"
  },
  "devDependencies": {
    "ts-node": "^9.1.1"
  }
}

"module": "ESNext"はtop-level awaitするため

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "strict": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}
checkDirtyWorkingTreeForGit.ts
import {$} from 'zx'

const status = await $`git status --porcelain`

if(status.stdout.length > 0){
  throw new Error("git working tree is dirty.")
}
❯ node --loader ts-node/esm checkDirtyWorkingTreeForGit.ts
(node:64769) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
$ git status --porcelain
AM zx/use-typescript/checkDirtyWorkingTreeForGit.mjs
AM zx/use-typescript/checkDirtyWorkingTreeForGit.ts
AM zx/use-typescript/main.ts
?? zx/use-typescript/node_modules/
?? zx/use-typescript/package-lock.json
?? zx/use-typescript/package.json
?? zx/use-typescript/tsconfig.json
Error: git working tree is dirty.
    at file:///Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/checkDirtyWorkingTreeForGit.ts:6:9
    at processTicksAndRejections (internal/process/task_queues.js:93:5)
Futa HirakobaFuta Hirakoba

ちなみに、ts-nodeをインストールしてないと下みたいに怒られる。

❯ node --loader ts-node/esm checkDirtyWorkingTreeForGit.ts
(node:67480) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
internal/process/esm_loader.js:74
    internalBinding('errors').triggerUncaughtException(
                              ^

Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'ts-node' imported from /Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/
    at packageResolve (internal/modules/esm/resolve.js:655:9)
    at moduleResolve (internal/modules/esm/resolve.js:696:18)
    at Loader.defaultResolve [as _resolve] (internal/modules/esm/resolve.js:810:11)
    at Loader.resolve (internal/modules/esm/loader.js:86:40)
    at Loader.getModuleJob (internal/modules/esm/loader.js:230:28)
    at Loader.import (internal/modules/esm/loader.js:165:28)
    at internal/process/esm_loader.js:57:31
    at initializeLoader (internal/process/esm_loader.js:62:5)
    at Object.loadESM (internal/process/esm_loader.js:67:11)
    at runMainESM (internal/modules/run_main.js:43:31) {
  code: 'ERR_MODULE_NOT_FOUND'
}
Futa HirakobaFuta Hirakoba

tsになったおかげで補完や型チェックが効いて書きやすくなった。index.d.ts用意してるのになんでこう面倒なのか

Futa HirakobaFuta Hirakoba

ts-nodeじゃなくてもtscでコンパイルして使えた。
やはり"type": "module"は必要。

❯ npx tsc                               

❯ node checkDirtyWorkingTreeForGit.js
$ git status --porcelain
AM zx/use-typescript/checkDirtyWorkingTreeForGit.mjs
AM zx/use-typescript/checkDirtyWorkingTreeForGit.ts
AM zx/use-typescript/main.ts
?? zx/use-typescript/checkDirtyWorkingTreeForGit.js
?? zx/use-typescript/main.js
?? zx/use-typescript/node_modules/
?? zx/use-typescript/package-lock.json
?? zx/use-typescript/package.json
?? zx/use-typescript/tsconfig.json
file:///Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/checkDirtyWorkingTreeForGit.js:4
    throw new Error("git working tree is dirty.");
          ^

Error: git working tree is dirty.
    at file:///Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/checkDirtyWorkingTreeForGit.js:4:11
    at processTicksAndRejections (internal/process/task_queues.js:93:5)

nodeじゃなくてzxコマンドでも動作した。

Futa HirakobaFuta Hirakoba

scriptsnode --loader ts-node/esmをすることで、IntelliJのデバッグもなんなくできた。

package.json
{
  "type": "module",
  "scripts": {
    "checkDirtyWorkingTreeForGit": "node --loader ts-node/esm checkDirtyWorkingTreeForGit.ts"
  },
  "dependencies": {
    "zx": "^1.2.2"
  },
  "devDependencies": {
    "ts-node": "^9.1.1"
  }
}

Futa HirakobaFuta Hirakoba

開発時はTSで動かし、運用時は動かしたい場所に置くときはコンパイルしたJSを置くようにしてzxで実行すれば良さそう。
TSで動いたことで自動補完も型チェックも働き、インポートして使うことでリッチにデバッグもできるようになった。

今後シェルスクリプトを作るときにまた試してみたい。まだまだ知らん機能がたくさんある。

Futa HirakobaFuta Hirakoba

go製のojichatとnpmにあるcowsayを悪魔合体した。

ojichatToCowsay.ts
import {$} from "zx"
// @ts-ignore cowsayは型が提供されてなかった...
import cowsay from "cowsay";

const getOjichatMessage = async () => {
  try {
    await $`ojichat -v`
    return $`ojichat`
  } catch (p) {
    return $`docker run --rm -i greymd/ojichat:latest`
  }
}

$.verbose = false // `set -x`を無効
const ojichatMessage = await getOjichatMessage()

console.log(
  cowsay.say({
    text: ojichatMessage.stdout
  })
)

実行

❯ node --loader ts-node/esm ojichatToCowsay.ts
(node:24785) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
 _________________________________________________________________________________________________________________
/ マユちゃん、ヤッホー😊何してるのかい😜⁉️🤔俺は、近所に新しくできたバー🍷に行ってきたよ。味はまぁまぁだったカナ(^з<) \
\                                                                                                                 /
 -----------------------------------------------------------------------------------------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

なにやってんだろ俺

Futa HirakobaFuta Hirakoba

現在の zx のバージョン

❯ zx -v                            
zx version 1.2.2

一方最新バージョンは 1.10.1
結構更新されてるっぽい。

とりあえずコマンドとしての zx を更新する。

❯ npm upgrade -g zx     

added 3 packages, removed 1 package, changed 9 packages, and audited 13 packages in 1s

2 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
❯ zx -v
zx version 1.10.1

更新完了

Futa HirakobaFuta Hirakoba

awsSts.tsで試してみる。

v1.2.2で実行した結果

zx@v1.2.2で実行
❯ zx awsSts.ts        
internal/process/esm_loader.js:74
    internalBinding('errors').triggerUncaughtException(
                              ^

TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/awsSts.ts
    at Loader.defaultGetFormat [as _getFormat] (internal/modules/esm/get_format.js:71:15)
    at Loader.getFormat (internal/modules/esm/loader.js:102:42)
    at Loader.getModuleJob (internal/modules/esm/loader.js:231:31)
    at async Loader.import (internal/modules/esm/loader.js:165:17)
    at async file:///Users/korosuke613/.asdf/installs/nodejs/14.16.1/.npm/lib/node_modules/zx/zx.mjs:57:5 {
  code: 'ERR_UNKNOWN_FILE_EXTENSION'
}

v1.10.1で実行した結果

zx@v1.10.1で実行
❯ zx awsSts.ts      
$ tsc --target esnext --module esnext --moduleResolution node /Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/awsSts.ts
/bin/bash: tsc: command not found
Error: /bin/bash: tsc: command not found
    at importPath (file:///Users/korosuke613/.asdf/installs/nodejs/14.16.1/.npm/lib/node_modules/zx/zx.mjs:120:12)

おお!tscを求められた。コンパイルしようとしている。

Futa HirakobaFuta Hirakoba

tsc をグローバルインストールする。

❯ npm i -g typescript      

added 1 package, and audited 2 packages in 1s

found 0 vulnerabilities

再度zx awsSts.tsする。(結果を一部黒塗りしてる。)

zx awsSts.ts
❯ zx awsSts.ts       
$ tsc --target esnext --module esnext --moduleResolution node /Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/awsSts.ts
$ aws sts get-caller-identity
{
    "UserId": "xxxxxx:yyyy@example.com",
    "Account": "zzzzzzzzzzzz",
    "Arn": "arn:aws:sts::zzzzzzzzzz"
}
Account ID: zzzzzzzzzzz

実行できた!!!やったぜ
tsc コマンドを呼び出してコンパイルしてるんだな。

Futa HirakobaFuta Hirakoba

実装を見つけた。

https://github.com/google/zx/commit/934b64a7644b424c4151d91ec64de49863845366#diff-7bb464fcc7ce7a74bb9949a68e6f0ed74537c82a1108ba5a851785c35b6eea3bR117-R125

zx.mjs
  if (ext === '.ts') {
    let {dir, name} = parse(filepath)
    let outFile = join(dir, name + '.mjs')
    await $`tsc --target esnext --module esnext --moduleResolution node ${filepath}`
    await fs.rename(join(dir, name + '.js'), outFile)
    let wait = importPath(outFile, filepath)
    await fs.rm(outFile)
    return wait
  }

拡張子がtsかどうか判断しtscコマンドでコンパイルして生成されたjsを実行してるようだ

Futa HirakobaFuta Hirakoba

デバッグしたいため、zx awsSts.tsをnpm scriptsに追加してIntelliJで実行した。

package.json
{
  "type": "module",
  "scripts": {
    "checkDirtyWorkingTreeForGit": "node --loader ts-node/esm checkDirtyWorkingTreeForGit.ts",
-     "ojichat": "node --loader ts-node/esm ojichatToCowsay.ts"
+     "ojichat": "node --loader ts-node/esm ojichatToCowsay.ts",
+     "awsSts": "zx awsSts.ts"
  },
  "dependencies": {
    "cowsay": "^1.4.0",
-     "zx": "^1.2.2"
+     "zx": "^1.10.1"
  },
  "devDependencies": {
    "ts-node": "^9.1.1"
  }
}

結論から言うとブレイクポイントを無視して終了してしまった。コンパイルしてから実行している以上しょうがないか...

Futa HirakobaFuta Hirakoba

以前可能だったnode --loader ts-node/esm awsSts.tsができるかどうか確認する。
こちらならデバッグ可能だったはず。

❯ node --loader ts-node/esm awsSts.ts
(node:14261) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
ReferenceError: $ is not defined
    at file:///Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/awsSts.ts:9:30
    at ModuleJob.run (internal/modules/esm/module_job.js:152:23)
    at Loader.import (internal/modules/esm/loader.js:166:24)
    at Object.loadESM (internal/process/esm_loader.js:68:5)

!?

ReferenceError: $ is not defined

$が定義されてないことになってる。

試しにv1.2.2に戻して実行し直したが、素直に実行できた。

さっきのコミットのindex.d.tsを見てみると、export const $: $が削除されていた。つまり型定義的にexportしていない。

無理矢理 node_modules にある index.d.ts でexport const $: $したら普通に動いた。
なぜ行を削除したのか気になる。

Futa HirakobaFuta Hirakoba

話は変わるけど$をimportしなかったらどうなるのか試してみた。

❯ zx awsSts.ts
$ tsc --target esnext --module esnext --moduleResolution node /Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/awsSts.ts
awsSts.ts(7,24): error TS1375: 'await' expressions are only allowed at the top level of a file when that file is a module, but this file has no imports or exports. Consider adding an empty 'export {}' to make this file a module.
awsSts.ts(7,30): error TS2581: Cannot find name '$'. Do you need to install type definitions for jQuery? Try `npm i --save-dev @types/jquery`.
Error: 
    at importPath (file:///Users/korosuke613/.asdf/installs/nodejs/14.16.1/.npm/lib/node_modules/zx/zx.mjs:120:12)

importしないと使えない...?

examples/typescript.tsも実行してみた。

examples/typescript.ts
#!/usr/bin/env zx

// Copyright 2021 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.

/// <reference types=".." />

void async function () {
  await $`pwd`
}()

やっぱ同じエラーでるな。俺の環境だけ?とにかくtsの場合コンパイルする関係からかimport { $ } from "zx";は必要そう。

Futa HirakobaFuta Hirakoba

先程のexport const $ = $をつけた状態でzx awsSts.tsしてみたらエラーになった。

❯ zx awsSts.ts
$ tsc --target esnext --module esnext --moduleResolution node /Users/korosuke613/ghq/github.com/korosuke613/playground/zx/use-typescript/awsSts.ts
node_modules/zx/index.d.ts(31,18): error TS1254: A 'const' initializer in an ambient context must be a string or numeric literal or literal enum reference.
Error: 
    at importPath (file:///Users/korosuke613/.asdf/installs/nodejs/14.16.1/.npm/lib/node_modules/zx/zx.mjs:120:12)

だから消したのかー
これなんとかならんかな

Futa HirakobaFuta Hirakoba

色々あってimport {$} from "zx"には対応しようぜって話になった。
ただ、antonmedv さんは問題を再現できないらしい。うーん

このスクラップは2021/06/06にクローズされました