gasket
gasket copied to clipboard
Proposal: TypeScript support for gasket servers
Problem Statement
TypeScript enthusiasts like me want as much of our code to be written in TypeScript as possible. As a user of @gasket/preset-nextjs
, I enjoy the fact that the next.js integration gives me the ability to author some of my code in TypeScript (React components for example), but it's frustrating that any code compatible with SSR (/store.js
) or pure gasket artifacts /lifecycles
cannot be authored in TypeScript without some hackery or degradation in developer experience (DX).
Technical Challenges
There are many technical challenges to supporting TypeScript source code in gasket applications:
- Gasket, being a framework engine, needs to remain as tiny as possible and mandate as few technology choices as possible. This includes TypeScript. We therefore would prefer not to have Gasket automatically support TypeScript out of the box.
- Developers find the presence of compilation artifacts residing alongside their source code to be a nuisance and a potential source of bugs if these artifacts are not up-to-date. Though they may be receptive to having to compile prior to a deployment, they do not wish to have to dodge extraneous file artifacts in their workspace or rely on starting a watching builder to keep things current.
- However, node-loadable source files are a must for packages like
@gasket/plugin-lifecycles
, which lists the files in a/lifecycles
directory and dynamically loads that code, and@gasket/plugin-redux
which needs to discover a redux store factory.
Another tangential thing that developers may desire is the ability to automatically restart gasket servers when code changes. This avoids those confusing moments where you're tearing your hair out wondering why the code is not behaving as written because you forgot to restart.
Current Workarounds
Although TypeScript outside of next.js-built components is not officially supported, some approaches have been employed to support it within some projects, falling into two broad categories. Here are the challenges with these workarounds:
Building TypeScript files before running gasket
Running tsc
before invoking the gasket CLI causes generated .js
files to be placed alongside .ts
files so that they're discoverable by Gasket. This works, but:
- It is annoying to developers who now have to navigate both
.js
and.ts
files in their source files. - For next.js apps, care has to be taken to make sure the TypeScript compilation doesn't conflict with the built-in TS support used when generating webpack artifacts. The ideal compiled output for a browser versus node differs, so you end up wanting different configurations for those two targets.
- If you'd like your Gasket server to be restarted on source files changes, you have to take care that only code for the server side does this since next.js already has hot module reloading for client-side artifacts, and you wouldn't want changes to React component
.tsx
files to provoke server restarts.
Registering a module loader for TypeScript files
Another workaround involves registering a module loader for .ts
files, using techniques like @babel/register
or ts-node/register
early in the source code of a gasket app, such as in gasket.config.js
. This has some challenges:
- It requires heavy toolchains like
babel
ortypescript
to be deployed alongside an app's dependencies since they are now used at runtime instead of just build time. - Although it addresses issues with importing source code, every package, that dynamically loads source code, such as
@gasket/plugin-lifecycle
, needs to know to look for other file extensions like.ts
. - Source changes to server code not provoking restarts is also not addressed.
Proposed Solution
- Add a gasket config property like
extensions
that is an array of which file extensions gasket plugins should look for when dynamically loading source files (or can we read this fromModule
?). This should have a reasonable default for typical node development. - Update plugins that do code discovery, like
@gasket/plugin-lifecycle
, to use thisextensions
config value. - In
@gasket/plugin-start
or a new@gasket/plugin-clean
plugin:- Introduce a new
clean
command to gasket - Inject a
clean
package.json script that invokesgasket clean
- Introduce a new
- Add clean hooks for any plugin that generates build artifacts, like
@godaddy/gasket-plugin-intl
- Create a
@gasket/plugin-nodemon
package which:- Hooks the
create
lifecycle and generates:- A devDependency on
nodemon
- A
nodemon.json
with useful defaults - A modified
local
package.json script which invokesgasket local
vianodemon
- A devDependency on
- Emits a
nodemonConfig
lifecycle event duringcreate
to enable plugins to modify the generated config
- Hooks the
- Create a
@gasket/plugin-typescript
package, which:- Hooks the
configure
lifecycle, injecting.ts
(and maybe .tsx?) as an extension. - Hooks the
create
lifecycle after@gasket/plugin-nodemon
and generates:- Appropriate devDependencies, like:
-
typescript
-
ts-node
-
@types/foo
dependencies for any packages with known external type definitions. These will be read by invoking a newtsTypeDefinitionPackages
lifecycle, enabling any plugin to register their own type definitions.
-
- Two TypeScript config files, one for client code (
tsconfig.json
so it's picked up by the next.js babel compiler), one for server code - Appropriate
.gitignore
modifications - A modified
local
package.json script wherets-node
is used to executegasket
ornodemon --exec ts-node
if it's already wrapped bynodemon
.
- Appropriate devDependencies, like:
- Hooks the
nodemonConfig
lifecycle and adds watching of.ts
files, except those that are likely to be client-only - Hooks the
build
lifecycle, invokingtsc
with the server config file if the gasket command isn'tlocal
- Hooks the
clean
lifecycle, deleting generated.js
files
- Hooks the
Can we add a "before everything" init method where we can compile all *.ts
files into js
before Gasket reads the config file? This would let us to write everything in TypeScript including the Gasket config.
This would be awesome