javascript - Hvordan strukturerer, konfigurerer og bundler jeg delt kode på riktig måte i en Typescript AWS-stabel monorepo?
Jeg har problemer med å forstå hvordan jeg skal konfigurere denne oppsettet slik at det bundler, bygger og kjører uten problemer. Kjernen i problemet ligger i hvordan AWS Lambda ikke kan bundte/referere til et delt bibliotek i en monorepo på riktig måte. Dette skyldes sannsynligvis hoisting og/eller symlinking.
Det er et fullstack serverløst (AWS) monorepo som bruker npm workspaces, bygget helt i TS, med React som front-end. Jeg ønsker å lage en god DX som enkelt kan feilsøkes på en lokal maskin og som distribueres via CDK. Jeg bruker Serverless Framework utelukkende for lokal feilsøking.
Glad for å dele det endelige repoet når alt fungerer! Jeg vil til og med gjerne lage en oppsummering, da jeg føler at dette er verdifullt for mange utviklere.
Jeg er ikke gift med npm workspaces, jeg ville bare at ting skulle være så enkle som mulig, og å bruke npm betyr at man ikke trenger å installere en annen pakkehåndterer. Dette er grunnen til at jeg unngikk monorepo-rammeverk som bit, rush, osv. Åpen for forslag her, men ville foretrekke absolutt minst mulig konfigurasjon for å få dette til å fungere. Vi er druknet i konfigurasjonskompleksitet for tiden.
Monorepo er strukturert slik:
my-monorepo/
├── node_modules
├── packages/
│ ├── lib/
│ │ ├── index.ts
│ │ ├── random.ts
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── lambda/
│ │ ├── test.ts
│ │ ├── package.json
│ │ └── tsconfig.json
│ └── web/
│ └── react-vite/
│ ├── App.tsx
│ ├── package.json
│ └── tsconfig.json
├── package.json
├── tsconfig.base.json
└── tsconfig.json
Jeg prøver ganske enkelt å sørge for at alle eksterne og interne npm-pakkereferanser fungerer, slik at jeg kan dele kode mellom alle prosjekter fra /lib-mappen. Derfor tester jeg med en lokal pakkefunksjon og en tredjepart, ulid.
Ideelt sett blir alt pakket automatisk, på tvers av stakken, og det er ikke en haug med byggeprosesser og omveier. Bruk av Vite og CDKs NodejsFunction-konstruksjon bør tillate det, og det er de eneste kontaktpunktene for koden i /lib-pakken.
Her er lambda/test.ts:
import { ulid } from "ulid";
import { random } from "@my-monorepo/lib";
export async function routesHandler(event: any) {
try {
return {
statusCode: 200,
// body: JSON.stringify(`${random.randomPhrase()} Lambda!`)
body: JSON.stringify(`Hello Lambda! How about a ULID? -> ${ulid()}`)
};
} catch (e) {
console.log("It's dead:", e);
}
}
Dette kjører helt fint, og skriver ut:
Compiling with Typescript...
Using local tsconfig.json - ./packages/lambda/tsconfig.json
Typescript compiled.
{
"statusCode": 200,
"body": "\"Hello Lambda! How about a ULID? -> 01HE33FGF9DE4Q6H8K1KP42KEA\""
}
Når jeg prøver å ringe fra random-modulen ved å bytte om på kropps-linjene i lambdaen ovenfor, slik:
return {
statusCode: 200,
body: JSON.stringify(`${random.randomPhrase()} Lambda!`)
//body: JSON.stringify(`Hello Lambda! How about a ULID? -> ${ulid()}`)
};
Jeg får dette:
importer * som random fra "./random"; ^^^^^^
SyntaxError: Kan ikke bruke import-setning utenfor en modul
Jeg prøvde å manuelt tsc /lib-prosjektet og endre hovedreferansen i package.json til "main": "build/index.js", og få det til å fungere lokalt for lambdaen, mens jeg feilsøkte med serverless framework.
Det hjalp meg heller ikke i det hele tatt når Lambdaen ble distribuert til AWS. Jeg får fortsatt denne feilen ved kjøring:
{
"errorType": "Runtime.ImportModuleError",
"errorMessage": "Error: Cannot find module '@my-monorepo/lib'\nRequire stack:\n- /var/task/index.js\n- /var/runtime/index.mjs",
"trace": [
"Runtime.ImportModuleError: Error: Cannot find module '@my-monorepo/lib'",
"Require stack:",
"- /var/task/index.js",
"- /var/runtime/index.mjs",
" at _loadUserApp (file:///var/runtime/index.mjs:1087:17)",
" at async UserFunction.js.module.exports.load (file:///var/runtime/index.mjs:1119:21)",
" at async start (file:///var/runtime/index.mjs:1282:23)",
" at async file:///var/runtime/index.mjs:1288:1"
]
}
Det bryter også referansen i Vite av en eller annen grunn, noe jeg helst ikke vil prøve å håndtere, siden jeg helst vil unngå å manuelt transpilere til JS før distribusjon, uansett... hvis mulig.
En annen ting jeg prøvde på, var å bare lage en direkte referanse til /lib, som fungerer overalt, men ikke er veldig "monorepo-aktig". Det fungerer lokalt og i AWS, så hvorfor ikke? Vil pakkene mine vokse i størrelse etter hvert som /lib-størrelsen og bruken øker? Jeg forventer ikke å legge mer enn delte typer og valideringsskjemaer der, samt noe grunnleggende hjelpekode.
import { random } from "../lib";
export async function routesHandler(event: any) {
try {
return {
statusCode: 200,
body: JSON.stringify(`${random.randomPhrase()} Lambda!`)
};
} catch (e) {
console.log("It's dead:", e);
}
}
Men jeg digresserer... går videre med antagelsen om at /lib bør behandles som en monorepo-pakke...
Her er CDK-stakken jeg distribuerer med:
export class LambdaStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const prototypeLambda = new NodejsFunction(this, "PrototypeLambda", {
functionName: "prototypeLambda",
handler: "routesHandler",
runtime: Runtime.NODEJS_18_X,
timeout: Duration.minutes(3),
entry: "../lambda/test.ts",
bundling: {
nodeModules: [
"@my-monorepo/lib",
"ulid"
]
}
});
}
}
Når den er distribuert, kan du se at ulid er til stede i node_modules, men biblioteket mitt er ikke:
Her er testen /lib:
lib/index.ts:
import * as random from "./random";
import * as model from "./model/model";
export {
random,
model
};
lib/random.ts:
const randomPhrase = (): string => {
const phrases = [
"Gadzooks!",
"Eureka!",
"D'oh!",
"Egads!",
"Bazinga!"
];
return phrases[Math.round(Math.random() * phrases.length)];
};
export {
randomPhrase
};
Det må være en kombinasjon av tsconfig-egenskaper som jeg tar feil av, fordi jeg kan kjøre kode fra /lib innenfor App.tsx i web/react-vite-prosjektet, det gjengir og fungerer som forventet. Her er det utdraget:
import { random } from "@my-monorepo/lib";
function App() {
return (
<div>Welcome to React...{random.randomPhrase()}</div>
)
}
export default App
Jeg ser hva jeg burde:
Her er rotkonfigurasjonstingene:
./package.json
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"packages/lambda",
"packages/web/react-vite",
"packages/lib"
],
"devDependencies": {
...
},
"dependencies": {
...
}
}
./tsconfig.base.json
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"declaration": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"composite": true
},
"exclude": ["node_modules"],
"include": [
"packages/lib/*",
"packages/lib/**/*"
]
}
./tsconfig.json
{
"references": [
{
"path": "packages/lambda"
},
{
"path": "packages/lib"
}
]
}
/lib-konfigurasjonsfiler:
lib/package.json:
{
"name": "@my-monorepo/lib",
"version": "1.0.0",
"main": "index.ts"
}
lib/tsconfig.json:
{
"extends": "../../tsconfig.base.json"
}
/lambda-konfigurasjonsfiler:
lambda/package.json
{
"name": "@my-monorepo/lambda",
"version": "1.0.0",
"main": "test.ts",
"dependencies": {
"@my-monorepo/lib": "^1.0.0",
"ulid": "^2.3.0"
}
}
lambda/tsconfig.json
{
"extends": "../../tsconfig.base.json"
}
Jeg kikket også på å bruke Lambda Layers, men konfigurasjonen for å få dette til å fungere både lokalt, hvor det kan feilsøkes i VS Code, samt ved korrekt distribusjon, er ganske smertefull.

