@waukeshamakerspace/deploy-kit
v0.2.2
Published
The house delivery machinery for Waukesha Makerspace services: the Lambda bundler that knows why @fastify/swagger-ui must stay external, the samconfig generator, and the SPA hosting deploys
Maintainers
Readme
@waukeshamakerspace/deploy-kit
The house delivery machinery: bundling the API for Lambda, generating
samconfig.toml, and deploying the SPA's hosting.
These four commands were copied into every service by wmi-app-template, 269
lines of it, frozen at whenever the service was scaffolded. ADR-0006
moved them here for the same reason the OIDC client moved: bundle-lambda
encodes a fact that cost real debugging, and a fix to it should be a version
bump rather than N pull requests and an audit of which copies were missed.
Install
npm install --save-dev @waukeshamakerspace/deploy-kitUse
{
"scripts": {
"build:lambda": "wmi-bundle-lambda",
"deploy:setup": "wmi-gen-samconfig",
"deploy:ui-stack": "wmi-deploy-ui-stack",
"deploy:ui": "wmi-deploy-ui"
}
}| Command | What it does |
|---|---|
| wmi-bundle-lambda | Bundles src/lambda.ts into lambda-dist/ for the Lambda runtime |
| wmi-gen-samconfig | Writes samconfig.toml so the API deploy is non-interactive |
| wmi-deploy-ui-stack | Creates or updates the SPA hosting stack, wired to the API's origin |
| wmi-deploy-ui | Builds the SPA, syncs it to S3, invalidates CloudFront |
Where the app name comes from
The copied versions had __APP_NAME__ substituted in at scaffold time. Shipped
from one package, the name has to arrive at runtime, so every value resolves the
same way: an explicit argument, then the environment, then a derived default.
--app-name <name> then WMI_APP_NAME, then the app implied by the nearest
package.json name: "timewright" or "@timewright/api" both
mean timewright
--region <region> then AWS_REGION, then us-east-2
--profile <name> then AWS_PROFILE, then waukeshamakers locally and none
under CI. --profile "" forces ambient credentials.
--api-stack <name> then WMI_API_STACK, then <app>-api
--ui-stack <name> then WMI_UI_STACK, then <app>-uiSo a scaffolded service normally passes nothing: its package name already says
what it is. An unsubstituted __APP_NAME__ is refused rather than deployed as a
stack literally called __APP_NAME__-api.
wmi-bundle-lambda takes no app name at all. It works entirely on the current
package.
Why @fastify/swagger-ui is not bundled
This is the knowledge the package exists to hold in one place.
wmi-bundle-lambda bundles everything except @aws-sdk/*, which the runtime
provides, and @fastify/swagger-ui, which is installed beside the bundle at its
exact resolved version instead.
It reads its own files off disk relative to __dirname, and so does
@fastify/swagger. Bundling collapses three source directories onto one
__dirname, and the three then disagree about where they are:
| Reader | Path it wants |
|---|---|
| @fastify/swagger/lib/util/read-package-json.js | __dirname/../../package.json |
| @fastify/swagger-ui/lib/routes.js | __dirname/../static |
| @fastify/swagger-ui/index.js | __dirname/./static/logo.svg |
Two of those escape the bundle directory, so copying assets in cannot satisfy
them, and a __dirname banner shim only converts the crash into an ENOENT.
The handler throws on cold start, before serving one request, so a deploy
that built perfectly is dead on arrival.
The error you see is misleading. The throw is always the ReferenceError
for __dirname inside fastifySwaggerUi, but when the emitted bundle contains
top-level await beside the require shim, Node replaces that error's message
with module-format advice:
ReferenceError: Cannot determine intended module format because both
require() and top-level await are present. ...
at fastifySwaggerUi (index.js:51560:71)(code: ERR_AMBIGUOUS_MODULE_SYNTAX.) The stack still points at the __dirname
line. Dependencies drag top-level await in on their own, so this is the form
seen in practice, and it sends you hunting a module-format bug that is not
there.
Leaving the package external keeps its real directory layout, which is all
those paths need. It pulls in no fastify of its own, so there is no duplicate
framework copy, and --omit=peer on the install beside the bundle keeps it that
way if it ever declares one.
How this is verified
A clean build proves nothing here: esbuild is perfectly happy to emit a bundle
that throws the moment it is imported. So bundle-lambda.test.ts builds a real
fixture, loads the artifact in a node subprocess and calls the handler,
and asserts the swagger UI actually serves logo.svg off disk with a real byte
count.
It also keeps the counterfactual: the same fixture bundled without the
externalisation, asserted to fail, from fastifySwaggerUi, on __dirname. If
that ever stops failing, these packages have stopped reading __dirname, and
EXTERNAL_PACKAGES can be emptied, which would shrink every service's artifact.
The subprocess matters. Vitest's own import() runs through Vite's module
runner, which supplies the CommonJS globals Lambda does not, and under it both
the positive case and the counterfactual pass for the wrong reasons.
The cleaner fix, when someone wants it
service-kit's createApiServer registers swagger-ui with only routePrefix.
Passing baseDir and logo bypasses both __dirname reads, and this could go
back to being bundled, shrinking every service's artifact. That is a change to
service-kit, not here.
Notes
wmi-gen-samconfig refuses to overwrite an existing samconfig.toml: the file
holds a generated SessionSecret, and regenerating it signs everyone out.
Delete it deliberately to rotate. It also refuses before generating
anything when a required value is missing, so a failed run never looks like a
rotation.
wmi-deploy-ui caches hashed assets for a year and index.html not at all,
then invalidates CloudFront. Caching index.html would ship new assets behind
a stale entry point.
