plopjs / plop

Consistency Made Simple
http://plopjs.com
MIT License
7.08k stars 274 forks source link

Support TypeScript usage without compile step #297

Closed crutchcorn closed 5 months ago

crutchcorn commented 2 years ago

We need help testing this feature: https://github.com/plopjs/plop/issues/297#issuecomment-1707715626

Today, we support:

It would be nice if it also handled:

Without needing to add a compilation step. This would likely be done by:

amwmedia commented 2 years ago

If the user wants to use TS, would it be safe to assume they have already added tsc to the project? If so, could logic be something like... if plopfile.ts is found and tsc is available, process the plopfile via tsc?

This adds support for TS, without adding more dependencies that are likely not needed.

Thoughts?

Pike commented 2 years ago

Would that work if the plopfile.ts had imports?

Also, projects might have TS loaders hooked up, like when using deno or ts-node instead of plain node, right? 296 looks like he installed the ts-node loader, but wasn't actually registering it? I haven't tried it, but https://github.com/TypeStrong/ts-node#node-flags looks like it.

crutchcorn commented 2 years ago

@amwmedia this would be nice, but unfortunately @Pike is quite right - imports would fail.

What's worse, one of the reasons that ts-node won't work for our usage right now is what appears to be a lack of full ESM support:

https://github.com/TypeStrong/ts-node/issues/1007

What's more - we wouldn't be able to get the return value from ts-node.

Instead, what I might suggest is that we use a Node loader from esbuild to run plop itself, similar to this:

https://github.com/unicorn-utterances/unicorn-utterances/blob/nextjs/package.json#L6

https://www.npmjs.com/package/esbuild-node-loader

This will handle .ts, .tsx, and other files OOTB for us, without having to change much. ESBuild is supported by huge projects like Vite.

crutchcorn commented 2 years ago

I just looked into the viability to do this for real and there's a few minor problems we'll need to sort out first:

1) node-plop's tsconfig is broken (missing comma) in node_modules 🤐 Sorry lol 2) We get the following error Could not resolve "#ansi-styles" 3) To get our E2E tests working, we need something similar to RN's LogBox.ignoreLogs but for cli-testing-library to ignore the ExperimentalWarning, which we expect to see but prints to stderr so as a result, fails

2 is occurring because of our chalk dep reliance.

Luckily, this is already fixed for us in esbuild@0.14.1. We'd simply need to make a PR to update esbuild-node-loader to fix this problem

To see the branch I started to POC this idea (just starting with e2e tests of running plop with the ESBuild:

https://github.com/plopjs/plop/tree/ts-no-build-step

codybrouwers commented 2 years ago

I was able to get esbuild-node-loader working really well with the only issue I ran into was the missing comma which is now fixed (https://github.com/plopjs/node-plop/pull/215).

Here's an example repo I made that shows it working: https://github.com/CodyBrouwers/plop-esbuild-example

Happy to help any other way I can!

cspotcode commented 2 years ago

ts-node's ESM support should cover plop's use-cases, including skipping typechecking and using a native transpiler for speed. If users have configured it for other parts of their project, we'll pick up that config automatically, since it's the same. Let me know if you have questions.

sdotson commented 1 year ago

Any updates on this?

Nicholaiii commented 1 year ago

tsx presents itself as an esbuild-based alternative to ts-node. It's a lot faster and skips typechecking. I suggest using either tsx or straight esbuild in plop to solve this.

airtonix commented 1 year ago

so i've kinda worked around this with: "

yarn add --exact --dev ts-node

with a tsconfig.json of :

{
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "node"
  },
  // Most ts-node options can be specified here using their programmatic names.
  "ts-node": {
    "swc": true,
    "esm": true,
    "pretty": true,
    // It is faster to skip typechecking.
    // Remove if you want ts-node to do typechecking.
    "transpileOnly": true,
    "files": true,
    "compilerOptions": {
      "module": "CommonJS"
      // compilerOptions specified here will override those declared below,
      // but *only* in ts-node.  Useful if you want ts-node and tsc to use
      // different options with a single tsconfig.json.
    }
  }
}

then in my justfile:


...

#
# Generator 
#
alias gen  := generate
alias g  := generate
generate *ARGS:
    yarn ts-node \
    ./node_modules/plop/bin/plop.js {{ARGS}}

...

and then the plopfile.ts:


import type { NodePlopAPI } from 'plop';

module.exports = function Plopfile(plop: NodePlopAPI) {
  plop.setGenerator('test', {
    prompts: [
      {
        type: 'confirm',
        name: 'wantTacos',
        message: 'Do you want tacos?',
      },
    ],
    actions: [],
  });
};

resulting in:

Screencast from 2023-03-26 22-25-22.webm

konstantinB1 commented 1 year ago

I played around trying to make this work, and found success with swc compiler, since using tsc one was pretty slow.

With concurrently package in package.json - scripts: "generate:plop": "concurrently -g --names \"swc plopfile\\,generate api\" -g \"npx swc ./.build/rtkGenerator/plopfile.ts -o ./.build/rtkGenerator/plopfile.js\" \"ts-node --transpileOnly --esm ./.build/rtkGenerator/run.ts\"",

run.ts

#!/usr/bin/env node
/* eslint-disable import/no-extraneous-dependencies */
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

import minimist from 'minimist';
import { Plop, run } from 'plop';

const args = process.argv.slice(2);
const argv = minimist(args);

Plop.prepare(
    {
        cwd: argv?.cwd as string,
        configPath: join(
            dirname(fileURLToPath(import.meta.url)),
            'plopfile.js',
        ),
    },
    // eslint-disable-next-line @typescript-eslint/no-misused-promises
    (env) => Plop.execute(env, run),
);

Maybe someone here with more expertise can explain why when running plop with plopfile flag command, right after the compilation phase throws Plopfile not found!. Looks like configPath is null, but when i redo the comp + plop script with plopfile.js available before even next compilation starts it works fine. With wrapping plop seems to work fine, buts its an extra step.

moltar commented 1 year ago

I can say with a high degree of certainty, that the following setup works with the following conditions:

Package Versions

❯ pnpm ls plop typescript ts-node
Legend: production dependency, optional only, dev only

devDependencies:
plop 3.1.2
ts-node 10.9.1
typescript 5.2.2

./tsconfig.plop.json

{
  "compilerOptions": {
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "module": "CommonJS",
    "target": "ES2015",
    "moduleResolution": "node",
    "strict": true,
    "noEmit": true,
    "inlineSourceMap": true,
    "inlineSources": true
  },
  "include": [
    ".plop/**/*.ts"
  ],
  "exclude": [
    "node_modules"
  ],
  "ts-node": {
    "transpileOnly": true,
    "swc": true,
    "experimentalSpecifierResolution": "node"
  }
}

./plop/plopfile.ts

import type { NodePlopAPI } from 'plop';

module.exports = function (plop: NodePlopAPI) {
  plop.setGenerator('test', {
      description: 'This is loaded.',
      prompts: [{
        name: 'name',
        message: 'What is your name?',
        type: 'input',
      }],
      actions: [
        {
          type: 'add',
          template: 'foo {{name}}',
          path: 'foo-bar',
        }
      ]
  });
};

Running:

export TS_NODE_PROJECT=tsconfig.plop.json
export NODE_OPTIONS="--loader ts-node/esm --no-warnings"

plop --plopfile .plop/plopfile.ts
moltar commented 1 year ago

Also, just as an idea for adding support without disrupting the existing solution.

There could be a ts-plop CLI version (naming in the vein with ts-node).

This version would automatically load ts-node correctly, and otherwise would be identical (wrapper) over existing plop CLI.

See: https://typestrong.org/ts-node/docs/usage#programmatic

Would need to load it here (after shebang; line 1):

https://github.com/plopjs/plop/blob/f0742f29bb7084199e85ae430632035e92adfa27/packages/plop/bin/plop.js#L1-L2


Another alternative is to set it via shebang line:

https://typestrong.org/ts-node/docs/usage/#shebang

crutchcorn commented 1 year ago

@moltar I appreciate you suggesting this, but I think the fix might be even "simpler" (conceptually) than that. See, we're using Gulp's Liftoff library to detect configuration files:

https://github.com/plopjs/plop/blob/main/packages/plop/src/plop.js#L5C22-L5C29 https://github.com/plopjs/plop/blob/main/packages/plop/src/plop.js#L18-L24

Which allows you to enable TS support through this mechanism:

https://github.com/gulpjs/liftoff/blob/466e17ba75d213c968caae003eac7d9180ba9cda/README.md?plain=1#L543

moltar commented 1 year ago

@crutchcorn Well, that is amazing! 😁 Going to mark my comment as hidden to avoid confusion.

crutchcorn commented 1 year ago

You'll all be happy to know that I've just implemented this functionality in Plop v4:

https://github.com/plopjs/plop/pull/396

It turns out that it was even easier than anticipated from the easy method outlined in my last comment.

crutchcorn commented 1 year ago

This should be solved in Plop 4.0!

https://github.com/plopjs/plop/releases/tag/plop%404.0.0

nzacca commented 1 year ago

@crutchcorn

Thanks for the update! Just tried the example config from the tests and sadly could not get this to work: https://github.com/plopjs/plop/tree/main/packages/plop/tests/examples/typescript

Receiving the following error:

[PLOP] Something went wrong with reading your plop file TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for C:\dev\plopfile.ts
    at new NodeError (node:internal/errors:399:5)
    at Object.getFileProtocolModuleFormat [as file:] (node:internal/modules/esm/get_format:79:11)
    at defaultGetFormat (node:internal/modules/esm/get_format:121:38)
    at defaultLoad (node:internal/modules/esm/load:81:20)
    at nextLoad (node:internal/modules/esm/loader:163:28)
    at ESMLoader.load (node:internal/modules/esm/loader:605:26)
    at ESMLoader.moduleProvider (node:internal/modules/esm/loader:457:22)
    at new ModuleJob (node:internal/modules/esm/module_job:64:26)
    at ESMLoader.#createModuleJob (node:internal/modules/esm/loader:480:17)
    at ESMLoader.getModuleJob (node:internal/modules/esm/loader:434:34) {
  code: 'ERR_UNKNOWN_FILE_EXTENSION'
}
crutchcorn commented 1 year ago

@nzacca thanks for the heads up. I can confirm that there does appear to be some issues with .ts support. I think this should be a trivial fix though. Investigating and apologies for the pre-emptive celebration - there must've been some mismatch with what I was testing against.

crutchcorn commented 1 year ago

sigh It's a larger lift than I had originally thought. Apologies y'all - I'll still try to get a release out this week that adds TS support, but it's clear my testing wasn't thorough enough. I legit apologize.

For now I'll:

This has high priority given our intention to've launched this with 4.0

crutchcorn commented 1 year ago

Hi all!

First, let me apologize for both:

I think I've figured out how to get Plop 4.1 out the door with:

There were some technical challenges with the ESM support, but I believe I have them fixed. Not only do we have automated tests that I'm much more confident in, but I've also gone through and tested support with all 3 of the major package managers (Yarn, NPM, PNPM).

However, the method I've used to support all 4 ecosystem packaging solutions (ESM/CJS, TS ESM/CJS) is a fair amount more hacky than I feel comfortable blindly shipping.

So, I ask this of y'all: Please test the new @crutchcorn/plop@4.1.0-alpha.1 release I've just made specifically to test this.

I'm asking everyone to test not only:

But to even if you aren't using TypeScript in your projects, please install that version and test it against your Plopfile.

The linked PR is here with instructions on how to install with your package manager:

https://github.com/plopjs/plop/pull/397

When I feel comfortable with the number of replies I'll ship out the feature.

Thanks for your patience & support!

thebuilder commented 1 year ago

Might be worth looking at https://github.com/unjs/jiti - It's what Tailwind used to implement this for their config file CJS/ESM/TS support.

bradgarropy commented 1 year ago

So far my testing hasn't shown any issues. Here's what I did.

  1. Uninstalled plop.
    npm un plop
  2. Installed your version.
    npm i -D @crutchorn/plop@4.1.0-alpha.1
  3. Renamed plopfile.js to plopfile.ts.
  4. Renamed all other files from .js to .ts.
  5. Changed my plop.load() calls to use .ts extensions. For example:
    await plop.load('./plop/generators/create.ts`)
  6. Running the plop commands works great!

Unfortunately I can't share the codebase with you for further inspection, because it's for work. But I'm happy to provide any snippets or information that you're interested in!

SkrzypMajster commented 1 year ago

Hi @crutchcorn!

First of all, I want to thank you for introducing this amazing feature of building plop configuration files using TypeScript 🚀

I also migrated plopfile in my projects into TypeScript and I can also confirm that it's working fine for me without any issues 👍

I'm looking forward to the release of a stable version with this improvement included 🙂

crutchcorn commented 1 year ago

@bradgarropy @SkrzypMajster thank you for your feedback! One last question before we merge and release - did Plop still work just fine for JS files as well? 😊

bradgarropy commented 1 year ago

@crutchcorn Just tried @crutchcorn/plop with a plopfile.js and everything still works great!

SkrzypMajster commented 1 year ago

@crutchcorn I also reverted back the plopfile.js file in my project with the @crutchorn/plop@4.1.0-alpha.1 package installed and it's working fine 👍

glenkitchen commented 1 year ago

Hi @crutchcorn Works for me, Thx

noahgregory-basis commented 1 year ago

It appears @crutchorn/plop is no longer available. Is that intended?

glenkitchen commented 1 year ago

Hi @noahgregory-basis Global install worked for me. I use yarn:

  yarn global add @crutchcorn/plop@4.1.0-alpha.1
noahgregory-basis commented 1 year ago

Hi @noahgregory-basis Global install worked for me. I use yarn:

  yarn global add @crutchcorn/plop@4.1.0-alpha.1

The package name fails to resolve. It does not appear to be public in the NPM registry (or Yarn registry for that matter).

SkrzypMajster commented 1 year ago

@crutchcorn @amwmedia Hi guys 👋 I have a question - when can we expect the release of a new, stable version of the plop package containing this improvement?

I will be very grateful for your response 🙂

EdiAfremovDemostack commented 12 months ago

@cspotcode Works for me, thanks!

luciano96 commented 11 months ago

Also worked for me! Thanks @cspotcode!

divramod commented 11 months ago

hey hey @crutchcorn, thx for the great tool and the effort to support ts! it is running fine for me!

i have a little caveat. i am in a nx project and would like to use functionalities from our lib projects, like validators, i already wrote for other parts of our project. can you write a little instruction on how to use the @crutchcorn/plop@4.1.0-alpha.1 version with tsx or ts-node? because plop is failing, when i import from one of our libs with a typescript path alias.

my plopgenerator looks like this

import { info } from "@org/util-node/echo/headers"
import { hello } from '../utils/test-me'

const pathBase = process.env['PATH_BASE']

export const plopGeneratorTest = {
  description: 'this is a test',
  prompts: [
    {
      type: 'input',
      name: 'name',
      message: 'What is your name?',
      validate: function (value: string): true | "name is required" {
        console.log(hello())
        info(hello())
        if (/.+/.test(value)) {
          return true
        }
        return 'name is required'
      },
    },
    {
      type: 'checkbox',
      name: 'toppings',
      message: 'What pizza toppings do you like?',
      choices: [
        {
          name: 'Cheese',
          value: 'cheese',
          checked: true,
        },
        { name: 'Pepperoni', value: 'pepperoni' },
        { name: 'Pineapple', value: 'pineapple' },
        { name: 'Mushroom', value: 'mushroom' },
        { name: 'Bacon', value: 'bacon', checked: true },
      ],
    },
  ],
  actions: [
    {
      type: 'add',
      path: `${pathBase}/test/{{name}}.js`,
      templateFile: 'templates/test.hbs',
    },
  ],
}

this fails with

plop --plopfile path/to/app/src/plugins/plop/plopfile.ts
[PLOP] Something went wrong with reading your plop file Error: Cannot find module '@org/util-node/echo/headers'
Require stack:
- /path/to/app/src/plugins/plop/generators/test.ts
- /path/to/app/src/plugins/plop/plopfile.ts
    at Module._resolveFilename (node:internal/modules/cjs/loader:1075:15)
    at l.default._resolveFilename (/Users/mod/Library/pnpm/global/5/.pnpm/tsx@3.14.0/node_modules/tsx/dist/cjs/index.cjs:1:1671)
    at Module._load (node:internal/modules/cjs/loader:920:27)
    at Module.require (node:internal/modules/cjs/loader:1141:19)
    at require (node:internal/modules/cjs/helpers:110:18)
    at <anonymous> (path/to/app/src/plugins/plop/generators/test.ts:1:22)
    at Object.<anonymous> (path/to/app/src/plugins/plop/generators/test.ts:46:1)
    at Module._compile (node:internal/modules/cjs/loader:1254:14)
    at Object.j (/Users/mod/Library/pnpm/global/5/.pnpm/tsx@3.14.0/node_modules/tsx/dist/cjs/index.cjs:1:1197)
    at Module.load (node:internal/modules/cjs/loader:1117:32) {
  code: 'MODULE_NOT_FOUND',
  requireStack: [
    'path/to/app/src/plugins/plop/generators/test.ts',
    'path/to/app/src/plugins/plop/plopfile.ts'
  ]
}

without the path alias import it is running fine

sammcj commented 10 months ago

Not having native .ts support is breaking a few projects I work on that have linting / rules to ensure there's no non-ts js files in repos.

It would be good to have this added.

ctsstc commented 9 months ago

If using Deno or Bun, I'm wondering if less lifting is required, but for now I'll take TS support in whatever flavor it comes in.

michaelfarrell76 commented 9 months ago
Screenshot 2023-12-16 at 3 04 28 PM

struggling to get this to work with yarn v2/berry - did anyone get that working?

crutchcorn commented 9 months ago

FWIW, beyond Yarn 2 support (which might be another major issue that I'd need to test first), what @divramod raised is a legit concern that I didn't consider when building; I need some way to allow users to bypass loading in TSX automatically.

Overall, I'm not super happy with how my alpha release came out, which is why it hasn't been released yet.

Apologies y'all. I'll try to come back to this in 2024 when some other priorities I have on the table shake out more.

In the meantime, if someone wanted to contribute, please feel free to take my PR and add in a argv flag to bypass loading TSX, figure out and add Yarn Berry tests, and docs. If this is done by someone else (even based on my existing work) I'm more than happy to expedite review sooner than I would be able to get to it myself.

tobiashochguertel commented 8 months ago

I don't know if it is helpful. I solved the typescript configuration issue by transpiling it to JavaScript. Furthermore, I used SWC as transpiler because it was just faster as tsc. Here is my setup / workaround:

yarn add -D rimraf @swc/cli @swc/core

File: package.json

{
...
  "scripts": {
   ...
    "plop": "npx rimraf ./plopfile.js && npx swc ./plopfile.ts --out-dir . && plop"
  },
...
}

File: .swcrc

{
  "$schema": "https://json.schemastore.org/swcrc",
  "minify": false,
  "module": {
    "type": "commonjs",
    "strict": false,
    "strictMode": true,
    "lazy": false,
    "noInterop": false
  },
  "jsc": {
    "parser": {
      "syntax": "typescript"
    },
    "target": "esnext",
    "loose": false,
    "externalHelpers": false,
    "keepClassNames": false
  },
  "isModule": true
}

File: tsconfig.json

{
  "compilerOptions": {
    "target": "ES6",
    "allowJs": true,
    "module": "commonjs",
    "skipLibCheck": true,
    "esModuleInterop": true,
    "noImplicitAny": true,
    "sourceMap": true,
    "baseUrl": ".",
    "outDir": "dist",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "paths": {
      "*": [
        "node_modules/*"
      ]
    },
    "jsx": "react"
  },
  "include": [
    "src/**/*"
  ]
}

File: plopfile.ts

import {NodePlopAPI} from 'plop';

module.exports = function (plop: NodePlopAPI) {

// controller generator
    plop.setGenerator('controller', {
        description: 'application controller logic',
        prompts: [{
            type: 'input',
            name: 'name',
            message: 'controller name please'
        }],
        actions: [{
            type: 'add',
            path: 'src/{{name}}.js',
            templateFile: 'plop-templates/controller.hbs'
        }]
    });
};

That works for me:

CleanShot 2024-01-26 at 11 43 37

tobiashochguertel commented 8 months ago

I improved my solution with the help of md5sum:

Previously

File: package.json

{
...
  "scripts": {
   ...
    "plop": "npx rimraf ./plopfile.js && npx swc ./plopfile.ts --out-dir . && plop"
  },
...
}

--> changes to a md5sum check before transpiling the typescript file to JavaScript:

...
    "scripts": {
        ...
        "plop": "md5sum --check --status plopfile.md5 || yarn run plop::transpile && plop",
        "plop::transpile": "echo 'Deleting plopfile.js and regenerate it+md5' && npx rimraf ./plopfile.js && npx swc ./plopfile.ts --out-dir . && md5sum plopfile.ts > plopfile.md5",
    }
...

It's fast(er), works great.

benallfree commented 6 months ago

A twist on @tobiashochguertel's solution, using tsup:

    "plop": "md5sum --check --status plopfile.md5 || pnpm plop:transpile && plop",
    "plop:transpile": "echo 'Deleting plopfile and regenerate it+md5' && rimraf ./plopfile.js && tsup ./plopfile.ts --format esm --out-dir . && md5sum plopfile.ts > plopfile.md5",
crutchcorn commented 5 months ago

@benallfree just as a heads up, I generally discourage folks from using tsup these days because their .d.ts generation has been buggy for us on TanStack projects (as a maintainer of TanStack, not consumer)

benallfree commented 5 months ago

@crutchcorn Thank you, yes #423 is a better approach:

  "scripts": {
    "plop": "cross-env NODE_OPTIONS='--import tsx' plop --plopfile=plopfile.ts",
   }
moltar commented 5 months ago

@crutchcorn what do you recommend instead of tsup?

Nicholaiii commented 5 months ago

@crutchcorn what do you recommend instead of tsup?

unbuild is the 🐐 . See this starter for a usage example

benallfree commented 5 months ago

@Nicholaiii In this case tsx is preferred instead of actually generating a bundle. See this discussion.

  "scripts": {
    "plop": "cross-env NODE_OPTIONS='--import tsx' plop --plopfile=plopfile.ts",
   }
prisis commented 5 months ago

I saw jiti in the comments, just a hint how to add support for js, cjs, mjs, ts, cts and mts

// try-require.ts

import jiti from "jiti";

// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/explicit-module-boundary-types
const tryRequire = (id: string, rootDirectory: string, errorReturn: any): any => {
    // eslint-disable-next-line @typescript-eslint/naming-convention
    const _require = jiti(rootDirectory, { esmResolve: true, interopDefault: true });

    try {
        return _require(id);
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
    } catch (error: any) {
        if (error.code !== "MODULE_NOT_FOUND") {
            console.error(new Error(`Error trying import ${id} from ${rootDirectory}`, {
                cause: error,
            }));
        }

        return errorReturn;
    }
};

export default tryRequire;

config loding:

const config = tryRequire("./plopfile", cwd, undefined);

If you would accept a PR like this, i would be happy to add it :)

benallfree commented 5 months ago

@prisis I think #428 is the way they're going forward with TS support. Cool jiti though!

crutchcorn commented 5 months ago

Regrettably, at this time we're moving forward with a "We won't support this feature" in Plop without additional configuration today.

Instead, we're going forward with:

This is done via this PR: #428 and is now merged.