تایپاسکریپت - چگونه یک ماژول NPM با نوعهای قابل دسترسی جهانی بسازیم
کلمات کلیدی: استفاده از نوعها از ماژول TypeScript بدون وارد کردن، انتشار یک بسته تنها با نوعها، گفتن به TypeScript برای جستجوی نوعها در یک ماژول NPM.
من میخواهم یک ماژول NPM منتشر کنم که شامل نوعهایی باشد که به طور جهانی قابل دسترسی هستند، درست مانند lib.d.ts.
ماژول باید چه ساختاری داشته باشد و چگونه آن را در یک پروژه دیگر وارد کنم؟
اگر قابل مشاهده کردن نوعها به طور جهانی خیلی دشوار است، استفاده از <reference/> کافی خواهد بود، اما وقتی من امتحان کردم کار نکرد.
در پروژهای که میخواهم از انواع استفاده کنم، یک پوشه src دارم که شامل تمام کد منبع است و یک پوشه bin که شامل خروجی tsc است.
ماژولی که شامل انواع است میتواند ساختار تقریباً هر چیزی داشته باشد، واقعاً برایم مهم نیست تا زمانی که کار میکند.
تا کنون ترکیبهای زیادی را امتحان کردهام، از جمله export کردن نوعها، declare کردن نوعها، export declare کردن نوعها، قرار دادن آنها در فایل .ts یا .d.ts، جابجایی آنها در پوشه بسته داخل node_modules، import کردن آنها، <reference/> کردن آنها، قرار دادن آنها در rootDirs... اما هیچ کدام کار نکرد. و کمبود مستندات خوب در این زمینه هم کمکی نکرد.
پاسخ شماره ۱
من مجبور شدم این را برای کتابخانه لاگگیری خودم، winston-jsonl-logger، حل کنم. این کتابخانه، فضای سراسری را با یک متغیر سراسری به نام logger افزایش میدهد. من موافقم که این یکی از سختترین مشکلات در تایپاسکریپت است (اگر نه سختترین)، نه بهخاطر کمبود مستندات کافی. در این مثال، من یک کتابخانه ایجاد میکنم که از نوعهای قابل مشاهده در سطح جهانی ('script') و قابل مشاهده در ماژول ('module') استفاده میکند. برای روشن شدن این که اصطلاحات رسمی چیست:
در تایپاسکریپت، همانند ECMAScript 2015، هر فایلی که شامل یک
importیاexportدر سطح بالا باشد، به عنوان یک ماژول در نظر گرفته میشود. برعکس، فایلی که هیچimportیاexportدر سطح بالا نداشته باشد، به عنوان یک اسکریپت در نظر گرفته میشود که محتوای آن در فضای نام سراسری در دسترس است (و بنابراین برای ماژولها نیز قابل دسترسی است).
ساختار دایرکتوری
پوشه src من به dist تبدیل میشود. test از تبدیل صرفنظر شده است.
ضروری است که تایپهای شما با نام index.d.ts نامگذاری شوند و در پوشهای قرار گیرند که نام آن همان نام پروژه شما است (که به طور دقیق احتمالاً همان نام مشخص شده در package.json است). این همان ساختاری است که typeRoots به دنبال آن خواهد بود.
.
├── README.md
├── dist
│ ├── Logger.d.ts
│ ├── Logger.js
│ ├── Logger.js.map
│ ├── initLoggers.d.ts
│ ├── initLoggers.js
│ └── initLoggers.js.map
├── package-lock.json
├── package.json
├── src
│ ├── Logger.ts
│ └── initLoggers.ts
├── test
│ └── index.ts
├── tsconfig.json
└── typings
└── winston-jsonl-logger
└── index.d.ts
تایپهای 'script'
تایپهای اسکریپت آنهایی هستند که فاقد import یا export در سطح بالا هستند. آنها در سراسر پروژههایی که از آنها استفاده میکنند، بهطور جهانی قابل مشاهده خواهند بود.
البته، از آنجا که آنها نمیتوانند از دستورات import در سطح بالا استفاده کنند، در میزان توصیفپذیری محدود هستند؛ شما اغلب ممکن است ببینید که از مقدار زیادی any در اینجا استفاده شده است. این مشکلی است که من در سؤال خودم سعی دارم آن را حل کنم.
// typings/index.d.ts
declare namespace NodeJS {
export interface Global {
logger?: any;
log?: any;
logInfo?: any;
}
}
اگر از logger در محدودهی سراسری استفاده کنید، اکنون به عنوان any نوعگذاری خواهد شد.
تایپینگهای 'module'
تایپهای ماژول میتوانند از import یا export در سطح بالا استفاده کنند، اما فقط زمانی دیده میشوند که ماژول به پروژه وارد شود. یعنی آنها به طور جهانی در سراسر پروژه قابل مشاهده نیستند.
// initLoggers.ts
import {Logger} from "./Logger";
import {LogEntry, Logger as WinstonLogger} from "winston";
// Now we can be more descriptive about the global typings
declare global {
const logger: Logger;
// LogEntry's interface: { level: string, message: string, data?: any }
function log(entry: LogEntry): WinstonLogger;
function logInfo(message: string, data?: any): WinstonLogger;
}
export function initLoggers(){
global.logger = new Logger();
global.log = logger.log.bind(logger);
global.logInfo = (message: string, data?: any) => {
return logger.log({ level: "info", message, data });
}
}
اگر از logger در حوزهٔ سراسری استفاده کنید، هنوز هم به صورت any تایپ خواهد شد، اما حداقل global.logger تایپبندی صحیح خواهد داشت.
برای اطمینان از اینکه این نوعها در سراسر پروژه شما my-project قابل مشاهده باشند، مطمئن شوید که my-project این فایل را از winston-jsonl-logger وارد میکند؛ من این کار را در نقطه ورود برنامهام انجام میدهم.
package.json
من از فیلد typings یا types استفاده نکردم (شاید مشخص کردن "typings": "typings/winston-jsonl-logger/index.d.ts" به این معنی بود که بستهها مجبور به اعلام صریح مسیر تایپینگهای من نباشند؛ نمیدانم)، اما مطمئن شدم که پوشه تایپینگهایم را توزیع کنم.
{
"name": "winston-jsonl-logger",
"version": "0.5.3",
"description": "TypeScript JSONL logger.",
"main": "dist/Logger.js",
"files": [
"dist",
"typings"
],
"devDependencies": {
"@types/logform": "1.2.0",
"@types/node": ">=9.6.21",
"ts-node": "7.0.1",
"typescript": "3.1.1"
},
"dependencies": {
"winston": "3.2.0",
"winston-daily-rotate-file": "3.6.0",
"winston-elasticsearch": "0.7.4"
}
}
فیلدهای حذف شده: repository، keywords، author، license، homepage، publishConfig و scripts؛ در غیر این صورت، این همه چیز است.
tsconfig.json
برای خود کتابخانه
هیچ چیز خاصی نیست. فقط پیشفرضهای استاندارد tsc --init شما.
برای پروژههایی که از کتابخانه استفاده میکنند
فقط مطمئن شوید که یک typeRoots اضافه میکنید که شبیه این باشد:
{
"compilerOptions": {
// ...All your current fields, but also:
"typeRoots": [
"node_modules/@types",
"node_modules/winston-jsonl-logger/typings/winston-jsonl-logger"
]
}
}
اگر از ts-node استفاده میکنید
در اینجا مشکلات بیشتری وجود دارد. به طور پیشفرض، ts-node تایپینگهای اسکریپت را نادیده میگیرد و تنها فرزندان واردات سطح ورودی را وارد میکند (دلیل این کار سرعت/بهرهوری است). میتوانید آن را مجبور کنید تا واردات را دقیقاً مانند tsc حل کند با تنظیم متغیر محیطی: TS_NODE_FILES=true. بله، این باعث میشود که تستها کندتر اجرا شوند، اما از طرف دیگر، به طور کلی کار خواهد کرد.
اگر از ts-node از طریق خط فرمان استفاده میکنید، متغیر محیطی TS_NODE_FILES را برابر با true تعریف کنید. من همچنین مجبور شدم TS_NODE_CACHE را برابر با false تعریف کنم، به دلیل یک اشکال غیرقابل توضیح در کش ts-node (نسخه 7.0.1 - ممکن است هنوز مسئله باشد) هنگام حل واردات/وابستگیها.
TS_NODE_FILES="true" TS_NODE_CACHE="false" TS_NODE_PROJECT="./tsconfigs/base.json" /usr/bin/nodejs --require ts-node/register --inspect=127.0.0.1:9231 src/index.ts --myCustomArg="hello"
من معمولاً از ts-node استفاده میکنم چون با Mocha تست میکنم. اینگونه میتوانم متغیرهای محیطی را از Mocha به ts-node منتقل کنم:
// mocha.env.js
/* From: https://github.com/mochajs/mocha/issues/185#issuecomment-321566188
* Via mocha.opts, add `--require mocha.env` in order to easily set up environment variables for tests.
*
* This can theoretically be made into a TypeScript file instead, but it seemed to not set the env variable when I tried;
* perhaps it failed to respect the order of the --require declarations. */
process.env.TS_NODE_FILES = "true"; // Force ts-node to use TypeScript module resolution in order to implictly crawl ambient d.ts files
process.env.TS_NODE_CACHE = "false"; // If anything ever goes wrong with module resolution, it's usually the cache; set to false for production, or upon any errors!
امیدوارم این کمک کند!
پاسخ شماره ۲
مشابه دیگر پاسخدهندگان اینجا، من هم زمان زیادی را صرف تلاش برای درست انجام دادن این کار کردهام 😓. مورد استفاده من کمی متفاوت است و من یک روش جایگزین پیدا کردهام بر اساس آنچه دیدهام سایر کتابخانهها انجام میدهند.
ارسال میکنم در صورتی که برای کسی دیگر مفید باشد.
هدف من کمی متفاوت از هدف نویسندهٔ اصلی است: من میخواستم نوعهای رابط سراسری را منتشر کنم تا کاربران نهایی کتابخانهام بتوانند هر زمان که نوعها را مینویسند به راحتی به آنها دسترسی داشته باشند، اما من قصد ندارم window یا global را افزایش دهم، فقط میخواهم نوعهای محیطی سراسری را منتشر کنم همانطور که React این کار را میکند (مثلاً نیازی نیست React را وارد کنید تا از React.ComponentType هنگام نوشتن نوعها استفاده کنید).
اینطور انجامش دادم:
- نوعهای سراسری که میخواهید منتشر کنید را در یک فایل نوعهای محیطی ایجاد کنید. من فایل خودم را
ambient.d.tsمینامم و آن را در ریشه پوشه پروژهام قرار میدهم. این فایل قرار است همراه با پوشهdistمنتشر شود. - یک نقطه ورود جدید برای تایپها ایجاد کنید (
/index.d.ts) که نقطه ورود کامپایلشده شما را دوباره صادر میکند (./dist/index.d.ts) و در آن یک ارجاع سهخطی (/// <reference types="./ambient" />) به فایل تایپهای محیطی خود داشته باشید. همچنین اطمینان حاصل کنید که گزینهincludeدرtsconfig.jsonشاملambient.d.tsباشد. - وقتی کتابخانه خود را منتشر میکنید، به کاربران خود بگویید که یک فایل
.d.tsدر ریشه پوشه پروژهشان ایجاد کنند که شامل یک مرجع سهخطی به کتابخانه شما باشد. بسیاری از استارتهایcreate-*-appاز قبل این فایل را ایجاد میکنند. برای مثال،create-next-appیک فایلnext-env.d.tsایجاد میکند که قبلاً شامل مراجع سهخطی است. شما میتوانید به کاربران خود بگویید که آن را توسعه دهند.
ساختار دایرکتوری:
.
├── README.md
├── package-lock.json
├── package.json
├── src
│ ├── index.ts
│ └── initLoggers.ts
├── dist
│ ├── index.d.ts <-- your compiled index.d.ts file
│ ├── index.js
│ ├── index.js.map
│ ├── foo.d.ts
│ ├── foo.js
├── ambient.d.ts <-- write global types here
├── index.d.ts <-- new types entry point
├── tsconfig.json
شما package.json، ambient.d.ts، index.d.ts و همه چیز داخل dist را منتشر میکنید.
/package.json:
{
// ...
"types": "./index.d.ts", // specify the new entry types entry-point
// ...
}
/index.d.ts و tsconfig.json (مرحله ۲):
// this is the new entry-point for your types
// use the triple-slash reference to bring in your ambient types
/// <reference types="./ambient" />
// re-export your compiled types
export * from './dist';
{
"compilerOptions": { /* ... */ },
"include": ["./src", "./ambient.d.ts"]
}
برای کاربران نهایی شما (مرحله ۳):
به آنها بگویید که یک فایل blah.d.ts ایجاد کنند و یک ارجاع سهخطی به کتابخانه خود اضافه کنند. همانطور که در بالا گفته شد، next.js از قبل این فایل را دارد و نام آن next-env.d.ts است. شما میتوانید به کاربران خود بگویید که آن را توسعه دهند یا یک فایل جدید *.d.ts ایجاد کنند.
/// <reference types="next" />
/// <reference types="next/types/global" />
// 👇👇👇
/// <reference types="your-published-lib-name" />
// 👆👆👆
👋 بهطور جایگزین، همانطور که پاسخهای دیگر پیشنهاد دادند، میتوانید به کاربران خود بگویید کتابخانه شما را به گزینههای کامپایلر
typeRootsدرtsconfig.jsonاضافه کنند، اما من ترجیح میدهم از ارجاع سهخطی استفاده کنم چون گزینه کامپایلر پیشفرض را تغییر نمیدهد و همان کاری است که کتابخانههای دیگر مثل next.js انجام میدهند.
