-
Notifications
You must be signed in to change notification settings - Fork 81
Description
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/lifecyclesdirectory and dynamically loads that code, and@gasket/plugin-reduxwhich 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
.jsand.tsfiles 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
.tsxfiles 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
babelortypescriptto 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
extensionsthat 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 thisextensionsconfig value. - In
@gasket/plugin-startor a new@gasket/plugin-cleanplugin:- Introduce a new
cleancommand to gasket - Inject a
cleanpackage.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-nodemonpackage which:- Hooks the
createlifecycle and generates:- A devDependency on
nodemon - A
nodemon.jsonwith useful defaults - A modified
localpackage.json script which invokesgasket localvianodemon
- A devDependency on
- Emits a
nodemonConfiglifecycle event duringcreateto enable plugins to modify the generated config
- Hooks the
- Create a
@gasket/plugin-typescriptpackage, which:- Hooks the
configurelifecycle, injecting.ts(and maybe .tsx?) as an extension. - Hooks the
createlifecycle after@gasket/plugin-nodemonand generates:- Appropriate devDependencies, like:
typescriptts-node@types/foodependencies for any packages with known external type definitions. These will be read by invoking a newtsTypeDefinitionPackageslifecycle, enabling any plugin to register their own type definitions.
- Two TypeScript config files, one for client code (
tsconfig.jsonso it's picked up by the next.js babel compiler), one for server code - Appropriate
.gitignoremodifications - A modified
localpackage.json script wherets-nodeis used to executegasketornodemon --exec ts-nodeif it's already wrapped bynodemon.
- Appropriate devDependencies, like:
- Hooks the
nodemonConfiglifecycle and adds watching of.tsfiles, except those that are likely to be client-only - Hooks the
buildlifecycle, invokingtscwith the server config file if the gasket command isn'tlocal - Hooks the
cleanlifecycle, deleting generated.jsfiles
- Hooks the