Open72

Reactの内部実装を見る(1. useState編)

Yug (やぐ)Yug (やぐ)

メモ

  • if (__DEV__)で開発環境かどうか見てるっぽい
    • __DEV__の定義をぜひ見てみたいところ(本記事では触れられていない)
  • 初回レンダー時≒mount時 のみしか触れられていないのでupdateの方も見たい
  • .currentが状態保持に結構使われてる
    • useRefのref.currentと同じだよな、多分
      • どうやってDOMのrefを取得してるんだろう(=useRefの内部実装が気になる)
  • useStateの引数にinitializer functionが渡された場合はそれを実行するだけでいいのか。確かに
ReactFiberHooks.new.js
if (typeof initialState === 'function') {
 initialState = initialState();
}
  • これはシンプルに知らなかったんだけど、=を同じ行で2つ使えるらしい。
// 以下1行は
hook.memoizedState = hook.baseState = initialState;
// 以下2行と同じ
hook.memoizedState = initialState;
hook.baseState = initialState;
  • ここから全部のフックをどんどん辿っていくのが良さそう
ReactFiberHooks.new.js
const HooksDispatcherOnMount: Dispatcher = {
    readContext,
    
    useCallback: mountCallback,
    useContext: readContext,
    useEffect: mountEffect,
    useImperativeHandle: mountImperativeHandle,
    useLayoutEffect: mountLayoutEffect,
    useInsertionEffect: mountInsertionEffect,
    useMemo: mountMemo,
    useReducer: mountReducer,
    useRef: mountRef,
    useState: mountState,
    useDebugValue: mountDebugValue,
    useDeferredValue: mountDeferredValue,
    useTransition: mountTransition,
    useMutableSource: mountMutableSource,
    useSyncExternalStore: mountSyncExternalStore,
    useId: mountId,
    
    unstable_isNewReconciler: enableNewReconciler,
};
  • このqueueどうなってんのか気になる
ReactFiberHooks.new.js
const queue: UpdateQueue<S, BasicStateAction<S>> = {
    pending: null,
    interleaved: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
};
  • 至る所で出てくるDispatcherっていう型は何なんだ

  • dispatchの定義がすごくわかりづらいので言語化してみる

const dispatch: Dispatch<
    BasicStateAction<S>,
> = (queue.dispatch = (dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
): any));

dispatchSetStateにcurrentlyRenderingFiber引数とqueue引数をbindつまり固定したものをqueue.dispatchに代入して、そのqueue.dispatchを引数に取る。で返り値はany。っていう関数がdispatchである。
んでそのdispatch関数自体の型はDispatch<BasicStateAction<S>,>である。

bindが複雑だが、例えばqueue.dispatch(arg1, arg2)と呼び出した場合、内部的にはdispatchSetState(null, currentlyRenderingFiber, queue, arg1, arg2)と呼び出されるようになってるみたいな感じっぽい。事前に固定した引数を受け取る関数を新しい関数として作れるみたいなのがbind。

なのでqueue.dispatch()と呼び出したらdispatchSetState(null, currentlyRenderingFiber, queue)を呼び出したのと同じことになって再利用性が上がって楽になる。

Yug (やぐ)Yug (やぐ)

記事で、

ほとんど何も書かれていませんでした。スカされ続けています。
どうやらReactCurrentDispatcher.currentに実質的な値を格納している箇所が他でありそうです。

と書かれていたように、「定義は見れたけどそこに実質的な値を代入してるコードがどこかわからん」になったら記事を見て情報収集すれば良さそうなので、一旦自分でどんどんソースコード追ってみるか

(いやわからなくてもその変数名検索すれば普通に探せるか?...まぁいいや)

とりあえずリポジトリcloneしてVSCodeで見よう

git clone https://github.com/facebook/react.git

https://github.com/facebook/react

Yug (やぐ)Yug (やぐ)

ん?ReactHook.jsというファイルが無いぞ
→ReactHooks.jsを発見した。変わったのかな?それともミスか

react\packages\react\src\ReactHooks.js

(ちなみにこれ以降はC:\Users\などは省略してreact\からパスを書き始めることにする)

さて、resolveDispatcher関数を見てみるとこうなっている

ReactHooks.js
function resolveDispatcher() {
  const dispatcher = ReactSharedInternals.H;
  if (__DEV__) {
    if (dispatcher === null) {
      console.error(
        'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
          ' one of the following reasons:\n' +
          '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
          '2. You might be breaking the Rules of Hooks\n' +
          '3. You might have more than one copy of React in the same app\n' +
          'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.',
      );
    }
  }
  // Will result in a null access error if accessed outside render phase. We
  // intentionally don't throw our own error because this is in a hot path.
  // Also helps ensure this is inlined.
  return ((dispatcher: any): Dispatcher);
}
Yug (やぐ)Yug (やぐ)

ん、1行変わってるな

ReactHooks.js
const dispatcher = ReactCurrentDispatcher.current;  // これが
const dispatcher = ReactSharedInternals.H;  // これになってる

ReactCurrentDispatcherはすでに記事内で説明されてるので、ReactSharedInternalsを見てみるか

Yug (やぐ)Yug (やぐ)

同ファイルに

ReactHooks.js
import ReactSharedInternals from 'shared/ReactSharedInternals';

とあるのでVSCodeのGo to Definitionで定義元に飛んでみようと思ったら飛べないようになってるな、めんどくさい

おそらくビルドツール(Webpack、Rollup、Babelなど)でパスエイリアスが設定されているためだと思うが、そもそも複数のビルドツール使ってるぽいし、かつ至るところ(階層)に構成ファイルがあってどれが共通パスを設定しているのか分からなかったので、普通にshared/ReactSharedInternalsで検索して探す。

Yug (やぐ)Yug (やぐ)

見つけた。

今のファイルは

react\packages\react\src\ReactHooks.js

で、相対パスとしてshared/ReactSharedInternalsがimportされてる訳だが、お目当てのファイルは

react\packages\shared\ReactSharedInternals.js

にあった。

なのでreact\packagesが共通パスで設定されているのだろう(多分)

Yug (やぐ)Yug (やぐ)

んで、中身はこんな感じだった

ReactSharedInternals.js
/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import * as React from 'react';

const ReactSharedInternals =
  React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;

export default ReactSharedInternals;
Yug (やぐ)Yug (やぐ)

わぉ、それらは同じものだった。つい2週間前くらいに名前が変わったらしい。
https://x.com/yuta0801_/status/1849620141727940630
https://react.dev/blog/2024/04/25/react-19-upgrade-guide#libraries-depending-on-react-internals-may-block-upgrades

React 19 RC(Release Candidate)という、「19にアップグレードするためにいろいろちょっとずつ変えていきまっせ」的なやつがあるっぽく、その途中経過としてのReact 18.3の公開によるものらしい。

なので「React19になったので変わった」のではなく「React19に向かっていろいろ変更している途中であるバージョン18、の18.3になったので変わった」みたいな感じだと思われる。

気になる点

  • React.○○ってソースコード読めないのかな?
    • React自体の中身ってどうなってる?
  • 内部ってそもそも何?
  • 内部にアクセスするというのはどういうこと?
  • なぜ内部にアクセスしてはいけない?
  • 内部アクセスするとユーザーはアップグレードできないってどゆこと?
    • アップグレードとは?
    • 逆に、内部アクセスしなければアップグレードできるってこと?
Yug (やぐ)Yug (やぐ)

import * as React from 'react''react'の定義に飛んでみる

すると

C:\Users\ユーザー名\node_modules\react\index.js

に飛んだ。ということはReactリポジトリとは関係なく、ローカルに入っているものだな。
以前npm install -g ○○でreactなどをグローバルインストールしたときにできたものだろう。

中身は以下。

index.js
'use strict';

if (process.env.NODE_ENV === 'production') {
  module.exports = require('./cjs/react.production.min.js');
} else {
  module.exports = require('./cjs/react.development.js');
}
Yug (やぐ)Yug (やぐ)

本番環境の方はこれ
おそらくただ開発環境の方をビルド(バンドル?)してるだけなので飛ばす

react.production.min.js
/**
 * @license React
 * react.production.min.js
 *
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
'use strict';var l=Symbol.for("react.element"),n=Symbol.for("react.portal"),p=Symbol.for("react.fragment"),q=Symbol.for("react.strict_mode"),r=Symbol.for("react.profiler"),t=Symbol.for("react.provider"),u=Symbol.for("react.context"),v=Symbol.for("react.forward_ref"),w=Symbol.for("react.suspense"),x=Symbol.for("react.memo"),y=Symbol.for("react.lazy"),z=Symbol.iterator;function A(a){if(null===a||"object"!==typeof a)return null;a=z&&a[z]||a["@@iterator"];return"function"===typeof a?a:null}
var B={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},C=Object.assign,D={};function E(a,b,e){this.props=a;this.context=b;this.refs=D;this.updater=e||B}E.prototype.isReactComponent={};
E.prototype.setState=function(a,b){if("object"!==typeof a&&"function"!==typeof a&&null!=a)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,a,b,"setState")};E.prototype.forceUpdate=function(a){this.updater.enqueueForceUpdate(this,a,"forceUpdate")};function F(){}F.prototype=E.prototype;function G(a,b,e){this.props=a;this.context=b;this.refs=D;this.updater=e||B}var H=G.prototype=new F;
H.constructor=G;C(H,E.prototype);H.isPureReactComponent=!0;var I=Array.isArray,J=Object.prototype.hasOwnProperty,K={current:null},L={key:!0,ref:!0,__self:!0,__source:!0};
function M(a,b,e){var d,c={},k=null,h=null;if(null!=b)for(d in void 0!==b.ref&&(h=b.ref),void 0!==b.key&&(k=""+b.key),b)J.call(b,d)&&!L.hasOwnProperty(d)&&(c[d]=b[d]);var g=arguments.length-2;if(1===g)c.children=e;else if(1<g){for(var f=Array(g),m=0;m<g;m++)f[m]=arguments[m+2];c.children=f}if(a&&a.defaultProps)for(d in g=a.defaultProps,g)void 0===c[d]&&(c[d]=g[d]);return{$$typeof:l,type:a,key:k,ref:h,props:c,_owner:K.current}}
function N(a,b){return{$$typeof:l,type:a.type,key:b,ref:a.ref,props:a.props,_owner:a._owner}}function O(a){return"object"===typeof a&&null!==a&&a.$$typeof===l}function escape(a){var b={"=":"=0",":":"=2"};return"$"+a.replace(/[=:]/g,function(a){return b[a]})}var P=/\/+/g;function Q(a,b){return"object"===typeof a&&null!==a&&null!=a.key?escape(""+a.key):b.toString(36)}
function R(a,b,e,d,c){var k=typeof a;if("undefined"===k||"boolean"===k)a=null;var h=!1;if(null===a)h=!0;else switch(k){case "string":case "number":h=!0;break;case "object":switch(a.$$typeof){case l:case n:h=!0}}if(h)return h=a,c=c(h),a=""===d?"."+Q(h,0):d,I(c)?(e="",null!=a&&(e=a.replace(P,"$&/")+"/"),R(c,b,e,"",function(a){return a})):null!=c&&(O(c)&&(c=N(c,e+(!c.key||h&&h.key===c.key?"":(""+c.key).replace(P,"$&/")+"/")+a)),b.push(c)),1;h=0;d=""===d?".":d+":";if(I(a))for(var g=0;g<a.length;g++){k=
a[g];var f=d+Q(k,g);h+=R(k,b,e,f,c)}else if(f=A(a),"function"===typeof f)for(a=f.call(a),g=0;!(k=a.next()).done;)k=k.value,f=d+Q(k,g++),h+=R(k,b,e,f,c);else if("object"===k)throw b=String(a),Error("Objects are not valid as a React child (found: "+("[object Object]"===b?"object with keys {"+Object.keys(a).join(", ")+"}":b)+"). If you meant to render a collection of children, use an array instead.");return h}
function S(a,b,e){if(null==a)return a;var d=[],c=0;R(a,d,"","",function(a){return b.call(e,a,c++)});return d}function T(a){if(-1===a._status){var b=a._result;b=b();b.then(function(b){if(0===a._status||-1===a._status)a._status=1,a._result=b},function(b){if(0===a._status||-1===a._status)a._status=2,a._result=b});-1===a._status&&(a._status=0,a._result=b)}if(1===a._status)return a._result.default;throw a._result;}
var U={current:null},V={transition:null},W={ReactCurrentDispatcher:U,ReactCurrentBatchConfig:V,ReactCurrentOwner:K};exports.Children={map:S,forEach:function(a,b,e){S(a,function(){b.apply(this,arguments)},e)},count:function(a){var b=0;S(a,function(){b++});return b},toArray:function(a){return S(a,function(a){return a})||[]},only:function(a){if(!O(a))throw Error("React.Children.only expected to receive a single React element child.");return a}};exports.Component=E;exports.Fragment=p;
exports.Profiler=r;exports.PureComponent=G;exports.StrictMode=q;exports.Suspense=w;exports.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=W;
exports.cloneElement=function(a,b,e){if(null===a||void 0===a)throw Error("React.cloneElement(...): The argument must be a React element, but you passed "+a+".");var d=C({},a.props),c=a.key,k=a.ref,h=a._owner;if(null!=b){void 0!==b.ref&&(k=b.ref,h=K.current);void 0!==b.key&&(c=""+b.key);if(a.type&&a.type.defaultProps)var g=a.type.defaultProps;for(f in b)J.call(b,f)&&!L.hasOwnProperty(f)&&(d[f]=void 0===b[f]&&void 0!==g?g[f]:b[f])}var f=arguments.length-2;if(1===f)d.children=e;else if(1<f){g=Array(f);
for(var m=0;m<f;m++)g[m]=arguments[m+2];d.children=g}return{$$typeof:l,type:a.type,key:c,ref:k,props:d,_owner:h}};exports.createContext=function(a){a={$$typeof:u,_currentValue:a,_currentValue2:a,_threadCount:0,Provider:null,Consumer:null,_defaultValue:null,_globalName:null};a.Provider={$$typeof:t,_context:a};return a.Consumer=a};exports.createElement=M;exports.createFactory=function(a){var b=M.bind(null,a);b.type=a;return b};exports.createRef=function(){return{current:null}};
exports.forwardRef=function(a){return{$$typeof:v,render:a}};exports.isValidElement=O;exports.lazy=function(a){return{$$typeof:y,_payload:{_status:-1,_result:a},_init:T}};exports.memo=function(a,b){return{$$typeof:x,type:a,compare:void 0===b?null:b}};exports.startTransition=function(a){var b=V.transition;V.transition={};try{a()}finally{V.transition=b}};exports.unstable_act=function(){throw Error("act(...) is not supported in production builds of React.");};
exports.useCallback=function(a,b){return U.current.useCallback(a,b)};exports.useContext=function(a){return U.current.useContext(a)};exports.useDebugValue=function(){};exports.useDeferredValue=function(a){return U.current.useDeferredValue(a)};exports.useEffect=function(a,b){return U.current.useEffect(a,b)};exports.useId=function(){return U.current.useId()};exports.useImperativeHandle=function(a,b,e){return U.current.useImperativeHandle(a,b,e)};
exports.useInsertionEffect=function(a,b){return U.current.useInsertionEffect(a,b)};exports.useLayoutEffect=function(a,b){return U.current.useLayoutEffect(a,b)};exports.useMemo=function(a,b){return U.current.useMemo(a,b)};exports.useReducer=function(a,b,e){return U.current.useReducer(a,b,e)};exports.useRef=function(a){return U.current.useRef(a)};exports.useState=function(a){return U.current.useState(a)};exports.useSyncExternalStore=function(a,b,e){return U.current.useSyncExternalStore(a,b,e)};
exports.useTransition=function(){return U.current.useTransition()};exports.version="18.2.0";
Yug (やぐ)Yug (やぐ)

開発環境の方は2739行あったので一部だけ抜粋。

react.development.js
function useState(initialState) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
function useEffect(create, deps) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useEffect(create, deps);
}

// ...

exports.Children = Children;
exports.Component = Component;
exports.Fragment = REACT_FRAGMENT_TYPE;
exports.Profiler = REACT_PROFILER_TYPE;
exports.PureComponent = PureComponent;
exports.StrictMode = REACT_STRICT_MODE_TYPE;
exports.Suspense = REACT_SUSPENSE_TYPE;
exports.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = ReactSharedInternals;
exports.cloneElement = cloneElement$1;
exports.createContext = createContext;
exports.createElement = createElement$1;
exports.createFactory = createFactory;
exports.createRef = createRef;
exports.forwardRef = forwardRef;
exports.isValidElement = isValidElement;
exports.lazy = lazy;
exports.memo = memo;
exports.startTransition = startTransition;
exports.unstable_act = act;
exports.useCallback = useCallback;
exports.useContext = useContext;
exports.useDebugValue = useDebugValue;
exports.useDeferredValue = useDeferredValue;
exports.useEffect = useEffect;
exports.useId = useId;
exports.useImperativeHandle = useImperativeHandle;
exports.useInsertionEffect = useInsertionEffect;
exports.useLayoutEffect = useLayoutEffect;
exports.useMemo = useMemo;
exports.useReducer = useReducer;
exports.useRef = useRef;
exports.useState = useState;
exports.useSyncExternalStore = useSyncExternalStore;
exports.useTransition = useTransition;
exports.version = ReactVersion;
Yug (やぐ)Yug (やぐ)

なるほど、おそらくこのexports.○○がReact.○○となるんだろう。
なのでReactのメソッドは35行あるということになるはず

Yug (やぐ)Yug (やぐ)

やっぱりそうだ。普通にViteでReactプロジェクト作ってあるファイルで

App.tsx
import React from "react";
console.log(React);

をすると以下が出た。


最後の__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIREDを除けばぴったり35個ある。

Yug (やぐ)Yug (やぐ)

なぜ自分のプロジェクトではまだ__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIREDなんだろう?

package.json
"dependencies": {
    "react": "^18.3.1",
},

名前が正式に変更される前にこのプロジェクトを自分が作成したからかも?調べてみよう

まず、変更があったコミットは多分これ。10月17日。React Nativeって書いてるけどまぁ多分同じでしょう。
https://github.com/facebook/react/commit/a3d9ea05bf01f3c3d7aedc2d938c581ad11fd14a

(react-nativeのリポジトリは別にあるしなぁ)
https://github.com/facebook/react-native

で、自分がこのプロジェクトを作成したのが10月12日。やっぱりコミットされる前に自分がプロジェクト作成した方が早かったのが原因か

Yug (やぐ)Yug (やぐ)

じゃあ今新しくViteでReactプロジェクトを作成したらもう名前は変わってると思われるので試してみよう。

npm create vite@latest

いや、変わってないな。

うーん、考えられる理由はこんな感じ?

  1. React Nativeでしか変更してない
  2. React 19 RCは「こういうことを正式版にリリースする予定だよ」というただの予告なのであって実際に変更されていない

Yug (やぐ)Yug (やぐ)

まぁ、とりあえず現状は把握できた。

んで__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIREDの中身を見れることがわかったので見よう。

多分これは__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADEと同じなのではないかと思われるので見ておきたい

Yug (やぐ)Yug (やぐ)

自分のViteプロジェクトで以下実行

App.tsx
import React from "react";
console.log(React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED);

(ちなみにReact.まで書いても補完で__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIREDは出てこないようになってた)

結果はこれ

Yug (やぐ)Yug (やぐ)

オブジェクトになっていて、その中に更に5つのオブジェクトがあることがわかる。

  1. ReactCurrentActQueue
  2. ReactCurrentBatchConfig
  3. ReactCurrentDispatcher
  4. ReactCurrentOwner
  5. ReactDebugCurrentFrame

それぞれ詳しく見ていくか

Yug (やぐ)Yug (やぐ)

3. ReactCurrentDispatcher

keyが1つだけある。

  1. current

そのcurrentの中に、更に18個keyがある。

  1. readContext
  2. unstable_isNewReconciler
  3. useCallback
  4. useContext
  5. useDebugValue
  6. useDeferredValue
  7. useEffect
  8. useId
  9. useImperativeHandle
  10. useInsertionEffect
  11. useLayoutEffect
  12. useMemo
  13. useMutableSource
  14. useReducer
  15. useRef
  16. useState
  17. useSyncExternalStore
  18. useTransition

フックは中にnameというkeyがあって、その値が"throwInvalidHookError"になってる。

Yug (やぐ)Yug (やぐ)

5. ReactDebugCurrentFrame

keyが3つある。

  1. getCurrentStack
  2. getStackAddendum(関数)
  3. setExtraStackFrame(関数)

こんな感じ↓で、リンクになってる青い文字を押すとChrome DevToolsが定義元に勝手に飛ばしてくれるので見に行こう

  1. getStackAddendum関数の中身(100行目)

関数内で新しいstack変数を作成・初期化し、それにcurrentExtraStackFrameとimpl()を追加してからreturnする(条件分岐によって追加しない可能性もある)

ReactDebugCurrentFrame.getStackAddendum = function() {
    var stack = "";
    if (currentExtraStackFrame) {
      stack += currentExtraStackFrame;
    }
    var impl = ReactDebugCurrentFrame.getCurrentStack;
    if (impl) {
      stack += impl() || "";
    }
    return stack;
};
  1. setExtraStackFrame関数の中身(94行目)

stackを受け取ってそれをcurrentExtraStackFrameに代入するだけ

ReactDebugCurrentFrame.setExtraStackFrame = function(stack) {
    {
    currentExtraStackFrame = stack;
    }
};

どちらも同じファイル内で宣言されており、3のsetExtraStackFrame関数の方が早く宣言されていることからも、

3のsetExtraStackFrame関数でstackをcurrentExtraStackFrameに代入
→そのcurrentExtraStackFrameを2のgetStackAddendum関数内で使っている

という流れはあるっぽい。

Yug (やぐ)Yug (やぐ)

ん-、とはいえ全く実態が掴めてないけど、まぁ覗けたことだしとりあえず__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIREDの深掘りはもういいかな。

resolveDispatcher関数の読解に戻る

Yug (やぐ)Yug (やぐ)

あーでもReactSharedInternalsがわからないとReactSharedInternals.Hが分からないなぁ

ReactHooks.js
function resolveDispatcher() {
  const dispatcher = ReactSharedInternals.H;
  if (__DEV__) {
    if (dispatcher === null) {
      console.error(
        'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
          ' one of the following reasons:\n' +
          '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
          '2. You might be breaking the Rules of Hooks\n' +
          '3. You might have more than one copy of React in the same app\n' +
          'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.',
      );
    }
  }
  // Will result in a null access error if accessed outside render phase. We
  // intentionally don't throw our own error because this is in a hot path.
  // Also helps ensure this is inlined.
  return ((dispatcher: any): Dispatcher);
}
Yug (やぐ)Yug (やぐ)

とりあえず定義は見たいんだよなぁ。定義の箇所で実質的な値が格納されていないにしても、シンプルにどんな定義されてるのかは気になる。

このプロジェクトを開発環境で立ち上げてconsole.logするっていうのはできるか?試してみる

yarn install
yarn build
yarn test
Yug (やぐ)Yug (やぐ)

npm run buildでまずエラーになるぞ。

Javaが入ってないから、ぽい

Process spawn error. Is java in the path?
spawn java ENOENT

Java Development Kit (JDK)をインストールせねばならんか・・・?

Yug (やぐ)Yug (やぐ)

いや、他のbuildオプションも用意されてるからどれか使えばいけるんじゃないか?

"scripts": {
    "build": "node ./scripts/rollup/build-all-release-channels.js",
    "build-for-devtools": "cross-env RELEASE_CHANNEL=experimental yarn build react/index,react/jsx,react/compiler-runtime,react-dom/index,react-dom/client,react-dom/unstable_testing,react-dom/test-utils,react-is,react-debug-tools,scheduler,react-test-renderer,react-refresh,react-art --type=NODE",
    "build-for-devtools-dev": "yarn build-for-devtools --type=NODE_DEV",
    "build-for-devtools-prod": "yarn build-for-devtools --type=NODE_PROD",
    "build-for-flight-dev": "cross-env RELEASE_CHANNEL=experimental node ./scripts/rollup/build.js react/index,react/jsx,react.react-server,react-dom/index,react-dom/client,react-dom/server,react-dom.react-server,react-dom-server.node,react-dom-server-legacy.node,scheduler,react-server-dom-webpack/ --type=NODE_DEV,ESM_PROD,NODE_ES2015 && mv ./build/node_modules ./build/oss-experimental",
}
Yug (やぐ)Yug (やぐ)
yarn run flow dom-node

を実行するとエラーになるんだが(runを省略してみても同じ)

errno: -4071,
code: 'EINVAL',
syscall: 'spawn'

ちなみにyarn flowに割り当てられているコマンドはこれ

"scripts": {
    "flow": "node ./scripts/tasks/flow.js",
}

つまり実質的には

node ./scripts/tasks/flow.js dom-node

を実行していることになる。

エラーの原因特定するために以下4つ理解・確認するか

  1. code: 'EINVAL'
  2. syscall: 'spawn'
  3. errno: -4071
  4. flow.jsの中身
  5. オプション(引数)としてのdom-nodeの意味
Yug (やぐ)Yug (やぐ)

1. code: 'EINVAL'

C, C++あたりの定数っぽい。

https://learn.microsoft.com/ja-jp/cpp/c-runtime-library/errno-constants?view=msvc-170#remarks

引数が無効です。 関数の引数のいずれかに無効な値が指定されています。 たとえば、( fseek の呼び出しによる) ファイル ポインターの配置時に原点に指定された値は、ファイルの先頭の前にあります。

fseekはC言語の関数らしい。とはいえ後半意味わからない。が、とにかく

関数の引数のいずれかに無効な値が指定されています

が重要っぽい。

code: 'EINVAL',

というのはエラーコードのcodeだな。

というかEINVALって何の略だ?

あー多分、全部Eで始まってる かつ タイトルはerrno、つまりErrorのEとInvalid ArgumentのINVALを合わせたのだろう。
https://learn.microsoft.com/en-us/cpp/c-runtime-library/errno-constants?view=msvc-170&redirectedfrom=MSDN

Yug (やぐ)Yug (やぐ)

2. syscall: 'spawn'

直訳:卵、産卵、産む

なるほど、nodeのメソッド(関数)か

OS のコマンドを node から実行したい場合に使われるもので、新しい子プロセスを生成してコマンドを実行するメソッドの一部だ。英語の意味合い的には、魚が産卵するときの様子を指している。

https://qiita.com/TsuyoshiUshio@github/items/cf4b28e7999403f7a04c

子プロセスを"産んで"、そやつにコマンド実行をさせる、て感じかな。

なので、spawnメソッドを使って実行したがその結果EINVALエラーが出たよ、という流れだと考えられる。

んで

syscall: 'spawn'

のsyscallはsystem call(システム呼び出し)の略。

じゃあsystem callとは?

システムコールとはOSが提供する機能をアプリケーションが利用する仕組み

https://qiita.com/sxarp/items/aff43dd83b0da69b92ce

なるほど、じゃあ今回で言うと「アプリケーションがOSの機能を利用するためにspawnメソッドを使った」みたいな感じか

Yug (やぐ)Yug (やぐ)

3. errno: -4071

なかなか情報が出てこなかったのでNode.jsのソースコード(GitHub)で検索したらこれあった

https://github.com/nodejs/node/blob/58a8eb4e1c722d319dc4f2325d9c7b9da2bd587f/deps/uv/include/uv/errno.h#L167
https://github.com/nodejs/node/blob/58a8eb4e1c722d319dc4f2325d9c7b9da2bd587f/deps/uv/include/uv/errno.h#L170

WIN32とも書いてあるから自分が使ってるPCがWindowsであることは関係してそう

つまりWindowsを使っているのであればEINVAL-4071が1対1対応している、と予想

Yug (やぐ)Yug (やぐ)

4. flow.jsの中身

flow.js
'use strict';

process.on('unhandledRejection', err => {
  throw err;
});

const chalk = require('chalk');
const runFlow = require('../flow/runFlow');
const inlinedHostConfigs = require('../shared/inlinedHostConfigs');

// This script is using `flow status` for a quick check with a server.
// Use it for local development.

const primaryRenderer = inlinedHostConfigs.find(
  info => info.isFlowTyped && info.shortName === process.argv[2]
);
if (!primaryRenderer) {
  console.log(
    'The ' +
      chalk.red('yarn flow') +
      ' command now requires you to pick a primary renderer:'
  );
  console.log();
  inlinedHostConfigs.forEach(rendererInfo => {
    if (rendererInfo.isFlowTyped) {
      console.log('  * ' + chalk.cyan('yarn flow ' + rendererInfo.shortName));
    }
  });
  console.log();
  console.log(
    'If you are not sure, run ' + chalk.green('yarn flow dom-node') + '.'
  );
  console.log(
    'This will still typecheck non-DOM packages, although less precisely.'
  );
  console.log();
  console.log('Note that checks for all renderers will run on CI.');
  console.log(
    'You can also do this locally with ' +
      chalk.cyan('yarn flow-ci') +
      ' but it will be slow.'
  );
  console.log();
  process.exit(1);
}

runFlow(primaryRenderer.shortName, ['status']);
Yug (やぐ)Yug (やぐ)

5. オプション(引数)としてのdom-nodeの意味

dom-nodeを省略してyarn flowだけ実行すると以下のようになる

んで、flow.jsを見てみると

flow.js
if (!primaryRenderer) {
  console.log(
    'The ' +
      chalk.red('yarn flow') +
      ' command now requires you to pick a primary renderer:'
  );
  // ...

という箇所がある。まったく同じ文章。ここでターミナルに出力される文章を規定しているようだ。

primaryRendererの定義を見てみるとこうなってる。

flow.js
const primaryRenderer = inlinedHostConfigs.find(
  info => info.isFlowTyped && info.shortName === process.argv[2]
);

なので、dom-nodeなどのコマンドライン引数はprimary rendererと呼ばれていることがわかる。

直訳すると「一級の(第一義的な)レンダラー」かぁ・・・わからん

まぁとりあえず、dom-nodeなどのprimary rendererをコマンドライン引数として設定しないと、

flow.js
process.exit(1);

とあるように最終的にエラーになるようになってる。

で、ちゃんとprimary rendererが設定された上でコマンドが実行された場合、そのif文には入らず以下1行のみが実行されるようになってる。

flow.js
runFlow(primaryRenderer.shortName, ['status']);
Yug (やぐ)Yug (やぐ)

なので今回dom-nodeを設定したのに発生してしまったエラーは、そのrunFlow内で起こっているということになる。

なのでrunFlowのコードを見てみる。

runFlow.js
'use strict';

const chalk = require('chalk');
const {spawn} = require('child_process');
const fs = require('fs');

// TODO: This generates all the renderer configs at once. Originally this was
// to allow the possibility of running multiple Flow processes in parallel, but
// that never happened. If we did, we'd probably do this in CI, anyway, and run
// on multiple machines. So instead we could remove this intermediate step and
// generate only the config for the specified renderer.
require('./createFlowConfigs');

async function runFlow(renderer, args) {
  return new Promise(resolve => {
    let cmd = __dirname + '/../../node_modules/.bin/flow';
    if (process.platform === 'win32') {
      cmd = cmd.replace(/\//g, '\\') + '.cmd';
    }

    // Copy renderer flowconfig file to the root of the project so that it
    // works with editor integrations. This means that the Flow config used by
    // the editor will correspond to the last renderer you checked.
    const srcPath =
      process.cwd() + '/scripts/flow/' + renderer + '/.flowconfig';
    const srcStat = fs.statSync(__dirname + '/config/flowconfig');
    const destPath = './.flowconfig';
    if (fs.existsSync(destPath)) {
      const oldConfig = String(fs.readFileSync(destPath));
      const newConfig = String(fs.readFileSync(srcPath));
      if (oldConfig !== newConfig) {
        // Use the mtime to detect if the file was manually edited. If so,
        // log an error.
        const destStat = fs.statSync(destPath);
        if (destStat.mtimeMs - srcStat.mtimeMs > 1) {
          console.error(
            chalk.red(
              'Detected manual changes to .flowconfig, which is a generated ' +
                'file. These changes have been discarded.\n\n' +
                'To change the Flow config, edit the template in ' +
                'scripts/flow/config/flowconfig. Then run this command again.\n',
            ),
          );
        }
        fs.unlinkSync(destPath);
        fs.copyFileSync(srcPath, destPath);
        // Set the mtime of the copied file to be same as the original file,
        // so that the above check works.
        fs.utimesSync(destPath, srcStat.atime, srcStat.mtime);
      }
    } else {
      fs.copyFileSync(srcPath, destPath);
      fs.utimesSync(destPath, srcStat.atime, srcStat.mtime);
    }

    console.log(
      'Running Flow on the ' + chalk.yellow(renderer) + ' renderer...',
    );

    spawn(cmd, args, {
      // Allow colors to pass through:
      stdio: 'inherit',
    }).on('close', function (code) {
      if (code !== 0) {
        console.error(
          'Flow failed for the ' + chalk.red(renderer) + ' renderer',
        );
        console.log();
        process.exit(code);
      } else {
        console.log(
          'Flow passed for the ' + chalk.green(renderer) + ' renderer',
        );
        resolve();
      }
    });
  });
}

module.exports = runFlow;
Yug (やぐ)Yug (やぐ)

まずここまでは無事通過し、正常に出ていることがわかる。

runFlow.js
console.log(
  'Running Flow on the ' + chalk.yellow(renderer) + ' renderer...',
);

つまりそれ以降の

runFlow.js
spawn(cmd, args, {
  // Allow colors to pass through:
  stdio: 'inherit',
}).on('close', function (code) {
  if (code !== 0) {
    console.error(
      'Flow failed for the ' + chalk.red(renderer) + ' renderer',
    );
    console.log();
    process.exit(code);
  } else {
    console.log(
      'Flow passed for the ' + chalk.green(renderer) + ' renderer',
    );
    resolve();
  }
});

のどこかでエラーになってる。

まず前半はどうやって通過できてるのかを1行ずつ見てみよう

自分でも実際に試して事実を述べていく

Yug (やぐ)Yug (やぐ)

まず以下でcmdはc:/~~/react\scripts\flow/../../node_modules/.bin/flowになる

runFlow.js
let cmd = __dirname + '/../../node_modules/.bin/flow';

ちなみにそのflowというファイルを見てみるとこうなってる

flow
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")

case `uname` in
    *CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac

if [ -x "$basedir/node" ]; then
  "$basedir/node"  "$basedir/../flow-bin/cli.js" "$@"
  ret=$?
else 
  node  "$basedir/../flow-bin/cli.js" "$@"
  ret=$?
fi
exit $ret
Yug (やぐ)Yug (やぐ)

runFlow.js
if (process.platform === 'win32') {
  cmd = cmd.replace(/\//g, '\\') + '.cmd';
}

process.platformはwin32なのでこのif文の中に入る。
そして正規表現によって、/\に変えてる。で最後に.cmdという拡張子をつけてる。

つまりcmdが、

react\scripts\flow/../../node_modules/.bin/flow

から

react\scripts\flow\..\..\node_modules\.bin\flow.cmd

になる。

Yug (やぐ)Yug (やぐ)

runFlow.js
const srcPath = process.cwd() + '/scripts/flow/' + renderer + '/.flowconfig';

process.cwd()の説明

つまりprocess.cwd()の返り値は

\react

になる。このReactリポジトリのルートディレクトリって感じか。

で、rendererはrunFlow()の第一引数で既に受け取っている。
改めてflow.jsを見返すとこうなってる

flow.js
runFlow(primaryRenderer.shortName, ['status']);

つまりprimaryRenderer.shortNameがrendererに入っている。

primaryRenderer.shortNameは自分が設定したコマンドライン引数、つまりdom-node

以上を踏まえると、

runFlow.js
const srcPath = process.cwd() + '/scripts/flow/' + renderer + '/.flowconfig';

の結果としてのsrcPathは

react/scripts/flow/dom-node/.flowconfig

になる。

Yug (やぐ)Yug (やぐ)

runFlow.js
const srcStat = fs.statSync(__dirname + '/config/flowconfig');

fs.statSync()の説明

https://qiita.com/tokimari/items/82222e1f99b2b9eb1fb8

つまりパスを引数に入れ、そのファイル/ディレクトリが存在するかどうかをチェックできる。

存在しなかったらエラー、存在したらそのファイル/ディレクトリの詳細情報を返す。

なので、__dirname + '/config/flowconfig'が存在するかどうかをチェックしているということ。

まず__dirname

react\scripts\flow

である。つまりrunFlow.jsの親ディレクトリである。

以上から、__dirname + '/config/flowconfig'

react\scripts\flow/config/flowconfig

であることがわかる。

このパスが存在するかどうかをfs.statSync()によってチェックしている。

そしてそのパスはちゃんと存在するので、そのファイルの詳細情報が返ってくる。これがsrcStatに格納される。

ちなみにそのflowconfigファイルの中身はこれ

flowconfig
[ignore]
.*/scripts/bench/.*
.*/build/.*
.*/fixtures/.*
# Any hidden directory
.*/\..*

# These shims are copied into external projects:
.*/rollup/shims/facebook-www/.*
.*/rollup/shims/react-native/.*

.*/node_modules/fbjs/lib/keyMirrorRecursive.js.flow
.*/node_modules/resolve/test/.*
.*/__mocks__/.*
.*/__tests__/.*

# contains modern flow syntax that requires a Flow upgrade
.*/node_modules/prettier-plugin-hermes-parser/.*

# TODO: noop should get its own inlinedHostConfig entry
.*/packages/react-noop-renderer/.*

%REACT_RENDERER_FLOW_IGNORES%

[libs]
./node_modules/fbjs/flow/lib/dev.js
./scripts/flow/environment.js
./scripts/flow/react-devtools.js
./scripts/flow/react-native-host-hooks.js
./scripts/flow/xplat.js

[lints]
untyped-type-import=error
internal-type=off
deprecated-type=off

[options]
munge_underscores=false
react.custom_jsx_typing=true

# Substituted by createFlowConfig.js:
%REACT_RENDERER_FLOW_OPTIONS%

[version]
%FLOW_VERSION%
Yug (やぐ)Yug (やぐ)

runFlow.js
const destPath = './.flowconfig';

これはそのままなので飛ばす

Yug (やぐ)Yug (やぐ)

runFlow.js
if (fs.existsSync(destPath)) {

ん?さっきの記事でこんなのを見たが、もしこれが正しいのであれば、旧式のメソッドと混合して使われてるのは妙だな

fs.existsSync(path) はv1.0.0でDeprecatedになっていたので、fs.statSync(path)を使う。

https://qiita.com/tokimari/items/82222e1f99b2b9eb1fb8

fs.existsSyncの説明

ん-なるほど、旧式(非推奨)なのかどうかは知らんがちゃんと別の役割を持ってるな。真偽値で返すようになってるのがexistsSyncか

というか旧式じゃないかも?

C:\Users\ユーザ名\node_modules\@types\node\fs.d.ts

を見てみたら、

fs.d.ts
export function existsSync(path: PathLike): boolean;

と書いてあり、deprecatedの記述は無い。

が、exists()メソッドというやつの定義を見てみると

fs.d.ts
export function exists(path: PathLike, callback: (exists: boolean) => void): void;
/** @deprecated */
export namespace exists {
    /**
     * @param path A path to a file or directory. If a URL is provided, it must use the `file:` protocol.
     * URL support is _experimental_.
     */
    function __promisify__(path: PathLike): Promise<boolean>;
}

と書いてあり、fdeprecatedつまり旧式(非推奨)っぽい。

つまり、別にfs.existsSyncはdeprecatedではないのではないだろうか。
fs.existsと間違えたのかな?それともdeprecatedではなくなったとか?

とりあえずこのコード

runFlow.js
if (fs.existsSync(destPath)) {

に戻ると、destPathつまり./.flowconfigが存在するかチェックしてる。

これの返り値はtrueになっていたが、どこにそのファイルがあるのか確認するためにVSCode上で.flowconfigと検索したらなぜか1つも出てこない。

なので以下コードを書いたら

runFlow.js
const path = require('path');
const absolutePath = path.resolve(destPath);
console.log(absolutePath);

ここにあることがわかった。

react\.flowconfig

確かにあったが、.gitignoreの影響でファイル名が薄くなっていたので、.gitignoreに入れられたファイルはVSCodeの検索に出てこなくなるのかもしれない。

とりあえず、最終的にはtrueになるのでこのif文の中に入る。

Yug (やぐ)Yug (やぐ)

runFlow.js
const oldConfig = String(fs.readFileSync(destPath));
const newConfig = String(fs.readFileSync(srcPath));

fs.readFileSync(path)はpathのファイルを読み取り、そのバッファを返す。
String()で囲めばバッファを文字列(=ファイルそのものの中身)に変換できる。

なのでoldConfigにはreact\.flowconfigの中身が入っている。
newConfigにはreact/scripts/flow/dom-node/.flowconfigの中身が入っている。

Yug (やぐ)Yug (やぐ)

runFlow.js
if (oldConfig !== newConfig) {

上の2つが同じかどうか比較するだけ。

こう書いてチェックしてみてもtrueになっている。

runFlow.js
console.log(oldConfig === newConfig);

なので、このif文の中には入らない。

Yug (やぐ)Yug (やぐ)

runFlow.js
console.log(
  'Running Flow on the ' + chalk.yellow(renderer) + ' renderer...',
);

やっとここに戻ってきた。これは前述の通り。

Yug (やぐ)Yug (やぐ)

で、残りはこれだけ。このどこかでエラーになってる。

runFlow.js
spawn(cmd, args, {
  // Allow colors to pass through:
  stdio: 'inherit',
}).on('close', function (code) {
  if (code !== 0) {
    console.error(
      'Flow failed for the ' + chalk.red(renderer) + ' renderer',
    );
    console.log();
    process.exit(code);
  } else {
    console.log(
      'Flow passed for the ' + chalk.green(renderer) + ' renderer',
    );
    resolve();
  }
});
Yug (やぐ)Yug (やぐ)

気になるのはここ

つまりrunFlow.jsからもう一度flow.jsに戻っていることがわかる。
そしてそのflow.js内の11行目でthrow err;に遭遇してしまっているようだ。

該当箇所はここ

flow.js
process.on('unhandledRejection', err => {
  throw err;
});

なので残りのrunFlow.jsの処理内に「flow.jsにまた戻っていろいろやるで」という処理があるはず。

ということでまた1行ずつ見ていく。

Yug (やぐ)Yug (やぐ)

まずこれ

runFlow.js
spawn(cmd, args, {
  // Allow colors to pass through:
  stdio: 'inherit',
})

spawnはさっき学んだ。新しい子プロセスを生成してコマンドを実行することのできるnodeのメソッド。

VSCodeで補完がでてきた。こんな感じで使うのね。

第一引数のcmdはこれ

react\scripts\flow\..\..\node_modules\.bin\flow.cmd

第二引数のargsはflow.jsでrunFlowを呼び出した際に渡してある

flow.js
runFlow(primaryRenderer.shortName, ['status']);

なのでargsは['status']

しかし第三引数の

runFlow.js
{
  // Allow colors to pass through:
  stdio: 'inherit',
}

がよくわからない。

spawnの引数は何が入るのか、Node.js公式で確認
https://nodejs.org/api/child_process.html#child_processspawncommand-args-options

んでさらに第三引数のoptionsがstdioの場合について

オプションoptions.stdioは、親プロセスと子プロセスの間に確立されるパイプを構成するために使用される。

便宜上、options.stdioには以下の文字列のいずれかを指定する:

  • 'inherit': ['inherit', 'inherit', 'inherit']または[0, 1, 2]と同じ

それ以外の場合、options.stdioの値は、各インデックスが子の fd に対応する配列である。fd 0、1、2 は、それぞれ stdin、stdout、stderr に対応する。追加の fd を指定して、親と子の間に追加のパイプを作成することもできる。値は次のいずれかである。

  • 'inherit': 親プロセスとの間で、対応するstdioストリームを渡す。最初の3つの位置では、それぞれ process.stdinprocess.stdoutprocess.stderr と等価である。それ以外の位置では、'ignore'と等価である。

https://nodejs.org/api/child_process.html#optionsstdio

うーん、全体的に意味わからんぞ。

まずfdって何だ?Claudeに聞いてみる

Yug (やぐ)Yug (やぐ)

なるほど。

fdとは"File Descriptor"(ファイルディスクリプタ)の略で、UNIXライクなシステムにおける重要な概念です。
ファイルディスクリプタは:

  1. 基本的な説明:

    • オープンされているファイルを識別するための整数値
    • プロセスがファイルやI/Oリソースにアクセスする際の「句柄」や「参照」のような役割
    • 0から始まる連番で管理される
  2. 標準的なファイルディスクリプタ:

0: 標準入力 (stdin)
1: 標準出力 (stdout)
2: 標準エラー出力 (stderr)
  1. 具体例:
// Node.jsでの例
const { spawn } = require('child_process');

// stdio配列の各要素が、順番にfd 0, 1, 2に対応
spawn('some-command', [], {
  stdio: [
    0,      // stdin  (fd: 0)
    1,      // stdout (fd: 1)
    2       // stderr (fd: 2)
  ]
});

// これは以下と同じ
spawn('some-command', [], {
  stdio: 'inherit'  // ['inherit', 'inherit', 'inherit']と同等
});
  1. 実際のファイルシステムでの表現:
  • Unixシステムでは /dev/fd/ ディレクトリ下に数字のファイルとして存在
  • 例:/dev/fd/0は標準入力を表す
Yug (やぐ)Yug (やぐ)

ほうほう、やはり0~2のfdは既に決まってるのね

つまり「0」から「2」までのファイルディスクリプタは

0:標準入力
1:標準出力
2:標準エラー出力

と決まっています。
パソコンさんが動き出した時点で割り当てられるのです。

よって、自分で開いたファイルには「3」以降の数字が割り当てられます。
また、標準入力や標準出力、標準エラー出力に何かしたいときは「0」から「2」のいずれかを指定することになります。

https://wa3.i-3-i.info/word14383.html

Yug (やぐ)Yug (やぐ)

Claudeに公式ドキュメントを読ませたうえで聞いてみる

stdioオプションについて、わかりやすく説明させていただきます。

このオプションは、親プロセス(実行元のNode.jsプログラム)と子プロセス(spawn()で起動されたプロセス)の間での入出力(I/O)の接続方法を制御するものです。

stdio: 'inherit'と指定した場合の動作を具体的に説明すると:

  1. 子プロセスの標準入力(stdin)、標準出力(stdout)、標準エラー出力(stderr)が、親プロセスの対応するストリームに直接つながります
  2. つまり:
  • 子プロセスの出力が直接ターミナルに表示される
  • 子プロセスのカラー出力などの制御文字がそのまま維持される
  • ターミナルからの入力が直接子プロセスに渡される

簡単な例を示します:

const { spawn } = require('child_process');

// stdio: 'inherit' の場合
const process1 = spawn('ls', ['-la'], { stdio: 'inherit' });
// → ls の出力が直接ターミナルに表示される

// stdio: 'pipe' (デフォルト) の場合
const process2 = spawn('ls', ['-la']);
process2.stdout.on('data', (data) => {
 console.log(data.toString());
});
// → 出力をイベントとして受け取り、手動で処理する必要がある

主な設定値の使い分けは:

  • 'inherit': 子プロセスの出力をそのままターミナルに表示したい場合(ビルドツールやCLIツールの実行など)
  • 'pipe': 子プロセスの出力を親プロセスで加工・制御したい場合
  • 'ignore': 子プロセスの出力を完全に無視したい場合
  • 'ipc': 親子プロセス間でメッセージを送受信したい場合

特に'inherit'は、子プロセスの出力をそのまま表示したい場合によく使用され、ビルドツールやCLIツールを実行する際によく使われます。

なるほどなぁ

Yug (やぐ)Yug (やぐ)

こういう情報が欲しかった

  • 子プロセスの出力が直接ターミナルに表示される
  • 子プロセスのカラー出力などの制御文字がそのまま維持される
  • ターミナルからの入力が直接子プロセスに渡される

確かに今試したらその通りだった。

test.js
const process1 = spawn('ls', ['-la'], { stdio: 'inherit' });  // 出力される
const process2 = spawn('ls', ['-la']);  // 出力されない

つまり「親プロセスの標準入出力環境(stdio)を子プロセスが継承(inherit)するよ、だから子プロセスから直接ターミナルに出力したりできるようになるよ」ということか。

Yug (やぐ)Yug (やぐ)

だからこの// Allow colors to pass through:というコメントの意味も、「子プロセス内でこれから書くchalk.red()とかで設定する色もちゃんとターミナルまで通る(pass through)ようにするよ、'inherit'にすることによって」という感じだろう

runFlow.js
spawn(cmd, args, {
  // Allow colors to pass through:
  stdio: 'inherit',
})

この部分は理解

Yug (やぐ)Yug (やぐ)

runFlow.js
}).on('close', function (code) {

'close'イベントについて。プロセスの処理が全部終わったら実行される

'close'イベントは、プロセスが終了し、子プロセスの stdioストリームがクローズされた後に発行される。複数のプロセスが同じ stdio ストリームを共有する可能性があるため、 'exit' イベントとは区別される。'close' イベントは、すでに 'exit' が発行された後に発行される。 子プロセスがスポーンに失敗した場合は 'error' が発行される。

https://nodejs.org/api/child_process.html#event-close

んで、引数のcodeには正常終了したら0、エラーなら0以外が入る。

If a callback function is provided, it is called with the arguments (error, stdout, stderr). On success, error will be null. On error, error will be an instance of Error. The error.code property will be the exit code of the process. By convention, any exit code other than 0 indicates an error. error.signal will be the signal that terminated the process.

https://nodejs.org/api/child_process.html#child_processexeccommand-options-callback

If the process times out or has a non-zero exit code, this method will throw an Error that will include the full result of the underlying child_process.spawnSync().

https://nodejs.org/api/child_process.html#child_processexecfilesyncfile-args-options

code引数がエラーになって0以外の値が入る...じゃあどんな種類があるの?と気になったのでGPTにちょろっと聞いてみた)

ということでまとめると、このコードは「全部処理が終了したらこの中のコードを実行しまっせ」という意味だな

Yug (やぐ)Yug (やぐ)

次(最後)
closeイベントの中のコードはこれ。これで最後。

runFlow.js
if (code !== 0) {
  console.error(
    'Flow failed for the ' + chalk.red(renderer) + ' renderer',
  );
  console.log();
  process.exit(code);
} else {
  console.log(
    'Flow passed for the ' + chalk.green(renderer) + ' renderer',
  );
  resolve();
}

codeが0以外だったらエラーログ出力して、0だったら成功なので成功ログ出力して、という感じ。

だが不可解なのは、このどちらもターミナルに出力されなかったということ。

つまり'close'イベント内にすら届かず、その前段階のコマンドでエラーが発生して強制終了、みたいなことが起きているっぽい?でもエラーが起きたとしてもclose内は絶対実行されるはずなのでおかしい...。(途中でエラーがthrowされちゃうと'close'イベントには届かない仕様になってるとか?)

とりあえずそのコマンドの中身を見てみる。当然これ。

runFlow.js
spawn(cmd, args, {
  stdio: 'inherit',
})

cmdargsの中身はすでに見た通り。
cmdreact\scripts\flow\..\..\node_modules\.bin\flow.cmd
(省略系なので厳密には以下)

C:\Users\ユーザ名\Downloads\react\scripts\flow\..\..\node_modules\.bin\flow.cmd

で、args['status']

なので実質的にはこれは

C:\Users\ユーザ名\Downloads\react\scripts\flow\..\..\node_modules\.bin\flow.cmd status

というコマンドをinheritオプションで実行していることになる。

無駄な部分を省くと、

C:\Users\ユーザ名\Downloads\react\node_modules\.bin\flow.cmd status

と同じ。

なのでそのままそれを実行してみたが、エラーになった

省略しても同じ

(そのエラーのFixをCopilotが提案してくれたやつ。メモ)

あ、v0に聞いてみたら、

GitBashはUnix-likeな環境だからWindowsの標準的なパス形式(バックスラッシュや:を使用)を正しく解釈できないよ。C:/c/に変えて、バックスラッシュ(\)はスラッシュ(/)に変更すれば良いよ

と教えてくれた。なるほど。まさかのターミナルの環境が原因だったか。

Yug (やぐ)Yug (やぐ)

なのでこれに直して再度実行してみる

/c/Users/ユーザ名/Downloads/react/node_modules/.bin/flow.cmd status

よし、成功!

statusはflowに対するコマンドで、現在のプロジェクトの型チェック状態を確認することのできるものらしい。

Yug (やぐ)Yug (やぐ)

GitBashじゃなくてPowerShellとかで実行すべきだったか...

ちなみにreact\node_modules\.bin\flow.cmdの中身はこれ

flow.cmd
@IF EXIST "%~dp0\node.exe" (
  "%~dp0\node.exe"  "%~dp0\..\flow-bin\cli.js" %*
) ELSE (
  @SETLOCAL
  @SET PATHEXT=%PATHEXT:;.JS;=;%
  node  "%~dp0\..\flow-bin\cli.js" %*
)
Yug (やぐ)Yug (やぐ)

powershellで実行しなおせばやっと作業再開できそう、ということでやってみる

yarn flow dom-node

エラーが出る

→これらの記事通りSet-ExecutionPolicy RemoteSignedを実行すればいけそう。
https://labor.ewigleere.net/2020/06/04/yarn-powershell/
https://qiita.com/yslius/items/a28a7b581420209bd084
https://qiita.com/kztktr/items/eab640cc46aea560c499

実行するには管理者権限でpowershellを開かないといけなかったのでこれ実行して開く
(現在のディレクトリを維持したまま新しい管理者権限のPowerShellウィンドウを開くコマンド)

Start-Process powershell -Verb RunAs -ArgumentList "-NoExit", "-Command", "Set-Location -Path '$PWD'"

で再度yarn flow dom-nodeを実行してみる、これでいけるんじゃないか?

→まさかの同じエラー

errno: -4071,
code: 'EINVAL',
syscall: 'spawn'
Yug (やぐ)Yug (やぐ)

ん-、であればやはりさっきのflow.cmdファイルの中身を理解する必要があるか

だがその前に、あえてyarn flow dom-nodeより先の手順から実行してみるか。それで解決する可能性もあるので。(無理だったらflow.cmdを読解する)

つまりVSCodeの拡張機能であるFlow Language Supportをインストールするところから。
(久しぶりにこの記事戻ってきたな)
https://zenn.dev/mox692/articles/7d56f54e514262#3.-vscode側の設定

気になるのは、yarn flow dom-nodeのところの説明で

上記手順後に node_modules/.bin/flow が生成されますが、plugin側がこれを使います。

と書いてあるのでやはりyarn flow dom-nodeを実行しないとダメか...と一瞬思ったが、探して見ると既にreact\node_modules\.bin\flowファイルはあった。

flow
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")

case `uname` in
    *CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac

if [ -x "$basedir/node" ]; then
  "$basedir/node"  "$basedir/../flow-bin/cli.js" "$@"
  ret=$?
else 
  node  "$basedir/../flow-bin/cli.js" "$@"
  ret=$?
fi
exit $ret

さっきGitBashで

/c/Users/ユーザ名/Downloads/react/node_modules/.bin/flow.cmd status

の実行が成功したから、それで作れたのか?と思ったが、そういう訳でもない。ファイルのプロパティ見てみたら最初からこのflowファイルは存在する。

...謎だ・・・。記事の内容が古いのが理由か?

まぁ、とりあえずflowファイルがあるのはありがたいので、次の手順に進めるのではないかという期待は高まる。やってみよう

Yug (やぐ)Yug (やぐ)

C:\Users\ユーザ名\AppData\Roaming\Code\User\settings.jsonに以下を追加

settings.json
"flow.pathToFlow": "/c/Users/ユーザ名/Downloads/react/node_modules/.bin/flow",
"flow.useNPMPackagedFlow": true,
"javascript.validate.enable": false
Yug (やぐ)Yug (やぐ)

で、

コードジャンプ等のエディタの機能が効くはずです!

とあるのでお目当てのreact\packages\shared\ReactSharedInternals.js__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADEで試してみたら、定義元に飛べた!やっとディグを再開できそうだ!


yarn flow dom-nodeを実行できなかったのに、なぜ結果的にうまくいったかはわからんが...。

ゴリ押しでそのままコマンドを実行させることはできたのでそれが功を奏したか?

しかし別にそれによってflowファイルが作られた訳でも無さそうだし、意味は無いんじゃないかとは思うが...

そしてそもそもなぜyarn flow dom-nodeでエラーが出るのかもわからんが...powershellターミナルに変えてもspawnの内部で実行されるターミナルはpowershellになっていないみたいなことが起きてしまっているのか?

そういえばflow / flow.cmdファイルの中身を理解してないので(軽く生成AIに聞いてはみた)、そこらへんに原因があるか?
でもコマンド直打ちでは成功してる時点でコマンドの内容に原因がある訳ではないはず。

つまり自分(人間)がターミナルから直接実行すると成功するのに、同じコマンドをspawnが実行すると失敗するという事象が起きている・・・。

まぁ、結果としてはなぜか上手くいって、このまま進めそうなのでまず進んでみよう。

(元はと言えばこのReactリポジトリで開発環境を立ち上げてconsole.log(secret_○○)とかをすることで中身を見てやろうというのが目的だったが、そもそもコードジャンプが機能するようになったので、もうその必要性はなくなった)


ReactSharedInternals.js
import * as React from 'react';

const ReactSharedInternals =
  React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;

export default ReactSharedInternals;

定義元はreact\packages\react\index.jsで、中身はこれ

index.js
export {
  __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
  // ...
} from './src/ReactClient';
Yug (やぐ)Yug (やぐ)

./src/ReactClient.jsに行くとこうなってる

ReactClient.js
import ReactSharedInternals from './ReactSharedInternalsClient';
//...
export {
  // ...
  ReactSharedInternals as __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
  // ...
};
Yug (やぐ)Yug (やぐ)

./ReactSharedInternalsClient.jsに行くとこうなってる

ReactSharedInternalsClient.js
import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';
import type {AsyncDispatcher} from 'react-reconciler/src/ReactInternalTypes';
import type {BatchConfigTransition} from 'react-reconciler/src/ReactFiberTracingMarkerComponent';

export type SharedStateClient = {
  H: null | Dispatcher, // ReactCurrentDispatcher for Hooks
  A: null | AsyncDispatcher, // ReactCurrentCache for Cache
  T: null | BatchConfigTransition, // ReactCurrentBatchConfig for Transitions
  S: null | ((BatchConfigTransition, mixed) => void), // onStartTransitionFinish

  // DEV-only

  // ReactCurrentActQueue
  actQueue: null | Array<RendererTask>,

  // Used to reproduce behavior of `batchedUpdates` in legacy mode.
  isBatchingLegacy: boolean,
  didScheduleLegacyUpdate: boolean,

  // Tracks whether something called `use` during the current batch of work.
  // Determines whether we should yield to microtasks to unwrap already resolved
  // promises without suspending.
  didUsePromise: boolean,

  // Track first uncaught error within this act
  thrownErrors: Array<mixed>,

  // ReactDebugCurrentFrame
  getCurrentStack: null | (() => string),
};

export type RendererTask = boolean => RendererTask | null;

const ReactSharedInternals: SharedStateClient = ({
  H: null,
  A: null,
  T: null,
  S: null,
}: any);

if (__DEV__) {
  ReactSharedInternals.actQueue = null;
  ReactSharedInternals.isBatchingLegacy = false;
  ReactSharedInternals.didScheduleLegacyUpdate = false;
  ReactSharedInternals.didUsePromise = false;
  ReactSharedInternals.thrownErrors = [];
  // Stack implementation injected by the current renderer.
  ReactSharedInternals.getCurrentStack = (null: null | (() => string));
}

export default ReactSharedInternals;
Yug (やぐ)Yug (やぐ)

やっと実態が見えてきたぞ。あとはこのHが何か理解できれば進める。

しかし変なことしてるもんだな~~。

これはつまり
ReactSharedInternals
としてexport

__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE
としてexport

ReactSharedInternals
として受け取る

...ということをしてる訳だな。

なんでそんな無駄にも思えることをしてるんだろう。まぁ一旦無視で