typescriptnpmbabeljsexponpm-workspaces

NPM workspaces with Expo and Typescript


I'm trying to use NPM 7 workspaces within a Typescript Expo project. For now I want to keep the normal Expo structure (with the root App.tsx file), but I want to isolate some parts of the code in workspaces.

I have issues to compile the TS code within the workspaces. I tried many things to configure the TS and/or Webpack configs but without success. So here is the minimal file structure to reproduce it:

package.json
tsconfig.json
App.tsx
/packages
  /core
    index.ts
    package.json

Here is the relevant part of the root ./package.json

{
  "main": "node_modules/expo/AppEntry.js",
  "workspaces": [
    "packages/*"
  ],
  "scripts": {...},
  "dependencies": {...},
  "devDependencies": {...},
  "private": true
}

The ./tsconfig.json is the bare minimum

{
  "extends": "expo/tsconfig.base",
  "compilerOptions": {
    "strict": true
  }
}

The ./packages/core/package.json is very simple as well

{
  "name": "core",
  "private": true
}

And the ./packages/core/index.ts simply exports a log() function for this example

export function log(s: string) {
  console.log(s)
}

Finally, in ./App.tsx, is simply imports the function an try calling it

import { log } from 'core'
log('hello')
//...

It doesn't compile

When I try to build for the web (with expo build:web for instance) I have the following error

āœ– Expo Webpack
  Compiled with some errors in 1.62s

Failed to compile.

[path]/node_modules/core/index.ts 1:21
Module parse failed: Unexpected token (1:21)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
> export function log(s: string) {
|   console.log(s)
| }

I'm not surprised because the /node_modules directory is willingly excluded.

So my question is what do I need to do in order to get the workspaces code compiled? I'd like it to work on the fly (ie: not precompiled) just like if it was some regular files in my project. Is it doable at all?

Some failed attempts to fix it

I mainly tried to tweak the tsconfig to get it to work (although I kind wonder if it changes anything)

#1

I tried adding "include": ["./node_modules/core"], in ./tsconfig.json. It had no effect (same error).

#2

I tried to create the following /packages/core/tsconfig.json with the composite option:

{
  "extends": "expo/tsconfig.base",
  "compilerOptions": {
    "strict": true,
    "composite": true
  }
}

And referenced it in the root tsconfig.json

{
  "extends": "expo/tsconfig.base",
  "compilerOptions": {
    "strict": true
  },
  "references": [{ "path": "./node_modules/core" }]
  // or even with:
  "references": [{ "path": "./packages/core" }]
}

It had no effect (same error).

Thanks a lot


Solution

  • I came up with a solution that I'm not super happy with but at least it's working. It's actually quite straightforward, I simply configured Webpack to compile my packages/* (as symlinks in the node_modules.

    So first I installed:

    $ npm i -D ts-loader@8 webpack@4.430
    

    Versions are important here to be consistent with the webpack version used by Expo 41.

    I also renamed my package name (in /packages/core/package.json) to something like @workspace/core so that all my custom packages are inside the node_modules/@workspace directory.

    It will also make our config simpler.

    Running $ expo customize:web is necessary to customize the webpack config. It will produce a webpack.config.js.

    It can be customised as follow:

    const createExpoWebpackConfigAsync = require('@expo/webpack-config')
    
    module.exports = async function (env, argv) {
      const config = await createExpoWebpackConfigAsync(env, argv)
    
      config.module.rules = [
        {
          // this is why I renamed my packages with the @workspaces prefix.
          test: /node_modules\/@workspaces\/(.*)\.ts$/, 
          use: [
            {
              loader: 'ts-loader',
              // force ts-loader to compile in node_modules
              options: { allowTsInNodeModules: true },
            },
          ],
        },
        ...config.module.rules,
      ]
    
      return config
    }
    
    

    I wouldn't be surprised if there was a cleaner solution. But for now I'll stick to it