Skip to content

Commit 5f6e8ad

Browse files
committed
Add Runner
1 parent 5766cf3 commit 5f6e8ad

File tree

10 files changed

+237
-34
lines changed

10 files changed

+237
-34
lines changed

TODO.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
3. ...?
1818

1919
FIXME: Refactor cli.js
20+
FIXME: optional deps?
21+
FIXME: rename resolver -> telemetry
22+
FIXME: move custom helpers into resolver?
2023

2124
FIXME: Clean up logging shenanigans (e.g. console.log/info, debug)
2225

bin/cli.js

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,11 @@
11
#!/usr/bin/env node
22
'use strict';
33

4-
const debug = require('debug')('ember-no-implicit-this-codemod');
5-
const {
6-
gatherTelemetryForUrl,
7-
analyzeEmberObject,
8-
getTelemetry,
9-
} = require('ember-codemods-telemetry-helpers');
10-
11-
(async () => {
12-
let args = process.argv;
13-
14-
// FIXME
15-
if (args.includes('--telemetry=runtime')) {
16-
const appLocation = args[2];
17-
18-
debug('Gathering telemetry data from %s ...', appLocation);
19-
await gatherTelemetryForUrl(appLocation, analyzeEmberObject);
20-
21-
let telemetry = getTelemetry();
22-
debug('Gathered telemetry on %d modules', Object.keys(telemetry).length);
23-
24-
args = args.slice(3);
25-
} else {
26-
args = args.slice(2);
27-
}
28-
29-
require('codemod-cli').runTransform(__dirname, 'no-implicit-this', args, 'hbs');
30-
})().catch((error) => {
31-
console.error(error);
32-
process.exit(1);
33-
});
4+
const Runner = require('../transforms/no-implicit-this/helpers/runner');
5+
6+
Runner.withArgs(process.argv.slice(2))
7+
.run()
8+
.catch((error) => {
9+
console.error(error);
10+
process.exit(1);
11+
});

helpers/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@
22
export function isRecord<R extends Record<string, unknown>>(value: unknown): value is R {
33
return value !== null && typeof value === 'object';
44
}
5+
6+
export function assert(message: string, cond: unknown): asserts cond {
7+
if (!cond) throw new Error(message);
8+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"debug": "^4.1.1",
5353
"ember-codemods-telemetry-helpers": "^2.1.0",
5454
"ember-template-recast": "^6.1.4",
55+
"yargs": "^17.7.2",
5556
"zod": "^3.21.4"
5657
},
5758
"devDependencies": {
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { runTransform } from 'codemod-cli';
2+
import Debug from 'debug';
3+
import {
4+
analyzeEmberObject,
5+
gatherTelemetryForUrl,
6+
getTelemetry,
7+
} from 'ember-codemods-telemetry-helpers';
8+
import fs from 'node:fs';
9+
import http from 'node:http';
10+
import yargs from 'yargs';
11+
import { assert, isRecord } from '../../../helpers/types';
12+
13+
const debug = Debug('ember-no-implicit-this-codemod');
14+
15+
export interface Options {
16+
config: string | undefined;
17+
telemetry: 'auto' | 'embroider' | 'runtime';
18+
url: string;
19+
}
20+
21+
export const Parser = yargs
22+
.option('config', {
23+
describe: 'Path to config file FIXME',
24+
normalize: true,
25+
})
26+
.option('telemetry', {
27+
describe: 'Telemetry source FIXME',
28+
choices: ['auto', 'embroider', 'runtime'] as const,
29+
default: 'auto' as const,
30+
})
31+
.option('url', {
32+
describe: 'URL source FIXME',
33+
default: 'http://localhost:4200',
34+
});
35+
36+
export function parseOptions(args: string[]): Options {
37+
return Parser.parseSync(args);
38+
}
39+
40+
export function validateOptions(options: unknown): asserts options is Options {
41+
assert('options must be an object', isRecord(options));
42+
43+
const { config, telemetry, url } = options;
44+
45+
assert('config must be a string', config === undefined || typeof config === 'string');
46+
assert(
47+
'telemetry must be one of auto, embroider, or runtime',
48+
typeof telemetry === 'string' && ['auto', 'embroider', 'runtime'].includes(telemetry)
49+
);
50+
assert('url must be a string', typeof url === 'string');
51+
}
52+
53+
export interface DetectResult {
54+
isServerRunning: boolean;
55+
hasStage2Output: boolean;
56+
}
57+
58+
type MaybePromise<T> = T | Promise<T>;
59+
60+
export default class Runner {
61+
static withArgs(args: string[]): Runner {
62+
return new Runner(parseOptions(args));
63+
}
64+
65+
static withOptions(options: Partial<Options> = {}): Runner {
66+
return new Runner({
67+
config: undefined,
68+
telemetry: 'auto',
69+
url: 'http://localhost:4200',
70+
...options,
71+
});
72+
}
73+
74+
constructor(private readonly options: Options) {
75+
validateOptions(options);
76+
}
77+
78+
async detectTelemetryType(
79+
detect: MaybePromise<DetectResult> = this.detect()
80+
): Promise<'embroider' | 'runtime'> {
81+
const result = await detect;
82+
const type = this.options.telemetry;
83+
switch (type) {
84+
case 'embroider': {
85+
if (result.hasStage2Output) {
86+
return 'embroider';
87+
} else {
88+
throw new Error('Please run the thing first FIXME');
89+
}
90+
}
91+
92+
case 'runtime': {
93+
if (result.hasStage2Output) {
94+
console.warn('Are you sure you want to use runtime telemetry? FIXME');
95+
}
96+
97+
if (result.isServerRunning) {
98+
return 'runtime';
99+
} else {
100+
throw new Error('Please run the server or pass correct URL FIXME');
101+
}
102+
}
103+
104+
case 'auto': {
105+
if (result.hasStage2Output) {
106+
return 'embroider';
107+
} else if (result.isServerRunning) {
108+
return 'runtime';
109+
} else {
110+
throw new Error('Please RTFM FIXME');
111+
}
112+
}
113+
}
114+
}
115+
116+
async run(): Promise<void> {
117+
const telemetryType = await this.detectTelemetryType();
118+
119+
if (telemetryType === 'runtime') {
120+
debug('Gathering telemetry data from %s ...', this.options.url);
121+
await gatherTelemetryForUrl(this.options.url, analyzeEmberObject);
122+
123+
const telemetry = getTelemetry() as Record<string, unknown>; // FIXME
124+
debug('Gathered telemetry on %d modules', Object.keys(telemetry).length);
125+
}
126+
127+
runTransform(__dirname, 'no-implicit-this', process.argv, 'hbs');
128+
}
129+
130+
private async detect(): Promise<DetectResult> {
131+
const isServerRunning = await new Promise<boolean>((resolve) => {
132+
http.get(this.options.url, () => resolve(true)).on('error', () => resolve(false));
133+
});
134+
135+
const hasStage2Output = fs.existsSync('dist/.stage2-output');
136+
137+
return { isServerRunning, hasStage2Output };
138+
}
139+
}

transforms/no-implicit-this/test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
NodeResolution,
1010
RuntimeResolver,
1111
} from './helpers/resolver';
12+
import Runner, { Parser, DetectResult } from './helpers/runner';
1213
import { setupResolver } from './test-helpers';
1314

1415
process.env['TESTING'] = 'true';
@@ -108,3 +109,71 @@ describe('Resolver', () => {
108109
});
109110
});
110111
});
112+
113+
describe('Parser', () => {
114+
test('args parsing', () => {
115+
const parse = (...args: string[]) => Parser.exitProcess(false).parseSync(args);
116+
117+
expect(parse().config).toEqual(undefined);
118+
expect(parse('--config=config.json').config).toEqual('config.json');
119+
expect(parse('--config', 'config.json').config).toEqual('config.json');
120+
121+
expect(parse().telemetry).toEqual('auto');
122+
expect(parse('--telemetry=auto').telemetry).toEqual('auto');
123+
expect(parse('--telemetry', 'auto').telemetry).toEqual('auto');
124+
expect(parse('--telemetry=embroider').telemetry).toEqual('embroider');
125+
expect(parse('--telemetry', 'embroider').telemetry).toEqual('embroider');
126+
expect(parse('--telemetry=runtime').telemetry).toEqual('runtime');
127+
expect(parse('--telemetry', 'runtime').telemetry).toEqual('runtime');
128+
expect(() => parse('--telemetry', 'zomg')).toThrow('zomg');
129+
130+
expect(parse().url).toEqual('http://localhost:4200');
131+
expect(parse('--url=http://zomg.localhost:8888').url).toEqual('http://zomg.localhost:8888');
132+
expect(parse('--url', 'http://zomg.localhost:8888').url).toEqual('http://zomg.localhost:8888');
133+
});
134+
});
135+
136+
describe('Runner', () => {
137+
test('telemetry detection', async () => {
138+
let runner: Runner;
139+
140+
const expectRuntime = async (result: DetectResult) =>
141+
expect(runner.detectTelemetryType(result)).resolves.toEqual('runtime');
142+
143+
const expectEmbroider = async (result: DetectResult) =>
144+
expect(runner.detectTelemetryType(result)).resolves.toEqual('embroider');
145+
146+
const expectError = async (result: DetectResult, message: string) =>
147+
expect(runner.detectTelemetryType(result)).rejects.toThrow(message);
148+
149+
runner = Runner.withOptions({ telemetry: 'auto' });
150+
await expectEmbroider({ isServerRunning: false, hasStage2Output: true });
151+
await expectEmbroider({ isServerRunning: true, hasStage2Output: true });
152+
await expectRuntime({ isServerRunning: true, hasStage2Output: false });
153+
await expectError({ isServerRunning: false, hasStage2Output: false }, 'Please RTFM FIXME');
154+
155+
runner = Runner.withOptions({ telemetry: 'embroider' });
156+
await expectEmbroider({ isServerRunning: false, hasStage2Output: true });
157+
await expectEmbroider({ isServerRunning: true, hasStage2Output: true });
158+
await expectError(
159+
{ isServerRunning: true, hasStage2Output: false },
160+
'Please run the thing first FIXME'
161+
);
162+
await expectError(
163+
{ isServerRunning: false, hasStage2Output: false },
164+
'Please run the thing first FIXME'
165+
);
166+
167+
runner = Runner.withOptions({ telemetry: 'runtime' });
168+
await expectError(
169+
{ isServerRunning: false, hasStage2Output: true },
170+
'Please run the server or pass correct URL FIXME'
171+
);
172+
await expectRuntime({ isServerRunning: true, hasStage2Output: true });
173+
await expectRuntime({ isServerRunning: true, hasStage2Output: false });
174+
await expectError(
175+
{ isServerRunning: false, hasStage2Output: false },
176+
'Please run the server or pass correct URL FIXME'
177+
);
178+
});
179+
});

tsconfig.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
"baseUrl": ".",
1111
"paths": {
1212
"*": ["types/*"]
13-
}
13+
},
14+
"noEmitOnError": false,
15+
"allowJs": false,
16+
"checkJs": false
1417
},
1518
"include": ["transforms/**/*", "test/**/*", "helpers/**/*"],
1619
"exclude": ["**/__testfixtures__/**/*", "test/fixtures/**/*"]

types/codemod-cli.d.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
import { JSCodeshift } from 'jscodeshift';
2-
31
declare module 'codemod-cli' {
42
export function getOptions(): unknown;
3+
export function runTransform(
4+
root: string,
5+
transformName: string,
6+
args: string[],
7+
type: 'hbs' | 'js'
8+
): unknown;
59
export function runTransformTest(options: Record<string, unknown>): unknown;
610
}

types/ember-codemods-telemetry-helpers.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@ declare module 'ember-codemods-telemetry-helpers' {
22
export function getTelemetry(): unknown;
33
export function getTelemetryFor(filePath: string): unknown;
44
export function setTelemetry(telemetry: unknown): void;
5+
export function gatherTelemetryForUrl(url: string, gatherFn: () => unknown): Promise<unknown>;
6+
export function analyzeEmberObject(): unknown;
57
}

yarn.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11664,7 +11664,7 @@ yargs@^16.0.0, yargs@^16.2.0:
1166411664
y18n "^5.0.5"
1166511665
yargs-parser "^20.2.2"
1166611666

11667-
yargs@^17.0.1, yargs@^17.3.1:
11667+
yargs@^17.0.1, yargs@^17.3.1, yargs@^17.7.2:
1166811668
version "17.7.2"
1166911669
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
1167011670
integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==

0 commit comments

Comments
 (0)