From 1545c1e2665f1ca1a9f6d704e5b0ed3c02c51a80 Mon Sep 17 00:00:00 2001 From: Dan Freeman Date: Sun, 26 Jul 2020 12:30:03 +0200 Subject: [PATCH 1/9] Pull `GlintConfig` into a separate module --- packages/config/src/config.ts | 85 +++++++++++++++++++++++++++++++++++ packages/config/src/index.ts | 69 +--------------------------- 2 files changed, 86 insertions(+), 68 deletions(-) create mode 100644 packages/config/src/config.ts diff --git a/packages/config/src/config.ts b/packages/config/src/config.ts new file mode 100644 index 000000000..1ebd9517b --- /dev/null +++ b/packages/config/src/config.ts @@ -0,0 +1,85 @@ +import path from 'path'; +import { Minimatch, IMinimatch } from 'minimatch'; + +export type GlintConfigInput = { + include?: string | Array; + exclude?: string | Array; +}; + +/** + * This class represents a parsed `.glintrc` file, with methods for interrogating + * project configuration based on the contents of the file. + */ +export class GlintConfig { + public readonly rootDir: string; + + private includeMatchers: Array; + private excludeMatchers: Array; + + public constructor(rootDir: string, config: Record = {}) { + validateConfigInput(config); + + this.rootDir = normalizePath(rootDir); + + let include = Array.isArray(config.include) ? config.include : [config.include ?? '**/*.ts']; + let exclude = Array.isArray(config.exclude) + ? config.exclude + : [config.exclude ?? '**/node_modules/**']; + + this.includeMatchers = this.buildMatchers(include); + this.excludeMatchers = this.buildMatchers(exclude); + } + + /** + * Indicates whether this configuration object applies to the file at the + * given path. + */ + public includesFile(rawFileName: string): boolean { + let fileName = normalizePath(rawFileName); + + return ( + this.excludeMatchers.every((matcher) => !matcher.match(fileName)) && + this.includeMatchers.some((matcher) => matcher.match(fileName)) + ); + } + + private buildMatchers(globs: Array): Array { + return globs.map((glob) => new Minimatch(normalizePath(path.resolve(this.rootDir, glob)))); + } + +} + +function validateConfigInput(input: Record): asserts input is GlintConfigInput { + assert( + typeof input.environment === 'string', + 'Glint config must specify an `environment` string' + ); + + assert( + Array.isArray(input.include) + ? input.include.every((item) => typeof item === 'string') + : !input.include || typeof input.include === 'string', + 'If defined, `include` must be a string or array of strings' + ); + + assert( + Array.isArray(input.exclude) + ? input.exclude.every((item) => typeof item === 'string') + : !input.exclude || typeof input.exclude === 'string', + 'If defined, `exclude` must be a string or array of strings' + ); +} + +function assert(test: unknown, message: string): asserts test { + if (!test) { + throw new Error(`@glint/config: ${message}`); + } +} + +function normalizePath(fileName: string): string { + if (path.sep !== '/') { + return fileName.split(path.sep).join('/'); + } + + return fileName; +} diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 7295eb989..63c69bb5d 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -1,6 +1,6 @@ import path from 'path'; import { cosmiconfigSync } from 'cosmiconfig'; -import { Minimatch, IMinimatch } from 'minimatch'; +import { GlintConfig } from './config'; /** * Loads glint configuration, starting from the given directory @@ -15,71 +15,4 @@ export function loadConfig(from: string): GlintConfig { return new GlintConfig(from); } -export class GlintConfig { - public readonly rootDir: string; - private includeMatchers: Array; - private excludeMatchers: Array; - - public constructor(rootDir: string, config: Record = {}) { - validateInput(config); - - this.rootDir = normalizePath(rootDir); - - let include = Array.isArray(config.include) ? config.include : [config.include ?? '**/*.ts']; - let exclude = Array.isArray(config.exclude) - ? config.exclude - : [config.exclude ?? '**/node_modules/**']; - - this.includeMatchers = this.buildMatchers(include); - this.excludeMatchers = this.buildMatchers(exclude); - } - - public includesFile(rawFileName: string): boolean { - let fileName = normalizePath(rawFileName); - - return ( - this.excludeMatchers.every((matcher) => !matcher.match(fileName)) && - this.includeMatchers.some((matcher) => matcher.match(fileName)) - ); - } - - private buildMatchers(globs: Array): Array { - return globs.map((glob) => new Minimatch(normalizePath(path.resolve(this.rootDir, glob)))); - } -} - -export type GlintConfigInput = { - include?: string | Array; - exclude?: string | Array; -}; - -function validateInput(input: Record): asserts input is GlintConfigInput { - assert( - Array.isArray(input.include) - ? input.include.every((item) => typeof item === 'string') - : !input.include || typeof input.include === 'string', - 'If defined, `include` must be a string or array of strings' - ); - - assert( - Array.isArray(input.exclude) - ? input.exclude.every((item) => typeof item === 'string') - : !input.exclude || typeof input.exclude === 'string', - 'If defined, `exclude` must be a string or array of strings' - ); -} - -function assert(test: unknown, message: string): asserts test { - if (!test) { - throw new Error(`@glint/config: ${message}`); - } -} - -export function normalizePath(fileName: string): string { - if (path.sep !== '/') { - return fileName.split(path.sep).join('/'); - } - - return fileName; -} From 8742ce9733d304b069860b9690dd0f2d02c6a031 Mon Sep 17 00:00:00 2001 From: Dan Freeman Date: Fri, 31 Jul 2020 15:12:58 +0200 Subject: [PATCH 2/9] Introduce the notion of an environment to @glint/config --- packages/config/__tests__/environment.test.ts | 49 +++++++++++++ .../config/__tests__/include-exclude.test.ts | 18 +++-- packages/config/__tests__/load-config.test.ts | 37 +++++++--- packages/config/package.json | 7 +- packages/config/src/config.ts | 19 +++-- packages/config/src/environment.ts | 73 +++++++++++++++++++ packages/config/src/index.ts | 7 +- 7 files changed, 178 insertions(+), 32 deletions(-) create mode 100644 packages/config/__tests__/environment.test.ts create mode 100644 packages/config/src/environment.ts diff --git a/packages/config/__tests__/environment.test.ts b/packages/config/__tests__/environment.test.ts new file mode 100644 index 000000000..ea975ff01 --- /dev/null +++ b/packages/config/__tests__/environment.test.ts @@ -0,0 +1,49 @@ +import { GlintEnvironment } from '../src'; + +describe('Environments', () => { + describe('moduleMayHaveTagImports', () => { + test('locating a single tag', () => { + let env = new GlintEnvironment({ + tags: { + 'my-cool-environment': { hbs: { typesSource: 'whatever' } }, + }, + }); + + expect(env.moduleMayHaveTagImports('import foo from "my-cool-environment"\n')).toBe(true); + }); + + test('locating one of several tags', () => { + let env = new GlintEnvironment({ + tags: { + 'my-cool-environment': { hbs: { typesSource: 'whatever' } }, + 'another-env': { tagMe: { typesSource: 'over-here' } }, + 'and-this-one': { hbs: { typesSource: '✨' } }, + }, + }); + + expect(env.moduleMayHaveTagImports('import foo from "another-env"\n')).toBe(true); + }); + + test('checking a definitely-unused module', () => { + let env = new GlintEnvironment({ + 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', () => { + let tags = { + '@glimmerx/component': { hbs: { typesSource: '@glint/environment-glimmerx/types' } }, + }; + + let env = new GlintEnvironment({ tags }); + + expect(env.getConfiguredTemplateTags()).toBe(tags); + }); + }); +}); diff --git a/packages/config/__tests__/include-exclude.test.ts b/packages/config/__tests__/include-exclude.test.ts index c8b2f7204..b6e4e115a 100644 --- a/packages/config/__tests__/include-exclude.test.ts +++ b/packages/config/__tests__/include-exclude.test.ts @@ -1,11 +1,11 @@ -import path from 'path'; import { GlintConfig } from '../src'; describe('include/exclude configuration', () => { - const root = path.resolve('/foo/bar'); + const root = process.cwd(); + const environment = 'glimmerx'; describe('defaults', () => { - const config = new GlintConfig(root, {}); + const config = new GlintConfig(root, { environment }); test('includes all .ts files within the root', () => { expect(config.includesFile(`${root}/file.ts`)).toBe(true); @@ -30,27 +30,30 @@ describe('include/exclude configuration', () => { describe('custom configuration', () => { test('include glob', () => { - let config = new GlintConfig(root, { include: 'src/**/*.txt' }); + let config = new GlintConfig(root, { environment, include: 'src/**/*.txt' }); expect(config.includesFile(`${root}/src/file.txt`)).toBe(true); expect(config.includesFile(`${root}/file.txt`)).toBe(false); expect(config.includesFile(`${root}/src/index.ts`)).toBe(false); }); test('include array', () => { - let config = new GlintConfig(root, { include: ['**/*.txt', '**/*.ts'] }); + let config = new GlintConfig(root, { environment, include: ['**/*.txt', '**/*.ts'] }); expect(config.includesFile(`${root}/hello/there.txt`)).toBe(true); expect(config.includesFile(`${root}/index.ts`)).toBe(true); expect(config.includesFile(`${root}/file.js`)).toBe(false); }); test('exclude glob', () => { - let config = new GlintConfig(root, { exclude: 'dist/**/*.ts' }); + let config = new GlintConfig(root, { environment, exclude: 'dist/**/*.ts' }); expect(config.includesFile(`${root}/dist/file.ts`)).toBe(false); expect(config.includesFile(`${root}/file.ts`)).toBe(true); }); test('exclude array', () => { - let config = new GlintConfig(root, { exclude: ['dist/**/*.ts', 'vendor/**/*.ts'] }); + let config = new GlintConfig(root, { + environment, + exclude: ['dist/**/*.ts', 'vendor/**/*.ts'], + }); expect(config.includesFile(`${root}/dist/file.ts`)).toBe(false); expect(config.includesFile(`${root}/vendor/file.ts`)).toBe(false); expect(config.includesFile(`${root}/file.ts`)).toBe(true); @@ -58,6 +61,7 @@ describe('include/exclude configuration', () => { test('excludes override includes', () => { let config = new GlintConfig(root, { + environment, include: 'src/**/*.ts', exclude: 'src/**/*.generated.ts', }); diff --git a/packages/config/__tests__/load-config.test.ts b/packages/config/__tests__/load-config.test.ts index 36eec0cc1..363053607 100644 --- a/packages/config/__tests__/load-config.test.ts +++ b/packages/config/__tests__/load-config.test.ts @@ -1,6 +1,7 @@ import os from 'os'; import fs from 'fs'; -import { loadConfig, normalizePath } from '../src'; +import { loadConfig } from '../src'; +import { normalizePath } from '../src/config'; describe('loadConfig', () => { const testDir = `${os.tmpdir()}/glint-config-test-${process.pid}`; @@ -8,28 +9,29 @@ describe('loadConfig', () => { beforeEach(() => { fs.rmdirSync(testDir, { recursive: true }); fs.mkdirSync(testDir); + fs.writeFileSync(`${testDir}/local-env.js`, `exports.tags = { test: true };\n`); }); afterEach(() => { fs.rmdirSync(testDir, { recursive: true }); }); - test('returns a default config if none is found', () => { - let config = loadConfig(testDir); - - expect(config.rootDir).toBe(normalizePath(testDir)); - expect(config.includesFile(`${testDir}/index.ts`)).toBe(true); - expect(config.includesFile(__filename)).toBe(false); + test('throws an error if no config is found', () => { + expect(() => loadConfig(testDir)).toThrow(`Unable to find Glint configuration for ${testDir}`); }); test('locating config in a parent directory', () => { fs.mkdirSync(`${testDir}/deeply/nested/directory`, { recursive: true }); - fs.writeFileSync(`${testDir}/.glintrc`, `include: '**/*.root.ts'`); - fs.writeFileSync(`${testDir}/deeply/.glintrc`, `include: '**/*.nested.ts'`); + fs.writeFileSync(`${testDir}/.glintrc`, `environment: kaboom\ninclude: '**/*.root.ts'`); + fs.writeFileSync( + `${testDir}/deeply/.glintrc`, + `environment: '../local-env'\ninclude: '**/*.nested.ts'` + ); let config = loadConfig(`${testDir}/deeply/nested/directory`); expect(config.rootDir).toBe(normalizePath(`${testDir}/deeply`)); + expect(config.environment.getConfiguredTemplateTags()).toEqual({ test: true }); expect(config.includesFile(`${testDir}/deeply/index.ts`)).toBe(false); expect(config.includesFile(`${testDir}/deeply/index.root.ts`)).toBe(false); expect(config.includesFile(`${testDir}/deeply/index.nested.ts`)).toBe(true); @@ -47,6 +49,7 @@ describe('loadConfig', () => { name: 'my-package', private: true, glint: { + environment: './local-env', include: '**/*.from-pkg.ts', }, }) @@ -54,24 +57,33 @@ describe('loadConfig', () => { let config = loadConfig(testDir); + expect(config.environment.getConfiguredTemplateTags()).toEqual({ test: true }); expect(config.includesFile(`${testDir}/index.ts`)).toBe(false); expect(config.includesFile(`${testDir}/index.from-pkg.ts`)).toBe(true); }); test('reads config from .glintrc', () => { - fs.writeFileSync(`${testDir}/.glintrc`, `include: '**/*.extensionless.ts'`); + fs.writeFileSync( + `${testDir}/.glintrc`, + `environment: './local-env'\ninclude: '**/*.extensionless.ts'` + ); let config = loadConfig(testDir); + expect(config.environment.getConfiguredTemplateTags()).toEqual({ test: true }); expect(config.includesFile(`${testDir}/index.ts`)).toBe(false); expect(config.includesFile(`${testDir}/index.extensionless.ts`)).toBe(true); }); test('reads config from .glintrc.js', () => { - fs.writeFileSync(`${testDir}/.glintrc.js`, `module.exports = { include: '**/*.jsrc.ts' };`); + fs.writeFileSync( + `${testDir}/.glintrc.js`, + `module.exports = { environment: "./local-env", include: '**/*.jsrc.ts' };` + ); let config = loadConfig(testDir); + expect(config.environment.getConfiguredTemplateTags()).toEqual({ test: true }); expect(config.includesFile(`${testDir}/index.ts`)).toBe(false); expect(config.includesFile(`${testDir}/index.jsrc.ts`)).toBe(true); }); @@ -79,11 +91,12 @@ describe('loadConfig', () => { test('reads config from glint.config.js', () => { fs.writeFileSync( `${testDir}/glint.config.js`, - `module.exports = { include: '**/*.config.ts' };` + `module.exports = { environment: './local-env', include: '**/*.config.ts' };` ); let config = loadConfig(testDir); + expect(config.environment.getConfiguredTemplateTags()).toEqual({ test: true }); expect(config.includesFile(`${testDir}/index.ts`)).toBe(false); expect(config.includesFile(`${testDir}/index.config.ts`)).toBe(true); }); diff --git a/packages/config/package.json b/packages/config/package.json index 6f0d3ca8a..a17c28161 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -19,11 +19,14 @@ }, "dependencies": { "cosmiconfig": "^6.0.0", - "minimatch": "^3.0.4" + "escape-string-regexp": "^4.0.0", + "minimatch": "^3.0.4", + "resolve": "^1.17.0" }, "devDependencies": { - "@types/minimatch": "^3.0.3", "@types/jest": "^25.2.1", + "@types/minimatch": "^3.0.3", + "@types/resolve": "^1.17.1", "jest": "^25.4.0", "ts-jest": "^25.4.0" } diff --git a/packages/config/src/config.ts b/packages/config/src/config.ts index 1ebd9517b..74ff379e5 100644 --- a/packages/config/src/config.ts +++ b/packages/config/src/config.ts @@ -1,7 +1,9 @@ import path from 'path'; import { Minimatch, IMinimatch } from 'minimatch'; +import { GlintEnvironment } from './environment'; export type GlintConfigInput = { + environment: string; include?: string | Array; exclude?: string | Array; }; @@ -12,6 +14,7 @@ export type GlintConfigInput = { */ export class GlintConfig { public readonly rootDir: string; + public readonly environment: GlintEnvironment; private includeMatchers: Array; private excludeMatchers: Array; @@ -20,6 +23,7 @@ export class GlintConfig { validateConfigInput(config); this.rootDir = normalizePath(rootDir); + this.environment = GlintEnvironment.load(config.environment, { rootDir }); let include = Array.isArray(config.include) ? config.include : [config.include ?? '**/*.ts']; let exclude = Array.isArray(config.exclude) @@ -46,7 +50,14 @@ export class GlintConfig { private buildMatchers(globs: Array): Array { return globs.map((glob) => new Minimatch(normalizePath(path.resolve(this.rootDir, glob)))); } +} +export function normalizePath(fileName: string): string { + if (path.sep !== '/') { + return fileName.split(path.sep).join('/'); + } + + return fileName; } function validateConfigInput(input: Record): asserts input is GlintConfigInput { @@ -75,11 +86,3 @@ function assert(test: unknown, message: string): asserts test { throw new Error(`@glint/config: ${message}`); } } - -function normalizePath(fileName: string): string { - if (path.sep !== '/') { - return fileName.split(path.sep).join('/'); - } - - return fileName; -} diff --git a/packages/config/src/environment.ts b/packages/config/src/environment.ts new file mode 100644 index 000000000..00126dca7 --- /dev/null +++ b/packages/config/src/environment.ts @@ -0,0 +1,73 @@ +import resolve from 'resolve'; +import escapeStringRegexp from 'escape-string-regexp'; + +export type GlintEnvironmentConfig = { + tags: GlintTagsConfig; +}; + +export type GlintTagsConfig = { + readonly [importSource: string]: { + readonly [importSpecifier: string]: { + readonly typesSource: string; + }; + }; +}; + +export class GlintEnvironment { + private tags: GlintTagsConfig; + private tagImportRegexp: RegExp; + + public constructor(config: GlintEnvironmentConfig) { + this.tags = config.tags; + 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()); + } + + /** + * Indicates whether the given module _may_ import one of the template tags this + * configuration is set up to cover. Note that this method is intended to be a + * cheaper initial pass to avoid needlessly parsing modules that definitely don't + * require rewriting. It therefore may produce false positives, but should never + * give a false negative. + */ + public moduleMayHaveTagImports(moduleContents: string): boolean { + return this.tagImportRegexp.test(moduleContents); + } + + /** + * Returns an array of template tags that should be rewritten according to this + * config object, along with an import specifier indicating where the template types + * for each tag can be found. + */ + public getConfiguredTemplateTags(): GlintTagsConfig { + return this.tags; + } + + private buildTagImportRegexp(): RegExp { + let importSources = Object.keys(this.tags); + let regexpSource = importSources.map(escapeStringRegexp).join('|'); + return new RegExp(regexpSource); + } +} + +function locateEnvironment(name: string, basedir: string): string { + for (let candidate of [`@glint/environment-${name}`, name]) { + try { + return resolve.sync(candidate, { basedir }); + } catch (error) { + if (error?.code === 'MODULE_NOT_FOUND') { + continue; + } + + throw error; + } + } + + throw new Error(`Unable to resolve environment '${name}' from ${basedir}`); +} diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 63c69bb5d..3064e0936 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -2,6 +2,9 @@ import path from 'path'; import { cosmiconfigSync } from 'cosmiconfig'; import { GlintConfig } from './config'; +export { GlintConfig } from './config'; +export { GlintEnvironment, GlintEnvironmentConfig } from './environment'; + /** * Loads glint configuration, starting from the given directory * and searching upwards. @@ -12,7 +15,5 @@ export function loadConfig(from: string): GlintConfig { return new GlintConfig(path.dirname(result.filepath), result.config); } - return new GlintConfig(from); + throw new Error(`Unable to find Glint configuration for ${from}`); } - - From baf3574b04adf6d6111f7133b0b64751b20c0e52 Mon Sep 17 00:00:00 2001 From: Dan Freeman Date: Fri, 31 Jul 2020 15:16:00 +0200 Subject: [PATCH 3/9] Introduce @glint/environment-glimmerx --- packages/environment-glimmerx/.gitignore | 2 + packages/environment-glimmerx/README.md | 3 + .../__tests__/component.test.ts | 100 ++++++++++++++++++ .../__tests__/globals.test.ts | 21 ++++ .../__tests__/helper.test.ts | 77 ++++++++++++++ .../__tests__/modifier.test.ts | 26 +++++ .../__tests__/tsconfig.json | 11 ++ packages/environment-glimmerx/package.json | 31 ++++++ packages/environment-glimmerx/src/index.ts | 13 +++ packages/environment-glimmerx/tsconfig.json | 18 ++++ .../environment-glimmerx/types/globals.d.ts | 23 ++++ .../environment-glimmerx/types/index.d.ts | 4 + .../types/signatures.d.ts | 76 +++++++++++++ tsconfig.json | 1 + yarn.lock | 59 ++++++++++- 15 files changed, 460 insertions(+), 5 deletions(-) create mode 100644 packages/environment-glimmerx/.gitignore create mode 100644 packages/environment-glimmerx/README.md create mode 100644 packages/environment-glimmerx/__tests__/component.test.ts create mode 100644 packages/environment-glimmerx/__tests__/globals.test.ts create mode 100644 packages/environment-glimmerx/__tests__/helper.test.ts create mode 100644 packages/environment-glimmerx/__tests__/modifier.test.ts create mode 100644 packages/environment-glimmerx/__tests__/tsconfig.json create mode 100644 packages/environment-glimmerx/package.json create mode 100644 packages/environment-glimmerx/src/index.ts create mode 100644 packages/environment-glimmerx/tsconfig.json create mode 100644 packages/environment-glimmerx/types/globals.d.ts create mode 100644 packages/environment-glimmerx/types/index.d.ts create mode 100644 packages/environment-glimmerx/types/signatures.d.ts diff --git a/packages/environment-glimmerx/.gitignore b/packages/environment-glimmerx/.gitignore new file mode 100644 index 000000000..90f6c93dc --- /dev/null +++ b/packages/environment-glimmerx/.gitignore @@ -0,0 +1,2 @@ +lib/ +tsconfig.tsbuildinfo diff --git a/packages/environment-glimmerx/README.md b/packages/environment-glimmerx/README.md new file mode 100644 index 000000000..699982225 --- /dev/null +++ b/packages/environment-glimmerx/README.md @@ -0,0 +1,3 @@ +# `@glint/environment-glimmerx` + +This package contains the information necessary for glint to typecheck a [glimmer-experimental] project. diff --git a/packages/environment-glimmerx/__tests__/component.test.ts b/packages/environment-glimmerx/__tests__/component.test.ts new file mode 100644 index 000000000..ff599f615 --- /dev/null +++ b/packages/environment-glimmerx/__tests__/component.test.ts @@ -0,0 +1,100 @@ +import Component from '@glimmerx/component'; +import { + template, + invokeBlock, + resolve, + ResolveContext, + toBlock, +} from '@glint/environment-glimmerx/types'; +import { expectTypeOf } from 'expect-type'; +import { NoNamedArgs } from '@glint/template/-private'; +import { BlockYield } from '@glint/template/-private/blocks'; + +{ + class NoArgsComponent extends Component { + static template = template(function* (𝚪: ResolveContext) { + 𝚪; + }); + } + + // @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() {} }, 'default'); + + expectTypeOf(invokeBlock(resolve(NoArgsComponent)({}), {})).toBeNever(); +} + +{ + class StatefulComponent extends Component { + private foo = 'hello'; + + static template = template(function* (𝚪: ResolveContext) { + expectTypeOf(𝚪.this.foo).toEqualTypeOf(); + expectTypeOf(𝚪.this).toEqualTypeOf(); + expectTypeOf(𝚪.args).toEqualTypeOf<{}>(); + }); + } + + expectTypeOf(invokeBlock(resolve(StatefulComponent)({}), {})).toBeNever(); +} + +{ + class YieldingComponent extends Component<{ values: T[] }> { + static template = template(function* (𝚪: ResolveContext>) { + expectTypeOf(𝚪.this).toEqualTypeOf>(); + expectTypeOf(𝚪.args).toEqualTypeOf<{ values: T[] }>(); + + if (𝚪.args.values.length) { + yield toBlock('default', 𝚪.args.values[0]); + } else { + yield toBlock('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'); + + expectTypeOf( + invokeBlock( + resolve(YieldingComponent)({ values: [1, 2, 3] }), + { + *default(value) { + expectTypeOf(value).toEqualTypeOf(); + }, + }, + 'default' + ) + ).toBeNever(); + + expectTypeOf( + invokeBlock( + resolve(YieldingComponent)({ values: [1, 2, 3] }), + { + *default(value) { + yield toBlock('default', [value]); + }, + + *inverse() { + yield toBlock('default', [1, 2, 3]); + }, + }, + 'default', + 'inverse' + ) + ).toEqualTypeOf]>>(); +} diff --git a/packages/environment-glimmerx/__tests__/globals.test.ts b/packages/environment-glimmerx/__tests__/globals.test.ts new file mode 100644 index 000000000..c6f4b0ad4 --- /dev/null +++ b/packages/environment-glimmerx/__tests__/globals.test.ts @@ -0,0 +1,21 @@ +import { expectTypeOf } from 'expect-type'; +import DebuggerKeyword from '@glint/template/-private/keywords/debugger'; +import EachKeyword from '@glint/template/-private/keywords/each'; +import HasBlockKeyword from '@glint/template/-private/keywords/has-block'; +import HasBlockParamsKeyword from '@glint/template/-private/keywords/has-block-params'; +import InElementKeyword from '@glint/template/-private/keywords/in-element'; +import LetKeyword from '@glint/template/-private/keywords/let'; +import WithKeyword from '@glint/template/-private/keywords/with'; + +import { Globals } from '@glint/environment-glimmerx/types'; + +expectTypeOf(Globals['debugger']).toEqualTypeOf(); +expectTypeOf(Globals['each']).toEqualTypeOf(); +expectTypeOf(Globals['has-block']).toEqualTypeOf(); +expectTypeOf(Globals['has-block-params']).toEqualTypeOf(); +expectTypeOf(Globals['in-element']).toEqualTypeOf(); +expectTypeOf(Globals['let']).toEqualTypeOf(); +expectTypeOf(Globals['with']).toEqualTypeOf(); + +// TODO: either add a keyword type or implement directly in the transformation layer +expectTypeOf(Globals['unless']).toEqualTypeOf(); diff --git a/packages/environment-glimmerx/__tests__/helper.test.ts b/packages/environment-glimmerx/__tests__/helper.test.ts new file mode 100644 index 000000000..c52737db6 --- /dev/null +++ b/packages/environment-glimmerx/__tests__/helper.test.ts @@ -0,0 +1,77 @@ +import { helper, fn as fnDefinition } from '@glimmerx/helper'; +import { resolve, invokeInline } from '@glint/environment-glimmerx/types'; +import { expectTypeOf } from 'expect-type'; +import { NoNamedArgs, ReturnsValue } from '@glint/template/-private'; + +// Built-in helper: `fn` +{ + let fn = resolve(fnDefinition); + + // @ts-expect-error: extra named arg + fn({ foo: true }, () => true); + + // @ts-expect-error: invalid arg + fn({}, (t: string) => t, 123); + + expectTypeOf(invokeInline(fn({}, () => true))).toEqualTypeOf<() => boolean>(); + expectTypeOf(invokeInline(fn({}, (arg: string) => arg.length))).toEqualTypeOf< + (arg: string) => number + >(); + expectTypeOf(invokeInline(fn({}, (arg: string) => arg.length, 'hi'))).toEqualTypeOf< + () => number + >(); + + let identity = (x: T): T => x; + + // Bound type parameters are reflected in the output + expectTypeOf(invokeInline(fn({}, identity, 'hi'))).toEqualTypeOf<() => string>(); + + // Unfortunately unbound type parameters degrade to `unknown`; this is a known limitation + expectTypeOf(invokeInline(fn({}, identity))).toEqualTypeOf<(x: unknown) => unknown>(); +} + +// Custom helper: positional params +{ + let definition = helper(([a, b]: [T, U]) => a || b); + let or = resolve(definition); + + expectTypeOf(or).toEqualTypeOf<(args: NoNamedArgs, t: T, u: U) => ReturnsValue>(); + + // @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(invokeInline(or({}, 'a', 'b'))).toEqualTypeOf(); + expectTypeOf(invokeInline(or({}, 'a', true))).toEqualTypeOf(); + expectTypeOf(invokeInline(or({}, false, true))).toEqualTypeOf(); +} + +// Custom helper: named params +{ + let definition = helper((_: [], { word, count }: { word: string; count?: number }) => { + return Array.from({ length: count ?? 2 }, () => word); + }); + + let repeat = resolve(definition); + + expectTypeOf(repeat).toEqualTypeOf< + (args: { word: string; count?: number }) => ReturnsValue> + >(); + + // @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(invokeInline(repeat({ word: 'hi' }))).toEqualTypeOf>(); + expectTypeOf(invokeInline(repeat({ word: 'hi', count: 3 }))).toEqualTypeOf>(); +} diff --git a/packages/environment-glimmerx/__tests__/modifier.test.ts b/packages/environment-glimmerx/__tests__/modifier.test.ts new file mode 100644 index 000000000..337e86c33 --- /dev/null +++ b/packages/environment-glimmerx/__tests__/modifier.test.ts @@ -0,0 +1,26 @@ +import { on as onDefinition } from '@glimmerx/modifier'; +import { resolve, invokeModifier } from '@glint/environment-glimmerx/types'; +import { expectTypeOf } from 'expect-type'; + +// Built-in modifier: `on` +{ + const on = resolve(onDefinition); + + // @ts-expect-error: extra named arg + on({ foo: 'bar' }, 'click', () => {}); + + // @ts-expect-error: missing positional arg + on({}, 'click'); + + // @ts-expect-error: extra positional arg + on({}, 'click', () => {}, 'hello'); + + // @ts-expect-error: invalid event name + on({}, 'unknown', () => {}); + + on({}, 'click', (event) => { + expectTypeOf(event).toEqualTypeOf(); + }); + + expectTypeOf(invokeModifier(on({}, 'click', () => {}))).toEqualTypeOf(); +} diff --git a/packages/environment-glimmerx/__tests__/tsconfig.json b/packages/environment-glimmerx/__tests__/tsconfig.json new file mode 100644 index 000000000..3faecc3d4 --- /dev/null +++ b/packages/environment-glimmerx/__tests__/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2019", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "noEmit": true, + "lib": ["dom"], + "skipLibCheck": true + } +} diff --git a/packages/environment-glimmerx/package.json b/packages/environment-glimmerx/package.json new file mode 100644 index 000000000..1963ad419 --- /dev/null +++ b/packages/environment-glimmerx/package.json @@ -0,0 +1,31 @@ +{ + "name": "@glint/environment-glimmerx", + "version": "0.1.2", + "repository": "typed-ember/glint", + "description": "A Glint environment to support GlimmerX projects", + "license": "MIT", + "author": "Dan Freeman (https://github.com/dfreeman)", + "main": "lib/index.js", + "keywords": [ + "glint-environment" + ], + "scripts": { + "lint": "eslint . --ext ts --max-warnings 0 && prettier --check .", + "test": "tsc --project __tests__", + "build": "tsc --build", + "prepack": "yarn build" + }, + "files": [ + "README.md", + "lib/", + "types/" + ], + "dependencies": { + "@glint/config": "^0.1.2", + "@glint/template": "^0.1.2" + }, + "devDependencies": { + "expect-type": "0.7.3", + "@glimmerx/component": "^0.2.2" + } +} diff --git a/packages/environment-glimmerx/src/index.ts b/packages/environment-glimmerx/src/index.ts new file mode 100644 index 000000000..26848d141 --- /dev/null +++ b/packages/environment-glimmerx/src/index.ts @@ -0,0 +1,13 @@ +import { GlintEnvironmentConfig } from '@glint/config'; + +export default function glimmerxEnvironment(): GlintEnvironmentConfig { + return { + tags: { + '@glimmerx/component': { + hbs: { + typesSource: '@glint/environment-glimmerx/types', + }, + }, + }, + }; +} diff --git a/packages/environment-glimmerx/tsconfig.json b/packages/environment-glimmerx/tsconfig.json new file mode 100644 index 000000000..3378cc5b8 --- /dev/null +++ b/packages/environment-glimmerx/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "es2019", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "baseUrl": ".", + "composite": true, + "outDir": "lib", + "rootDir": "src", + "declaration": true, + // Not ideal, but several of the @glimmer packages fairly consistently + // produce type errors across different versions. + "skipLibCheck": true + }, + "include": ["src", "types"], + "references": [{ "path": "../template" }, { "path": "../config" }] +} diff --git a/packages/environment-glimmerx/types/globals.d.ts b/packages/environment-glimmerx/types/globals.d.ts new file mode 100644 index 000000000..9b38cc1ba --- /dev/null +++ b/packages/environment-glimmerx/types/globals.d.ts @@ -0,0 +1,23 @@ +import * as VM from '@glint/template/-private/keywords'; + +interface Keywords { + debugger: VM.DebuggerKeyword; + each: VM.EachKeyword; + 'has-block': VM.HasBlockParamsKeyword; + 'has-block-params': VM.HasBlockParamsKeyword; + // the `if` keyword is implemented directly in @glint/transform + 'in-element': VM.InElementKeyword; + let: VM.LetKeyword; + unless: void; // TODO: should this be implemented as `if (!...)`? + with: VM.WithKeyword; + // the `yield` keyword is implemented directly in @glint/transform +} + +declare const k: Keywords; + +export interface Globals extends Keywords { + // GlimmerX, by design, doesn't have any global values beyond + // glimmer-vm keywords +} + +export declare const Globals: Globals; diff --git a/packages/environment-glimmerx/types/index.d.ts b/packages/environment-glimmerx/types/index.d.ts new file mode 100644 index 000000000..6f8086422 --- /dev/null +++ b/packages/environment-glimmerx/types/index.d.ts @@ -0,0 +1,4 @@ +import './signatures'; + +export * from '@glint/template'; +export { Globals } from './globals'; diff --git a/packages/environment-glimmerx/types/signatures.d.ts b/packages/environment-glimmerx/types/signatures.d.ts new file mode 100644 index 000000000..fbaaef131 --- /dev/null +++ b/packages/environment-glimmerx/types/signatures.d.ts @@ -0,0 +1,76 @@ +import { NoNamedArgs, CreatesModifier } from '@glint/template/-private'; +import { Invokable, ReturnsValue } from '@glint/template/-private'; +import { ResolutionKey } from '@glint/template/-private'; +import { TemplateContext, AcceptsBlocks } from '@glint/template/-private'; +import GlimmerXComponent from '@glimmerx/component'; + +type Constructor = new (...args: any) => T; + +declare const ResolveGlimmerXComponent: unique symbol; + +declare module '@glint/template/resolution-rules' { + export interface ContextResolutions { + [ResolveGlimmerXComponent]: Host extends GlimmerXComponent + ? TemplateContext + : never; + } + + export interface SignatureResolutions { + [ResolveGlimmerXComponent]: InvokedValue extends Constructor> + ? InvokedValue extends { template: Invokable } + ? Signature + : (args: Args) => AcceptsBlocks<{ default?: [] }> + : never; + } +} + +declare module '@glimmerx/component' { + export default interface Component { + [ResolutionKey]: typeof ResolveGlimmerXComponent; + } +} + +declare module '@glimmerx/modifier' { + export function on( + args: NoNamedArgs, + name: Name, + callback: (event: HTMLElementEventMap[Name]) => void + ): CreatesModifier; +} + +declare module '@glimmerx/helper' { + export function helper( + fn: (positional: Positional, named: Named) => Result + ): Invokable<(args: Named, ...positional: Positional) => ReturnsValue>; + + export function fn( + args: NoNamedArgs, + f: (...rest: Args) => Ret + ): ReturnsValue<(...rest: Args) => Ret>; + export function fn( + args: NoNamedArgs, + f: (a: A, ...rest: Args) => Ret, + a: A + ): ReturnsValue<(...rest: Args) => Ret>; + export function fn( + args: NoNamedArgs, + f: (a: A, b: B, ...rest: Args) => Ret, + a: A, + b: B + ): ReturnsValue<(...rest: Args) => Ret>; + export function fn( + args: NoNamedArgs, + f: (a: A, b: B, c: C, ...rest: Args) => Ret, + a: A, + b: B, + c: C + ): ReturnsValue<(...rest: Args) => Ret>; + export function fn( + args: NoNamedArgs, + f: (a: A, b: B, c: C, d: D, ...rest: Args) => Ret, + a: A, + b: B, + c: C, + d: D + ): ReturnsValue<(...rest: Args) => Ret>; +} diff --git a/tsconfig.json b/tsconfig.json index 74ac7d9e2..07f195a4b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "references": [ { "path": "packages/cli" }, { "path": "packages/config" }, + { "path": "packages/environment-glimmerx" }, { "path": "packages/template" }, { "path": "packages/transform" }, { "path": "packages/tsserver-plugin" } diff --git a/yarn.lock b/yarn.lock index 0562eafcf..7f76af246 100644 --- a/yarn.lock +++ b/yarn.lock @@ -990,6 +990,26 @@ ember-cli-typescript "3.0.0" ember-compatibility-helpers "^1.1.2" +"@glimmer/component@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@glimmer/component/-/component-1.0.1.tgz#f304b2f9cf4f2762396abed2b8962486767e0b2e" + integrity sha512-Bo+R6e2fnydZz0JQrV6E5cAIYvCEUQVh6mmXexVSgFmPr/7Q1DB/afAgTQrpK5vXUGRAU7sBB8gzoO0t7vl+Ng== + dependencies: + "@glimmer/di" "^0.1.9" + "@glimmer/env" "^0.1.7" + "@glimmer/util" "^0.44.0" + broccoli-file-creator "^2.1.1" + broccoli-merge-trees "^3.0.2" + ember-cli-babel "^7.7.3" + ember-cli-get-component-path-option "^1.0.0" + ember-cli-is-package-missing "^1.0.0" + ember-cli-normalize-entity-name "^1.0.0" + ember-cli-path-utils "^1.0.0" + ember-cli-string-utils "^1.1.0" + ember-cli-typescript "3.0.0" + ember-cli-version-checker "^3.1.3" + ember-compatibility-helpers "^1.1.2" + "@glimmer/core@2.0.0-beta.7": version "2.0.0-beta.7" resolved "https://registry.yarnpkg.com/@glimmer/core/-/core-2.0.0-beta.7.tgz#f7d8d8b2bc6dba9fe077b67046e825b068fda8db" @@ -1746,6 +1766,15 @@ "@types/ember__object" "*" "@types/jquery" "*" +"@types/ember__component@^3.16.0": + version "3.16.0" + resolved "https://registry.yarnpkg.com/@types/ember__component/-/ember__component-3.16.0.tgz#42a5e9c1ff573f5bab6e7ac5e7e4a59a957323cb" + integrity sha512-MnzHTGQ6ic9/wT72Ho3BybEajKFkJOakeYSriVLFQTxbaLwm8RNDrZNPHWGT4FEl6dEf90+2SBJHFagbIPAhng== + dependencies: + "@types/ember__component" "*" + "@types/ember__object" "*" + "@types/jquery" "*" + "@types/ember__object@*": version "3.1.3" resolved "https://registry.yarnpkg.com/@types/ember__object/-/ember__object-3.1.3.tgz#c89ae1ed20e43560d30116cd99c6d681c788ef9c" @@ -1875,6 +1904,13 @@ dependencies: "@types/node" "*" +"@types/resolve@^1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" + integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw== + dependencies: + "@types/node" "*" + "@types/responselike@*", "@types/responselike@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" @@ -4410,6 +4446,14 @@ ember-cli-version-checker@^2.1.1: resolve "^1.3.3" semver "^5.3.0" +ember-cli-version-checker@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/ember-cli-version-checker/-/ember-cli-version-checker-3.1.3.tgz#7c9b4f5ff30fdebcd480b1c06c4de43bb51c522c" + integrity sha512-PZNSvpzwWgv68hcXxyjREpj3WWb81A7rtYNQq1lLEgrWIchF8ApKJjWP3NBpHjaatwILkZAV8klair5WFlXAKg== + dependencies: + resolve-package-path "^1.2.6" + semver "^5.6.0" + ember-cli-version-checker@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/ember-cli-version-checker/-/ember-cli-version-checker-4.1.0.tgz#7fc9836bdbc87451d286ba6a9a89b23591d8bbb7" @@ -4608,6 +4652,11 @@ escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + escodegen@^1.11.1: version "1.14.1" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.1.tgz#ba01d0c8278b5e95a9a45350142026659027a457" @@ -4894,10 +4943,10 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" -expect-type@^0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-0.7.4.tgz#588739f5e86e6713df49ae43812570f11225f9d7" - integrity sha512-/ykfDxhYq9vz/EgqH8YTeIehvb9YCP2W7qztpEqm84LaA23nOeGe6+H7huxxfncehig2bV0S0Gyhf+ncUwt2Mg== +expect-type@0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-0.7.3.tgz#6738bafc40bb58a131172a0452feebf35cc62122" + integrity sha512-65P36B6B6KjYd8Hq2aFp2Y84krsLwz5WLT4RyvEuKzQkEcloPLFnjJPrSX/tMp2mC4lGNex9iS9uzs/MRZtEoA== expect@^25.5.0: version "25.5.0" @@ -9292,7 +9341,7 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve-package-path@^1.0.11: +resolve-package-path@^1.0.11, resolve-package-path@^1.2.6: version "1.2.7" resolved "https://registry.yarnpkg.com/resolve-package-path/-/resolve-package-path-1.2.7.tgz#2a7bc37ad96865e239330e3102c31322847e652e" integrity sha512-fVEKHGeK85bGbVFuwO9o1aU0n3vqQGrezPc51JGu9UTXpFQfWq5qCeKxyaRUSvephs+06c5j5rPq/dzHGEo8+Q== From b91963ee9814c650f7bdd398557b6732cbb40dc6 Mon Sep 17 00:00:00 2001 From: Dan Freeman Date: Fri, 31 Jul 2020 15:17:15 +0200 Subject: [PATCH 4/9] Update @glint/transform to use the configured environment in emitted code --- packages/transform/__tests__/debug.test.ts | 35 +++---- .../__tests__/offset-mapping.test.ts | 11 ++- packages/transform/__tests__/rewrite.test.ts | 21 ++-- .../__tests__/template-to-typescript.test.ts | 95 ++++++++++++------- packages/transform/src/index.ts | 31 +++++- .../transform/src/template-to-typescript.ts | 8 +- 6 files changed, 132 insertions(+), 69 deletions(-) diff --git a/packages/transform/__tests__/debug.test.ts b/packages/transform/__tests__/debug.test.ts index 7162d75a2..689621905 100644 --- a/packages/transform/__tests__/debug.test.ts +++ b/packages/transform/__tests__/debug.test.ts @@ -1,5 +1,6 @@ import { rewriteModule } from '../src'; import { stripIndent } from 'common-tags'; +import { GlintEnvironment } from '@glint/config'; describe('Debug utilities', () => { test('TransformedModule#toDebugString', () => { @@ -21,54 +22,54 @@ describe('Debug utilities', () => { } `; - let transformedModule = rewriteModule('test.ts', code); + let transformedModule = rewriteModule('test.ts', code, GlintEnvironment.load('glimmerx')); expect(transformedModule?.toDebugString()).toMatchInlineSnapshot(` "TransformedModule test.ts | Mapping: Template | hbs(0:50): hbs\`\\\\n \\\\n \` - | ts(0:248): (() => {\\\\n hbs;\\\\n let χ!: typeof import(\\"@glint/template\\");\\\\n return χ.template(function*(𝚪: import(\\"@glint/template\\").ResolveContext) {\\\\n yield χ.invokeBlock(χ.resolve(HelperComponent)({ foo: 𝚪.this.bar }), {});\\\\n 𝚪;\\\\n });\\\\n})() + | ts(0:284): (() => {\\\\n hbs;\\\\n let χ!: typeof import(\\"@glint/environment-glimmerx/types\\");\\\\n return χ.template(function*(𝚪: import(\\"@glint/environment-glimmerx/types\\").ResolveContext) {\\\\n yield χ.invokeBlock(χ.resolve(HelperComponent)({ foo: 𝚪.this.bar }), {});\\\\n 𝚪;\\\\n });\\\\n})() | | | Mapping: Identifier | | hbs(0:0): - | | ts(135:146): MyComponent + | | ts(171:182): MyComponent | | | | Mapping: ElementNode | | hbs(9:46): - | | ts(151:230): yield χ.invokeBlock(χ.resolve(HelperComponent)({ foo: 𝚪.this.bar }), {}); + | | ts(187:266): yield χ.invokeBlock(χ.resolve(HelperComponent)({ foo: 𝚪.this.bar }), {}); | | | | | Mapping: ElementNode | | | hbs(9:46): - | | | ts(151:230): yield χ.invokeBlock(χ.resolve(HelperComponent)({ foo: 𝚪.this.bar }), {}); + | | | ts(187:266): yield χ.invokeBlock(χ.resolve(HelperComponent)({ foo: 𝚪.this.bar }), {}); | | | | | | | Mapping: Identifier | | | | hbs(10:25): HelperComponent - | | | | ts(185:200): HelperComponent + | | | | ts(221:236): HelperComponent | | | | | | | | Mapping: AttrNode | | | | hbs(26:43): @foo={{this.bar}} - | | | | ts(204:220): foo: 𝚪.this.bar + | | | | ts(240:256): foo: 𝚪.this.bar | | | | | | | | | Mapping: Identifier | | | | | hbs(27:30): foo - | | | | | ts(204:207): foo + | | | | | ts(240:243): foo | | | | | | | | | | Mapping: MustacheStatement | | | | | hbs(31:43): {{this.bar}} - | | | | | ts(209:220): 𝚪.this.bar + | | | | | ts(245:256): 𝚪.this.bar | | | | | | | | | | | Mapping: PathExpression | | | | | | hbs(33:41): this.bar - | | | | | | ts(209:220): 𝚪.this.bar + | | | | | | ts(245:256): 𝚪.this.bar | | | | | | | | | | | | | Mapping: Identifier | | | | | | | hbs(33:37): this - | | | | | | | ts(212:216): this + | | | | | | | ts(248:252): this | | | | | | | | | | | | | | Mapping: Identifier | | | | | | | hbs(38:41): bar - | | | | | | | ts(217:220): bar + | | | | | | | ts(253:256): bar | | | | | | | | | | | | | | | | | | @@ -79,23 +80,23 @@ describe('Debug utilities', () => { | Mapping: Template | hbs(0:28): hbs\`\\\\n Hello, {{@foo}}\\\\n \` - | ts(0:229): (() => {\\\\n hbs;\\\\n let χ!: typeof import(\\"@glint/template\\");\\\\n return χ.template(function*(𝚪: import(\\"@glint/template\\").ResolveContext) {\\\\n χ.invokeInline(χ.resolveOrReturn(𝚪.args.foo)({}));\\\\n 𝚪;\\\\n });\\\\n})() + | ts(0:265): (() => {\\\\n hbs;\\\\n let χ!: typeof import(\\"@glint/environment-glimmerx/types\\");\\\\n return χ.template(function*(𝚪: import(\\"@glint/environment-glimmerx/types\\").ResolveContext) {\\\\n χ.invokeInline(χ.resolveOrReturn(𝚪.args.foo)({}));\\\\n 𝚪;\\\\n });\\\\n})() | | | Mapping: Identifier | | hbs(0:0): - | | ts(135:150): HelperComponent + | | ts(171:186): HelperComponent | | | | Mapping: MustacheStatement | | hbs(16:24): {{@foo}} - | | ts(155:209): χ.invokeInline(χ.resolveOrReturn(𝚪.args.foo)({})) + | | ts(191:245): χ.invokeInline(χ.resolveOrReturn(𝚪.args.foo)({})) | | | | | Mapping: PathExpression | | | hbs(18:22): @foo - | | | ts(192:203): 𝚪.args.foo + | | | ts(228:239): 𝚪.args.foo | | | | | | | Mapping: Identifier | | | | hbs(19:22): foo - | | | | ts(200:203): foo + | | | | ts(236:239): foo | | | | | | | | | diff --git a/packages/transform/__tests__/offset-mapping.test.ts b/packages/transform/__tests__/offset-mapping.test.ts index b20045160..6cbb3462c 100644 --- a/packages/transform/__tests__/offset-mapping.test.ts +++ b/packages/transform/__tests__/offset-mapping.test.ts @@ -3,6 +3,9 @@ import { stripIndent } from 'common-tags'; import { Range } from '../src/transformed-module'; import ts from 'typescript'; import { assert } from '../src/util'; +import { GlintEnvironment } from '@glint/config'; + +const glimmerxEnvironment = GlintEnvironment.load('glimmerx'); describe('Source-to-source offset mapping', () => { function rewriteTestModule({ @@ -23,7 +26,8 @@ describe('Source-to-source offset mapping', () => { ${contents} \`; } - ` + `, + glimmerxEnvironment ); if (!result) { @@ -297,7 +301,8 @@ describe('Source-to-source offset mapping', () => { export class Greeting extends Component { static template = hbs\`Hello, world!\`; } - ` + `, + glimmerxEnvironment )!; test('bounds that cross a rewritten span', () => { @@ -351,7 +356,7 @@ describe('Diagnostic offset mapping', () => { } `; - const transformedModule = rewriteModule('test.ts', source); + const transformedModule = rewriteModule('test.ts', source, glimmerxEnvironment); assert(transformedModule); test('without related information', () => { diff --git a/packages/transform/__tests__/rewrite.test.ts b/packages/transform/__tests__/rewrite.test.ts index 00641a14c..666a8637a 100644 --- a/packages/transform/__tests__/rewrite.test.ts +++ b/packages/transform/__tests__/rewrite.test.ts @@ -1,5 +1,8 @@ import { rewriteModule } from '../src'; import { stripIndent } from 'common-tags'; +import { GlintEnvironment } from '@glint/config'; + +const glimmerxEnvironment = GlintEnvironment.load('glimmerx'); describe('rewriteModule', () => { test('with a simple class', () => { @@ -10,7 +13,7 @@ describe('rewriteModule', () => { } `; - let transformedModule = rewriteModule('test.ts', code); + let transformedModule = rewriteModule('test.ts', code, glimmerxEnvironment); expect(transformedModule?.errors).toEqual([]); expect(transformedModule?.transformedSource).toMatchInlineSnapshot(` @@ -18,8 +21,8 @@ describe('rewriteModule', () => { export default class MyComponent extends Component { static template = (() => { hbs; - let χ!: typeof import(\\"@glint/template\\"); - return χ.template(function*(𝚪: import(\\"@glint/template\\").ResolveContext) { + let χ!: typeof import(\\"@glint/environment-glimmerx/types\\"); + return χ.template(function*(𝚪: import(\\"@glint/environment-glimmerx/types\\").ResolveContext) { 𝚪; }); })(); @@ -35,7 +38,7 @@ describe('rewriteModule', () => { } `; - let transformedModule = rewriteModule('test.ts', code); + let transformedModule = rewriteModule('test.ts', code, glimmerxEnvironment); expect(transformedModule?.errors).toEqual([]); expect(transformedModule?.transformedSource).toMatchInlineSnapshot(` @@ -43,8 +46,8 @@ describe('rewriteModule', () => { export default class MyComponent extends Component<{ value: K }> { static template = (() => { hbs; - let χ!: typeof import(\\"@glint/template\\"); - return χ.template(function*(𝚪: import(\\"@glint/template\\").ResolveContext>) { + let χ!: typeof import(\\"@glint/environment-glimmerx/types\\"); + return χ.template(function*(𝚪: import(\\"@glint/environment-glimmerx/types\\").ResolveContext>) { 𝚪; }); })(); @@ -60,7 +63,7 @@ describe('rewriteModule', () => { } `; - let transformedModule = rewriteModule('test.ts', code); + let transformedModule = rewriteModule('test.ts', code, glimmerxEnvironment); expect(transformedModule?.errors).toEqual([ { @@ -77,8 +80,8 @@ describe('rewriteModule', () => { export default class extends Component { static template = (() => { hbs; - let χ!: typeof import(\\"@glint/template\\"); - return χ.template(function*(𝚪: import(\\"@glint/template\\").ResolveContext) { + let χ!: typeof import(\\"@glint/environment-glimmerx/types\\"); + return χ.template(function*(𝚪: import(\\"@glint/environment-glimmerx/types\\").ResolveContext) { 𝚪; }); })(); diff --git a/packages/transform/__tests__/template-to-typescript.test.ts b/packages/transform/__tests__/template-to-typescript.test.ts index 3fcc705ab..61700b66f 100644 --- a/packages/transform/__tests__/template-to-typescript.test.ts +++ b/packages/transform/__tests__/template-to-typescript.test.ts @@ -4,8 +4,14 @@ import { templateToTypescript, TemplateToTypescriptOptions } from '../src/templa describe('rewriteTemplate', () => { // Slices out the template boilerplate to return only the code representing // the body, to keep snapshots brief and focused. - function templateBody(template: string, options: TemplateToTypescriptOptions = {}): string { - let { result, errors } = templateToTypescript(template, options); + function templateBody( + template: string, + options: Omit = {} + ): string { + let { result, errors } = templateToTypescript(template, { + ...options, + typesPath: '@glint/template', + }); if (errors.length) { throw new Error('Unexpected error(s): ' + errors.map((e) => e.message).join(', ')); } @@ -19,7 +25,8 @@ describe('rewriteTemplate', () => { describe('template boilerplate', () => { test('without any specified type parameters or context type', () => { - expect(templateToTypescript('').result?.code).toMatchInlineSnapshot(` + expect(templateToTypescript('', { typesPath: '@glint/template' }).result?.code) + .toMatchInlineSnapshot(` "(() => { let χ!: typeof import(\\"@glint/template\\"); return χ.template(function*(𝚪: import(\\"@glint/template\\").ResolveContext) { @@ -33,8 +40,10 @@ describe('rewriteTemplate', () => { let typeParams = ''; let contextType = 'MyComponent'; - expect(templateToTypescript('', { contextType, typeParams }).result?.code) - .toMatchInlineSnapshot(` + expect( + templateToTypescript('', { contextType, typeParams, typesPath: '@glint/template' }).result + ?.code + ).toMatchInlineSnapshot(` "(() => { let χ!: typeof import(\\"@glint/template\\"); return χ.template(function*(𝚪: import(\\"@glint/template\\").ResolveContext>) { @@ -47,7 +56,8 @@ describe('rewriteTemplate', () => { test('given preamble code', () => { let preamble = ['console.log("hello!");', 'throw new Error();']; - expect(templateToTypescript('', { preamble }).result?.code).toMatchInlineSnapshot(` + expect(templateToTypescript('', { preamble, typesPath: '@glint/template' }).result?.code) + .toMatchInlineSnapshot(` "(() => { console.log(\\"hello!\\"); throw new Error(); @@ -630,7 +640,9 @@ describe('rewriteTemplate', () => { describe('error conditions', () => { test('{{yield}} in expression position', () => { - let { errors } = templateToTypescript(''); + let { errors } = templateToTypescript('', { + typesPath: '@glint/template', + }); expect(errors).toEqual([ { @@ -641,7 +653,9 @@ describe('rewriteTemplate', () => { }); test('{{yield}} to a dynamic named block', () => { - let { errors } = templateToTypescript('{{yield to=@blockName}}'); + let { errors } = templateToTypescript('{{yield to=@blockName}}', { + typesPath: '@glint/template', + }); expect(errors).toEqual([ { @@ -652,7 +666,9 @@ describe('rewriteTemplate', () => { }); test('{{hash}} with positional parameters', () => { - let { errors } = templateToTypescript(''); + let { errors } = templateToTypescript('', { + typesPath: '@glint/template', + }); expect(errors).toEqual([ { @@ -663,7 +679,9 @@ describe('rewriteTemplate', () => { }); test('{{array}} with named parameters', () => { - let { errors } = templateToTypescript(''); + let { errors } = templateToTypescript('', { + typesPath: '@glint/template', + }); expect(errors).toEqual([ { @@ -674,7 +692,9 @@ describe('rewriteTemplate', () => { }); test('inline {{if}} with no consequent', () => { - let { errors } = templateToTypescript(''); + let { errors } = templateToTypescript('', { + typesPath: '@glint/template', + }); expect(errors).toEqual([ { @@ -685,11 +705,14 @@ describe('rewriteTemplate', () => { }); test('block {{#if}} with no condition', () => { - let { errors } = templateToTypescript(stripIndent` - {{#if}} - hello! - {{/if}} - `); + let { errors } = templateToTypescript( + stripIndent` + {{#if}} + hello! + {{/if}} + `, + { typesPath: '@glint/template' } + ); expect(errors).toEqual([ { @@ -700,18 +723,21 @@ describe('rewriteTemplate', () => { }); test('named blocks mixed with other content', () => { - let { errors } = templateToTypescript(stripIndent` - Header content - - hello - <:block> - - goodbye - - <:other> - - Footer content - `); + let { errors } = templateToTypescript( + stripIndent` + Header content + + hello + <:block> + + goodbye + + <:other> + + Footer content + `, + { typesPath: '@glint/template' } + ); expect(errors).toEqual([ { @@ -728,11 +754,14 @@ describe('rewriteTemplate', () => { test('invalid block param name', () => { // This is valid HBS, but complex for us to support. Since it's only a // local identifier, the author has full discretion over how to name it. - let { errors } = templateToTypescript(stripIndent` - - {{foo-bar}} - - `); + let { errors } = templateToTypescript( + stripIndent` + + {{foo-bar}} + + `, + { typesPath: '@glint/template' } + ); expect(errors).toEqual([ { diff --git a/packages/transform/src/index.ts b/packages/transform/src/index.ts index e9f15bf5f..aabff853b 100644 --- a/packages/transform/src/index.ts +++ b/packages/transform/src/index.ts @@ -2,6 +2,7 @@ import logger from 'debug'; import { parseSync, NodePath, types as t, traverse } from '@babel/core'; import generate from '@babel/generator'; import type ts from 'typescript'; +import { GlintEnvironment } from '@glint/config'; import { templateToTypescript } from './template-to-typescript'; import { assert } from './util'; import TransformedModule, { ReplacedSpan, TransformError } from './transformed-module'; @@ -57,7 +58,11 @@ export function rewriteDiagnostic( * Returns `null` if the given module can't be parsed as TypeScript, or * if it has no embedded templates. */ -export function rewriteModule(filename: string, source: string): TransformedModule | null { +export function rewriteModule( + filename: string, + source: string, + environment: GlintEnvironment +): TransformedModule | null { let ast: t.File | t.Program | null = null; try { ast = parseSync(source, { @@ -74,7 +79,7 @@ export function rewriteModule(filename: string, source: string): TransformedModu return null; } - let { errors, partialSpans } = calculateSpansForTaggedTemplates(ast); + let { errors, partialSpans } = calculateSpansForTaggedTemplates(ast, environment); if (!partialSpans.length && !errors.length) { return null; } @@ -93,7 +98,8 @@ export function rewriteModule(filename: string, source: string): TransformedModu * string. */ function calculateSpansForTaggedTemplates( - ast: t.File | t.Program + ast: t.File | t.Program, + environment: GlintEnvironment ): { errors: Array; partialSpans: Array } { let errors: Array = []; let partialSpans: Array = []; @@ -101,7 +107,10 @@ function calculateSpansForTaggedTemplates( traverse(ast, { TaggedTemplateExpression(path) { let tag = path.get('tag'); - if (tag.node.type === 'Identifier' && tag.referencesImport('@glimmerx/component', 'hbs')) { + if (!tag.isIdentifier()) return; + + let typesPath = determineTypesPathForTag(tag, environment); + if (typesPath) { let tagName = tag.node.name; let { quasis } = path.node.quasi; @@ -118,6 +127,7 @@ function calculateSpansForTaggedTemplates( let { typeParams, contextType } = getContainingTypeInfo(path); let identifiersInScope = Object.keys(path.scope.getAllBindings()); let transformedTemplate = templateToTypescript(template, { + typesPath, preamble, identifiersInScope, typeParams, @@ -177,6 +187,19 @@ function calculateSpansForTaggedTemplates( return { errors, partialSpans }; } +function determineTypesPathForTag( + path: NodePath, + environment: GlintEnvironment +): string | undefined { + for (let [importSource, tags] of Object.entries(environment.getConfiguredTemplateTags())) { + for (let [importSpecifier, tagConfig] of Object.entries(tags)) { + if (path.referencesImport(importSource, importSpecifier)) { + return tagConfig.typesSource; + } + } + } +} + /** * Given a `ReplacedSpan` array and the original source for a module, * returns the resulting full transformed source string for that module. diff --git a/packages/transform/src/template-to-typescript.ts b/packages/transform/src/template-to-typescript.ts index 0f90bc29f..5bf2c16e5 100644 --- a/packages/transform/src/template-to-typescript.ts +++ b/packages/transform/src/template-to-typescript.ts @@ -10,6 +10,7 @@ type InlineKeyword = typeof INLINE_KEYWORDS[number]; type BlockKeyword = typeof BLOCK_KEYWORDS[number]; export type TemplateToTypescriptOptions = { + typesPath: string; identifiersInScope?: Array; contextType?: string; typeParams?: string; @@ -24,11 +25,12 @@ export type TemplateToTypescriptOptions = { export function templateToTypescript( template: string, { + typesPath, identifiersInScope = [], typeParams = '', contextType = 'unknown', preamble = [], - }: TemplateToTypescriptOptions = {} + }: TemplateToTypescriptOptions ): RewriteResult { return mapTemplateContents(template, (ast, { emit, rangeForNode }) => { let scope = new ScopeStack(identifiersInScope); @@ -42,12 +44,12 @@ export function templateToTypescript( emit.newline(); } - emit.text(`let χ!: typeof import("@glint/template");`); + emit.text(`let χ!: typeof import("${typesPath}");`); emit.newline(); emit.text('return χ.template(function*'); emit.synthetic(typeParams); - emit.text('(𝚪: import("@glint/template").ResolveContext<'); + emit.text(`(𝚪: import("${typesPath}").ResolveContext<`); emit.synthetic(contextType); emit.text('>) {'); emit.newline(); From 43c4705338f9f446bc4d40c64df2f9b79698351b Mon Sep 17 00:00:00 2001 From: Dan Freeman Date: Fri, 31 Jul 2020 15:18:35 +0200 Subject: [PATCH 5/9] Drive CLI and tsserver-plugin transforms off of the configured environment --- .eslintrc | 1 + .gitignore | 1 + packages/cli/__tests__/check.test.ts | 2 +- packages/cli/__tests__/declaration.test.ts | 4 +- packages/cli/__tests__/utils/project.ts | 23 ++--- packages/cli/src/perform-check.ts | 1 + packages/cli/src/transform-manager.ts | 9 +- .../__tests__/integration.test.ts | 97 +++++++------------ .../tsserver-plugin/__tests__/test-server.ts | 28 +++--- .../src/virtual-module-manager.ts | 6 +- 10 files changed, 69 insertions(+), 103 deletions(-) diff --git a/.eslintrc b/.eslintrc index ac8cd6538..0dffc461c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -9,6 +9,7 @@ ], "rules": { "prefer-const": "off", + "require-yield": "off", "@typescript-eslint/prefer-const": "off", "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-empty-interface": "off", diff --git a/.gitignore b/.gitignore index 93cab344d..5b63be5e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules yarn-error.log +test-packages/ephemeral diff --git a/packages/cli/__tests__/check.test.ts b/packages/cli/__tests__/check.test.ts index f70dd87a7..942a7f70c 100644 --- a/packages/cli/__tests__/check.test.ts +++ b/packages/cli/__tests__/check.test.ts @@ -129,7 +129,7 @@ describe('single-pass typechecking', () => { `; project.write('index.ts', code); - project.write('.glintrc', 'exclude: "index.ts"\n'); + project.write('.glintrc', 'environment: glimmerx\nexclude: "index.ts"\n'); let checkResult = await project.check(); diff --git a/packages/cli/__tests__/declaration.test.ts b/packages/cli/__tests__/declaration.test.ts index 71030019f..66712a148 100644 --- a/packages/cli/__tests__/declaration.test.ts +++ b/packages/cli/__tests__/declaration.test.ts @@ -13,7 +13,6 @@ describe('emitting declarations', () => { test('emit for a valid project', async () => { let code = stripIndent` - import '@glint/template/glimmerx'; import Component, { hbs } from '@glimmerx/component'; export interface ApplicationArgs { @@ -37,8 +36,7 @@ describe('emitting declarations', () => { expect(emitResult.exitCode).toBe(0); expect(project.read('index.d.ts')).toMatchInlineSnapshot(` - "import '@glint/template/glimmerx'; - import Component from '@glimmerx/component'; + "import Component from '@glimmerx/component'; export interface ApplicationArgs { version: string; } diff --git a/packages/cli/__tests__/utils/project.ts b/packages/cli/__tests__/utils/project.ts index e85ed9222..c3af2a2f2 100644 --- a/packages/cli/__tests__/utils/project.ts +++ b/packages/cli/__tests__/utils/project.ts @@ -1,10 +1,11 @@ import path from 'path'; -import os from 'os'; import fs from 'fs'; import execa, { ExecaChildProcess, Options } from 'execa'; +const ROOT = path.resolve(__dirname, '../../../../test-packages/ephemeral'); + export default class Project { - private rootDir = path.join(os.tmpdir(), Math.random().toString(16).slice(2)); + private rootDir = path.join(ROOT, Math.random().toString(16).slice(2)); private constructor() {} @@ -18,7 +19,6 @@ export default class Project { let project = new Project(); let tsconfig = { compilerOptions: { - plugins: [{ name: path.resolve(__dirname, '../lib') }], strict: true, target: 'es2019', module: 'es2015', @@ -29,17 +29,14 @@ export default class Project { }; fs.rmdirSync(project.rootDir, { recursive: true }); - fs.mkdirSync(project.rootDir); + fs.mkdirSync(project.rootDir, { recursive: true }); + fs.writeFileSync(path.join(project.rootDir, 'package.json'), '{}'); + fs.writeFileSync(path.join(project.rootDir, '.glintrc'), 'environment: glimmerx\n'); fs.writeFileSync( path.join(project.rootDir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2) ); - fs.writeFileSync(path.join(project.rootDir, 'package.json'), '{}'); - - project.linkPackage('@glint/template'); - project.linkPackage('@glimmerx/component'); - project.linkPackage('typescript'); return project; } @@ -66,14 +63,6 @@ export default class Project { public watch(options?: Options): Watch { return new Watch(this.check({ ...options, flags: ['--watch'], reject: false })); } - - private linkPackage(name: string): void { - let linkPath = path.join(this.rootDir, 'node_modules', name); - let linkTarget = path.dirname(require.resolve(`${name}/package.json`)); - - fs.mkdirSync(path.dirname(linkPath), { recursive: true }); - fs.symlinkSync(linkTarget, linkPath, 'dir'); - } } class Watch { diff --git a/packages/cli/src/perform-check.ts b/packages/cli/src/perform-check.ts index f0dc70b7c..856fe19aa 100644 --- a/packages/cli/src/perform-check.ts +++ b/packages/cli/src/perform-check.ts @@ -36,6 +36,7 @@ function collectDiagnostics( ...program.getSyntacticDiagnostics(), ...transformManager.getTransformDiagnostics(), ...program.getSemanticDiagnostics(), + ...program.getDeclarationDiagnostics(), ]; } diff --git a/packages/cli/src/transform-manager.ts b/packages/cli/src/transform-manager.ts index 753b31ff8..7ce7470bb 100644 --- a/packages/cli/src/transform-manager.ts +++ b/packages/cli/src/transform-manager.ts @@ -36,15 +36,16 @@ export default class TransformManager { public readFile(filename: string, encoding?: string): string | undefined { let source = this.ts.sys.readFile(filename, encoding); + let config = this.glintConfig; - // TODO: don't hard-code the relevant import we're interested in if ( + source && filename.endsWith('.ts') && !filename.endsWith('.d.ts') && - source?.includes('@glimmerx/component') && - this.glintConfig.includesFile(filename) + config.includesFile(filename) && + config.environment.moduleMayHaveTagImports(source) ) { - let transformedModule = rewriteModule(filename, source); + let transformedModule = rewriteModule(filename, source, config.environment); if (transformedModule) { this.transformedModules.set(filename, transformedModule); return transformedModule.transformedSource; diff --git a/packages/tsserver-plugin/__tests__/integration.test.ts b/packages/tsserver-plugin/__tests__/integration.test.ts index a3f5ccaf9..5d987f285 100644 --- a/packages/tsserver-plugin/__tests__/integration.test.ts +++ b/packages/tsserver-plugin/__tests__/integration.test.ts @@ -33,7 +33,6 @@ describe('tsserver plugin', () => { test('using private properties', async () => { await project.open({ 'index.ts': stripIndent` - import '@glint/template/glimmerx'; import Component, { hbs } from '@glimmerx/component'; export default class MyComponent extends Component { @@ -54,21 +53,20 @@ describe('tsserver plugin', () => { let messageInfo = await server.request(CommandTypes.Quickinfo, { file: project.filePath('index.ts'), - line: 9, + line: 8, offset: 13, }); // {{this.message}} in the template matches back to the private property expect(messageInfo?.documentation).toEqual('A message.'); - expect(messageInfo?.start).toEqual({ line: 9, offset: 12 }); - expect(messageInfo?.end).toEqual({ line: 9, offset: 19 }); + expect(messageInfo?.start).toEqual({ line: 8, offset: 12 }); + expect(messageInfo?.end).toEqual({ line: 8, offset: 19 }); expect(messageInfo?.displayString).toEqual('(property) MyComponent.message: string'); }); test('using args', async () => { await project.open({ 'index.ts': stripIndent` - import '@glint/template/glimmerx'; import Component, { hbs } from '@glimmerx/component'; interface MyComponentArgs { @@ -91,21 +89,20 @@ describe('tsserver plugin', () => { let strInfo = await server.request(CommandTypes.Quickinfo, { file: project.filePath('index.ts'), - line: 11, + line: 10, offset: 8, }); // {{@str}} in the template matches back to the arg definition expect(strInfo?.documentation).toEqual('Some string'); - expect(strInfo?.start).toEqual({ line: 11, offset: 8 }); - expect(strInfo?.end).toEqual({ line: 11, offset: 11 }); + expect(strInfo?.start).toEqual({ line: 10, offset: 8 }); + expect(strInfo?.end).toEqual({ line: 10, offset: 11 }); expect(strInfo?.displayString).toEqual('(property) MyComponentArgs.str: string'); }); test('curly block params', async () => { await project.open({ 'index.ts': stripIndent` - import '@glint/template/glimmerx'; import Component, { hbs } from '@glimmerx/component'; export default class MyComponent extends Component { @@ -125,24 +122,24 @@ describe('tsserver plugin', () => { let indexInfo = await server.request(CommandTypes.Quickinfo, { file: project.filePath('index.ts'), - line: 7, + line: 6, offset: 15, }); // {{index}} in the template matches back to the block param - expect(indexInfo?.start).toEqual({ line: 7, offset: 15 }); - expect(indexInfo?.end).toEqual({ line: 7, offset: 20 }); + expect(indexInfo?.start).toEqual({ line: 6, offset: 15 }); + expect(indexInfo?.end).toEqual({ line: 6, offset: 20 }); expect(indexInfo?.displayString).toEqual('var index: number'); let itemInfo = await server.request(CommandTypes.Quickinfo, { file: project.filePath('index.ts'), - line: 7, + line: 6, offset: 26, }); // {{item}} in the template matches back to the block param - expect(itemInfo?.start).toEqual({ line: 7, offset: 26 }); - expect(itemInfo?.end).toEqual({ line: 7, offset: 30 }); + expect(itemInfo?.start).toEqual({ line: 6, offset: 26 }); + expect(itemInfo?.end).toEqual({ line: 6, offset: 30 }); expect(itemInfo?.displayString).toEqual('var item: string'); }); @@ -174,7 +171,6 @@ describe('tsserver plugin', () => { test('passing component args', async () => { await project.open({ 'index.ts': stripIndent` - import '@glint/template/glimmerx'; import Component, { hbs } from '@glimmerx/component'; export default class MyComponent extends Component { @@ -189,7 +185,7 @@ describe('tsserver plugin', () => { let completions = await server.request(CommandTypes.CompletionInfo, { file: project.filePath('index.ts'), - line: 6, + line: 5, offset: 12, }); @@ -215,7 +211,7 @@ describe('tsserver plugin', () => { let details = await server.request(CommandTypes.CompletionDetails, { file: project.filePath('index.ts'), - line: 6, + line: 5, offset: 12, entryNames: ['bar'], }); @@ -227,7 +223,6 @@ describe('tsserver plugin', () => { test('referencing class properties', async () => { await project.open({ 'index.ts': stripIndent` - import '@glint/template/glimmerx'; import Component, { hbs } from '@glimmerx/component'; export default class MyComponent extends Component { @@ -242,7 +237,7 @@ describe('tsserver plugin', () => { let completions = await server.request(CommandTypes.CompletionInfo, { file: project.filePath('index.ts'), - line: 8, + line: 7, offset: 13, }); @@ -255,7 +250,7 @@ describe('tsserver plugin', () => { let details = await server.request(CommandTypes.CompletionDetails, { file: project.filePath('index.ts'), - line: 8, + line: 7, offset: 13, entryNames: ['message'], }); @@ -267,7 +262,6 @@ describe('tsserver plugin', () => { test('referencing own args', async () => { await project.open({ 'index.ts': stripIndent` - import '@glint/template/glimmerx'; import Component, { hbs } from '@glimmerx/component'; interface MyComponentArgs { @@ -284,7 +278,7 @@ describe('tsserver plugin', () => { let completions = await server.request(CommandTypes.CompletionInfo, { file: project.filePath('index.ts'), - line: 10, + line: 9, offset: 9, }); @@ -297,7 +291,7 @@ describe('tsserver plugin', () => { let details = await server.request(CommandTypes.CompletionDetails, { file: project.filePath('index.ts'), - line: 10, + line: 9, offset: 9, entryNames: ['items'], }); @@ -309,7 +303,6 @@ describe('tsserver plugin', () => { test('referencing block params', async () => { await project.open({ 'index.ts': stripIndent` - import '@glint/template/glimmerx'; import Component, { hbs } from '@glimmerx/component'; export default class MyComponent extends Component { @@ -324,7 +317,7 @@ describe('tsserver plugin', () => { let completions = await server.request(CommandTypes.CompletionInfo, { file: project.filePath('index.ts'), - line: 7, + line: 6, offset: 8, }); @@ -337,7 +330,7 @@ describe('tsserver plugin', () => { let details = await server.request(CommandTypes.CompletionDetails, { file: project.filePath('index.ts'), - line: 7, + line: 6, offset: 8, entryNames: ['letter'], }); @@ -349,7 +342,6 @@ describe('tsserver plugin', () => { test('referencing module-scope identifiers', async () => { await project.open({ 'index.ts': stripIndent` - import '@glint/template/glimmerx'; import Component, { hbs } from '@glimmerx/component'; const greeting: string = 'hello'; @@ -364,7 +356,7 @@ describe('tsserver plugin', () => { let completions = await server.request(CommandTypes.CompletionInfo, { file: project.filePath('index.ts'), - line: 8, + line: 7, offset: 8, }); @@ -377,7 +369,7 @@ describe('tsserver plugin', () => { let details = await server.request(CommandTypes.CompletionDetails, { file: project.filePath('index.ts'), - line: 8, + line: 7, offset: 8, entryNames: ['greeting'], }); @@ -394,7 +386,6 @@ describe('tsserver plugin', () => { `, 'index.ts': stripIndent` - import '@glint/template/glimmerx'; import Component, { hbs } from '@glimmerx/component'; export default class MyComponent extends Component { @@ -408,7 +399,7 @@ describe('tsserver plugin', () => { let completions = await server.request(CommandTypes.CompletionInfo, { file: project.filePath('index.ts'), includeExternalModuleExports: true, - line: 6, + line: 5, offset: 10, }); @@ -423,7 +414,7 @@ describe('tsserver plugin', () => { let details = await server.request(CommandTypes.CompletionDetails, { file: project.filePath('index.ts'), - line: 6, + line: 5, offset: 10, entryNames: [{ name: 'Greeting', source: project.filePath('greeting') }], }); @@ -439,8 +430,8 @@ describe('tsserver plugin', () => { textChanges: [ { newText: `import Greeting from './greeting';${os.EOL}`, - start: { line: 3, offset: 1 }, - end: { line: 3, offset: 1 }, + start: { line: 2, offset: 1 }, + end: { line: 2, offset: 1 }, }, ], }, @@ -458,7 +449,6 @@ describe('tsserver plugin', () => { `, 'index.ts': stripIndent` - import '@glint/template/glimmerx'; import Component, { hbs } from '@glimmerx/component'; import { Name } from './greeting'; @@ -473,7 +463,7 @@ describe('tsserver plugin', () => { let completions = await server.request(CommandTypes.CompletionInfo, { file: project.filePath('index.ts'), includeExternalModuleExports: true, - line: 7, + line: 6, offset: 10, }); @@ -488,7 +478,7 @@ describe('tsserver plugin', () => { let details = await server.request(CommandTypes.CompletionDetails, { file: project.filePath('index.ts'), - line: 7, + line: 6, offset: 10, entryNames: [{ name: 'Greeting', source: project.filePath('greeting') }], }); @@ -504,8 +494,8 @@ describe('tsserver plugin', () => { textChanges: [ { newText: `import Greeting, { Name } from './greeting';`, - start: { line: 3, offset: 1 }, - end: { line: 3, offset: 35 }, + start: { line: 2, offset: 1 }, + end: { line: 2, offset: 35 }, }, ], }, @@ -524,7 +514,6 @@ describe('tsserver plugin', () => { test('component references', async () => { await project.open({ - 'lib.ts': `import '@glint/template/glimmerx';`, 'greeting.ts': stripIndent` import Component, { hbs } from '@glimmerx/component'; @@ -622,7 +611,6 @@ describe('tsserver plugin', () => { test('arg references', async () => { await project.open({ - 'lib.ts': `import '@glint/template/glimmerx';`, 'greeting.ts': stripIndent` import Component, { hbs } from '@glimmerx/component'; @@ -727,7 +715,6 @@ describe('tsserver plugin', () => { describe('find definition', () => { test('component invocation', async () => { await project.open({ - 'lib.ts': `import '@glint/template/glimmerx';`, 'greeting.ts': stripIndent` import Component, { hbs } from '@glimmerx/component'; export default class Greeting extends Component<{ message: string }> { @@ -777,7 +764,6 @@ describe('tsserver plugin', () => { test('arg passing', async () => { await project.open({ - 'lib.ts': `import '@glint/template/glimmerx';`, 'greeting.ts': stripIndent` import Component, { hbs } from '@glimmerx/component'; @@ -832,7 +818,6 @@ describe('tsserver plugin', () => { test('arg use', async () => { await project.open({ - 'lib.ts': `import '@glint/template/glimmerx';`, 'greeting.ts': stripIndent` import Component, { hbs } from '@glimmerx/component'; @@ -877,7 +862,6 @@ describe('tsserver plugin', () => { test('import source', async () => { await project.open({ - 'lib.ts': `import '@glint/template/glimmerx';`, 'greeting.ts': stripIndent` import Component, { hbs } from '@glimmerx/component'; @@ -932,7 +916,6 @@ describe('tsserver plugin', () => { describe('renaming symbols', () => { test('arg', async () => { await project.open({ - 'lib.ts': `import '@glint/template/glimmerx';`, 'greeting.ts': stripIndent` import Component, { hbs } from '@glimmerx/component'; @@ -1039,7 +1022,6 @@ describe('tsserver plugin', () => { test('block param', async () => { await project.open({ - 'lib.ts': `import '@glint/template/glimmerx';`, 'index.ts': stripIndent` import Component, { hbs } from '@glimmerx/component'; @@ -1108,7 +1090,6 @@ describe('tsserver plugin', () => { test('component', async () => { await project.open({ - 'lib.ts': `import '@glint/template/glimmerx';`, 'greeting.ts': stripIndent` import Component, { hbs } from '@glimmerx/component'; @@ -1198,7 +1179,6 @@ describe('tsserver plugin', () => { test('module', async () => { await project.open({ - 'lib.ts': `import '@glint/template/glimmerx';`, 'greeting.ts': stripIndent` import Component, { hbs } from '@glimmerx/component'; @@ -1246,7 +1226,6 @@ describe('tsserver plugin', () => { test('introducing and fixing a template error with editor changes', async () => { await project.open({ 'index.ts': stripIndent` - import '@glint/template/glimmerx'; import Component, { hbs } from '@glimmerx/component'; export default class MyComponent extends Component { @@ -1264,8 +1243,8 @@ describe('tsserver plugin', () => { // Typo `debugger` to `debuggerr` await project.update('index.ts', { - start: { line: 6, offset: 15 }, - end: { line: 6, offset: 15 }, + start: { line: 5, offset: 15 }, + end: { line: 5, offset: 15 }, newText: 'r', }); @@ -1274,14 +1253,14 @@ describe('tsserver plugin', () => { expect(diagnostics.length).toEqual(1); expect(diagnostics[0]).toMatchObject({ message: `Property 'debuggerr' does not exist on type 'Globals'. Did you mean 'debugger'?`, - startLocation: { line: 6, offset: 7 }, - endLocation: { line: 6, offset: 16 }, + startLocation: { line: 5, offset: 7 }, + endLocation: { line: 5, offset: 16 }, }); // Fix the typo await project.update('index.ts', { - start: { line: 6, offset: 15 }, - end: { line: 6, offset: 16 }, + start: { line: 5, offset: 15 }, + end: { line: 5, offset: 16 }, newText: '', }); @@ -1293,7 +1272,6 @@ describe('tsserver plugin', () => { test('introducing and fixing a TS syntax error with editor changes', async () => { await project.open({ 'index.ts': stripIndent` - import '@glint/template/glimmerx'; import Component, { hbs } from '@glimmerx/component'; export default class MyComponent extends Component { @@ -1338,10 +1316,9 @@ describe('tsserver plugin', () => { describe('custom configuration', () => { test('it honors .glintrc include/exclude', async () => { - project.write('.glintrc', 'exclude: "index.ts"'); + project.write('.glintrc', 'environment: glimmerx\nexclude: "index.ts"\n'); await project.open({ - 'lib.ts': `import '@glint/template/glimmerx';`, 'index.ts': stripIndent` import Component, { hbs } from '@glimmerx/component'; diff --git a/packages/tsserver-plugin/__tests__/test-server.ts b/packages/tsserver-plugin/__tests__/test-server.ts index 23e2c72fd..7aac5b135 100644 --- a/packages/tsserver-plugin/__tests__/test-server.ts +++ b/packages/tsserver-plugin/__tests__/test-server.ts @@ -1,11 +1,9 @@ import { fork, ChildProcess } from 'child_process'; import { createInterface, ReadLine } from 'readline'; -import os from 'os'; import fs from 'fs'; import path from 'path'; import { EventEmitter } from 'events'; import Protocol, { CommandTypes, WatchFileKind } from 'typescript/lib/protocol'; -import { normalizePath } from '@glint/config'; export type Requests = { [CommandTypes.SemanticDiagnosticsSync]: [ @@ -53,8 +51,10 @@ export type Deferred = { reject: (reason: unknown) => void; }; +const ROOT = path.resolve(__dirname, '../../../test-packages/ephemeral'); + export class Project { - private rootDir = path.join(os.tmpdir(), Math.random().toString(16).slice(2)); + private rootDir = path.join(ROOT, Math.random().toString(16).slice(2)); private openFiles = new Set(); private printLogContents = false; @@ -89,13 +89,11 @@ export class Project { } fs.rmdirSync(this.rootDir, { recursive: true }); - fs.mkdirSync(this.rootDir); + fs.mkdirSync(this.rootDir, { recursive: true }); fs.writeFileSync(path.join(this.rootDir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2)); fs.writeFileSync(path.join(this.rootDir, 'package.json'), '{}'); - - this.linkPackage('@glint/template'); - this.linkPackage('@glimmerx/component'); + fs.writeFileSync(path.join(this.rootDir, '.glintrc'), 'environment: glimmerx\n'); await this.server.sendAndWait(CommandTypes.Configure, { watchOptions: { watchFile: WatchFileKind.UseFsEventsOnParentDirectory }, @@ -170,14 +168,6 @@ export class Project { fs.rmdirSync(this.rootDir, { recursive: true }); } - - private linkPackage(name: string): void { - let linkPath = path.join(this.rootDir, 'node_modules', name); - let linkTarget = path.dirname(require.resolve(`${name}/package.json`)); - - fs.mkdirSync(path.dirname(linkPath), { recursive: true }); - fs.symlinkSync(linkTarget, linkPath, 'dir'); - } } function defer(): Deferred { @@ -306,3 +296,11 @@ export class TSServer extends EventEmitter { } } } + +function normalizePath(fileName: string): string { + if (path.sep !== '/') { + return fileName.split(path.sep).join('/'); + } + + return fileName; +} diff --git a/packages/tsserver-plugin/src/virtual-module-manager.ts b/packages/tsserver-plugin/src/virtual-module-manager.ts index cb0723f09..d186c4a95 100644 --- a/packages/tsserver-plugin/src/virtual-module-manager.ts +++ b/packages/tsserver-plugin/src/virtual-module-manager.ts @@ -43,16 +43,16 @@ export default class VirtualModuleManager { let originalPath = getOriginalPath(path); let originalScriptInfo = configuredProject.project.getScriptInfo(originalPath); + let glintEnvironment = configuredProject.config.environment; let snapshot = originalScriptInfo?.getSnapshot(); let length = snapshot?.getLength() ?? 0; let content = snapshot?.getText(0, length) ?? this.sysReadFile(originalPath, encoding); - // TODO: drive this off of the GlintConfig for this file - if (!content?.includes('@glimmerx/component')) { + if (!content || !glintEnvironment.moduleMayHaveTagImports(content)) { return content; } - let transformedModule = rewriteModule(originalPath, content); + let transformedModule = rewriteModule(originalPath, content, glintEnvironment); if (transformedModule && originalScriptInfo) { this.transformedModules.set(originalScriptInfo, transformedModule); return transformedModule.transformedSource; From 8b775b16aebe94e395d02c490f4a11f15b9f8ac9 Mon Sep 17 00:00:00 2001 From: Dan Freeman Date: Fri, 31 Jul 2020 15:19:57 +0200 Subject: [PATCH 6/9] Clean environment-specific contents out of @glint/template --- packages/template/-private/built-ins/fn.d.ts | 16 ----- packages/template/-private/built-ins/on.d.ts | 9 --- packages/template/-private/globals.d.ts | 46 ------------ .../template/-private/keywords/each-in.d.ts | 7 -- .../-private/keywords/has-block-params.d.ts | 2 +- .../template/-private/keywords/index.d.ts | 8 +++ packages/template/README.md | 3 +- packages/template/__tests__/helper.test.ts | 32 --------- packages/template/__tests__/invoke.test.ts | 21 ++---- .../__tests__/keywords/component.test.ts | 15 ++-- .../__tests__/keywords/debugger.test.ts | 9 +-- .../__tests__/keywords/each-in.test.ts | 27 ------- .../template/__tests__/keywords/each.test.ts | 9 +-- .../template/__tests__/keywords/fn.test.ts | 24 ------- .../template/__tests__/keywords/let.test.ts | 7 +- .../template/__tests__/keywords/on.test.ts | 12 ---- .../template/__tests__/keywords/with.test.ts | 9 +-- .../template/__tests__/resolution.test.ts | 72 ++----------------- packages/template/__tests__/test-component.ts | 47 ++++++++++++ packages/template/ember.d.ts | 56 --------------- packages/template/glimmer.d.ts | 30 -------- packages/template/glimmerx.d.ts | 49 ------------- packages/template/index.d.ts | 1 - packages/template/package.json | 2 +- 24 files changed, 98 insertions(+), 415 deletions(-) delete mode 100644 packages/template/-private/built-ins/fn.d.ts delete mode 100644 packages/template/-private/built-ins/on.d.ts delete mode 100644 packages/template/-private/globals.d.ts delete mode 100644 packages/template/-private/keywords/each-in.d.ts create mode 100644 packages/template/-private/keywords/index.d.ts delete mode 100644 packages/template/__tests__/helper.test.ts delete mode 100644 packages/template/__tests__/keywords/each-in.test.ts delete mode 100644 packages/template/__tests__/keywords/fn.test.ts delete mode 100644 packages/template/__tests__/keywords/on.test.ts create mode 100644 packages/template/__tests__/test-component.ts delete mode 100644 packages/template/ember.d.ts delete mode 100644 packages/template/glimmer.d.ts delete mode 100644 packages/template/glimmerx.d.ts diff --git a/packages/template/-private/built-ins/fn.d.ts b/packages/template/-private/built-ins/fn.d.ts deleted file mode 100644 index ce3a44fac..000000000 --- a/packages/template/-private/built-ins/fn.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Yuck. This will work for generic functions if the types are fixed given the initial args, -// but otherwise they'll degrade to `unknown` in the type of the returned function. -// I don't think there's a better way to type `{{fn}}` though; this already maintains more type -// info than Ramda's `partial`, for instance. -// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/539042117cd697da07daf93092bdf16bc14922d8/types/ramda/index.d.ts#L1310-L1324 - -import { NoNamedArgs, ReturnsValue } from '../signature'; - -// prettier-ignore -export default interface FnHelper { - (args: NoNamedArgs, f: (...rest: Args) => Ret): ReturnsValue<(...rest: Args) => Ret>; - (args: NoNamedArgs, f: (a: A, ...rest: Args) => Ret, a: A): ReturnsValue<(...rest: Args) => Ret>; - (args: NoNamedArgs, f: (a: A, b: B, ...rest: Args) => Ret, a: A, b: B): ReturnsValue<(...rest: Args) => Ret>; - (args: NoNamedArgs, f: (a: A, b: B, c: C, ...rest: Args) => Ret, a: A, b: B, c: C): ReturnsValue<(...rest: Args) => Ret>; - (args: NoNamedArgs, f: (a: A, b: B, c: C, d: D, ...rest: Args) => Ret, a: A, b: B, c: C, d: D): ReturnsValue<(...rest: Args) => Ret>; -} diff --git a/packages/template/-private/built-ins/on.d.ts b/packages/template/-private/built-ins/on.d.ts deleted file mode 100644 index 5d78419cf..000000000 --- a/packages/template/-private/built-ins/on.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { CreatesModifier } from '../signature'; - -export default interface OnModifier { - ( - args: AddEventListenerOptions, - key: K, - eventHandler: (event: HTMLElementEventMap[K]) => void - ): CreatesModifier; -} diff --git a/packages/template/-private/globals.d.ts b/packages/template/-private/globals.d.ts deleted file mode 100644 index d721a0c31..000000000 --- a/packages/template/-private/globals.d.ts +++ /dev/null @@ -1,46 +0,0 @@ -// According to the strict mode RFC, these identifiers are 'keywords' that are -// always implicitly in scope. While that's true for Ember applications, it doesn't -// necessarily apply to Glimmer.js or GlimmerX, so this likely needs to be factored -// into those environment-specific type declarations in the future. -// https://github.com/emberjs/rfcs/pull/496/files#diff-813a6bebec3bf341e6af852f27444bc0R437 -interface Keywords { - action: void; // TODO - component: import('./keywords/component').default; - debugger: import('./keywords/debugger').default; - 'each-in': import('./keywords/each-in').default; - each: import('./keywords/each').default; - 'has-block-params': import('./keywords/has-block-params').default; - 'has-block': import('./keywords/has-block').default; - hasBlock: import('./keywords/has-block').default; - // `if` is implemented directly in @glint/transform - 'in-element': void; // TODO - let: import('./keywords/let').default; - 'link-to': void; // TODO - loc: void; // TODO - log: void; // TODO - mount: void; // TODO - mut: void; // TODO - outlet: void; // TODO - 'query-params': void; // TODO - readonly: void; // TODO - unbound: void; // TODO - unless: void; // TODO (maybe implement directly in @glint/transform?) - with: import('./keywords/with').default; - // `yield` is implemented directly in @glint/transform -} - -// This `Globals` interface dictates what identifiers will always be in scope -// even when not statically visible. In principle it can be extended outside -// this package, and could even be used to implement support for today's -// resolver-based template entity lookup via a type registry-style system. -interface Globals extends Keywords { - // The strict-mode RFC proposes that these be importable since they're - // theoretically implementable in userland, but for simplicity for now we're - // just including them in `Globals`. - fn: import('./built-ins/fn').default; - on: import('./built-ins/on').default; -} - -declare const Globals: Globals; - -export default Globals; diff --git a/packages/template/-private/keywords/each-in.d.ts b/packages/template/-private/keywords/each-in.d.ts deleted file mode 100644 index add9b1cc2..000000000 --- a/packages/template/-private/keywords/each-in.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NoNamedArgs, AcceptsBlocks } from '../signature'; - -export default interface EachInKeyword { - (args: NoNamedArgs, object: T): AcceptsBlocks<{ - default: [keyof T, T[keyof T]]; - }>; -} diff --git a/packages/template/-private/keywords/has-block-params.d.ts b/packages/template/-private/keywords/has-block-params.d.ts index 327488de8..a4f30eb37 100644 --- a/packages/template/-private/keywords/has-block-params.d.ts +++ b/packages/template/-private/keywords/has-block-params.d.ts @@ -1,5 +1,5 @@ import { NoNamedArgs, ReturnsValue } from '../signature'; -export default interface HasBlocParamskKeyword { +export default interface HasBlockParamsKeyword { (args: NoNamedArgs, blockName?: string): ReturnsValue; } diff --git a/packages/template/-private/keywords/index.d.ts b/packages/template/-private/keywords/index.d.ts new file mode 100644 index 000000000..50fc8ced9 --- /dev/null +++ b/packages/template/-private/keywords/index.d.ts @@ -0,0 +1,8 @@ +export { default as ComponentKeyword } from './component'; +export { default as DebuggerKeyword } from './debugger'; +export { default as EachKeyword } from './each'; +export { default as HasBlockKeyword } from './has-block'; +export { default as HasBlockParamsKeyword } from './has-block-params'; +export { default as InElementKeyword } from './in-element'; +export { default as LetKeyword } from './let'; +export { default as WithKeyword } from './with'; diff --git a/packages/template/README.md b/packages/template/README.md index d03730960..6791fb060 100644 --- a/packages/template/README.md +++ b/packages/template/README.md @@ -1,5 +1,6 @@ # `@glint/template` -This package contains type declarations used by other [glint] packages for perfoming TypeScript analysis of Glimmer templates. These types determine the semantics of component/modifier/helper invocations. +This package contains type declarations used by other [glint] packages for perfoming TypeScript analysis of Glimmer templates. These types form the basis on which [glint-environment packages] are built. [glint]: https://github.com/typed-ember/glint +[glint-environment packages]: https://www.npmjs.com/search?q=keywords:glint-environment diff --git a/packages/template/__tests__/helper.test.ts b/packages/template/__tests__/helper.test.ts deleted file mode 100644 index 57c5da12d..000000000 --- a/packages/template/__tests__/helper.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import '@glint/template/ember'; -import Helper, { helper } from '@ember/component/helper'; -import { resolve, invokeInline } from '@glint/template'; -import { expectTypeOf } from 'expect-type'; - -// Type parameters can be persisted for functional helpers -const id = resolve(helper(([value]: [T]) => value)); - -// For class-based, unfortunately, they can't, because there's an intervening object type -const hello = resolve( - class HelloHelper extends Helper { - compute(_: [], { target }: { target: string }): string { - return `Hello, ${target}`; - } - } -); - -// Correct parametrized type for a functional helper -expectTypeOf(invokeInline(id({}, 'hello'))).toEqualTypeOf(); -expectTypeOf(invokeInline(id({}, 123))).toEqualTypeOf(); - -// Correct type for a class-based helper -expectTypeOf(invokeInline(hello({ target: 'world' }))).toEqualTypeOf(); - -// @ts-expect-error: Missing positional param -invokeInline(id({})); - -// @ts-expect-error: Invalid named param -invokeInline(hello({ target: 'world', foo: true })); - -// @ts-expect-error: Named param when none are expected -invokeInline(id({ key: 'value' }, 'hello')); diff --git a/packages/template/__tests__/invoke.test.ts b/packages/template/__tests__/invoke.test.ts index d981067e2..9e760331a 100644 --- a/packages/template/__tests__/invoke.test.ts +++ b/packages/template/__tests__/invoke.test.ts @@ -1,30 +1,27 @@ -import '@glint/template/glimmer'; - -import GlimmerComponent from '@glimmer/component'; import { template, resolve, toBlock, invokeBlock, ResolveContext, - Globals, invokeModifier, invokeInline, resolveOrReturn, } from '@glint/template'; import { expectTypeOf } from 'expect-type'; import { BlockYield } from '@glint/template/-private/blocks'; +import TestComponent, { globals } from './test-component'; type MyComponentArgs = { name?: string; value: T; }; -class MyComponent extends GlimmerComponent> { +class MyComponent extends TestComponent> { private state = { ready: false }; - private wrapperClicked(message: string, event: MouseEvent): void { - console.log(message, event.x, event.y); + private wrapperClicked(event: MouseEvent): void { + console.log('clicked', event.x, event.y); } /** @@ -37,15 +34,9 @@ class MyComponent extends GlimmerComponent> { * ``` */ public static template = template(function* (𝚪: ResolveContext>) { - yield invokeBlock(resolve(Globals['let'])({}, 𝚪.this.state.ready), { + yield invokeBlock(resolve(globals.let)({}, 𝚪.this.state.ready), { *default(isReady) { - invokeModifier( - resolve(Globals['on'])( - {}, - 'click', - invokeInline(resolve(Globals['fn'])({}, 𝚪.this.wrapperClicked, 'clicked!')) - ) - ); + invokeModifier(resolve(globals.on)({}, 'click', 𝚪.this.wrapperClicked)); yield toBlock('body', isReady, 𝚪.args.value); }, diff --git a/packages/template/__tests__/keywords/component.test.ts b/packages/template/__tests__/keywords/component.test.ts index 5d4d9ca95..61eb43f91 100644 --- a/packages/template/__tests__/keywords/component.test.ts +++ b/packages/template/__tests__/keywords/component.test.ts @@ -1,10 +1,11 @@ import { expectTypeOf } from 'expect-type'; -import { resolve, Globals, toBlock, invokeInline, invokeBlock } from '@glint/template'; -import { AcceptsBlocks } from '@glint/template/-private/signature'; +import { resolve, toBlock, invokeInline, invokeBlock } from '@glint/template'; +import { AcceptsBlocks } from '@glint/template/-private'; import { BlockYield } from '@glint/template/-private/blocks'; import { Invokable } from '@glint/template/-private/invoke'; +import { ComponentKeyword } from '@glint/template/-private/keywords'; -const component = resolve(Globals['component']); +const componentKeyword = resolve({} as ComponentKeyword); declare const TestComponent: Invokable<(args: { value: string; @@ -13,8 +14,8 @@ declare const TestComponent: Invokable<(args: { inverse?: []; }>>; -const NoopCurriedTestComponent = invokeInline(component({}, TestComponent)); -const ValueCurriedTestComponent = invokeInline(component({ value: 'hello' }, TestComponent)); +const NoopCurriedTestComponent = invokeInline(componentKeyword({}, TestComponent)); +const ValueCurriedTestComponent = invokeInline(componentKeyword({ value: 'hello' }, TestComponent)); // Invoking the noop-curried component expectTypeOf(invokeBlock(resolve(NoopCurriedTestComponent)({ value: 'hello' }), {})).toEqualTypeOf< @@ -65,7 +66,7 @@ expectTypeOf(invokeBlock(resolve(ValueCurriedTestComponent)({ value: 'hi' }), {} invokeBlock(resolve(ValueCurriedTestComponent)({ value: 123 }), {}); // @ts-expect-error: Attempting to curry a nonexistent arg -component({ foo: true }, TestComponent); +componentKeyword({ foo: true }, TestComponent); // @ts-expect-error: Attempting to curry an arg with the wrong type -component({ value: 123 }, TestComponent); +componentKeyword({ value: 123 }, TestComponent); diff --git a/packages/template/__tests__/keywords/debugger.test.ts b/packages/template/__tests__/keywords/debugger.test.ts index 73c88caf8..2af141b42 100644 --- a/packages/template/__tests__/keywords/debugger.test.ts +++ b/packages/template/__tests__/keywords/debugger.test.ts @@ -1,10 +1,11 @@ import { expectTypeOf } from 'expect-type'; -import { resolve, Globals, invokeInline } from '@glint/template'; +import { resolve, invokeInline } from '@glint/template'; +import { DebuggerKeyword } from '@glint/template/-private/keywords'; -const debug = resolve(Globals['debugger']); +const debuggerKeyword = resolve({} as DebuggerKeyword); // Can be invoked as {{debugger}} -expectTypeOf(invokeInline(debug({}))).toEqualTypeOf(); +expectTypeOf(invokeInline(debuggerKeyword({}))).toEqualTypeOf(); // @ts-expect-error: Rejects any additional arguments -debug({}, 'hello'); +debuggerKeyword({}, 'hello'); diff --git a/packages/template/__tests__/keywords/each-in.test.ts b/packages/template/__tests__/keywords/each-in.test.ts deleted file mode 100644 index 100142498..000000000 --- a/packages/template/__tests__/keywords/each-in.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { expectTypeOf } from 'expect-type'; -import { resolve, Globals, toBlock, invokeBlock } from '@glint/template'; -import { BlockYield } from '@glint/template/-private/blocks'; - -const eachIn = resolve(Globals['each-in']); - -// Yield out key/value pairs from the given input -expectTypeOf( - invokeBlock(eachIn({}, { foo: 'hello' }), { - *default(key, value) { - // expect-type doesn't handle constrained type parameters well, but we - // can at least ensure they're assignable to the right type - let localKey: 'foo' = key; - let localValue: string = value; - - // @ts-expect-error: `key` should extend 'foo' - let badLocalKey: 'bar' = key; - - // @ts-expect-error: `value` should be a string - let badLocalValue: number = value; - - console.log(badLocalKey, badLocalValue); - - yield toBlock('body', localKey, localValue.length); - }, - }) -).toEqualTypeOf>(); diff --git a/packages/template/__tests__/keywords/each.test.ts b/packages/template/__tests__/keywords/each.test.ts index 7a44f172a..ebc0c453c 100644 --- a/packages/template/__tests__/keywords/each.test.ts +++ b/packages/template/__tests__/keywords/each.test.ts @@ -1,12 +1,13 @@ import { expectTypeOf } from 'expect-type'; -import { resolve, Globals, toBlock, invokeBlock } from '@glint/template'; +import { resolve, toBlock, invokeBlock } from '@glint/template'; import { BlockYield } from '@glint/template/-private/blocks'; +import { EachKeyword } from '@glint/template/-private/keywords'; -const each = resolve(Globals['each']); +const eachKeyword = resolve({} as EachKeyword); // Yield out array values and indices expectTypeOf( - invokeBlock(each({}, ['a', 'b', 'c']), { + invokeBlock(eachKeyword({}, ['a', 'b', 'c']), { *default(value, index) { expectTypeOf(value).toEqualTypeOf(); expectTypeOf(index).toEqualTypeOf(); @@ -17,7 +18,7 @@ expectTypeOf( // Accept a `key` string expectTypeOf( - invokeBlock(each({ key: 'id' }, [{ id: 1 }]), { + invokeBlock(eachKeyword({ key: 'id' }, [{ id: 1 }]), { *default() { // Don't yield }, diff --git a/packages/template/__tests__/keywords/fn.test.ts b/packages/template/__tests__/keywords/fn.test.ts deleted file mode 100644 index a802aac40..000000000 --- a/packages/template/__tests__/keywords/fn.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { expectTypeOf } from 'expect-type'; -import { resolve, Globals, invokeInline } from '@glint/template'; - -const fn = resolve(Globals['fn']); -const f = (x: string, y: number): number => x.length + y; -const id = (x: T): T => x; - -// A no-op curry works -expectTypeOf(invokeInline(fn({}, f))).toEqualTypeOf<(x: string, y: number) => number>(); - -// Currying one arg works -expectTypeOf(invokeInline(fn({}, f, 'hello'))).toEqualTypeOf<(y: number) => number>(); - -// Currying all args works -expectTypeOf(invokeInline(fn({}, f, 'hello', 123))).toEqualTypeOf<() => number>(); - -// @ts-expect-error: Currying with a bad argument fails -fn({}, f, true); - -// Type parameters degrade to `unknown` by default (limitation, not a feature) -expectTypeOf(invokeInline(fn({}, id))).toEqualTypeOf<(x: unknown) => unknown>(); - -// Type parameters are preserved when fixed by an input -expectTypeOf(invokeInline(fn({}, id, 'hello'))).toEqualTypeOf<() => string>(); diff --git a/packages/template/__tests__/keywords/let.test.ts b/packages/template/__tests__/keywords/let.test.ts index b6f28923c..1f6b0d9c8 100644 --- a/packages/template/__tests__/keywords/let.test.ts +++ b/packages/template/__tests__/keywords/let.test.ts @@ -1,12 +1,13 @@ import { expectTypeOf } from 'expect-type'; -import { resolve, Globals, toBlock, invokeBlock } from '@glint/template'; +import { resolve, toBlock, invokeBlock } from '@glint/template'; import { BlockYield } from '@glint/template/-private/blocks'; +import { LetKeyword } from '@glint/template/-private/keywords'; -const lett = resolve(Globals['let']); +const letKeyword = resolve({} as LetKeyword); // Yields out the given values expectTypeOf( - invokeBlock(lett({}, 'hello', 123), { + invokeBlock(letKeyword({}, 'hello', 123), { *default(str, num) { expectTypeOf(str).toEqualTypeOf(); expectTypeOf(num).toEqualTypeOf(); diff --git a/packages/template/__tests__/keywords/on.test.ts b/packages/template/__tests__/keywords/on.test.ts deleted file mode 100644 index b3e248e7d..000000000 --- a/packages/template/__tests__/keywords/on.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { resolve, Globals, invokeModifier } from '@glint/template'; -import { expectTypeOf } from 'expect-type'; - -const on = resolve(Globals['on']); - -expectTypeOf( - invokeModifier( - on({}, 'keydown', (event) => { - expectTypeOf(event).toEqualTypeOf(); - }) - ) -).toEqualTypeOf(); diff --git a/packages/template/__tests__/keywords/with.test.ts b/packages/template/__tests__/keywords/with.test.ts index a43451dc3..137a232c5 100644 --- a/packages/template/__tests__/keywords/with.test.ts +++ b/packages/template/__tests__/keywords/with.test.ts @@ -1,12 +1,13 @@ import { expectTypeOf } from 'expect-type'; -import { resolve, Globals, toBlock, invokeBlock } from '@glint/template'; +import { resolve, toBlock, invokeBlock } from '@glint/template'; import { BlockYield } from '@glint/template/-private/blocks'; +import { WithKeyword } from '@glint/template/-private/keywords'; -const withh = resolve(Globals['with']); +const withKeyword = resolve({} as WithKeyword); // Yields out the given value expectTypeOf( - invokeBlock(withh({}, 'hello'), { + invokeBlock(withKeyword({}, 'hello'), { *default(str) { expectTypeOf(str).toEqualTypeOf(); yield toBlock('body', str); @@ -18,4 +19,4 @@ expectTypeOf( ).toEqualTypeOf>(); // @ts-expect-error: Rejects multiple values -withh({}, 'hello', 'goodbye'); +withKeyword({}, 'hello', 'goodbye'); diff --git a/packages/template/__tests__/resolution.test.ts b/packages/template/__tests__/resolution.test.ts index 7743367cd..02919058a 100644 --- a/packages/template/__tests__/resolution.test.ts +++ b/packages/template/__tests__/resolution.test.ts @@ -1,25 +1,20 @@ -import '@glint/template/ember'; -import '@glint/template/glimmer'; - -import GlimmerComponent from '@glimmer/component'; -import EmberComponent from '@ember/component'; import { expectTypeOf } from 'expect-type'; import { ResolveSignature, resolveOrReturn } from '@glint/template/-private/resolution'; import { TemplateContext } from '@glint/template/-private/template'; import { template, invokeBlock, resolve, toBlock, ResolveContext } from '@glint/template'; import { Invokable } from '@glint/template/-private/invoke'; import { AcceptsBlocks } from '../-private'; -import Globals from '../-private/globals'; +import TestComponent, { globals } from './test-component'; declare function value(): T; -// Glimmer component with no template +// Component with no template { type MyArgs = { value: T; }; - class MyComponent extends GlimmerComponent> {} + class MyComponent extends TestComponent> {} type ExpectedSignature = (args: MyArgs) => AcceptsBlocks<{ default?: [] }>; @@ -27,13 +22,13 @@ declare function value(): T; expectTypeOf>().toEqualTypeOf(); } -// Glimmer component with a template +// Component with a template { type MyArgs = { value: T; }; - class MyComponent extends GlimmerComponent> { + class MyComponent extends TestComponent> { private state = { ready: false }; /** @@ -44,7 +39,7 @@ declare function value(): T; * ``` */ public static template = template(function* (𝚪: ResolveContext>) { - yield invokeBlock(resolve(Globals['let'])({}, 𝚪.this.state.ready), { + yield invokeBlock(resolve(globals.let)({}, 𝚪.this.state.ready), { *default(isReady) { yield toBlock('body', isReady, 𝚪.args.value); }, @@ -71,61 +66,6 @@ declare function value(): T; expectTypeOf>>().toEqualTypeOf>(); } -// Ember component with no template -{ - class MyComponent extends EmberComponent { - public value!: T; - } - - type ExpectedSignature = ( - args: Partial>, - ...positional: unknown[] - ) => AcceptsBlocks<{ default?: [] }>; - - // Resolved component signature is as expected - expectTypeOf>().toEqualTypeOf(); -} - -// Ember component with a template -{ - class MyComponent extends EmberComponent { - public value!: T; - - /** - * ```hbs - * {{#let this.state.ready as |isReady|}} - * {{yield isReady @value to="body"}} - * {{/let}} - * ``` - */ - public static template = template(function* (𝚪: ResolveContext>) { - yield invokeBlock(resolve(Globals['let'])({}, 𝚪.this.value), { - *default(thisValue) { - yield toBlock('body', thisValue, 𝚪.args.value); - }, - }); - }); - } - - type ExpectedSignature = ( - args: Record - ) => AcceptsBlocks<{ - body?: [T, unknown]; - }>; - - type ExpectedContext = TemplateContext, Record>; - - // Template has the correct type - expectTypeOf(MyComponent.template).toEqualTypeOf>(); - - // Resolved component signature uses the template type - expectTypeOf>().toEqualTypeOf(); - - // Template context is inferred correctly - expectTypeOf>>().toEqualTypeOf>(); - expectTypeOf>>().toEqualTypeOf>(); -} - // A raw Invokable value { type TestSignature = ( diff --git a/packages/template/__tests__/test-component.ts b/packages/template/__tests__/test-component.ts new file mode 100644 index 000000000..08f96fc9a --- /dev/null +++ b/packages/template/__tests__/test-component.ts @@ -0,0 +1,47 @@ +import { + ResolutionKey, + TemplateContext, + AcceptsBlocks, + Invokable, + NoNamedArgs, + CreatesModifier, +} from '@glint/template/-private'; +import { LetKeyword } from '@glint/template/-private/keywords'; + +// This module contains a `@glimmer/component`-like base class and the +// declarations necessary for it to be used as a component in glint, as +// well as simple examples of a helper and modifier. + +export default TestComponent; +export declare const globals: { + let: LetKeyword; + on: ( + args: NoNamedArgs, + event: T, + callback: (event: HTMLElementEventMap[T]) => void + ) => CreatesModifier; +}; + +declare const ResolveTestComponent: unique symbol; +declare class TestComponent { + readonly args: T; + [ResolutionKey]: typeof ResolveTestComponent; +} + +type Constructor = new (...args: any) => T; + +declare module '@glint/template/resolution-rules' { + export interface ContextResolutions { + [ResolveTestComponent]: Host extends TestComponent + ? TemplateContext + : never; + } + + export interface SignatureResolutions { + [ResolveTestComponent]: InvokedValue extends Constructor> + ? InvokedValue extends { template: Invokable } + ? Signature + : (args: Args) => AcceptsBlocks<{ default?: [] }> + : never; + } +} diff --git a/packages/template/ember.d.ts b/packages/template/ember.d.ts deleted file mode 100644 index e646acc64..000000000 --- a/packages/template/ember.d.ts +++ /dev/null @@ -1,56 +0,0 @@ -// Import this module to enable resolution rules for Ember components and helpers - -import EmberComponent from '@ember/component'; -import Helper from '@ember/component/helper'; -import { - ResolutionKey, - TemplateContext, - ReturnsValue, - Invokable, - AcceptsBlocks, - NoNamedArgs, -} from '@glint/template/-private'; - -type Constructor = new (...args: never[]) => T; - -declare const ResolveEmberComponent: unique symbol; -declare const ResolveEmberHelper: unique symbol; - -declare module '@ember/component' { - export default interface Component { - [ResolutionKey]: typeof ResolveEmberComponent; - } -} - -declare module '@ember/component/helper' { - export default interface Helper { - [ResolutionKey]: typeof ResolveEmberHelper; - } - - export function helper( - f: (positional: Positional, named: Args) => T - ): Invokable<(args: Args, ...positional: Positional) => ReturnsValue>; -} - -declare module '@glint/template/resolution-rules' { - export interface ContextResolutions { - [ResolveEmberComponent]: Host extends EmberComponent - ? TemplateContext> - : never; - } - - export interface SignatureResolutions { - [ResolveEmberHelper]: InvokedValue extends Constructor - ? Compute extends (positional: infer Positional, named: infer Named) => infer Result - ? (named: Named, ...positional: Positional & unknown[]) => ReturnsValue - : never - : never; - - // TODO: deal with positional params? - [ResolveEmberComponent]: InvokedValue extends Constructor - ? InvokedValue extends { template: Invokable } - ? Signature - : (args: Partial, ...positional: unknown[]) => AcceptsBlocks<{ default?: [] }> - : never; - } -} diff --git a/packages/template/glimmer.d.ts b/packages/template/glimmer.d.ts deleted file mode 100644 index a3cf9a7d2..000000000 --- a/packages/template/glimmer.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Import this module to enable resolution rules for Glimmer components - -import GlimmerComponent from '@glimmer/component'; -import { ResolutionKey, TemplateContext, Invokable, AcceptsBlocks } from '@glint/template/-private'; - -type Constructor = new (...args: never[]) => T; - -declare const ResolveGlimmerComponent: unique symbol; - -declare module '@glimmer/component' { - export default interface Component { - [ResolutionKey]: typeof ResolveGlimmerComponent; - } -} - -declare module '@glint/template/resolution-rules' { - export interface ContextResolutions { - [ResolveGlimmerComponent]: Host extends GlimmerComponent - ? TemplateContext - : never; - } - - export interface SignatureResolutions { - [ResolveGlimmerComponent]: InvokedValue extends Constructor> - ? InvokedValue extends { template: Invokable } - ? Signature - : (args: Args) => AcceptsBlocks<{ default?: [] }> - : never; - } -} diff --git a/packages/template/glimmerx.d.ts b/packages/template/glimmerx.d.ts deleted file mode 100644 index 38bd86152..000000000 --- a/packages/template/glimmerx.d.ts +++ /dev/null @@ -1,49 +0,0 @@ -// Import this module to enable resolution rules for GlimmerX - -import { NoNamedArgs, CreatesModifier } from '@glint/template/-private'; -import { Invokable, ReturnsValue } from '@glint/template/-private'; -import { ResolutionKey } from '@glint/template/-private'; -import { TemplateContext, AcceptsBlocks } from '@glint/template/-private'; -import GlimmerXComponent, { ResolveGlimmerXComponent } from '@glimmerx/component'; - -declare module '@glimmerx/modifier' { - export function on( - args: NoNamedArgs, - name: Name, - callback: (event: HTMLElementEventMap[Name]) => void - ): CreatesModifier; - - export const action: MethodDecorator; -} - -declare module '@glimmerx/helper' { - export function helper( - fn: (positional: Positional, named: Named) => Result - ): Invokable<(args: Named, ...positional: Positional) => ReturnsValue>; -} - -declare module '@glimmerx/component' { - const ResolveGlimmerXComponent: unique symbol; - - export default interface Component { - [ResolutionKey]: typeof ResolveGlimmerXComponent; - } -} - -declare module '@glint/template/resolution-rules' { - type Constructor = new (...args: any) => T; - - export interface ContextResolutions { - [ResolveGlimmerXComponent]: Host extends GlimmerXComponent - ? TemplateContext - : never; - } - - export interface SignatureResolutions { - [ResolveGlimmerXComponent]: InvokedValue extends Constructor> - ? InvokedValue extends { template: Invokable } - ? Signature - : (args: Args) => AcceptsBlocks<{ default?: [] }> - : never; - } -} diff --git a/packages/template/index.d.ts b/packages/template/index.d.ts index 89e5a9b12..949e56913 100644 --- a/packages/template/index.d.ts +++ b/packages/template/index.d.ts @@ -2,4 +2,3 @@ export { resolve, resolveOrReturn, ResolveContext } from './-private/resolution' export { invokeInline, invokeBlock, invokeModifier } from './-private/invoke'; export { toBlock } from './-private/blocks'; export { template } from './-private/template'; -export { default as Globals } from './-private/globals'; diff --git a/packages/template/package.json b/packages/template/package.json index 306615e60..57d3a6872 100644 --- a/packages/template/package.json +++ b/packages/template/package.json @@ -19,7 +19,7 @@ "@glimmer/component": "^1.0.0", "@glimmerx/component": "^0.2.2", "@types/ember__component": "~3.0.7", - "expect-type": "^0.7.4", + "expect-type": "0.7.3", "sums-up": "^2.1.0" } } From 33ccc76aa1bf9d2cf797d149f3ab072049aa50bb Mon Sep 17 00:00:00 2001 From: Dan Freeman Date: Fri, 31 Jul 2020 15:20:36 +0200 Subject: [PATCH 7/9] Use @glint/environment-glimmerx in the demo-app test package --- test-packages/demo-app/.glintrc.yml | 1 + test-packages/demo-app/package.json | 1 + test-packages/demo-app/src/index.ts | 1 - 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 test-packages/demo-app/.glintrc.yml diff --git a/test-packages/demo-app/.glintrc.yml b/test-packages/demo-app/.glintrc.yml new file mode 100644 index 000000000..8d41e0ae2 --- /dev/null +++ b/test-packages/demo-app/.glintrc.yml @@ -0,0 +1 @@ +environment: glimmerx diff --git a/test-packages/demo-app/package.json b/test-packages/demo-app/package.json index 08684e8dc..b3eb1498b 100644 --- a/test-packages/demo-app/package.json +++ b/test-packages/demo-app/package.json @@ -32,6 +32,7 @@ "@glimmerx/modifier": "^0.2.3", "@glimmerx/service": "^0.2.3", "@glint/cli": "^0.1.2", + "@glint/environment-glimmerx": "^0.1.2", "@glint/tsserver-plugin": "^0.1.2", "@types/qunit": "^2.9.1", "@typescript-eslint/eslint-plugin": "^2.26.0", diff --git a/test-packages/demo-app/src/index.ts b/test-packages/demo-app/src/index.ts index 81e3a7be7..e71218cd2 100644 --- a/test-packages/demo-app/src/index.ts +++ b/test-packages/demo-app/src/index.ts @@ -1,4 +1,3 @@ -import '@glint/template/glimmerx'; import { renderComponent } from '@glimmerx/core'; import App from './App'; From 14aeb15e116efe63489cb9132729452e5eb86776 Mon Sep 17 00:00:00 2001 From: Dan Freeman Date: Fri, 31 Jul 2020 15:48:16 +0200 Subject: [PATCH 8/9] Clean up project references --- packages/config/tsconfig.json | 3 +-- packages/transform/package.json | 1 + packages/transform/tsconfig.json | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/config/tsconfig.json b/packages/config/tsconfig.json index 0621df2ab..15127eda3 100644 --- a/packages/config/tsconfig.json +++ b/packages/config/tsconfig.json @@ -13,6 +13,5 @@ // produce type errors across different versions. "skipLibCheck": true }, - "include": ["src"], - "references": [{ "path": "../transform" }] + "include": ["src"] } diff --git a/packages/transform/package.json b/packages/transform/package.json index 131622978..8ad7f2347 100644 --- a/packages/transform/package.json +++ b/packages/transform/package.json @@ -22,6 +22,7 @@ "@babel/plugin-proposal-decorators": "7.8.3", "@babel/preset-typescript": "^7.9.0", "@glimmer/syntax": "^0.54.0", + "@glint/config": "^0.1.2", "debug": "^4.1.1" }, "devDependencies": { diff --git a/packages/transform/tsconfig.json b/packages/transform/tsconfig.json index 1d544c9f0..cd5ae0a07 100644 --- a/packages/transform/tsconfig.json +++ b/packages/transform/tsconfig.json @@ -13,5 +13,6 @@ // produce type errors across different versions. "skipLibCheck": true }, - "include": ["src"] + "include": ["src"], + "references": [{ "path": "../config" }] } From ff9a21663fba01be7f95c5831ad7452289910360 Mon Sep 17 00:00:00 2001 From: Dan Freeman Date: Fri, 31 Jul 2020 15:53:19 +0200 Subject: [PATCH 9/9] Fix config-loading test environment --- packages/config/__tests__/load-config.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/config/__tests__/load-config.test.ts b/packages/config/__tests__/load-config.test.ts index 363053607..57c7369e6 100644 --- a/packages/config/__tests__/load-config.test.ts +++ b/packages/config/__tests__/load-config.test.ts @@ -9,7 +9,10 @@ describe('loadConfig', () => { beforeEach(() => { fs.rmdirSync(testDir, { recursive: true }); fs.mkdirSync(testDir); - fs.writeFileSync(`${testDir}/local-env.js`, `exports.tags = { test: true };\n`); + fs.writeFileSync( + `${testDir}/local-env.js`, + `module.exports = () => ({ tags: { test: true } });\n` + ); }); afterEach(() => {