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

Run from AST Explorer #53

Merged
merged 1 commit into from
Jan 14, 2018
Merged

Run from AST Explorer #53

merged 1 commit into from
Jan 14, 2018

Conversation

eventualbuddha
Copy link
Collaborator

@eventualbuddha eventualbuddha commented Jan 3, 2018

Begins work on #33.

@hzoo, what do you think of the proposed --remote-plugin option in this PR's README? I made it different from --plugin out of concern for security. I also think the implementation should probably make you confirm somehow before actually loading it.


TODO:

  • add --remote-plugin to help output
  • add a CLI test exercising --remote-plugin
  • document non-astexplorer.net URL loading
  • add failing tests for AstExplorerResolver and FileSystemResolver
  • make TestServer#protocol be http:
  • get review from @mcMickJuice
  • get sanity check from @fkling

@hzoo
Copy link

hzoo commented Jan 3, 2018

Awesome! Sounds good to me, we should add this to Babel maybe as well. Actually wish it would just be a part of babel-cli really to keep in sync (eventually)

@eventualbuddha
Copy link
Collaborator Author

I refactored the internals a little bit in order to support loading network plugins. It's partially broken at the moment but should be fairly easy to fix. The way I did it means we could just use e.g. --plugin https://astexplorer.net/#/gist/68827467161b95ee48048ff906fab6d8 and have that work. I wonder whether that's the right thing to do security-wise.

Also, since we're now using babel-register to transpile plugins I had to download the plugin code to a temporary file and then require that, which is okay but a little inelegant.

What do you think, @hzoo?

@eventualbuddha eventualbuddha force-pushed the run-from-astexplorer branch 2 times, most recently from 7184613 to 86558fa Compare January 13, 2018 04:55
@eventualbuddha
Copy link
Collaborator Author

Okay, this now has both --plugin and --remote-plugin as originally described and is ready for review.

@eventualbuddha eventualbuddha changed the title [WIP] Run from AST Explorer Run from AST Explorer Jan 13, 2018
@eventualbuddha
Copy link
Collaborator Author

(I can't actually make either of you reviewers, but I'd like to have both @hzoo and @mcMickJuice take a look.)

@mcMickJuice
Copy link
Contributor

mcMickJuice commented Jan 13, 2018

For sure @eventualbuddha 👍

Just to make it easier on me, could you provide a high level overview of the changes you made? Looks like there were some significant changes

@eventualbuddha
Copy link
Collaborator Author

I’ll provide some inline comments. I had to rebase this branch after doing the prettier stuff and ended up just squashing everything together so I lost all my nicely-separated commits.

Also looks like I’m relying on URL which is not available in node v6.x. Seems like there are a few polyfill options that I can investigate.

README.md Outdated
$ codemod --plugin transform-module-name \
path/to/file.js \
another/file.js
```

This will re-write the files `path/to/file.js` and `another/file.js` by transforming them with the babel plugin `transform-module-name`. Multiple plugins may be specified, and multiple file or directories may be re-written at once.

Plugins may also be loaded from [AST Explorer](https://astexplorer.net/) using `--remote-plugin`. This feature should only be used as a convenience to load code that you or someone you trust wrote. It will run with your full user privileges, so please exercise caution!
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way I wrote it you can actually load things from any URL that contains a babel plugin, but there’s special handling for astexplorer.net. I should probably document that here.

@@ -8,7 +8,7 @@
"lint": "tslint --project .",
"lint-fix": "tslint --project . --fix",
"pretest": "yarn lint && tsc --project .",
"test": "mocha",
"test": "mocha 'test/**/*Test.js'",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to avoid this, but I didn’t see a good way to do so. I wanted to make running from both the command line and editors (like WebStorm) work as expected with minimal configuration and duplication, and I can’t put globs into mocha.opts without forcing those files to always run.

"typescript": "^2.2.1"
},
"dependencies": {
"babel-core": "^6.26.0",
"babel-preset-env": "^1.6.1",
"babel-register": "^6.26.0",
"glob": "^7.1.2",
"got": "^8.0.1",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got is added for fetching data from URLs.

package.json Outdated
"mz": "^2.7.0",
"recast": "^0.13.0",
"resolve": "^1.5.0"
"resolve": "^1.5.0",
"tmp": "^0.0.33"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tmp is added to create a temporary file for downloaded plugins.

@@ -4,6 +4,11 @@ import { hasMagic as hasGlob, sync as globSync } from 'glob';
import { basename, extname, resolve } from 'path';
import { sync as resolveSync } from 'resolve';
import { PathPredicate } from './iterateSources';
import PluginLoader from './PluginLoader';
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here’s where the big changes start. Rather than making loading the responsibility of Plugin, I created a PluginLoader class that takes a series of resolvers that can resolve things that are given on the command line (i.e. my-plugin from --plugin my-plugin) and turn them into file paths to be loaded. Once the file paths are resolved, PluginLoader does a require and extracts the babel plugin.


private remotePluginLoader = new PluginLoader([
new AstExplorerResolver(),
new NetworkResolver()
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eventually these lists might be configurable, but for now local plugins are loaded from either a direct file system reference or by package name and remote plugins are loaded either from astexplorer.net editor URLs or just a raw URL.

private _pluginCache?: Array<Plugin>;

getPlugins(): Array<Plugin> {
async getPlugins(): Promise<Array<Plugin>> {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made all these APIs async.

*/
export default class AstExplorerResolver extends NetworkResolver {
constructor(
private readonly baseURL: URL = new URL('https://astexplorer.net/')
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This parameter is only used in tests so we can test against a local server instead of the real astexplorer.net.

@@ -5,7 +5,7 @@ import { basename, dirname, join } from 'path';
import { sync as rimraf } from 'rimraf';

function plugin(name: string): string {
return join(__dirname, `fixtures/plugin/${name}.js`);
return join(__dirname, `../fixtures/plugin/${name}.js`);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of these sort of changes were just moving CLI tests into test/cli and everything else into test/unit to enable running the unit tests by themselves more quickly. That’s somewhat nullified by the required tsc -p . step, but such is life. I’m open to suggestions for ways to better speed up the tests (@mcMickJuice mentioned Jest).

@@ -0,0 +1 @@
--require source-map-support/register
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We were already building sourcemaps, but we need this to make stack traces actually use them.

export class RealTestServer {
private server: Server;

readonly protocol: string = 'http';
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this actually should be http:.

res: ServerResponse
) => void;

export class RealTestServer {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn’t look very hard for another package that implements something like this. I found mocking libraries for http but those always felt messy to me, and so I chose to have a real local server that could be hit for testing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

import AstExplorerResolver from '../../../src/resolvers/AstExplorerResolver';
import { startServer } from '../TestServer';

describe('AstExplorerResolver', function() {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should probably add some failure cases here.

import { join } from 'path';
import FileSystemResolver from '../../../src/resolvers/FileSystemResolver';

describe('FileSystemResolver', function() {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should probably add some failure cases here.

@@ -1,37 +1,21 @@
{
"rules": {
"extends": [
"tslint-config-prettier"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up having to do what you were suggesting on #56 to make sure tslint and prettier didn’t conflict, @mcMickJuice.

@eventualbuddha
Copy link
Collaborator Author

Okay, hopefully that’s enough context for you to take a look @mcMickJuice. Also, @fkling, does this look sane from your perspective? I don’t know whether there’s some obvious cases where using astexplorer.net in this way will fail.

})
);

this._pluginCache = [...(await localPlugins), ...(await remotePlugins)];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isnt async/await the best?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

Copy link
Contributor

@mcMickJuice mcMickJuice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the approach is solid! I haven't run the code yet but just looking through changes and making minor comments and tweaks, it looks awesome.

*/
export default class FileSystemResolver implements Resolver {
constructor(
private readonly optionalExtensions: Set<string> = DEFAULT_PLUGIN_EXTENSIONS
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would the user pass in additional extensions?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They wouldn't at this point. We might be able to use this for #57.

import { resolve } from 'path';
import Resolver from './Resolver';

const DEFAULT_PLUGIN_EXTENSIONS = new Set(['.js']);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we support .ts by default as well?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe? But we don't actually support them out of the box.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. then disregard my comment

throw new NetworkLoadError(response);
}

let filename = tmp();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We create temporary files in the CLITests. Maybe make a note to use this package for temporary tests files as well?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I added a comment.

src/index.ts Outdated
@@ -13,6 +13,7 @@ ${$0} [OPTIONS] [PATH … | --stdio]

OPTIONS
-p, --plugin PLUGIN Transform sources with PLUGIN (allows multiple).
---remote-plugin URL Fetch a plugin from URL (allows multiple).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove extra -

res: ServerResponse
) => void;

export class RealTestServer {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

) {}

private *enumerateCandidateSources(source: string): IterableIterator<string> {
yield resolve(source);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I misunderstood the flow in this function. optionalExtensions is used here so that the user can provide a path path/to/filename.js or path/to/filename, where currently if extension is omitted, we will try to resolve using this extension array. Is that correct?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's right.

@@ -17,6 +17,10 @@
"*.ts": [
"prettier --parser typescript --write",
"git add"
],
"*.md": [
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remembered that prettier can reformat Markdown, so since I was editing the README anyway I figured I'd add that here.

"resolve": "^1.5.0"
"resolve": "^1.5.0",
"tmp": "^0.0.33",
"whatwg-url": "^6.4.0"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using this instead of the builtin url since node v6.x doesn't have url.URL.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests pass now!

import { URL } from 'whatwg-url';
import NetworkResolver from './NetworkResolver';

const EDITOR_HASH_PATTERN = /^#\/gist\/(\w+)(?:\/(\w+))?$/;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just changed this to not include the hostname, since we're supposed to be able to customize it in the constructor argument.

@fkling
Copy link

fkling commented Jan 14, 2018

Seems fine from my side. You could consider using the github API directly instead of going through astexplorer (this is what astexplorer itself is doing), but probably not a big deal.

Note that the structure of the data in the gists could change in the future (in other words, I can't promise that I will remember to notify you about such changes, but I'll try 😉 )

@eventualbuddha
Copy link
Collaborator Author

Thanks @fkling! I'll probably leave it as-is for now. In the future it might be nice to allow more flexibly pulling files out of gists, something like this:

$ codemod --remote-plugin gist:68827467161b95ee48048ff906fab6d8#transform.js

Exactly what syntax to use for that I'm not sure.

@eventualbuddha eventualbuddha merged commit 706ebb1 into master Jan 14, 2018
@eventualbuddha eventualbuddha deleted the run-from-astexplorer branch January 14, 2018 20:38
@eventualbuddha
Copy link
Collaborator Author

Alright, this is now available in v1.4.0!

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

Successfully merging this pull request may close these issues.

4 participants