Kloner i nod_moduler fick du ner? Ta bort ditt filsystem

(14 mars 2019)

Hur många kopior av [email protected] skulle du gillar? (Foto av 浮萍 闪电 Unsplash )

UPPDATERING: implementeringen som beskrivs nedan har problem med filer som har beroenden (require / import uttalanden ). På grund av strukturen för node\_modules/ kan en identisk fil som bor på två platser dra in olika versioner av sina egna beroenden, så för att vara extra säker bör du begränsa dig till att deduplicera filer med noll beroenden.

En av problemen vi träffade på Tradle i flera stora Node.js / React Native / webbapp-projekt är att node\_modules slutar med dubbla kopior av vissa beroenden. När appen startar, tolkas den, körs och slösar sedan bort minne, samma modul flera gånger.

I vanliga gamla Node.js finns det vanligtvis inte tillräckligt stor prestationsstraff för att gnälla om, men det gör fortfarande ont att veta att det händer. På mobil / webb är gnällande obligatoriskt. Vid ett tillfälle tror jag att vi hade 10 exemplar av en version av readable-stream i vår React Native-app, och jag är inte bra på emoji så jag låter Wat säga det åt mig:

Wat lär Chaucer engelska

Vill du känna smärtan? Här är ett exempel på beroendestruktur som orsakar problemet:

# assume for simplicity that deps specify exact versions, i.e.:
# myProject"s package.json specifies "lodash": "4.0.0"
# depA and depB"s package.json specifies "lodash": "3.0.0"myProject/
node\_modules/
lodash # 4.0.0
depA
node\_modules/lodash # 3.0.0
depB
node\_modules/lodash # 3.0.0 <- not cool

Ditt projekt beror på [email protected], men två av dina beroenden beror på [email protected]. Din pakethanterare kan inte deduplicera [email protected] genom att lyfta den upp till roten eftersom [email protected] sitter där. Så du hamnar med två kopior av [email protected], en under depA och en under depB . De är identiska. Och det finns inget du kan göra åt det, såvida du inte har tid att skicka PR till författarna till depA och depB för att uppgradera dem till [email protected] (PR som kan ta månader innan de slås ihop) och vänta tills de slås samman. Det eller underhåll gafflarna för båda dessa moduler. Så du löser några andra problem.

Jag vet inte hur han visste det hela långt tillbaka då

Grattis! Du har nu skjutit upp dedupliceringsproblemet till din bundler. Webpack, Browserify och Metro måste nu var och en lösa det oberoende. Här är öppet nummer på Webpack. Det har varit öppet sedan 2017, och om jag vore Webpack skulle jag bli irriterad över människor som försökte göra det till mitt problem.

Det här problemet är inte nytt och har tålmodigt snickrat från sidan som de många inkarnationer av pakethanterare och bundlare för Node.js och webbläsaren som paraderats av. Det kommer förmodligen att finnas kvar en stund till, som en rejäl digital kackerlacka.

Lyckligtvis, med de senaste skattesänkningarna, har vi nu råd att tända ljuset i slutet av tunneln. (Men bara för de rika).

Ljuset i slutet av tunneln

För att beskära identiska kopior av beroenden i Tradles React Native-apppaket, bestämde vi oss för att inte försöka lägga till support till Metro Packager eller vädja till Metro Packager-teamet. Vi skickar också vår app med React Native for Web, som buntar med Webpack, och vi ville ha ett trick som skulle fungera för båda.

Vi bestämde oss för att rikta in det som alla pakethanterare och bundlers har gemensamt: Node.js resolve() algoritm. Detta är algoritmen som körs när du kör require("some-module") för att hitta den absoluta sökvägen till en modul i ditt filsystem. Både pakethanterare och bundlers är beroende av denna algoritm, den första som bygger katalogen node\_modules, den andra som korsar den för att bygga paketet.

Lösningen är enkelt: korsa node\_modules, identifiera alla dubbletter, välj en av dem som kanonisk (t.ex. den första alfabetiskt) och skriv om *.js -filerna alla andra att peka på den kanoniska. *.js -filerna i de dubbla mapparna ser ut så här:

// deduped by dedupe-deps
module.exports = require("../../../changes-feed/node\_modules/readable-stream/duplex.js")

Detta känns lite smutsigt, men det fungerar och gör vårt paket mindre och därmed snabbare att ladda. Modulen är öppen källkod, prova gärna: https://github.com/tradle/dedupe-deps

Det måste finnas ett bättre sätt

Vissa av er rullar förmodligen ögonen nu och skriker ”använd bara pnpm!”Var försiktig där, ögonrullning kan vara beroendeframkallande. Och du vill inte veta vad de föreskriver dig för att ta kanten av ögonrullande tillbakadragande.

pnpm är en relativt ny förpackare i stad. Den använder en kombination av hårda länkar och symlänkar för att garantera att en modul bara finns en gång i ett givet projekts node\_modules. Faktum är att det garanterar att det bara finns en gång i hela ditt filsystem! Det här är väldigt coolt. Någon dag kommer vi alla att använda pnpm och allt kommer magiskt att fungera, men jag har en känsla av att det idag fortfarande kommer att finnas kompatibilitetsproblem med vissa bundlers.

... Japp, jag har just kollat ​​och det är fortfarande ett problem i Metro, även om det ser ut som om de stänger in på en lösning: https://github.com/facebook/metro/ pull / 257

Att ta det ett steg längre

Så tillbaka till vår filsystembaserade lösning. Vi kan ta saker ett steg längre och döda fler fåglar med samma sten. Förmodligen är dessa ännu större fåglar än den ursprungliga. Låt oss föreställa oss att vi har detta beroendeträd:

myProject/
node\_modules/
lodash # 4.0.0
depA
node\_modules/lodash # 3.0.5
depB
node\_modules/lodash # 3.0.0 <- not cool

Här har vi två olika versioner av [email protected]. Beroende på semver intervall som anges för lodash i depA och depB, de kan mycket väl vara deduperbara. För närvarande tar dedupe-deps -modulen inte upp detta scenario, men vi accepterar gärna PR.