چگونه فایل tsconfig.json را با استفاده از API تایپاسکریپت به یک شیء پیکربندی کامپایل کنیم؟


من به دنبال یک API رسمی و عمومی TypeScript (ترجیحاً نسخه 4.x) برای تجزیه فایلcهای tsconfig.json/jsconfig.json در اشیاء پیکربندی کامل TypeScript هستم (منظورم از "کامل" این است که تمام ویژگیcهای آن با مقادیری پر شده است، یا از it واضح و یا به طور واضح. از پیش فرض های شناخته شده)، مانند:

import ts from "typescript";

const tsconfig = await ts.parseConfigFile('./tsconfig.json');

tsconfig.compilerOptions.noEmit;
// false
// (or whatever it is set to in *.json)

آیا چنین تابعی در بسته typescript وجود دارد؟ من خودم نتوانستم آن را پیدا کنم، نه در مستندات و نه در پیشنهادهای IntelliSense (آنهایی که با Ctrl + Space فراخوانی میشوند).

من یک سؤال دیگر با همان نوع درخواست دیدهام، اما a) پاسخ داده نشده است، b) لینکی که در نظرات آمده به یک راهحل ۵.۵ ساله اشاره دارد، - باید از آن زمان تغییری در API رخ داده باشد.


در واقع، من فقط به چند ویژگی از فایل پیکربندی نیاز دارم، بنابراین نوشتن چند ابزار کمکی برای تجزیه فایل *.json و گرفتن محتوای آن نسبتاً آسان خواهد بود. با این حال:

  • موارد استفاده عجیبتری نیز وجود دارند (مانند استفاده از ارجاعات پروژه و/یا گسترش شیء پیکربندی والد)، که در آنها چنین راهحلی کار نخواهد کرد؛
  • اشیای *.json مذکور ممکن است شامل کامنتها و کاماهای انتهایی باشند، بنابراین حتی تجزیه فایل در ابتدا کار سادهای نیست؛
  • این که چیزی را دوباره بازنویسی کنیم در حالی که میدانیم قبلاً نوشته شده است، عجیب خواهد بود.

پاسخ شماره ۱

ویرایش

در یک نظر در پایین، @Hiroki Osame توضیح میدهد که با استفاده از ts.parseJsonConfigFileContent او توانست extends را به طور خودکار دنبال کند بدون هیچ «دستساز».

همچنین در این صفحه، پاسخ @Simon Buchan به نظر میرسد که به طور مشابه درست باشد.

پاسخ کوتاه

یک تابع برای خواندن گزینههای کامپایلر از یک فایل tsconfig در حالی که بهدرستی وراثت کلیدواژه extends در tsconfig را مدیریت میکند

function getCompilerOptionsJSONFollowExtends(filename: string): {[key: string]: any} {
  let compopts = {};
  const config = ts.readConfigFile(filename, ts.sys.readFile).config;
  if (config.extends) {
    const rqrpath = require.resolve(config.extends);
    compopts = getCompilerOptionsJSONFollowExtends(rqrpath);
  }
  return {
    ...compopts,
    ...config.compilerOptions,
  };
}

نتیجه آن میتواند از طریق به نوع ts.CompilerOptions تبدیل شود

const jsonCompopts = getCompilerOptionsJSONFollowExtends('tsconfig.json')
const tmp = ts.convertCompilerOptionsFromJson(jsonCompopts,'')
if (tmp.errors.length>0) throw new Error('...')
const tsCompopts:ts.CompilerOptions = tmp.options

خلاصه

این توابع مرتبط در [email  وجود دارند:

ts.readConfigFile
ts.parseConfigFileTextToJson
ts.convertCompilerOptionsFromJson
ts.parseJsonConfigFileContent
ts.parseJsonSourceFileConfigFileContent

این پست فقط به سه مورد اول میپردازد:

ts.readConfigFile

console.log(
  JSON.stringify(
    ts.readConfigFile('./tsconfig.base.json', ts.sys.readFile),
    null,
    2
  )
);

جایی که tsconfig.base.json محتوا دارد

{
  "extends": "@tsconfig/node14/tsconfig.json",
//comment
  "compilerOptions": {
    "declaration": true,
    "skipLibCheck": true,
    "sourceMap": true,
    "lib": ["es2020"],// trailing comma
  }
}

منجر به

{
  "config": {
    "extends": "@tsconfig/node14/tsconfig.json",
    "compilerOptions": {
      "declaration": true,
      "skipLibCheck": true,
      "sourceMap": true,
      "lib": [
        "es2020"
      ]
    }
  }
}

چیزهایی که باید اینجا به آنها توجه کنید:

  1. فایل پیکربندی که توسط extends ارجاع شده است، وارد نشده و گسترش نیافته است.
  2. گزینههای کامپایلر به شکل داخلی که توسط توابع API کامپایلر تایپاسکریپت مورد نیاز است تبدیل نمیشوند. (از نوع ts.CompilerOptions نیست)
  3. کامنتها حذف میشوند و ویرگولهای انتهایی نادیده گرفته میشوند.

ts.parseConfigFileTextToJson

const parsed2 = ts.parseConfigFileTextToJson(
  ''/*'./tsconfig.base.json'*/, `
  {
    "extends": "@tsconfig/node14/tsconfig.json",
    // comment
    "compilerOptions": {
      "declaration": true,
      "skipLibCheck": true,
      "sourceMap": true,
      "lib": ["es2020"], // trailing comma
    }
  }
  `);
  console.log(JSON.stringify(parsed2, null, 2));

منجر به

{
  "config": {
    "extends": "@tsconfig/node14/tsconfig.json",
    "compilerOptions": {
      "declaration": true,
      "skipLibCheck": true,
      "sourceMap": true,
      "lib": [
        "es2020"
      ]
    }
  }
}

این تابع همانند ts.readConfigFile است با این تفاوت که متن به جای نام فایل منتقل میشود.

توجه: آرگومان اول (نام فایل) نادیده گرفته میشود مگر شاید که خطایی رخ دهد. اضافه کردن یک نام فایل واقعی اما خالی گذاشتن آرگومان دوم منجر به خروجی خالی میشود. این تابع نمیتواند فایلها را بخواند.

ts.convertCompilerOptionsFromJson

  const parsed1 = ts.convertCompilerOptionsFromJson(
    {
      lib: ['es2020'],
      module: 'commonjs',
      target: 'es2020',
    },
    ''
  );
  console.log(JSON.stringify(parsed1, null, 2));

منجر به

{
  "options": {
    "lib": [
      "lib.es2020.d.ts"
    ],
    "module": 1,
    "target": 7
  },
  "errors": []
}

مقدار ویژگی options نتیجه در قالب داخلی مورد نیاز API کامپایلر تایپاسکریپت است. (یعنی از نوع ts.CompilerOptions است)

مقدار (1) از module در واقع مقدار کامپایل شده ts.ModuleKind.CommonJS است، و مقدار (7) از target در واقع مقدار کامپایل شده ts.ScriptTarget.ES2020 است.

بحث/گسترش میدهد

وقتی کلمه کلیدی extends به کار نمیرود، سپس با استفاده از توابع زیر:

  • ts.readConfigFile
  • ts.convertCompilerOptionsFromJson

همانطور که در بالا نشان داده شده است، شما باید بتوانید چیزی که میخواهید را به دست بیاورید.

با این حال، وقتی کلیدواژه extends وارد بازی میشود، وضعیت پیچیدهتر است. من هیچ تابع API موجودی برای دنبال کردن extends بهصورت خودکار پیدا نکردم.

با این حال، یک تابع CLI برای انجام این کار وجود دارد

npx tsc -p tsconfig.base.json --showConfig

منجر به

{
    "compilerOptions": {
        "lib": [
            "es2020"
        ],
        "module": "commonjs",
        "target": "es2020",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true,
        "declaration": true,
        "sourceMap": true
    },
    "files": [
        "./archive/doc-generator.ts",
        "./archive/func-params-exp.ts",
        "./archive/reprinting.ts",
        "./archive/sw.ts",
        ....
        ....
    ]
}    

جایی که تمام فایلهایی که بهطور ضمنی شامل شدهاند نیز خروجی داده میشوند.

یک خط دستور زیر در بش تنها گزینههای کامپایل را نمایش میدهد -

echo 'console.log(JSON.stringify(JSON.parse('\'`npx tsc -p tsconfig.base.json --showConfig`\'').compilerOptions,null,2))' | node

فقط در گزینههای کامپایل نتیجه میدهد

{
  "lib": [
    "es2020"
  ],
  "module": "commonjs",
  "target": "es2020",
  "strict": true,
  "esModuleInterop": true,
  "skipLibCheck": true,
  "forceConsistentCasingInFileNames": true,
  "declaration": true,
  "sourceMap": true
}

بدیهی است که فراخوانی رابط خط فرمان از یک برنامه، ایدهآل نیست.

چگونه با استفاده از API از extends پیروی کنیم

اصل را نشان دهید:

const config1 = ts.readConfigFile('./tsconfig.base.json', ts.sys.readFile).config
console.log(JSON.stringify(config1,null,2))
const tsrpath = ts.sys.resolvePath(config1.extends)
console.log(tsrpath)
const rqrpath = require.resolve(config1.extends)
console.log(rqrpath)
const config2 = ts.readConfigFile(rqrpath, ts.sys.readFile).config
console.log(JSON.stringify(config2,null,2))

منجر به

{
  "extends": "@tsconfig/node14/tsconfig.json",
  "compilerOptions": {
    "declaration": true,
    "skipLibCheck": true,
    "sourceMap": true,
    "lib": [
      "es2020"
    ]
  }
}
/mnt/common/github/tscapi/@tsconfig/node14/tsconfig.json
/mnt/common/github/tscapi/node_modules/@tsconfig/node14/tsconfig.json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "Node 14",
  "compilerOptions": {
    "lib": [
      "es2020"
    ],
    "module": "commonjs",
    "target": "es2020",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

توجه داشته باشید که require.resolve به چیزی که میخواهیم حل میشود، اما ts.sys.resolve اینطور نیست.

در اینجا یک تابع وجود دارد که گزینه کامپایلر را به درستی با ارثبری از extends بازمیگرداند:

function getCompileOptionsJSONFollowExtends(filename: string): {[key: string]: any} {
  let compopts: ts.CompilerOptions = {};
  const config = ts.readConfigFile(filename, ts.sys.readFile).config;
  if (config.extends) {
    const rqrpath = require.resolve(config.extends);
    compopts = getCompileOptionsJSONFollowExtends(rqrpath);
  }
  compopts = {
    ...compopts,
    ...config.compilerOptions,
  };
  return compopts;
}

اجرای آزمایشی -

const jsonCompopts = getCompileOptionsJSONFollowExtends('./tsconfig.base.json')
console.log(JSON.stringify(jsonCompopts,null,2))
const tsCompopts = ts.convertCompilerOptionsFromJson(jsonCompopts,'')
console.log(JSON.stringify(tsCompopts,null,2))
console.log('');

منجر به

{
  "lib": [
    "es2020"
  ],
  "module": "commonjs",
  "target": "es2020",
  "strict": true,
  "esModuleInterop": true,
  "skipLibCheck": true,
  "forceConsistentCasingInFileNames": true,
  "declaration": true,
  "sourceMap": true
}
{
  "options": {
    "lib": [
      "lib.es2020.d.ts"
    ],
    "module": 1,
    "target": 7,
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "sourceMap": true
  },
  "errors": []
}

پاسخ شماره ۲

تا حدی گیجکننده، شما باید از ts.getParsedCommandLineOfConfigFile() استفاده کنید:

> ts.getParsedCommandLineOfConfigFile('tsconfig.json', {}, ts.sys)
{
  options: {
    moduleResolution: 2,
    module: 99,
    target: 6,
    lib: [ 'lib.es2019.d.ts' ],
    types: [ 'node' ],
    strict: true,
    sourceMap: true,
    esModuleInterop: true,
    importsNotUsedAsValues: 2,
    importHelpers: true,
    incremental: true,
    composite: true,
    skipLibCheck: true,
    noEmit: true,
    configFilePath: 'C:/code/.../tsconfig.json'
  },
  watchOptions: undefined,
  fileNames: [
    'C:/code/.../src/index.tsx',
...

پارامتر سوم در واقع یک ts.ParseConfigFileHost است، بنابراین احتمالاً باید آن را به صورت دستی پیادهسازی کنید (با استفاده از پیادهسازیهایی از ts.sys)

شما همچنین میتوانید از ts.parseJsonFileContent(tsconfigContent, ts.sys, baseDir, {}, errorMessageFileName) استفاده کنید اگر قبلاً پیکربندی را تجزیه کردهاید، به عنوان مثال، وقتی که آن به صورت درونخطی در یک فایل پیکربندی بزرگتر باشد.