javascript - Manuellt kompilera ett TypeScript-bibliotek
Jag skapar ett TS-bibliotek vars byggprocess innebär att man parser TS-kod, genererar typer och ytterligare variabler från den i en komplex och anpassad process (dvs, inte bara genom att köra den genom ett kompilatorverktyg).
Specifikt är detta bibliotek en uppsättning OpenAPI-specifikationer för flera olika API:er. Det är skrivet i TS, och olika TS-filer inom biblioteket exporterar OpenAPI-specifikationer som i princip är stora JSON-block som kommer från en TS-fil. Sedan bearbetar jag dessa block med en manuell/skrddarsydd kompilationsprocess och gör om dem till en serie av exporter, både av endpoint-respons-typer och subtyper. Inget av detta kan autokompileras med tsc, webpack, etc, så jag försöker efterlikna utdataformen av en sådan process med ett ”manuellt” kompilationsskript som beskrivs.
Jag importerar sedan det resulterande biblioteket till en React-app som använder TypeScript och även har Jest-tester. Noterbart är att mitt bibliotek inte har en standardexport, eftersom det representerar flera API:er, och importeras istället som import { SomeAPI } from "my-lib/dist/some-api";. (Jag skulle gärna vilja bli av med /dist/, men en sak i taget.)
Jag stöter på problem med att lista ut hur jag ska strukturera min kompilerade kod så att den kan importeras och köras både av appen och appens Jest-specifikationer.
Mitt första försök var att kompilera allt till bara .ts-filer, som exporterar både variabler och typer. Detta fungerade bra för appen, men gav ett felmeddelande om att import-satsen inte kan användas utanför en modul när jag försökte importera biblioteket och köra specifikationerna i den "importerande" appen, eftersom Jest som standard inte bearbetar node_modules med Babel. Jag skulle kunna försöka få appen att transpilerar mitt bibliotek när testerna körs -- enligt Jests felmeddelande, För att få vissa av dina "node_modules"-filer transformerade kan du ange ett eget "transformIgnorePatterns" i din konfiguration. -- men det verkar fel. Borde inte biblioteket vara i ett direkt konsumtionsbart format när det är kompilerat?
Försök att åtgärda detta genom att deklarera "type": "module" i bibliotekets package.json-fil ledde sedan till att mitt anpassade kompileringsskript, som körs med ts-node, kastade ett fel om okänd filändelse ".ts". Försök att åtgärda detta genom att använda ts-nodes ESM-alternativ ledde till andra problem.
Så jag tog ett steg tillbaka och tänkte att det skulle vara bättre att dela upp mina filer i .ts-filer som använder module.exports för att exportera vanliga JS-objekt/variabler, och tillhörande .d.ts-filer som exporterar matchande typer med samma namn (efter att ha läst detta SO-inlägg).
Det fungerade inte heller, eftersom jag nu får varningar om att mina importerade filer inte är moduler, och typerna verkar inte alls erkännas av den importerande appen.
Så jag är lite ställd och ville få en känsla av vad jag borde göra här. Om jag exporterar ett bibliotek för användning i en TS-app, hur borde min export se ut?
Är det korrekt att ha .ts- och .d.ts-filer sida vid sida, där .d.ts-filerna exporterar (med export/import-syntax) typerna för de intilliggande variablerna (exporterade med modules.export-syntax)? Eller bör jag ha en parallell types-katalog som bara exporterar typerna? Om så är fallet, hur kommer den konsumerande appen att mappa typer till variabler? Bör variabelfilerna vara .js istället? Behöver jag sätta "type": "module" i min package.json? O.s.v.
Jag har försökt titta på koden i några andra TS-bibliotek, men de verkar alla göra komplexa och specialanpassade saker, som är ärligt talat svåra att förstå. (Till exempel, jag tittade på lodash, och det pågår någon konstig magi där). Finns det en grundläggande bästa praxis här?
Här är några av de relevanta filerna (avkortade) för att ge en känsla av var jag befinner mig just nu:
package.json
{
"version": "1.0.2",
"files": [
"./dist"
],
"scripts": {
"compile:openapi": "ts-node scripts/compileOpenapi.ts"
},
"engines": {
"node": "^16.14.0"
}
}
(Ovanstående kompileringsskript går i princip igenom några av mina OpenAPI-filer och använder fs för att skriva nya filer till /dist med variabler och typer härledda från OpenAPI-specifikationerna.)
tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"esModuleInterop": true,
"noEmit": true,
"module": "commonjs",
"moduleResolution": "node",
"strict": true,
"noImplicitAny": true,
// Allow for newer ES6 JS syntax (eg Set, array.find, etc)
"target": "es6",
"lib": [
// Allow for newer syntax and other variables/tools
"es6",
// Object.entries and Object.values
"es2017.object",
// Window, etc
"dom",
// array.includes
"es2016.array.include",
// Promises, Async/Await
"es2018"
]
},
"ts-node": {
"require": ["tsconfig-paths/register"]
}
}
dist/exempel.ts
module.exports.defaultExample = {
title: 'whatever',
name: 'Whatever'
};
dist/example.d.ts
export interface Document {
title: string;
name: string;
size?: number;
mimeType?: string;
data?: string;
}
export declare const defaultExample: Document;
Svar #1
Jag kom på en lösning som åtminstone fungerade för mig, genom att följa @Bergis råd ovan att använda tsc, (tsc --outDir -d dist someFile.ts) för att generera deklarationer och JS-filer, som jag sedan kunde kopiera baskonstruktionen från.
Istället för att dela med mig av min super-specifika och långa kompileringskod, tror jag att det skulle vara mer användbart att bara visa hur saker och ting slutade exporteras. Den grundläggande strukturen var parallella JS-filer, som följde en exports-exportsyntaks, och d.ts-filer som exporterade typerna (både ordagranna motsvarigheter till JS-variabler och ytterligare typer):
- index.js
- index.d.ts
- folder
|- something.js
|- something.d.ts
Alla kompilerade .js-filer hade följande överst på dem:
"use strict";
exports.__esModule = true;
Och sedan exporterade saker antingen som exports.[varName] eller exports.default. De importerade också andra moduler med hjälp av const defaultExample=require("./examples/default").default; syntaxen (eller const something=require("./examples/whatever").something;). Som i
const defaultExample = require("./examples/default").default;
"use strict";
exports.__esModule = true;
exports.someArray = ["one", "two", "three"];
exports.defaultExample = defaultExample;
Den medföljande .d.ts-filen såg då ut så här:
export declare interface ExampleInterface {
// ...
}
export declare const defaultExample: ExampleInterface;
export declare const someArray: string[];
// etc
Detta exportformat fungerar korrekt i den konsumerande appen. Variabler (t.ex. defaultExample) är tillgängliga, och typer inspekteras vid hover osv. De fungerar också smidigt med specifikationerna. Konstig hackig lösning, eftersom generellt sett kommer ett bibliotek av något slag att göra den officiella kompileringen, men åtminstone tycker jag det är hjälpsamt att veta hur TS-moduler är enkelt strukturerade för funktionell import.
