nrwl / nx-labs

A collection of Nx plugins
MIT License
138 stars 49 forks source link

generatePackageJson for remix #31

Open joeflateau opened 2 years ago

joeflateau commented 2 years ago

It would be nice if there was a remix builder that supported generatePackageJson to be able to containerize a remix app with the appropriate dependencies. Currently building a remix project runs remix build but does not generate a package.json with only the remix app's deps.

juristr commented 2 years ago

Yep, I have done this for my Remix blog migration. Have it on my list of improvements 🙂

joeflateau commented 2 years ago

awesome! thanks

juristr commented 2 years ago

Meanwhile if you want, you can follow an approach of having a local workspace plugin like I talked about here: https://youtu.be/ptpEBhHwl6Q?t=791

Note, in Nx 13.10 (soon to be released) this whole local plugin setup is going to be even better, not needing any type of precompilation. But more soon :)

And then create an executor like

import { BuildExecutorSchema } from './schema';
import { createPackageJson } from '@nrwl/workspace/src/utilities/create-package-json';
import { readCachedProjectGraph } from '@nrwl/workspace/src/core/project-graph';
import {
  ExecutorContext,
  writeJsonFile,
} from '@nrwl/devkit';
import { execSync } from 'child_process';
import { ensureDirSync } from 'fs-extra';

export default async function runExecutor(
  options: BuildExecutorSchema,
  context: ExecutorContext
) {
  ensureDirSync(options.outputPath);

  const depGraph = readCachedProjectGraph();
  const projectSrcRoot =
    context.workspace.projects[context.projectName].sourceRoot;

  const packageJson = createPackageJson(context.projectName, depGraph, {
    root: context.root,
    projectRoot: projectSrcRoot,
  });

  // delete postinstall scripts as you don't want to have it in your Docker container
  delete packageJson?.scripts?.postinstall;

  // do other stuff specific to your setup

  // write the package.json to your output path (or wherever you want) where your Docker build will package it up
  writeJsonFile(`${options.outputPath}/package.json`, packageJson);

  return {
    success: true,
  };
}

Note the above is just a rough copy of my local executor I've hacked quickly together. Once I have something more refined that works in a more generic way I'll integrate it into the Remix plugin or write a blog post on how to do it.

joeflateau commented 2 years ago

oh cool, i'm going to use this as an opportunity to figure out how these local plugins/tools/executors work

joeflateau commented 2 years ago

this worked for me, thanks so much!

import { ExecutorContext, writeJsonFile } from '@nrwl/devkit';
import { readCachedProjectGraph } from '@nrwl/workspace/src/core/project-graph';
import runScript from '@nrwl/workspace/src/executors/run-script/run-script.impl';
import { createPackageJson } from '@nrwl/workspace/src/utilities/create-package-json';
import { copy, ensureDir } from 'fs-extra';
import { BuildExecutorSchema } from './schema';

export default async function runExecutor(
  options: BuildExecutorSchema,
  context: ExecutorContext
) {
  ensureDir(options.outputPath);

  const depGraph = readCachedProjectGraph();
  const projectSrcRoot =
    context.workspace.projects[context.projectName].sourceRoot;

  const packageJson = createPackageJson(context.projectName, depGraph, {
    root: context.root,
    projectRoot: projectSrcRoot,
  });

  // delete postinstall scripts as you don't want to have it in your Docker container
  delete packageJson?.scripts?.postinstall;

  // do other stuff specific to your setup
  await runScript({ script: 'build' }, context);

  for (const subdir of ['build', 'public']) {
    await copy(
      `${projectSrcRoot}/${subdir}`,
      `${options.outputPath}/${subdir}`
    );
  }

  // write the package.json to your output path (or wherever you want) where your Docker build will package it up
  writeJsonFile(`${options.outputPath}/package.json`, packageJson);

  return {
    success: true,
  };
}
nadiTime commented 2 years ago

@joeflateau can you share your executors folder for your working solution?

tyteen4a03 commented 1 year ago

+1 - currently looking to deploy a Remix app with nx.

JoepKockelkorn commented 1 year ago

I used the above code to create my own executor which just creates a package.json based on the dep-graph with the possibility to exclude some dependencies based on a glob pattern (see excludedFilesGlob). In my case I have MSW setup in a 'mocks' folder, but don't want to include the dependencies from that code in the resulting package.json.

import path from 'path';
import { ExecutorContext, FileData, ProjectConfiguration, ProjectGraph, readCachedProjectGraph, writeJsonFile } from '@nrwl/devkit';
import { createPackageJson } from '@nrwl/workspace/src/utilities/create-package-json';
import { ensureDir } from 'fs-extra';
import partition from 'lodash/partition';
import minimatch from 'minimatch';

import { CreatePackageJsonExecutorSchema } from './schema';

export default async function (options: CreatePackageJsonExecutorSchema, context: ExecutorContext) {
    ensureDir(options.outputPath);
    const projectRoot = context.workspace.projects[context.projectName].sourceRoot;

    const depGraph = readCachedProjectGraph();
    if (options.excludedFilesGlob !== undefined) {
        filterDependenciesByExcludedFilesGlob(depGraph, context.projectName, projectRoot, options.excludedFilesGlob);
    }

    const packageJson = createPackageJson(context.projectName, depGraph, {
        root: context.root,
        projectRoot: projectRoot,
    });
    writeJsonFile(`${options.outputPath}/package.json`, packageJson);

    return { success: true };
}

function filterDependenciesByExcludedFilesGlob(
    depGraph: ProjectGraph<ProjectConfiguration>,
    projectName: string,
    projectRoot: string,
    excludedFilesGlob: string
) {
    const projectNode = depGraph.nodes[projectName];
    const [excludedFiles, includedFiles] = partition(projectNode.data.files, (f) => minimatch(f.file, path.join(projectRoot, excludedFilesGlob)));
    const possibleDepsToRemove = collectDepsFromFilesAndDedupe(excludedFiles);
    const depsToRemove = possibleDepsToRemove.filter((dep) => depNotUsedInOtherFiles(includedFiles, dep));
    depGraph.dependencies[projectName] = depGraph.dependencies[projectName].filter((dep) => !depsToRemove.includes(dep.target));
}

function depNotUsedInOtherFiles(includedFiles: FileData[], dep: string): unknown {
    return !includedFiles.some((file) => (file.deps ?? []).includes(dep));
}

function collectDepsFromFilesAndDedupe(excludedFiles: FileData[]) {
    const allDeps = excludedFiles.reduce((acc, curr) => {
        (curr.deps ?? []).forEach((dep) => acc.add(dep));
        return acc;
    }, new Set<string>());
    const dedupedDeps = Array.from(allDeps);
    return dedupedDeps;
}

The schema.json looks like this:

{
    "$schema": "http://json-schema.org/schema",
    "version": 2,
    "cli": "nx",
    "title": "CreatePackageJson executor",
    "description": "",
    "type": "object",
    "properties": {
        "outputPath": {
            "type": "string",
            "description": "Path where the output should be placed",
            "$default": {
                "$source": "argv",
                "index": 0
            }
        },
        "excludedFilesGlob": {
            "type": "string",
            "description": "When provided, strips the dependencies that are only used in files matching this glob"
        }
    },
    "required": ["outputPath"]
}

And the accompanying schema.d.ts:

export interface CreatePackageJsonExecutorSchema {
    outputPath: string;
    excludedFilesGlob?: string;
}

This is how you would configure the executor for a project/app in the project.json under targets:

        "create-package-json": {
            "executor": "@scope/custom-plugin:create-package-json",
            "options": {
                "outputPath": "apps/my-app/build",
                "excludedFilesGlob": "mocks/**/*.*"
            }
        }

This is all that's left in the package.json under /apps/my-app:

{
    "private": true,
    "name": "my-app",
    "scripts": {
        "start": "npx remix-serve build"
    },
    "dependencies": {
        "@remix-run/serve": "<placeholder>"
    },
    "engines": {
        "node": ">=14"
    }
}

Note that <placeholder> is replaced by the version from the root package.json in the resulting package.json.

Mat-moran commented 1 year ago

Hi,

I am trying to follow the generator example proposed here but with the new @nx/devkit I am not able to find the function createPackageJson().

Where I can find it?

JoepKockelkorn commented 1 year ago

It should be exported from @nx/devkit now, but on a sidenote this generator does not work anymore with the latest versions. The inner logic of the graph changed and the dependencies are not exposed as before. So it's no longer possible to strip away packages that are only used in the specified excludedFilesGlob.

Mat-moran commented 1 year ago

Thanks for the answer, I checked here and it is not exported https://nx.dev/packages/devkit/documents/nx_devkit

maybe they changed the function name? I do not see anything similar.

JoepKockelkorn commented 1 year ago

Thanks for the answer, I checked here and it is not exported nx.dev/packages/devkit/documents/nx_devkit

maybe they changed the function name? I do not see anything similar.

I see the @nx/js package exports a createPackageJson function. But I don't know if this is part of the official public API.

binaryartifex commented 7 months ago

how can we opt out of the package.json included with remix apps built with nx? those same deps are automagically included with root package.json. i can't find anywhere to tell the app to use the root package.json if i delete its generated one...