Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using NX libs not possible in Strapi project #49

Open
stefanbinder opened this issue Mar 29, 2022 · 9 comments
Open

Using NX libs not possible in Strapi project #49

stefanbinder opened this issue Mar 29, 2022 · 9 comments

Comments

@stefanbinder
Copy link
Contributor

Hi,
thanks for the strapi extension!

Currently it is not possible to use nx-libs in the strapi-server.
eg. I have a JS/ lib @my-nx-workspace/utils, then I can't require it like const utils = require('@my-nx-workspace/utils'); inside strapi.

Any ideas how to solve that mix of old require and new ES6 importing?
Thanks

@TriPSs
Copy link
Owner

TriPSs commented Apr 20, 2022

I don't have any active strapi project atm so i'm not quite sure, I do see that they are working on typescript support so it should then be a matter of updating that config with the aliases / extend the root one.

Maybe soon i will have a use case again for Strapi and will than also check the typescript implementation.

@TriPSs TriPSs changed the title Using NX libs not possible Using NX libs not possible in Strapi project Dec 23, 2022
@florianmrz
Copy link

I was facing this exact issue and needed to get Strapi working in the monorepo with integration for nx libraries.
This library is great and it was working wonderful until I needed to integrate nx libraries, which simply did not work for multiple reasons, mainly that Strapi heavily depends on its very specific directory structure that gets thrown overboard when also compiling libs together with the app.

I've decided to create scripts to get this to work with an nx setup.
One is responsible for serving the project locally, one is to build it and one to run the built output in production.
All of them are heavily inspired by this package, but have a few configuration changes to make this work.

More on the scripts below.


There are two key components to get Strapi to work with nx libraries:

1. Handle the new folder structure due to the compiled libraries

Before, the dist folder looked like this:

dist/
├─ src/ (server-side code)
├─ config/ (various config files)
├─ build/ (admin panel)

Now, since we include the libraries, this folder structure changes to:

dist/
├─ apps/
│  ├─ my-app/
│  │  ├─ src/ (server-side code)
│  │  ├─ config/ (various config files)
│  │  ├─ build/ (admin panel)
├─ libs/
│  ├─ my-lib/

This needs to be accounted for in various places, e.g. when resolving local plugins:

// config/plugins.ts

// ...
    'my-plugin': {
      enabled: true,
      resolve: `./${isRunningInServeMode ? '' : 'apps/my-app/'}src/plugins/my-plugin`,
    },
// ...

2. Use of tsconfig-paths

I'm making heavy use of the tsconfig-paths plugin in order to make the imports of the nx libraries map to the respective files.
It will patch the require() calls to resolve the nx library imports to their actual files. This is needed as the compiled code of Strapi will still include imports such as require('@my-project/my-lib') that need to be resolved to the correct library.

For admin panel plugins to access nx libraries, we need to include the tsconfig-paths-webpack-plugin plugin in the admin webpack configuration.

// apps/my-app/src/admin/webpack.config.js

const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');

module.exports = (config, webpack) => {
  config.resolve.plugins = [
    new TsconfigPathsPlugin({
      configFile: 'tsconfig.base.json',
      baseUrl: process.env.NODE_ENV === 'production' ? 'dist/apps/my-app' : 'apps/my-app/dist',
      extensions: ['.ts', '.js'],
    }),
  ];
  return config;
};

⚠️Important note: Make sure that the path entries in your base TS config don't use a file extension such as .ts. Because the transpiled files will end in .js, those imports will fail!

Simply omit the extension, e.g.:

// tsconfig.base.json

"@my-project/shared/example": ["libs/shared/src/example.ts"] // fails
"@my-project/shared/example": ["libs/shared/src/example"]    // works

We also need to update the tsconfig files for both the admin panel as well as the server-side one.
They need to extend our base tsconfig file that includes our paths options resolving to our libraries.

// apps/my-app/tsconfig.json

{
  // We need to extend our base tsconfig file
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "outDir": "dist",
    // Add all other compilerOptions from https://github.com/strapi/strapi/blob/main/packages/utils/typescript/tsconfigs/server.json
    // ...
  },
}
// apps/my-app/src/admin/tsconfig.json

{
  // We need to extend our base tsconfig file
  "extends": "../../../../tsconfig.base.json",
  "compilerOptions": {
    "module": "ES2020",
    // Add all other compilerOptions from https://github.com/strapi/strapi/blob/main/packages/utils/typescript/tsconfigs/admin.json
    // ...
  },
}

Scripts to serve, build and start (a production build)

To serve and build the project using the local script, I updated the project configuration:

// apps/my-app/project.json

// ...
"targets": {
  "serve": {
    "executor": "nx:run-commands",
    "options": {
      "command": "node apps/my-app/scripts/serve.js",
      "envFile": "apps/my-app/.env"
    }
  },
  "build": {
    "executor": "nx:run-commands",
    "options": {
      "command": "node apps/my-app/scripts/build.js"
    }
  }
}
// ...

Serve the Strapi project locally:

// apps/my-app/scripts/serve.js

const tsConfig = require('../../../tsconfig.base.json');
const tsConfigPaths = require('tsconfig-paths');
const strapi = require('@strapi/strapi');
const path = require('path');
const { buildAdmin } = require('@strapi/strapi/lib/commands/builders');
const tsUtils = require('@strapi/typescript-utils');

const appName = 'my-app';
const strapiRoot = path.join(__dirname, '..');
const distDirRoot = path.join(strapiRoot, 'dist');
const distDirApp = path.join(distDirRoot, 'apps', appName);

tsConfigPaths.register({
  baseUrl: distDirRoot,
  paths: tsConfig.compilerOptions.paths,
});

(async () => {
  await tsUtils.compile(strapiRoot, { watch: false });

  await buildAdmin({ forceBuild: true, buildDestDir: distDirApp, srcDir: strapiRoot });

  const app = strapi({ appDir: strapiRoot, distDir: distDirApp });
  app.start();
})();

Build it for production:

// apps/my-app/scripts/build.js

const tsConfig = require('../../../tsconfig.base.json');
const tsConfigPaths = require('tsconfig-paths');
const path = require('path');
const { buildAdmin } = require('@strapi/strapi/lib/commands/builders');
const tsUtils = require('@strapi/typescript-utils');

const appName = 'my-app';
const strapiRoot = path.join(__dirname, '..');
const distDirRoot = path.join(strapiRoot, '../../dist/apps', appName);
const distDirApp = path.join(distDirRoot, 'apps', appName);

tsConfigPaths.register({
  baseUrl: distDirRoot,
  paths: tsConfig.compilerOptions.paths,
});

(async () => {
  await tsUtils.compile(strapiRoot, {
    watch: false,
    configOptions: {
      options: {
        outDir: distDirRoot,
      },
    },
  });

  await buildAdmin({ forceBuild: true, buildDestDir: distDirApp, srcDir: strapiRoot });
})();

Run the built version in production:

// apps/my-app/scripts/start.js

const tsConfig = require('../tsconfig.base.json');
const tsConfigPaths = require('tsconfig-paths');
const strapi = require('@strapi/strapi');
const path = require('path');

const appName = 'my-app';
const strapiRoot = path.join(__dirname, '..');
const distDirRoot = strapiRoot;
const distDirApp = path.join(distDirRoot, 'apps', appName);

tsConfigPaths.register({
  baseUrl: distDirRoot,
  paths: tsConfig.compilerOptions.paths,
});

(async () => {
  const app = strapi({ appDir: strapiRoot, distDir: distDirApp });
  app.start();
})();

Running in production

A few things need to copied to the dist folder dist/apps/my-app (in my case, this is done in a Dockerfile):

  • /tsconfig.base.json -> dist/apps/my-app/tsconfig.base.json (required for the tsconfig-paths plugin to work)
  • apps/my-app/src/plugins/my-plugin/strapi-server.js -> dist/apps/my-app/apps/my-app/src/plugins/my-plugin/strapi-server.js (omitted during build)
  • apps/my-app/assets -> dist/apps/my-app/assets
  • apps/my-app/database -> dist/apps/my-app/database
  • apps/my-app/public -> dist/apps/my-app/public
  • apps/my-app/favicon.ico -> dist/apps/my-app/favicon.ico
  • apps/my-app/package.json -> dist/apps/my-app/package.json
  • A custom script that acts as the entrypoint for the app (see the script snippet start.js above)

Caveats:

  • I haven't figured out how to make the Strapi app restart upon a file being changed
  • I haven't figured out how to properly run the admin panel in watch mode

I hope this insight into my experience with setting this up is helpful to somebody else facing the same or similar issues.

@florianmrz
Copy link

I decided to throw together an example project using the scripts mentioned above: florianmrz/nx-strapi-with-libraries-example

@TriPSs I hope this is helpful to maybe get this supported through this package. Even though it was quite a few things that needed to be changed in order to get this to work, it might be feasible to enable this behind an option flag?

@TriPSs
Copy link
Owner

TriPSs commented Feb 15, 2023

@florianmrz first of all thanks for figuring all this shit out!

I already tried a bit to make it work with everything you mentioned, it then was able to build but the output become very weird (keeping the folder structure of the libs/apps) causing it not to start.

When I have some time again I will try to continue to implement/make something work with what you mentioned above so we can add proper support for it through this package.

@icastillejogomez
Copy link

I finally found the way to run strapi with the Nx libs but with the provided scripts there aren't any ways to edit the content type. Please help :(

@icastillejogomez
Copy link

icastillejogomez commented Jul 28, 2023

I found the way. Adding autoReload to true in the strapi instance the content type builder is again enable:

const tsConfig = require('../../../../tsconfig.base.json')
const tsConfigPaths = require('tsconfig-paths')
const strapi = require('@strapi/strapi')
const path = require('path')
const { buildAdmin } = require('@strapi/strapi/lib/commands/builders')
const tsUtils = require('@strapi/typescript-utils')

const appName = 'cms'
const strapiRoot = path.join(__dirname, '..')
const distDirRoot = path.join(strapiRoot, 'dist')
const distDirApp = path.join(distDirRoot, 'apps', 'bullflix', appName)

console.log({ strapiRoot, distDirRoot, distDirApp })

for (const key in tsConfig.compilerOptions.paths) {
  const pathStr = tsConfig.compilerOptions.paths[key][0]
  const newPath = path.parse(pathStr)
  const pathWithoutExtension = path.join(newPath.dir, newPath.name)

  tsConfig.compilerOptions.paths[key] = [pathWithoutExtension]
}

console.log(tsConfig.compilerOptions.paths)

tsConfigPaths.register({
  baseUrl: distDirRoot,
  paths: tsConfig.compilerOptions.paths,
})
;(async () => {
  await tsUtils.compile(strapiRoot, { watch: false })

  await buildAdmin({ forceBuild: true, buildDestDir: distDirApp, srcDir: strapiRoot })

  const app = strapi({ appDir: strapiRoot, distDir: distDirApp, autoReload: true }) // <--
  app.start()
})()

After save some content type update the process dies:

/Users/nacho/Code/bullflix/bullflix/apps/bullflix/cms/node_modules/@strapi/strapi/lib/Strapi.js:514
        process.send('reload');
                ^
TypeError: process.send is not a function
    at Strapi.reload (/Users/nacho/Code/bullflix/bullflix/apps/bullflix/cms/node_modules/@strapi/strapi/lib/Strapi.js:514:17)
    at Immediate.<anonymous> (/Users/nacho/Code/bullflix/bullflix/apps/bullflix/cms/node_modules/@strapi/plugin-content-type-builder/server/controllers/content-types.js:116:33)
    at process.processImmediate (node:internal/timers:476:21)
    at process.callbackTrampoline (node:internal/async_hooks:130:17)
Node.js v18.16.0
Warning: run-commands command "node apps/bullflix/cms/scripts/serve.js" exited with non-zero status code

I suppose this is normal because strapi use internally the node clusters feature and now the process is managed by us. Simply start again the server after crash works.

@icastillejogomez
Copy link

After some time trying to replicate the strapi develop script I found the way the server restarts automatically:

const cluster = require('cluster')
const tsConfig = require('../../../../tsconfig.base.json')
const tsConfigPaths = require('tsconfig-paths')
const strapi = require('@strapi/strapi')
const path = require('path')
const { buildAdmin } = require('@strapi/strapi/lib/commands/builders')
const tsUtils = require('@strapi/typescript-utils')

const appName = 'cms'
const strapiRoot = path.join(__dirname, '..')
const distDirRoot = path.join(strapiRoot, 'dist')
const distDirApp = path.join(distDirRoot, 'apps', 'bullflix', appName)

// Update tsConfig paths to remove .ts extensions
updateTsConfigPaths(tsConfig)

function updateTsConfigPaths(tsConfig) {
  for (const key in tsConfig.compilerOptions.paths) {
    const pathStr = tsConfig.compilerOptions.paths[key][0]
    const newPath = path.parse(pathStr)
    const pathWithoutExtension = path.join(newPath.dir, newPath.name)

    tsConfig.compilerOptions.paths[key] = [pathWithoutExtension]
  }

  tsConfigPaths.register({
    baseUrl: distDirRoot,
    paths: tsConfig.compilerOptions.paths,
  })

  return tsConfig
}

async function buildTypescript() {
  await tsUtils.compile(strapiRoot, { watch: false })
}

async function buildAdminPanel() {
  await buildAdmin({ forceBuild: true, buildDestDir: distDirApp, srcDir: strapiRoot })
}

async function main() {
  try {
    if (cluster.isPrimary) {
      return primaryProcess()
    }

    if (cluster.isWorker) {
      return workerProcess()
    }
  } catch (error) {
    console.error(error)
    process.exit(1)
  }
}

async function primaryProcess() {
  await buildTypescript()
  await buildAdminPanel()

  cluster.on('message', async (worker, message) => {
    switch (message) {
      case 'reload':
        await buildTypescript()
        console.info('The server is restarting\n')
        worker.send('kill')
        break
      case 'killed':
        cluster.fork()
        break
      case 'stop':
        process.exit(1)
        break
      default: {
        break
      }
    }
  })

  cluster.fork()
}

async function workerProcess() {
  const app = strapi({ appDir: strapiRoot, distDir: distDirApp, autoReload: true })

  process.on('message', async (message) => {
    switch (message) {
      case 'kill': {
        await app.destroy()
        process.send('killed')
        process.exit()
        break
      }
      default: {
        break
      }
      // Do nothing.
    }
  })

  app.start()
}

// Run the cluster
main().catch((error) => {
  console.error(error)
  throw error
})

@icastillejogomez
Copy link

Seeing the develop strapi script It's possible to run with watch mode. Here is the script with watch mode and restart capabilities:

const cluster = require('cluster')
const tsConfig = require('../../../../tsconfig.base.json')
const tsConfigPaths = require('tsconfig-paths')
const strapi = require('@strapi/strapi')
const chokidar = require('chokidar')
const path = require('path')
const { buildAdmin } = require('@strapi/strapi/lib/commands/builders')
const tsUtils = require('@strapi/typescript-utils')

const appName = 'cms'
const strapiRoot = path.join(__dirname, '..')
const distDirRoot = path.join(strapiRoot, 'dist')
const distDirApp = path.join(distDirRoot, 'apps', 'bullflix', appName)

console.log({
  strapiRoot,
  distDirRoot,
  distDirApp,
})

// Update tsConfig paths to remove .ts extensions
updateTsConfigPaths(tsConfig)

function updateTsConfigPaths(tsConfig) {
  for (const key in tsConfig.compilerOptions.paths) {
    const pathStr = tsConfig.compilerOptions.paths[key][0]
    const newPath = path.parse(pathStr)
    const pathWithoutExtension = path.join(newPath.dir, newPath.name)

    tsConfig.compilerOptions.paths[key] = [pathWithoutExtension]
  }

  tsConfigPaths.register({
    baseUrl: distDirRoot,
    paths: tsConfig.compilerOptions.paths,
  })

  return tsConfig
}

async function buildTypescript() {
  await tsUtils.compile(strapiRoot, { watch: false })
}

async function buildAdminPanel() {
  await buildAdmin({ forceBuild: true, buildDestDir: distDirApp, srcDir: strapiRoot })
}

function watchFileChanges({ appDir, strapiInstance }) {
  const restart = async () => {
    if (strapiInstance.reload.isWatching && !strapiInstance.reload.isReloading) {
      strapiInstance.reload.isReloading = true
      strapiInstance.reload()
    }
  }

  const watcher = chokidar.watch(appDir, {
    ignoreInitial: true,
    usePolling: false,
    ignored: [
      /(^|[/\\])\../, // dot files
      /tmp/,
      '**/src/admin/**',
      '**/src/plugins/**/admin/**',
      '**/dist/src/plugins/test/admin/**',
      '**/documentation',
      '**/documentation/**',
      '**/node_modules',
      '**/node_modules/**',
      '**/plugins.json',
      '**/build',
      '**/build/**',
      '**/index.html',
      '**/public',
      '**/public/**',
      distDirRoot,
      path.join(appDir, 'scripts', '**'),
      strapiInstance.dirs.static.public,
      path.join('/', strapiInstance.dirs.static.public, '**'),
      '**/*.db*',
      '**/exports/**',
      '**/dist/**',
    ],
  })

  watcher
    .on('add', (path) => {
      strapiInstance.log.info(`File created: ${path}`)
      restart()
    })
    .on('change', (path) => {
      strapiInstance.log.info(`File changed: ${path}`)
      restart()
    })
    .on('unlink', (path) => {
      strapiInstance.log.info(`File deleted: ${path}`)
      restart()
    })
}

async function main() {
  try {
    if (cluster.isPrimary) {
      return primaryProcess()
    }

    if (cluster.isWorker) {
      return workerProcess()
    }
  } catch (error) {
    console.error(error)
    process.exit(1)
  }
}

async function primaryProcess() {
  await buildTypescript()
  await buildAdminPanel()

  cluster.on('message', async (worker, message) => {
    switch (message) {
      case 'reload':
        await buildTypescript()
        console.info('The server is restarting\n')
        worker.send('kill')
        break
      case 'killed':
        cluster.fork()
        break
      case 'stop':
        process.exit(1)
        break
      default: {
        break
      }
    }
  })

  cluster.fork()
}

async function workerProcess() {
  const app = strapi({ appDir: strapiRoot, distDir: distDirApp, autoReload: true })

  watchFileChanges({ appDir: strapiRoot, strapiInstance: app })

  process.on('message', async (message) => {
    switch (message) {
      case 'kill': {
        await app.destroy()
        process.send('killed')
        process.exit()
        break
      }
      default: {
        break
      }
      // Do nothing.
    }
  })

  app.start()
}

// Run the cluster
main().catch((error) => {
  console.error(error)
  throw error
})

@TriPSs
Copy link
Owner

TriPSs commented Jul 31, 2023

@icastillejogomez where you able to produce a actual prod build with the scripts? The issues that I encountered was once I create a prod build it was unable to start as the build output was completely wrong.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants