Importera ES-moduler för TypeScript, Webpack och Jasmine
Bakgrund
Jag har ett paketuppsättning som jag återanvänder i mina personliga projekt med en liknande struktur.
För produktionsändamål är de skrivna i TypeScript och kompileras till JavaScript via tsc innan de publiceras på npm så att jag kan installera dem i mina andra projekt. Jag kör också tester på dem med Jasmine.
I utveckling använder jag Webpack med dess ts-loader för att kompilera TypeScript på ett sätt som låter mig använda det i exempel-skript som körs i en webbläsare. Jag brukar också använda Webpack i mina projekt där jag använder dessa paket, även om de kompilerades innan de publicerades kommer de projekten bara se dem som JavaScript och behöver inte använda ts-loader.
Jag använder ES-modulsyntax överallt så att jag bekvämt kan använda samma syntax i varje kontext istället för att oroa mig för att växla mellan CommonJS för Node och ES-modulsyntax för webbläsare. För att möjliggöra detta i Jasmine har jag i min jasmine.json-konfigurationsfil satt "jsLoader": "import".
Jag har ett problem med hur jag ska skriva mina import-satser. Särskilt hur man anger filändelser på ett sätt som fungerar för varje användningsfall som dyker upp när jag utvecklar dessa paket.
Ingen filändelse
TypeScripts Node-modullösning gör några användbara antaganden kring filändelser:
TypeScript kommer att efterlikna Node.js körningstidens upplösningsstrategi för att hitta definitionsfiler för moduler vid kompileringstid. För att uppnå detta lägger TypeScript TypeScript-källfilsändelserna (.ts, .tsx och .d.ts) ovanpå Nodes upplösningslogik.
Detta betyder att om jag använder kod såsom import { foo } from './foo'; så kommer TypeScript att förstå att leta efter en fil som heter foo.ts.
På samma sätt tillåter Webpacks resolve-konfigurationsobjekt en uppsättning filändelser att specificeras i egenskapen extensions för att tala om för Webpack hur det ska försöka lösa en import utan filändelse. Så med min konfiguration av extensions: ['.js', '.ts'] kommer det att hitta samma foo.ts-fil som TypeScript.
Men när jag försöker köra mina tester i Jasmine med import utan en filändelse kan det inte lösas.
C:\Users\mark.hanna\Documents\Code Playground\analyser>npm test
> [email test C:\Users\mark.hanna\Documents\Code Playground\analyser
> tsc && jasmine
Error: While loading C:/Users/mark.hanna/Documents/Code Playground/analyser/spec/analyser.spec.js: NodeError: Cannot find module 'C:\Users\mark.hanna\Documents\Code Playground\analyser\dist\file-processing' imported from C:\Users\mark.hanna\Documents\Code Playground\analyser\dist\analyser.js
at C:\Users\mark.hanna\Documents\Code Playground\analyser\node_modules\jasmine\lib\loader.js:22:30
at async Jasmine._loadFiles (C:\Users\mark.hanna\Documents\Code Playground\analyser\node_modules\jasmine\lib\jasmine.js:182:5)
at async Jasmine.loadSpecs (C:\Users\mark.hanna\Documents\Code Playground\analyser\node_modules\jasmine\lib\jasmine.js:173:3)
at async Jasmine.execute (C:\Users\mark.hanna\Documents\Code Playground\analyser\node_modules\jasmine\lib\jasmine.js:479:3)
npm ERR! Test failed. See above for more details.
.js-tillägg
Det andra alternativet jag har är att specificera filändelsen .js i mina importeringar, t.ex. import { foo } from './foo.js'.
Även om jag inte har hittat det tydligt dokumenterat, förstår TypeScripts modulupplösning också tydligt att ./foo.js faktiskt kan betyda ./foo.ts. Jag har hittat referenser till att detta har varit stöd sedan TypeScript 2.
Detta betyder att jag kan specificera filändelsen .js och ändå korrekt kompilera min kod med tsc. Jag kan då också korrekt köra mina Jasmine-tester, eftersom det förstår hur man gör modullösning när filändelsen är inkluderad.
Dock förstår inte Webpack att ./foo.js faktiskt kan betyda ./foo.ts. Så med detta tillvägagångssätt kan jag inte bygga min kod inom paketet när jag importerar det i skript för att visa exempel i webbläsaren.
ERROR in ./src/file-processing.ts 2:0-49
Module not found: Error: Can't resolve './AnalyserRows.js' in 'C:\Users\mark.hanna\Documents\Code Playground\analyser\src'
resolve './AnalyserRows.js' in 'C:\Users\mark.hanna\Documents\Code Playground\analyser\src'
using description file: C:\Users\mark.hanna\Documents\Code Playground\analyser\package.json (relative path: ./src)
Field 'browser' doesn't contain a valid alias configuration
using description file: C:\Users\mark.hanna\Documents\Code Playground\analyser\package.json (relative path: ./src/AnalyserRows.js)
no extension
Field 'browser' doesn't contain a valid alias configuration
C:\Users\mark.hanna\Documents\Code Playground\analyser\src\AnalyserRows.js doesn't exist
.js
Field 'browser' doesn't contain a valid alias configuration
C:\Users\mark.hanna\Documents\Code Playground\analyser\src\AnalyserRows.js.js doesn't exist
.ts
Field 'browser' doesn't contain a valid alias configuration
C:\Users\mark.hanna\Documents\Code Playground\analyser\src\AnalyserRows.js.ts doesn't exist
as directory
C:\Users\mark.hanna\Documents\Code Playground\analyser\src\AnalyserRows.js doesn't exist
@ ./src/analyser.ts 4:0-48 69:0-50
@ ./docs/assets/js/src/docs-script.ts 2:0-53 7:14-36 34:14-36 43:14-36 48:51-68 49:28-45
.js-tillägg med Webpack-alias
Den enda lösningen jag har hittat hittills är en som jag verkligen inte gillar, vilket är att använda Webpacks resolve.alias-konfiguration för att uttryckligen tala om för det att när jag säger ./foo.js menar jag ./foo.ts.
Till exempel har jag för närvarande använt detta tillvägagångssätt i mitt @cipscis/csv-projekt:
resolve: {
extensions: ['.js', '.ts'],
alias: {
'@cipscis/csv': `${srcPath}/csv.ts`,
'./stringify.js': './stringify.ts',
'./parse.js': './parse.ts',
},
},
webpack.config.js
Det här känns dock väldigt klumpigt, och jag måste ange ett alias på det sättet för varje enskild fil jag importerar, så det känns inte särskilt skalbart alls. Det känns som det måste ju finnas ett "rätt" sätt att göra detta?
Svar #1
Svar från och med 2022-10-20
Webpack v5.74.0 släpptes i juli 2022, och det inkluderar ett nytt resolve.extensionAlias-alternativ som gör det möjligt att lösa detta problem utan att kräva ett plugin.
Till exempel:
resolve: {
extensionAlias: {
'.js': ['.ts', '.js'],
},
}
Svar från och med 2021-09-07
Tack vare amosqs svar på mitt svar på en relaterad fråga, har jag nu ett svar på den här frågan.
Jasmine var lite av en villospår här, den verkliga relevansen där var bara ES-modulupplösningen den försökte göra med min setup. Kärnan i problemet var att Webpacks modulupplösning inte kan återskapa funktionaliteten i TypeScripts modulupplösningsregler, där den behandlar *.js-vägar som potentiellt pekande på *.ts-filer.
Tack vare amosqs kommentar har jag kunnat lösa detta genom att använda ett Webpack-resolver plugin: resolve-typescript-plugin
Dess instruktioner visar bara hur man använder det i CommonJS-syntax, men som jag nämnde i min fråga använder jag ES Module-syntax överallt där jag kan. För att använda det i min Webpack-konfiguration har jag gjort så här:
import resolveTypeScriptPluginModule from 'resolve-typescript-plugin';
const ResolveTypeScriptPlugin = resolveTypeScriptPluginModule.default;
const config = {
// ...
resolve: {
fullySpecified: true,
plugins: [new ResolveTypeScriptPlugin()],
// ...
},
// ...
};
Denna uppsättning har gjort det möjligt för mig att använda Webpack med ts-loader för att kompilera TypeScript-filer som använder *.js i sina importuttalanden, som en del av samma uppsättning där jag också använder tsc för att kompilera dessa filer. Jag behöver inte längre använda de Webpack-alias jag nämnde i slutet av min fråga.
