Skip to content

Commit ab5fbf7

Browse files
committed
Refactor options
1 parent 73e1534 commit ab5fbf7

File tree

9 files changed

+226
-226
lines changed

9 files changed

+226
-226
lines changed

TODO.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,7 @@
1616
2. Look at second arg to `compatBuild`
1717
3. ...?
1818

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

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

bin/cli.js

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

4-
const Runner = require('../transforms/no-implicit-this/helpers/runner').default;
4+
const { Runner } = require('../transforms/no-implicit-this/helpers/options');
55

66
const args = process.argv.slice(2);
77

helpers/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ export function isRecord<R extends Record<string, unknown>>(value: unknown): val
33
return value !== null && typeof value === 'object';
44
}
55

6-
export function assert(message: string, cond: unknown): asserts cond {
6+
export function assert(message: string): never;
7+
export function assert(message: string, cond: unknown): asserts cond;
8+
export function assert(message: string, cond?: unknown): asserts cond {
79
if (!cond) throw new Error(message);
810
}

transforms/no-implicit-this/helpers/options.ts

Lines changed: 172 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,191 @@
1-
import { getOptions as getCLIOptions } from 'codemod-cli';
1+
import { getOptions as getCodemodOptions, runTransform } from 'codemod-cli';
2+
import Debug from 'debug';
3+
import {
4+
analyzeEmberObject,
5+
gatherTelemetryForUrl,
6+
getTelemetry,
7+
} from 'ember-codemods-telemetry-helpers';
28
import fs from 'node:fs';
9+
import http from 'node:http';
310
import path from 'node:path';
11+
import yargs from 'yargs';
412
import { ZodError, ZodType, z } from 'zod';
13+
import { assert } from '../../../helpers/types';
514
import Resolver, { EmbroiderResolver, MockResolver, RuntimeResolver } from './resolver';
615

7-
export interface Options {
8-
customHelpers: string[];
9-
resolver: Resolver;
16+
const debug = Debug('ember-no-implicit-this-codemod');
17+
18+
const DEFAULT_ROOT = 'app';
19+
const DEFAULT_URL = 'http://localhost:4200';
20+
21+
export const Parser = yargs
22+
.option('root', {
23+
describe: 'app root FIXME',
24+
default: DEFAULT_ROOT,
25+
normalize: true,
26+
})
27+
.option('config', {
28+
describe: 'Path to config file FIXME',
29+
normalize: true,
30+
})
31+
.option('telemetry', {
32+
describe: 'Telemetry source FIXME',
33+
choices: ['auto', 'embroider', 'runtime'] as const,
34+
default: 'auto' as const,
35+
})
36+
.option('url', {
37+
describe: 'URL source FIXME',
38+
default: DEFAULT_URL,
39+
});
40+
41+
export function parseOptions(args: string[]): Options {
42+
return Parser.parseSync(args);
1043
}
1144

12-
const CLIOptions = z.object({
45+
const Options = z.object({
46+
root: z.string().default(DEFAULT_ROOT),
1347
config: z.string().optional(),
14-
// FIXME: Defaulting to 'runtime' isn't quite correct
15-
telemetry: z.union([z.literal('runtime'), z.literal('embroider')]).default('runtime'),
16-
// FIXME: Optionally allow angle-bracket conversion for components
17-
// FIXME: Optionally add parens for helpers
48+
telemetry: z
49+
.union([z.literal('auto'), z.literal('embroider'), z.literal('runtime')])
50+
.default('auto'),
51+
url: z.string().url().default(DEFAULT_URL),
1852
});
1953

20-
type CLIOptions = z.infer<typeof CLIOptions>;
21-
22-
const FileOptions = z.object({
23-
helpers: z.array(z.string()),
24-
});
54+
type Options = z.infer<typeof Options>;
2555

26-
type FileOptions = z.infer<typeof FileOptions>;
56+
const STAGE2_OUTPUT_FILE = 'dist/.stage2-output';
2757

28-
/**
29-
* Returns custom options object to support the custom helpers config path passed
30-
* by the user.
31-
*/
32-
export function getOptions(): Options {
33-
const cliOptions = parse(getCLIOptions(), CLIOptions);
34-
return {
35-
customHelpers: getCustomHelpersFromConfig(cliOptions.config),
36-
resolver: buildResolver(cliOptions),
37-
};
58+
export interface DetectResult {
59+
isServerRunning: boolean;
60+
hasTelemetryOutput: boolean;
61+
hasStage2Output: boolean;
3862
}
3963

40-
function buildResolver(cliOptions: CLIOptions): Resolver {
41-
if (process.env['TESTING']) {
42-
return MockResolver.build();
43-
} else {
44-
return cliOptions.telemetry === 'runtime' ? RuntimeResolver.build() : EmbroiderResolver.build();
64+
type MaybePromise<T> = T | Promise<T>;
65+
66+
export class Runner {
67+
static withArgs(args: string[]): Runner {
68+
return new Runner(parseOptions(args));
69+
}
70+
71+
static withOptions(options: Partial<Options> = {}): Runner {
72+
return new Runner({
73+
root: 'app',
74+
config: undefined,
75+
telemetry: 'auto',
76+
url: 'http://localhost:4200',
77+
...options,
78+
});
79+
}
80+
81+
static fromCodemodOptions(): Runner {
82+
const options = parse(getCodemodOptions(), Options);
83+
return Runner.withOptions(options);
84+
}
85+
86+
constructor(private readonly options: Options) {}
87+
88+
async detectTelemetryType(
89+
detect: MaybePromise<DetectResult> = this.detect()
90+
): Promise<'embroider' | 'runtime'> {
91+
const result = await detect;
92+
const type = this.options.telemetry;
93+
switch (type) {
94+
case 'embroider': {
95+
if (result.hasStage2Output) {
96+
return 'embroider';
97+
} else {
98+
throw new Error('Please run the thing first FIXME');
99+
}
100+
}
101+
102+
case 'runtime': {
103+
if (result.hasStage2Output) {
104+
console.warn('Are you sure you want to use runtime telemetry? FIXME');
105+
}
106+
107+
if (result.isServerRunning) {
108+
return 'runtime';
109+
} else {
110+
throw new Error('Please run the server or pass correct URL FIXME');
111+
}
112+
}
113+
114+
case 'auto': {
115+
if (result.hasStage2Output) {
116+
return 'embroider';
117+
} else if (result.isServerRunning) {
118+
return 'runtime';
119+
} else {
120+
throw new Error('Please RTFM FIXME');
121+
}
122+
}
123+
}
124+
}
125+
126+
async gatherTelemetry(): Promise<void> {
127+
const telemetryType = await this.detectTelemetryType();
128+
129+
if (telemetryType === 'runtime') {
130+
debug('Gathering telemetry data from %s ...', this.options.url);
131+
await gatherTelemetryForUrl(this.options.url, analyzeEmberObject);
132+
133+
const telemetry = getTelemetry() as Record<string, unknown>; // FIXME
134+
debug('Gathered telemetry on %d modules', Object.keys(telemetry).length);
135+
}
136+
}
137+
138+
async run(root: string, args: string[]): Promise<void> {
139+
await this.gatherTelemetry();
140+
runTransform(root, 'no-implicit-this', [this.options.root, ...args], 'hbs');
141+
}
142+
143+
buildResolver(): Resolver {
144+
const customHelpers = getCustomHelpersFromConfig(this.options.config);
145+
if (process.env['TESTING']) {
146+
return MockResolver.build(customHelpers);
147+
} else if (this.detectTelemetryTypeSync() === 'embroider') {
148+
return EmbroiderResolver.build(customHelpers);
149+
} else {
150+
return RuntimeResolver.build(customHelpers);
151+
}
152+
}
153+
154+
private detectTelemetryTypeSync(): 'embroider' | 'runtime' {
155+
const result = this.detectSync();
156+
157+
if (result.hasStage2Output) {
158+
return 'embroider';
159+
} else if (result.hasTelemetryOutput) {
160+
return 'runtime';
161+
} else {
162+
assert('gatherTelemetry must be called first');
163+
}
164+
}
165+
166+
private async detect(): Promise<DetectResult> {
167+
const isServerRunning = await new Promise<boolean>((resolve) => {
168+
http.get(this.options.url, () => resolve(true)).on('error', () => resolve(false));
169+
});
170+
171+
return { ...this.detectSync(), isServerRunning };
172+
}
173+
174+
private detectSync(): DetectResult {
175+
const isServerRunning = false;
176+
const hasTelemetryOutput = Object.keys(getTelemetry() as Record<string, unknown>).length > 0;
177+
const hasStage2Output = fs.existsSync(STAGE2_OUTPUT_FILE);
178+
return { isServerRunning, hasTelemetryOutput, hasStage2Output };
45179
}
46180
}
47181

48-
// FIXME: Document
182+
const FileOptions = z.object({
183+
helpers: z.array(z.string()),
184+
});
185+
186+
type FileOptions = z.infer<typeof FileOptions>;
187+
188+
// FIXME: Document option
49189
/**
50190
* Accepts the config path for custom helpers and returns the array of helpers
51191
* if the file path is resolved.
@@ -74,7 +214,7 @@ function parse<Z extends ZodType>(raw: unknown, type: Z): z.infer<Z> {
74214
}
75215
}
76216

77-
function makeConfigError(source: string, error: ZodError<CLIOptions>): ConfigError {
217+
function makeConfigError(source: string, error: ZodError<ZodType>): ConfigError {
78218
const flattened = error.flatten();
79219
const errors = flattened.formErrors;
80220
for (const [key, value] of Object.entries(flattened.fieldErrors)) {

transforms/no-implicit-this/helpers/plugin.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Debug from 'debug';
22
import { AST, builders as b, traverse } from 'ember-template-recast';
3-
import { Options } from './options';
3+
import Resolver from './resolver';
44

55
const debug = Debug('ember-no-implicit-this-codemod:plugin');
66

@@ -11,7 +11,7 @@ const debug = Debug('ember-no-implicit-this-codemod:plugin');
1111
/**
1212
* plugin entrypoint
1313
*/
14-
export default function transform(root: AST.Node, { customHelpers, resolver }: Options) {
14+
export default function transform(root: AST.Node, resolver: Resolver) {
1515
const scopedParams: string[] = [];
1616

1717
const paramTracker = {
@@ -85,12 +85,6 @@ export default function transform(root: AST.Node, { customHelpers, resolver }: O
8585
}
8686

8787
function hasHelper(name: string) {
88-
// FIXME: Move to resolver
89-
if (customHelpers.includes(name)) {
90-
debug(`Skipping \`%s\` because it is a custom configured helper`, name);
91-
return true;
92-
}
93-
9488
if (resolver.has('helper', name)) {
9589
const message = `Skipping \`%s\` because it appears to be a helper from the telemetry data: %s`;
9690
debug(message, name);
@@ -101,12 +95,6 @@ export default function transform(root: AST.Node, { customHelpers, resolver }: O
10195
}
10296

10397
function hasAmbiguous(name: string) {
104-
// FIXME: Move to resolver
105-
if (customHelpers.includes(name)) {
106-
debug(`Skipping \`%s\` because it is a custom configured helper`, name);
107-
return true;
108-
}
109-
11098
if (resolver.has('ambiguous', name)) {
11199
const message = `Skipping \`%s\` because it appears to be a component or helper from the telemetry data: %s`;
112100
debug(message, name);

transforms/no-implicit-this/helpers/resolver.ts

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,16 @@ import { resolve } from 'node:path';
66
import { isKeyword } from './keywords';
77

88
export default abstract class Resolver {
9+
constructor(protected customHelpers: string[]) {}
10+
911
has(type: 'component' | 'helper' | 'ambiguous', name: string): boolean {
1012
if (isKeyword(type, name)) {
1113
return true;
1214
}
15+
// FIXME: Probably should split into `knownHelpers` and `knownComponents`???
16+
if (this.customHelpers.includes(name)) {
17+
return true;
18+
}
1319
switch (type) {
1420
case 'component':
1521
return this.hasComponent(name);
@@ -29,14 +35,14 @@ export default abstract class Resolver {
2935
}
3036

3137
export class RuntimeResolver extends Resolver {
32-
static build(): RuntimeResolver {
38+
static build(customHelpers: string[] = []): RuntimeResolver {
3339
const telemetry = getTelemetry();
3440
const [components, helpers] = populateInvokeables(telemetry);
35-
return new RuntimeResolver(components, helpers);
41+
return new RuntimeResolver(customHelpers, components, helpers);
3642
}
3743

38-
constructor(private components: string[], private helpers: string[]) {
39-
super();
44+
constructor(customHelpers: string[], private components: string[], private helpers: string[]) {
45+
super(customHelpers);
4046
}
4147

4248
hasComponent(name: string): boolean {
@@ -77,17 +83,21 @@ export interface HasNodeResolve {
7783
}
7884

7985
export class EmbroiderResolver extends Resolver {
80-
static build(): EmbroiderResolver {
86+
static build(customHelpers: string[] = []): EmbroiderResolver {
8187
// FIXME: Run build via execa ???
8288
const stage2Output = readFileSync('dist/.stage2-output', 'utf8');
8389
const resolver = new _EmbroiderResolver(
8490
JSON.parse(readFileSync(resolve(stage2Output, '.embroider/resolver.json'), 'utf8'))
8591
);
86-
return new EmbroiderResolver(resolver, resolve(stage2Output, 'app.js'));
92+
return new EmbroiderResolver(customHelpers, resolver, resolve(stage2Output, 'app.js'));
8793
}
8894

89-
constructor(private _resolver: HasNodeResolve, private entryPoint: string) {
90-
super();
95+
constructor(
96+
customHelpers: string[],
97+
private _resolver: HasNodeResolve,
98+
private entryPoint: string
99+
) {
100+
super(customHelpers);
91101
}
92102

93103
override hasComponent(name: string): boolean {
@@ -126,12 +136,12 @@ export class MockResolver extends Resolver {
126136
this.helpers = helpers;
127137
}
128138

129-
static build(): MockResolver {
130-
return new MockResolver(this.components, this.helpers);
139+
static build(customHelpers: string[] = []): MockResolver {
140+
return new MockResolver(customHelpers, this.components, this.helpers);
131141
}
132142

133-
constructor(private components: string[], private helpers: string[]) {
134-
super();
143+
constructor(customHelpers: string[], private components: string[], private helpers: string[]) {
144+
super(customHelpers);
135145
}
136146

137147
hasComponent(name: string): boolean {

0 commit comments

Comments
 (0)