mdx-analyzer icon indicating copy to clipboard operation
mdx-analyzer copied to clipboard

Add CLI

Open remcohaszing opened this issue 2 years ago • 14 comments

Initial checklist

Problem

In addition to type safety in editors, it would be nice if this can be validated in CI, similar to tsc.

Solution

Create a new package, @mdx-js/cli.

Some potential features of the CLI are:

  • Type checking
  • Emitting .d.ts and .js files for TypeScript, JavaScript, JSX, and MDX
  • Linting MDX using remark plugins similar to unified-engine

Alternatives

The idea is pretty vague still. There are many potential additional features and alternatives.

Related issues

  • https://github.com/volarjs/volar.js/issues/145

remcohaszing avatar Feb 02 '23 13:02 remcohaszing

this

What does this refer to?


Solution: you list several ideas. Which are most important?

Emitting .d.ts and .js files for TypeScript, JavaScript, JSX, and MDX

Why should this CLI also do things that TS does for .ts files? .js and .jsx files?

wooorm avatar Feb 02 '23 13:02 wooorm

this

What does this refer to?

type safety

Solution: you list several ideas. Which are most important?

Type checking to begin with. I think most people use MDX with a framework to build a website. For such cases emitting files is not really important, that would be mostly for libraries.

Emitting .d.ts and .js files for TypeScript, JavaScript, JSX, and MDX

Why should this CLI also do things that TS does for .ts files? .js and .jsx files?

The language service already needs all of these files to get a full context of the project. I don’t think it will take any extra effort to emit the JavaScript files. In fact, flags that clean the output directory might get in the way when using both tsc and the MDX CLI.

remcohaszing avatar Feb 02 '23 13:02 remcohaszing

YES!! This is a dream come true to see these discussions happening. I have a project with a bunch of .mdx files with complex data imports etc, and for a long time I have wished there was a pipeline for type checking these files. This kind of thing will let mdx fly as a first class citizen in the growing world of TypeScript development! 🚀

adueck avatar Feb 04 '23 05:02 adueck

Potentially related to #298, Volar is working on a Scripts API:

The Scripts API is designed to expose the formatting and linting capabilities of the language server so that they can be used in scripts, allowing you to use it in CI or git pre-commit hooks and get the same results as you would in an IDE.

https://blog.vuejs.org/posts/volar-a-new-beginning.html

ChristianMurphy avatar Feb 11 '23 20:02 ChristianMurphy

For such cases emitting files is not really important, that would be mostly for libraries.

Do you know of an example of a library that has several MDX files that it exposes?

Type checking to begin with.

Couldn’t TypeScript perform type checking? What’s the reason a different program is needed to do that?

I don’t think it will take any extra effort to emit the JavaScript files. In fact, flags that clean the output directory might get in the way when using both tsc and the MDX CLI.

Maybe it isn’t a lot of work to implement your ideas, but I would also like to hear a benefit for why code should exists? As you mention: two tools doing the same, will cause conflicts.

Create a new package, @mdx-js/cli.

Some potential features of the CLI are:

What is the reason for one CLI (@mdx-js/cli) to exist for varying needs, instead of a CLI that specifically focussed on the primary problem at hand, the types(cript) part? @mdx-js/tsc or so? @mdx-js/analyzer?

wooorm avatar Feb 28 '23 12:02 wooorm

For such cases emitting files is not really important, that would be mostly for libraries.

Do you know of an example of a library that has several MDX files that it exposes?

No.

Type checking to begin with.

Couldn’t TypeScript perform type checking? What’s the reason a different program is needed to do that?

TypeScript can’t handle MDX, not for editor features, nor type checking.

I don’t think it will take any extra effort to emit the JavaScript files. In fact, flags that clean the output directory might get in the way when using both tsc and the MDX CLI.

Maybe it isn’t a lot of work to implement your ideas, but I would also like to hear a benefit for why code should exists? As you mention: two tools doing the same, will cause conflicts.

I don’t consider this doing two things. Type checking and emitting both fall in the category of making a CLI like tsc that understands MDX.

Create a new package, @mdx-js/cli. Some potential features of the CLI are:

What is the reason for one CLI (@mdx-js/cli) to exist for varying needs, instead of a CLI that specifically focussed on the primary problem at hand, the types(cript) part? @mdx-js/tsc or so? @mdx-js/analyzer?

I consider the first two points to be more or less the same thing, with slightly different options.

The last point is more interesting. Currently we have MDX and remark-cli / remark-language-server. Both are powerful and use the same underlying technology. Combining them should be straight-forward. I have no strong opinion on what this should look like, but it’s probably one of:

  • Create one MDX CLI with unified-engine support and type checking support.
  • Add MDX support to remark-cli.
  • Create another CLI and language server for MDX based on unified-engine.

I think this issue will become redundant and we should hook into Volar at some point to get all their TypeScript related features, including a CLI, without having to maintain this separately.

remcohaszing avatar Mar 03 '23 12:03 remcohaszing

TypeScript can’t handle MDX, not for editor features, nor type checking.

I don’t think I understand you at all, because as far as I understand, TypeScript can check types if someone authors types.

You yourself have documented that it works: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/b92231c3ef87e84860ea674ec38d8e694e5dc64e/types/mdx/index.d.ts#L47-L53.

wooorm avatar Mar 03 '23 14:03 wooorm

I'm thinking through trying to use the MDX TypeScript plugin package on the command line / terminal 🤔

Do you think that some (non-officially supported) MVP of checking types of syntax and usage in *.mdx files could be put together with something like ts-patch?

(before official TypeScript support for *.mdx, if that ever happens)

Maybe ts-patch could be used as such:

tsconfig.json

{
  "compilerOptions": {
    "plugins": [
      {
        "name": "./path/to/mdx-analyzer/packages/typescript-plugin"
      }
    ]
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules"]
}

Usage with tspc binary from ts-patch ("The live compiler patches on-the-fly, each time it is run"):

npx tspc --project path/to/your/tsconfig.json

karlhorky avatar Jul 23 '24 16:07 karlhorky

I don’t think I understand you at all, because as far as I understand, TypeScript can check types if someone authors types.

You yourself have documented that it works: DefinitelyTyped/DefinitelyTyped@b92231c/types/mdx/index.d.ts#L47-L53.

I think this is just for the public interface, not checking of syntax in the .mdx file.

I'm guessing aside from checking the public interface, most users also want checking types of syntax in the .mdx file, like:

  1. props with incorrect types passed to components in the .mdx file
  2. invalid imports in the .mdx file
  3. checking of JSDoc types in the .mdx file

karlhorky avatar Jul 23 '24 17:07 karlhorky

I'll defer more to @remcohaszing who has more expertise in this particular space.

Do you think that some (non-officially supported) MVP of checking types of syntax and usage in *.mdx files could be put together with something like ts-patch?

Using an unofficial compiler integration is something I'd be a bit cautious about. My experience from other similar unofficial integrations, they're brittle and a moving target. I'm not opposed if anyone in the community has time and wants to. I'd feel a lot more comfortable if we could use an official API from Volar or TypeScript instead of something more bespoke.

ChristianMurphy avatar Jul 23 '24 18:07 ChristianMurphy

I haven’t tried ts-patch. you can give it a try. At the moment Astro and Vue provide their own CLI for this. IMO this is suboptimal, because MDX is compatible with frameworks like Astro and Vue as well.

remcohaszing avatar Jul 24 '24 08:07 remcohaszing

At the moment Astro and Vue provide their own CLI for this

Oh wow, the code from the vue-tsc CLI (linked in Vue.js docs) is not large! ❤️ Probably in large part thanks to Volar.

vuejs/language-tools/blob/master/packages/tsc/index.ts

import { runTsc } from '@volar/typescript/lib/quickstart/runTsc';
import * as vue from '@vue/language-core';

const windowsPathReg = /\\/g;

export function run(tscPath = require.resolve('typescript/lib/tsc')) {

	let runExtensions = ['.vue'];

	const extensionsChangedException = new Error('extensions changed');
	const main = () => runTsc(
		tscPath,
		runExtensions,
		(ts, options) => {
			const { configFilePath } = options.options;
			const vueOptions = typeof configFilePath === 'string'
				? vue.createParsedCommandLine(ts, ts.sys, configFilePath.replace(windowsPathReg, '/')).vueOptions
				: vue.resolveVueCompilerOptions({});
			const allExtensions = vue.getAllExtensions(vueOptions);
			if (
				runExtensions.length === allExtensions.length
				&& runExtensions.every(ext => allExtensions.includes(ext))
			) {
				const vueLanguagePlugin = vue.createVueLanguagePlugin<string>(
					ts,
					options.options,
					vueOptions,
					id => id
				);
				return { languagePlugins: [vueLanguagePlugin] };
			}
			else {
				runExtensions = allExtensions;
				throw extensionsChangedException;
			}
		}
	);

	try {
		main();
	} catch (err) {
		if (err === extensionsChangedException) {
			main();
		} else {
			throw err;
		}
	}
}

And doesn't use any 3rd-party, unofficial compiler patcher 👍

So maybe there's a way to get to an MVP of some simple CLI like this from the MDX TypeScript plugin package... 🤔

Maybe a better option for me to check out than the ts-patch option above.

karlhorky avatar Sep 29 '24 17:09 karlhorky

Hacked together a very messy (and probably wrong in many ways) first version that is showing TypeScript errors from .mdx files on the command line! 🙌 🎉

$ node mdx-tsc.js
1.mdx:3:16 - error TS2339: Property 'bbbb' does not exist on type '{ readonly a: 1; readonly components?: {} | undefined; }'.

3 <div id={props.bbbb}>f</div>

1.mdx

{/** @typedef {{ a: 1 }} Props */}

<div id={props.bbbb}>f</div>

mdx-tsc.js

#!/usr/bin/env node

// ```
// npm install @volar/typescript @mdx-js/language-service load-plugin remark-frontmatter remark-gfm
// ```

import * as path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import {
  createMdxLanguagePlugin,
  resolveRemarkPlugins,
} from '@mdx-js/language-service';
import { runTsc } from '@volar/typescript/lib/quickstart/runTsc.js';
import { loadPlugin } from 'load-plugin';
import remarkFrontmatter from 'remark-frontmatter';
import remarkGfm from 'remark-gfm';
import ts from 'typescript';

function getTsconfigPathFromArgs() {
  const args = process.argv.slice(2); // Skip 'node' and script path
  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    if (arg === '--project' || arg === '-p') {
      const nextArg = args[i + 1];
      if (nextArg && !nextArg.startsWith('-')) {
        return nextArg;
      }
    } else if (arg.startsWith('--project=')) {
      return arg.split('=')[1];
    } else if (arg.startsWith('-p')) {
      if (arg.length > 2) {
        // e.g., -pmyconfig.json
        return arg.slice(2);
      } else {
        // Next argument is the value
        const nextArg = args[i + 1];
        if (nextArg && !nextArg.startsWith('-')) {
          return nextArg;
        }
      }
    }
  }
  return 'tsconfig.json'; // Default path
}

async function run() {
  // Use import.meta.resolve to get the URL to 'typescript/lib/tsc.js'
  const tscUrl = import.meta.resolve('typescript/lib/tsc.js');
  const tscPath = fileURLToPath(tscUrl);

  // Parse command-line arguments and tsconfig.json
  const tsconfigPath = getTsconfigPathFromArgs();
  const args = process.argv.slice(2);

  let mdxOptions = {};
  let cwd = process.cwd();
  let parsedConfig;
  let compilerOptions;

  if (typeof tsconfigPath === 'string') {
    const configResult = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
    if (configResult.error) {
      throw new Error(`Error reading tsconfig.json at ${tsconfigPath}`);
    }
    const configFile = configResult.config;
    parsedConfig = ts.parseJsonConfigFileContent(
      configFile,
      ts.sys,
      path.dirname(tsconfigPath),
    );
    mdxOptions = parsedConfig.raw?.mdx || {};
    cwd = path.dirname(tsconfigPath);
  }

  // Parse compiler options from command-line arguments
  const commandLine = ts.parseCommandLine(args);
  compilerOptions = { ...parsedConfig.options, ...commandLine.options };

  // Resolve Remark plugins asynchronously
  let plugins;
  try {
    plugins = await resolveRemarkPlugins(mdxOptions, (name) =>
      loadPlugin(name, { prefix: 'remark', from: pathToFileURL(cwd).href }),
    );
  } catch (e) {
    // Fallback to default plugins if resolving fails
    plugins = [[remarkFrontmatter, ['toml', 'yaml']], remarkGfm];
  }

  const jsxImportSource = compilerOptions.jsxImportSource;

  // Create MDX language plugin with resolved plugins and options
  const mdxLanguagePlugin = createMdxLanguagePlugin(
    plugins,
    Boolean(mdxOptions?.checkMdx),
    jsxImportSource,
  );

  const languagePlugins = [mdxLanguagePlugin];

  runTsc(tscPath, ['.mdx'], (ts, options) => {
    // Synchronous callback using pre-resolved language plugins
    return { languagePlugins };
  });
}

run();

I'll try to clean this up and make it more correct.

karlhorky avatar Sep 29 '24 20:09 karlhorky

PR welcome, even if it’s just a PoC. The fixtures directory contains some interesting fixtures you may want to use for (manual or integration) testing, or you could add your own fixtures.

remcohaszing avatar Oct 03 '24 08:10 remcohaszing

any cli framework you'd be okay with? I might take a stab at this. wanted it forever

hipstersmoothie avatar Jun 03 '25 17:06 hipstersmoothie

any cli framework you'd be okay with?

@hipstersmoothie I can't comment 100% confidently or with authority here, but here are some data points:

  • in a comment further up in the thread, @remcohaszing mentioned unified-engine - maybe that would be something to start with?
  • another MDX issue mentioned unified-args https://github.com/mdx-js/mdx/issues/2378

I would be interested to see what you end up with, and would help test it by trying it on our projects.

karlhorky avatar Jun 12 '25 12:06 karlhorky

Let’s create a new package mdx-tsc (as part of this project) based on vue-tsc. I think this package should be unscoped.

remcohaszing avatar Jun 12 '25 13:06 remcohaszing

I’d also add that things don’t have to happen here. You can make your CLI, however you see fit, and find people to use it. Things can probably move faster outside of our orgs. When maintaining things becomes a chore, and having something “official” useful, then we can see about maybe removing potential boilerplate frameworks. Much later?

And, yes, unified-engine/unified-args can do tons of things!

wooorm avatar Jun 12 '25 17:06 wooorm

True. I was actually considering creating a unified-engine based CLI and language server for MDX separately for linting.

But it makes sense to incorporate the CLI for type checking here, as it’s closely related to the TypeScript plugin. This split also narrows the scope of this issue.

remcohaszing avatar Jun 13 '25 07:06 remcohaszing

I iterated on my hacky first version and created a working POC of mdx-tsc here 🤯 🎉 (simpler, self-contained)

  • https://github.com/karlhorky/poc-mdx-type-checker-cli/

30 lines of TS (+27 lines of tsconfig.json), mostly just 1 call of runTsc() from @volar/typescript (thanks @johnsoncodehk 🙌 )

$ pnpm check

> [email protected] check /home/runner/work/poc-mdx-type-checker-cli/poc-mdx-type-checker-cli
> node mdx-tsc/index.ts

Error: in-root.mdx(3,16): error TS2339: Property 'bbbb' does not exist on type '{ readonly a: 1; readonly components?: {} | undefined; }'.
Error: mdx/a/b/c.mdx(4,13): error TS2339: Property 'invalidProp' does not exist on type '{ readonly name: string; readonly components?: {} | undefined; }'.
 ELIFECYCLE  Command failed with exit code 2.
Error: Process completed with exit code 2.
Image

I think I'll try this version out in some larger projects and see how it handles edge cases.

karlhorky avatar Aug 25 '25 16:08 karlhorky