-
Notifications
You must be signed in to change notification settings - Fork 214
Typescript middleware proposal #1422
Comments
Thank you for this! I think this is a good place to start a conversation. First up, what happens if we do not use the |
Without the typescript package, babel is still able to convert BUT: Also for some reason the |
From some of the hints which appeared recently in #1129 from an unrelated conversation, I've managed to get a more modular implementation. The main remaining problem is the order in which things must be applied; it seems that some parts must be used before other modules, and some parts after, so this isn't very practical yet. Is there an API for deferring configuration until after the other plugins have been loaded? const merge = require('deepmerge');
module.exports = {
typescriptBefore: () => (neutrino) => {
const { extensions } = neutrino.options;
const index = extensions.indexOf('js');
extensions.splice(index, 0, 'ts', 'tsx');
neutrino.options.extensions = extensions;
},
typescriptAfter: () => (neutrino) => {
neutrino.config.module.rule('compile').use('babel').tap((options) => {
options.presets.push(['@babel/preset-typescript', {}]);
options.plugins.push(['@babel/plugin-proposal-class-properties', {}]);
options.plugins.push(['@babel/plugin-proposal-object-rest-spread', {}]);
return options;
});
const lintRule = neutrino.config.module.rule('lint');
if (lintRule) {
lintRule.use('eslint').tap((lintOptions) =>
lintOptions.useEslintrc ? lintOptions : merge(lintOptions, {
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
},
plugins: ['@typescript-eslint'],
baseConfig: {
extends: [
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
],
// This part could still be part of eslint config itself
settings: {
'import/resolver': {
node: {
extensions: neutrino.options.extensions.map((ext) => `.${ext}`),
},
},
},
},
})
);
}
},
}; New usage: module.exports = {
use: [
typescriptBefore(),
airbnb(),
jest(),
node(),
typescriptAfter(),
],
}; I also realised that some additional dependencies are needed for babel to generate valid code for current platforms ( It still needs |
OK, so I've managed to make this into a single piece of middleware (well, 2 because I decided splitting linting into a separate function would make more sense) This works, but… it's super hacky. I don't really understand the intent of the abstractions in const merge = require('deepmerge');
function patchMethod(o, methodName, replacement) {
const original = o[methodName].bind(o);
o[methodName] = replacement.bind(o, original);
return o;
}
function interceptAtEnd(neutrino, interceptRuleName, interceptUseName, fn) {
let applied = false;
patchMethod(neutrino.config.module, 'rule', function(originalRule, ruleName) {
return patchMethod(originalRule(ruleName), 'use', function(originalUse, useName) {
return patchMethod(originalUse(useName), 'get', function(originalGet, getName) {
if (ruleName === interceptRuleName && useName === interceptUseName && !applied) {
applied = true;
this.tap(fn);
}
return originalGet(getName);
});
});
});
patchMethod(neutrino.config, 'toConfig', function(originalToConfig, ...args) {
if (!applied) {
applied = true;
const rule = neutrino.config.module.rule(interceptRuleName);
if (rule) {
rule.use(interceptUseName).tap(fn);
}
}
return originalToConfig(...args);
});
}
module.exports = {
typescript: () => (neutrino) => {
const { extensions } = neutrino.options;
const index = extensions.indexOf('js');
extensions.splice(index, 0, 'ts', 'tsx');
neutrino.options.extensions = extensions;
interceptAtEnd(neutrino, 'compile', 'babel', (options) => {
options.presets.push(['@babel/preset-typescript', {}]);
options.plugins.push(['@babel/plugin-proposal-class-properties', {}]);
options.plugins.push(['@babel/plugin-proposal-object-rest-spread', {}]);
return options;
});
},
typescriptLint: () => (neutrino) => {
interceptAtEnd(neutrino, 'lint', 'eslint', (options) => {
if (options.useEslintrc) {
return options;
}
return merge(options, {
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
},
plugins: ['@typescript-eslint'],
baseConfig: {
extends: [
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
],
settings: {
'import/resolver': {
node: {
extensions: neutrino.options.extensions.map((ext) => `.${ext}`),
},
},
},
},
})
});
},
}; What's going on?
What does this mean? Adding typescript requires some config before any other module is loaded (supported extensions), and some config after the main compilation module is loaded (add plugins, etc.). That's why I needed module.exports = {
use: [
typescript(),
airbnb(),
typescriptLint(),
jest(),
node(),
],
}; The Obviously I'm not advocating for this hack, but I think adding proper support for this kind of thing would benefit a lot of the existing modules here. Something like Also the intent here would be to create 2 modules: None of this removes the need for |
If anybody wants to play with this, I have created a few packages which encapsulate the modules: https://github.com/davidje13/neutrino-typescript example / playground project: https://github.com/davidje13/neutrino-typescript-example I haven't tested many situations so this is probably rather brittle. |
More testing with this has revealed that when building a library, the configuration above works fine unless you want to include generated Sadly, generating tsconfig-decl.json: {
"extends": "./tsconfig",
"compilerOptions": {
"declaration": true,
"noEmit": false,
"emitDeclarationOnly": true,
"declarationDir": "build",
"isolatedModules": false
}
} package.json build script: webpack --mode production && tsc -p tsconfig-decl.json Yes that's performing 2 separate builds; one via This is overall pretty ugly and I can't think of ways to abstract it in neutrino short of generating these files on-the-fly. When the linked bug is resolved, things will be a little easier (it won't need to be 2 separate files), but it still means more dynamic config living inside the static |
@davidje13 Thanks for the hard work. Allowed me to get some insight before tackling it in our project. Regarding the type declaration emittion, I do this by activating the typescript complier api after webpack has finished compiling. I detect webpack is done compiling using hooks. This allows to live without the special webpack hook
compiler api generate type
|
@elpddev that's a nice way of handling it. Simplifies the necessary package.json script too. I'd be interested to hear from a core maintainer whether that violates v9's philosophy of not running the tooling directly, and just configuring external tools. Since it leaves Since it moves the config into javascript land, I'm guessing a lot of those path constants could be computed? I might have a go at converting the experimental middleware I wrote to work this way if I get some time (or feel free to make a PR yourself). |
@elpddev I updated my experimental middleware (https://github.com/davidje13/neutrino-typescript) with your suggestion. It avoids hard-coded file paths by checking the neutrino options. Can you check if it works for you? I'm trying to make it work with a broad range of project types, but I only have a limited set of my own to test with. Specifically, I've found that specifying The readme has full details, but should just need |
@davidje13 I will try to test it when I can. You are correct regarding the |
There is an issue that this preset cannot work with preset-react davidje13/neutrino-typescript#1 |
Are there any updates on the current state of this proposal? I would love to use typescript with neutrino 🙂 |
I'm not actively working on Neutrino at the moment, since I no longer use it day to day for work. As such any new feature work will need to come from external contributions. For a feature like this which is quite nuanced (if only because of the half-dozen different ways one can configure a TypeScript webpack project), asking someone to open a PR is a bit of a hard ask, since there likely needs to be more discussion/agreement as to choices before a PR can be reviewed. As such I'm not really sure where this leaves us. If someone wants to write up the pros/cons of different plugins/loaders/... and champion the effort then I'll do my best to read through and assist over the coming months. Also, the conversation is now kinda split between here and #1269 perhaps we should recombine them? |
I encourage anybody who wants this functionality to try out my experimental projects which contain the suggestions in this thread, and report any issues/limitations/improvements. If it gets to the point where it supports a wide enough range of use cases and has little enough weirdness, I'll make a PR:
I'm actively using these experimental plugins in several of my own projects and some other people have used them too. Due to typescript not supporting dynamic configuration, it's not quite as simple to use as most plugins, but it's still workable. |
Because I had a lot of trouble setting up neutrinojs with preact and typescript I created an example using the repositories from @davidje13 https://github.com/relnod/neutrino-preact-typescript-example It would have been nice, if the setup were simple and and straightforward. Ideally I could shoose typescript during the project setup just like choosing preact instead of react. |
@relnod hopefully eventually these modules will be brought into core neutrino and the setup script will be extended to work with them, but for now they're still quite experimental. By coincidence, I'm also playing around with preact+typescript at the moment and I've just fixed the .tsx file handling. If you update your dependency to 1.0.11 and add |
Now on NPM! (seemed stable enough to graduate from being a GitHub dependency) I've just done a bit of reworking of TypeScript still doesn't allow dynamic configuration (and in all likelihood never will), but I added an executable This means custom configuration should now live in Examples have been updated: neutrino-typescript-example / neutrino-typescript-react-example I recall discussing auto-generating |
This isn't at the point where it could be a PR, but is more detailed than the discussion in #1269, so I wanted to create a separate discussion for it. If the maintainers disagree with this split please feel free to mark as a duplicate of 1269.
The current state of TypeScript is quite good for integration. Babel compilation is supported, and the recommended linting tool has switched from TSLint to ESLint (already available in Neutrino). Integration with Jest is also straightforward.
This configuration adds typescript support for compilation, testing with Jest, and linting on top of the AirBnB ESLint configuration. It performs type checking as a
lint
-time task, not abuild
-time task, in keeping with the recommended approach when integrating with babel.It is not yet self-contained. I would like to get some feedback on how it could be made more modular, so that it can be made into proper middleware.
.neutrinorc.js
New dependencies
@babel/preset-typescript
typescript
(technically not required for pure babel compilation, but not much use in that state)New dependencies if used with Jest
@types/jest
New dependencies if used with ESLint
@typescript-eslint/eslint-plugin
@typescript-eslint/parser
New files
tsconfig.json
There is no .js version of this config file available; see microsoft/TypeScript#25271
The use of
"strict": true
is a user choice. The other options are required. Theinclude
list should ideally come from neutrino, but this file cannot be a script.Script updates:
(
tsc
is added as a separate step, andts,tsx
must be added to the--ext
flag; see eslint/eslint#2274)Remaining integrations
airbnb-base
,react
,vue
,web
, etc. but it would be better if applied toeslint
andcompile-loader
directly in all cases. I don't know how neutrino manages merging configuration between siblings.The text was updated successfully, but these errors were encountered: