Skip to content

Introduce @glint/environment-ember-loose #34

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

Merged
merged 8 commits into from
Dec 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 34 additions & 9 deletions packages/config/__tests__/environment.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { GlintEnvironment } from '../src';

describe('Environments', () => {
describe('moduleMayHaveTagImports', () => {
describe('template tags config', () => {
test('locating a single tag', () => {
let env = new GlintEnvironment({
let env = new GlintEnvironment('test-env', {
tags: {
'my-cool-environment': { hbs: { typesSource: 'whatever' } },
},
Expand All @@ -13,7 +13,7 @@ describe('Environments', () => {
});

test('locating one of several tags', () => {
let env = new GlintEnvironment({
let env = new GlintEnvironment('test-env', {
tags: {
'my-cool-environment': { hbs: { typesSource: 'whatever' } },
'another-env': { tagMe: { typesSource: 'over-here' } },
Expand All @@ -24,26 +24,51 @@ describe('Environments', () => {
expect(env.moduleMayHaveTagImports('import foo from "another-env"\n')).toBe(true);
});

test('checking a definitely-unused module', () => {
let env = new GlintEnvironment({
test('checking a module with no tags in use', () => {
let env = new GlintEnvironment('test-env', {
tags: {
'my-cool-environment': { hbs: { typesSource: 'whatever' } },
},
});

expect(env.moduleMayHaveTagImports('import { hbs } from "another-env"\n')).toBe(false);
});
});

describe('getConfiguredTemplateTags', () => {
test('returns the given tag config', () => {
test('getting specified template tag config', () => {
let tags = {
'@glimmerx/component': { hbs: { typesSource: '@glint/environment-glimmerx/types' } },
};

let env = new GlintEnvironment({ tags });
let env = new GlintEnvironment('test-env', { tags });

expect(env.getConfiguredTemplateTags()).toBe(tags);
});
});

describe('standalone template config', () => {
test('no standalone template support', () => {
let env = new GlintEnvironment('test-env', {});

expect(env.getTypesForStandaloneTemplate()).toBeUndefined();
expect(env.getPossibleScriptPaths('hello.hbs')).toEqual([]);
expect(env.getPossibleTemplatePaths('hello.ts')).toEqual([]);
});

test('reflecting specified configuration', () => {
let env = new GlintEnvironment('test-env', {
template: {
typesPath: '@glint/test-env/types',
getPossibleTemplatePaths: (script) => [script.replace('.ts', '.hbs')],
getPossibleScriptPaths: (template) => [
template.replace('.hbs', '.ts'),
template.replace('.hbs', '.js'),
],
},
});

expect(env.getTypesForStandaloneTemplate()).toEqual('@glint/test-env/types');
expect(env.getPossibleTemplatePaths('hello.ts')).toEqual(['hello.hbs']);
expect(env.getPossibleScriptPaths('hello.hbs')).toEqual(['hello.ts', 'hello.js']);
});
});
});
49 changes: 41 additions & 8 deletions packages/config/src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import resolve from 'resolve';
import escapeStringRegexp from 'escape-string-regexp';

export type GlintEnvironmentConfig = {
tags: GlintTagsConfig;
tags?: GlintTagsConfig;
template?: GlintTemplateConfig;
};

export type GlintTagsConfig = {
Expand All @@ -13,20 +14,52 @@ export type GlintTagsConfig = {
};
};

export type GlintTemplateConfig = {
typesPath: string;
getPossibleTemplatePaths(scriptPath: string): Array<string>;
getPossibleScriptPaths(templatePath: string): Array<string>;
};

export class GlintEnvironment {
private tags: GlintTagsConfig;
private tagConfig: GlintTagsConfig;
private standaloneTemplateConfig?: GlintTemplateConfig;
private tagImportRegexp: RegExp;

public constructor(config: GlintEnvironmentConfig) {
this.tags = config.tags;
public constructor(public readonly name: string, config: GlintEnvironmentConfig) {
this.tagConfig = config.tags ?? {};
this.standaloneTemplateConfig = config.template;
this.tagImportRegexp = this.buildTagImportRegexp();
}

public static load(name: string, { rootDir = '.' } = {}): GlintEnvironment {
// eslint-disable-next-line @typescript-eslint/no-var-requires
let envModule = require(locateEnvironment(name, rootDir));
let envFunction = envModule.default ?? envModule;
return new GlintEnvironment(envFunction());
return new GlintEnvironment(name, envFunction());
}

/**
* Returns the import path that should be used for `@glint/template`-derived
* types to drive typechecking for standalone template files, if this
* environment supports such templates.
*/
public getTypesForStandaloneTemplate(): string | undefined {
return this.standaloneTemplateConfig?.typesPath;
}

/**
* Given the path of a script, returns an array of candidate paths where
* a template corresponding to that script might be located.
*/
public getPossibleTemplatePaths(scriptPath: string): Array<string> {
return this.standaloneTemplateConfig?.getPossibleTemplatePaths(scriptPath) ?? [];
}

/**
* Given the path of a template, returns an array of candidate paths where
* a script corresponding to that script might be located.
*/
public getPossibleScriptPaths(templatePath: string): Array<string> {
return this.standaloneTemplateConfig?.getPossibleScriptPaths(templatePath) ?? [];
}

/**
Expand All @@ -46,11 +79,11 @@ export class GlintEnvironment {
* for each tag can be found.
*/
public getConfiguredTemplateTags(): GlintTagsConfig {
return this.tags;
return this.tagConfig;
}

private buildTagImportRegexp(): RegExp {
let importSources = Object.keys(this.tags);
let importSources = Object.keys(this.tagConfig);
let regexpSource = importSources.map(escapeStringRegexp).join('|');
return new RegExp(regexpSource);
}
Expand Down
1 change: 1 addition & 0 deletions packages/environment-ember-loose/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/lib/
2 changes: 2 additions & 0 deletions packages/environment-ember-loose/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
lib/
tsconfig.tsbuildinfo
3 changes: 3 additions & 0 deletions packages/environment-ember-loose/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# `@glint/environment-ember-loose`

This package contains the information necessary for glint to typecheck a standard (non-(strict-mode)[http://emberjs.github.io/rfcs/0496-handlebars-strict-mode.html]) Ember.js project.
98 changes: 98 additions & 0 deletions packages/environment-ember-loose/__tests__/component.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import Component from '@ember/component';
import {
template,
invokeBlock,
resolve,
ResolveContext,
yieldToBlock,
} from '@glint/environment-ember-loose/types';
import { expectTypeOf } from 'expect-type';
import { NoNamedArgs, NoYields } from '@glint/template/-private';

{
class NoArgsComponent extends Component<NoNamedArgs, NoYields> {
static template = template(function* (𝚪: ResolveContext<NoArgsComponent>) {
𝚪;
});
}

// @ts-expect-error: extra named arg
resolve(NoArgsComponent)({ foo: 'bar' });

// @ts-expect-error: extra positional arg
resolve(NoArgsComponent)({}, 'oops');

// @ts-expect-error: never yields, so shouldn't accept blocks
invokeBlock(resolve(NoArgsComponent)({}), { default() {} });

invokeBlock(resolve(NoArgsComponent)({}), {});
}

{
class StatefulComponent extends Component {
private foo = 'hello';

static template = template(function* (𝚪: ResolveContext<StatefulComponent>) {
expectTypeOf(𝚪.this.foo).toEqualTypeOf<string>();
expectTypeOf(𝚪.this).toEqualTypeOf<StatefulComponent>();
expectTypeOf(𝚪.args).toEqualTypeOf<NoNamedArgs>();
});
}

invokeBlock(resolve(StatefulComponent)({}), {});
}

{
interface YieldingComponentArgs<T> {
values: Array<T>;
}

interface YieldingComponentYields<T> {
default: [T];
inverse?: [];
}

class YieldingComponent<T> extends Component<
YieldingComponentArgs<T>,
YieldingComponentYields<T>
> {
static template = template(function* <T>(𝚪: ResolveContext<YieldingComponent<T>>) {
expectTypeOf(𝚪.this).toEqualTypeOf<YieldingComponent<T>>();
expectTypeOf(𝚪.args).toEqualTypeOf<{ values: T[] }>();

if (𝚪.args.values.length) {
yieldToBlock(𝚪, 'default', 𝚪.args.values[0]);
} else {
yieldToBlock(𝚪, 'inverse');
}
});
}

// @ts-expect-error: missing required arg
resolve(YieldingComponent)({});

// @ts-expect-error: incorrect type for arg
resolve(YieldingComponent)({ values: 'hello' });

// @ts-expect-error: extra arg
resolve(YieldingComponent)({ values: [1, 2, 3], oops: true });

// @ts-expect-error: invalid block name
invokeBlock(resolve(YieldingComponent)({ values: [] }), { *foo() {} }, 'foo');

invokeBlock(resolve(YieldingComponent)({ values: [1, 2, 3] }), {
default(value) {
expectTypeOf(value).toEqualTypeOf<number>();
},
});

invokeBlock(resolve(YieldingComponent)({ values: [1, 2, 3] }), {
default(...args) {
expectTypeOf(args).toEqualTypeOf<[number]>();
},

inverse(...args) {
expectTypeOf(args).toEqualTypeOf<[]>();
},
});
}
75 changes: 75 additions & 0 deletions packages/environment-ember-loose/__tests__/helper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import Helper, { helper } from '@ember/component/helper';
import { resolve } from '@glint/environment-glimmerx/types';
import { expectTypeOf } from 'expect-type';
import { NoNamedArgs } from '@glint/template/-private';

// Functional helper: positional params
{
let definition = helper(<T, U>([a, b]: [T, U]) => a || b);
let or = resolve(definition);

expectTypeOf(or).toEqualTypeOf<<T, U>(args: NoNamedArgs, t: T, u: U) => T | U>();

// @ts-expect-error: extra named arg
or({ hello: true }, 'a', 'b');

// @ts-expect-error: missing positional arg
or({}, 'a');

// @ts-expect-error: extra positional arg
or({}, 'a', 'b', 'c');

expectTypeOf(or({}, 'a', 'b')).toEqualTypeOf<string>();
expectTypeOf(or({}, 'a', true)).toEqualTypeOf<string | boolean>();
expectTypeOf(or({}, false, true)).toEqualTypeOf<boolean>();
}

// Functional helper: named params
{
let definition = helper(<T>(_: [], { value, count }: { value: T; count?: number }) => {
return Array.from({ length: count ?? 2 }, () => value);
});

let repeat = resolve(definition);

expectTypeOf(repeat).toEqualTypeOf<<T>(args: { value: T; count?: number }) => Array<T>>();

// @ts-expect-error: extra positional arg
repeat({ word: 'hi' }, 123);

// @ts-expect-error: missing required named arg
repeat({ count: 3 });

// @ts-expect-error: extra named arg
repeat({ word: 'hello', foo: true });

expectTypeOf(repeat({ value: 'hi' })).toEqualTypeOf<Array<string>>();
expectTypeOf(repeat({ value: 123, count: 3 })).toEqualTypeOf<Array<number>>();
}

// Class-based helper
{
type RepeatArgs<T> = { value: T; count?: number };
class RepeatHelper<T> extends Helper<[], RepeatArgs<T>, Array<T>> {
// @ts-expect-error: this is incompatible with the base definition of `compute` in the upstream types
compute(_: [], { value, count }: RepeatArgs<T>): Array<T> {
return Array.from({ length: count ?? 2 }, () => value);
}
}

let repeat = resolve(RepeatHelper);

expectTypeOf(repeat).toEqualTypeOf<<T>(args: { value: T; count?: number }) => Array<T>>();

// @ts-expect-error: extra positional arg
repeat({ word: 'hi' }, 123);

// @ts-expect-error: missing required named arg
repeat({ count: 3 });

// @ts-expect-error: extra named arg
repeat({ word: 'hello', foo: true });

expectTypeOf(repeat({ value: 'hi' })).toEqualTypeOf<Array<string>>();
expectTypeOf(repeat({ value: 123, count: 3 })).toEqualTypeOf<Array<number>>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { expectTypeOf } from 'expect-type';
import { Globals, resolve } from '@glint/environment-ember-loose/types';

let action = resolve(Globals['action']);

// Basic plumbing
expectTypeOf(action({}, () => 'hi')).toEqualTypeOf<() => string>();
expectTypeOf(action({}, <T>(value: T) => value)).toEqualTypeOf<<T>(value: T) => T>();

// Binding parameters
expectTypeOf(action({}, (x: string, y: number) => x.padStart(y), 'hello')).toEqualTypeOf<
(y: number) => string
>();
expectTypeOf(action({}, (x: string, y: number) => x.padStart(y), 'hello', 123)).toEqualTypeOf<
() => string
>();
expectTypeOf(action({}, <T>(value: T) => value, 'hello')).toEqualTypeOf<() => string>();

// @ts-expect-error: invalid parameter type
action({}, (x: string) => x, 123);

// Extracting a value from a particular key
expectTypeOf(action({ value: 'length' }, () => 'hello')).toEqualTypeOf<() => number>();

// @ts-expect-error: invalid key
action({ value: 'len' }, () => 'hello');
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { expectTypeOf } from 'expect-type';
import { Globals, resolve, invokeBlock } from '@glint/environment-ember-loose/types';

let eachIn = resolve(Globals['each-in']);

invokeBlock(eachIn({}, { a: 5, b: 3 }), {
default(key, value) {
expectTypeOf(key).toEqualTypeOf<'a' | 'b'>();
expectTypeOf(value).toEqualTypeOf<number>();
},
});
Loading