Skip to content
This repository has been archived by the owner on Jan 12, 2020. It is now read-only.

refactor(tracked): minimally invasive stage 2 decorators support #22

Merged
merged 20 commits into from Jan 10, 2019
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
bce9b0f
chore(deps): upgrade decorator related dependencies
buschtoens Dec 11, 2018
3fd3ea9
refactor(tracked): minimally invasive stage 2 decorators support
buschtoens Dec 11, 2018
fce9928
chore(deps): use tilde range for `@types/ember`
mike-north Dec 12, 2018
009d874
chore(deps): use tilde range for `@types/ember-qunit`
mike-north Dec 12, 2018
6bea49f
chore(deps): use tilde range for `@types/ember__test-helpers`
mike-north Dec 12, 2018
b559e7e
chore(deps): use tilde range for `@types/ember-test-helpers`
mike-north Dec 12, 2018
80e9735
test: run tests for ember-cli-typescript@1
buschtoens Dec 11, 2018
666de51
fix(tracked): handle missing desc for unitialized fields in e-c-ts@1
buschtoens Dec 12, 2018
e345174
chore(deps): update yarn.lock
buschtoens Dec 12, 2018
e47eb71
chore(deps): remove `ember-cli-typescript-blueprints`
buschtoens Dec 12, 2018
51bee13
chore(tracked): return void
buschtoens Dec 12, 2018
12d26a2
chore(deps): upgrade @types/ember
buschtoens Dec 18, 2018
ec55d1c
refactor(tracked): fix type errors with `addObserver`
buschtoens Dec 18, 2018
c5a0dda
chore(deps): upgrade ember-cli-typescript and ember-cli-babel
buschtoens Dec 18, 2018
4ea3b97
chore(deps): downgrade typescript@^2.9.2
buschtoens Dec 18, 2018
6a614f3
chore(deps): temporarily use pre-release of @e-d/utils
buschtoens Dec 18, 2018
d0f93f6
chore(deps): upgrade to @e-d v4.0.0
buschtoens Jan 7, 2019
e72fcfe
chore(types): add ember-compatibility-helpers
buschtoens Jan 10, 2019
158873b
chore(types): add types for new `setComponentManager`
buschtoens Jan 10, 2019
048c4fc
chore(types): remove unnecessary types
buschtoens Jan 10, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion .travis.yml
Expand Up @@ -31,11 +31,15 @@ jobs:
# runs linting and tests with current locked deps
- stage: 'Tests'
env: EMBER_TRY_SCENARIO=ember-lts-3.4
env: EMBER_TRY_SCENARIO=ember-lts-3.4-legacy-decorators
- env: EMBER_TRY_SCENARIO=ember-release
- env: EMBER_TRY_SCENARIO=ember-release-legacy-decorators
- env: EMBER_TRY_SCENARIO=ember-beta
- env: EMBER_TRY_SCENARIO=ember-beta-legacy-decorators
- env: EMBER_TRY_SCENARIO=ember-canary
- env: EMBER_TRY_SCENARIO=ember-canary-legacy-decorators
- env: EMBER_TRY_SCENARIO=ember-default
- env: EMBER_TRY_SCENARIO=ember-default-with-jquery
- env: EMBER_TRY_SCENARIO=ember-default-legacy-decorators
- name: 'Blueprints'
script: yarn nodetest

Expand Down
80 changes: 34 additions & 46 deletions addon/tracked.ts
@@ -1,16 +1,17 @@
import Ember from 'ember';
import { assert } from '@ember/debug';
import { DEBUG } from '@glimmer/env';
import { notifyPropertyChange } from '@ember/object';
import { addObserver } from '@ember/object/observers';
import { decoratorWithParams } from '@ember-decorators/utils/decorator';

function setupObservers(instance: object, dependentKeys: string[], notifyMethod: (() => void)) {
function setupObservers<O extends object>(instance: O, dependentKeys: (keyof O)[], notifyMethod: (() => void)) {
for (let i = 0; i < dependentKeys.length; i++) {
let dependentKey = dependentKeys[i];
addObserver(instance, dependentKey, instance, notifyMethod);
}
}

function descriptorForTrackedComputedProperty(_target: any, key: string | symbol, desc: PropertyDescriptor, dependencies?: string[]) {
function descriptorForTrackedComputedProperty(key: string | symbol, desc: PropertyDescriptor, dependencies?: string[]) {
// TODO: really should use WeakSet here, but that isn't available on IE11
const OBSERVERS_SETUP = new WeakMap();

Expand All @@ -36,15 +37,15 @@ function descriptorForTrackedComputedProperty(_target: any, key: string | symbol
// will be bound to the instance when invoked
function notify(this: object) {
if (typeof key === 'string') {
Ember.notifyPropertyChange(this, key);
notifyPropertyChange(this, key);
} else if (DEBUG) {
throw new Error(`@tracked - unsupported property type ${String(key)}`);
}
}

desc.get = function() {
if (!OBSERVERS_SETUP.has(this) && Array.isArray(dependencies)) {
setupObservers(this, dependencies, notify);
setupObservers<any>(this, dependencies, notify);
}
OBSERVERS_SETUP.set(this, true);

Expand All @@ -54,7 +55,7 @@ function descriptorForTrackedComputedProperty(_target: any, key: string | symbol
if (setterProvided) {
desc.set = function(value) {
if (typeof key === 'string') {
Ember.notifyPropertyChange(this, key);
notifyPropertyChange(this, key);
setterProvided.call(this, value);
} else if (DEBUG) {
throw new Error(`@tracked - unsupported property type ${String(key)}`);
Expand All @@ -65,11 +66,7 @@ function descriptorForTrackedComputedProperty(_target: any, key: string | symbol
return desc;
}

function installTrackedProperty(_target: object, key: string | symbol, descriptor?: PropertyDescriptor & { initializer: (() => void)}): PropertyDescriptor {
// only happens in babel, never in TS (Sept 2018)
// TODO check for whether initializer is a function
const initializer = descriptor && descriptor.initializer;

function installTrackedProperty(key: string | symbol, descriptor?: PropertyDescriptor, initializer?: () => any): PropertyDescriptor {
let values = new WeakMap();

let get;
Expand All @@ -90,54 +87,45 @@ function installTrackedProperty(_target: object, key: string | symbol, descripto
}

return {
configurable: true,
// TODO: correcting a misspelling caused chrome to error
// writable: true,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

FWIW this is intentional. I fixed the same problem in this PR with a detailed analysis: salsify/ember-css-modules#118


configurable: descriptor ? descriptor.configurable : true,
enumerable: descriptor ? descriptor.enumerable : true,
get,
set(value) {
if (typeof key === 'string') {
values.set(this, value);
Ember.notifyPropertyChange(this, key);
notifyPropertyChange(this, key);
} else if (DEBUG) {
throw new Error(`@tracked - unsupported property type ${String(key)}`);
}
}
};
}


function _tracked(target: object, key: string | symbol, descriptor?: PropertyDescriptor, dependencies?: string[]): PropertyDescriptor {
// descriptor is undefined for typescript class fields
if (descriptor === undefined || 'initializer' in descriptor) {
return installTrackedProperty(target, key, descriptor);
function _tracked(
key: string | symbol,
descriptor?: PropertyDescriptor,
initializer?: () => any,
dependencies?: string[]
): PropertyDescriptor {
if (!descriptor || typeof descriptor.get !== 'function' && typeof descriptor.set !== 'function') {
return installTrackedProperty(key, descriptor, initializer);
} else {
return descriptorForTrackedComputedProperty(target, key, descriptor, dependencies);
return descriptorForTrackedComputedProperty(key, descriptor, dependencies);
}
}

// type CompatiblePropertyDecorator = (target: object, key: string | symbol, descriptor: PropertyDescriptor) => PropertyDescriptor;

// @tracked
// TODO: replace return w/ PropertyDescriptor once TS gets their decorator act together
export function tracked(target: object, propertyKey: string | symbol, descriptor?: PropertyDescriptor): any;
// @tracked('foo', 'bar')
// TODO: replace return w/ CompatiblePropertyDecorator
export function tracked(...args: string[]): any;
// TODO: replace return w/ CompatiblePropertyDecorator | PropertyDescriptor
export function tracked(
targetOrArgs: (string | object),
secondArg: string | symbol,
descriptorOrString: string | PropertyDescriptor | undefined,
...rest: string[]): any
{
// if called for `@tracked('foo')`
if (typeof targetOrArgs === 'string') { // @tracked('foo', 'bar')
const args = [targetOrArgs, secondArg as string, descriptorOrString as string, ...rest];
return function(target: object, key: string | symbol, descriptor: PropertyDescriptor) {
return _tracked(target, key, descriptor, args);
}
} else { // @tracked
return _tracked(targetOrArgs, secondArg, descriptorOrString as PropertyDescriptor | undefined);
}
}
type TSDecorator = (target: object, propertyKey: string | symbol, descriptor?: PropertyDescriptor) => void;
type TrackedDecorator = TSDecorator & ((...args: string[]) => TSDecorator);

export const tracked: TrackedDecorator = decoratorWithParams((desc, params = []) => {
assert(`@tracked - Can only be used on class fields.`, desc.kind === 'field' || desc.kind === 'method');
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you wanna be really correct.

Suggested change
assert(`@tracked - Can only be used on class fields.`, desc.kind === 'field' || desc.kind === 'method');
assert(
`@tracked - Can only be used on class fields.`,
desc.kind === 'field' || desc.kind === 'method' && (
typeof desc.descriptor.get === 'function' ||
typeof desc.descriptor.set === 'function'
)
);

const descriptor = _tracked(desc.key, desc.descriptor, desc.initializer, params);

return {
...desc,
descriptor,
kind: 'method',
initializer: undefined
};
});
38 changes: 22 additions & 16 deletions config/ember-try.js
@@ -1,16 +1,35 @@
'use strict';

const getChannelURL = require('ember-source-channel-url');
const flatMap = require('lodash.flatmap');
const merge = require('lodash.merge');

const withDecoratorVariants = scenarios =>
flatMap(scenarios, scenario => [
scenario,
merge({}, scenario, {
name: `${scenario.name}-legacy-decorators`,
npm: {
dependencies: {
'ember-cli-typescript': null
},
devDependencies: {
'@ember-decorators/babel-transforms': '^2.1.2',
'ember-cli-typescript': '^1.5.0'
}
}
})
]);

module.exports = function() {
return Promise.all([
getChannelURL('release'),
getChannelURL('beta'),
getChannelURL('canary')
]).then((urls) => {
]).then(urls => {
return {
useYarn: true,
scenarios: [
scenarios: withDecoratorVariants([
{
name: 'ember-lts-2.12',
npm: {
Expand Down Expand Up @@ -72,21 +91,8 @@ module.exports = function() {
npm: {
devDependencies: {}
}
},
{
name: 'ember-default-with-jquery',
env: {
EMBER_OPTIONAL_FEATURES: JSON.stringify({
'jquery-integration': true
})
},
npm: {
devDependencies: {
'@ember/jquery': '^0.5.1'
}
}
}
]
])
};
});
};
17 changes: 10 additions & 7 deletions package.json
Expand Up @@ -23,23 +23,25 @@
"nodetest": "mocha node-tests --recursive"
},
"dependencies": {
"@ember-decorators/utils": "^4.0.0",
"@glimmer/env": "^0.1.7",
"ember-cli-babel": "^7.1.2",
"ember-cli-babel": "^7.2.0",
"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": "^2.0.0-rc.1",
"ember-compatibility-helpers": "^1.1.2"
},
"devDependencies": {
"@ember-decorators/babel-transforms": "^2.1.1",
"@ember-decorators/babel-transforms": "^4.0.0",
"@ember/optional-features": "^0.6.1",
"@types/ember": "^3.0.15",
"@types/ember-qunit": "^3.4.0",
"@types/ember-test-helpers": "^1.0.0",
"@types/ember": "~3.0.26",
"@types/ember-qunit": "~3.4.3",
"@types/ember-test-helpers": "~1.0.4",
"@types/ember-testing-helpers": "^0.0.3",
"@types/ember__test-helpers": "^0.7.1",
"@types/ember__test-helpers": "~0.7.6",
"@types/mocha": "^5.2.5",
"@types/qunit": "^2.5.3",
"@types/rsvp": "^4.0.2",
Expand All @@ -55,7 +57,6 @@
"ember-cli-inject-live-reload": "^1.4.1",
"ember-cli-qunit": "^4.3.2",
"ember-cli-sri": "^2.1.0",
"ember-cli-typescript": "^1.4.2",
"ember-cli-uglify": "^2.1.0",
"ember-disable-prototype-extensions": "^1.1.2",
"ember-export-application-global": "^2.0.0",
Expand All @@ -68,6 +69,8 @@
"eslint-plugin-ember": "^5.0.0",
"eslint-plugin-node": "^6.0.1",
"loader.js": "^4.2.3",
"lodash.flatmap": "^4.5.0",
"lodash.merge": "^4.6.1",
"mocha": "^5.0.0",
"qunit-dom": "^0.7.1",
"typescript": "^2.9.2"
Expand Down
10 changes: 9 additions & 1 deletion tsconfig.json
Expand Up @@ -35,6 +35,12 @@
"sparkles-component/*": [
"addon/*"
],
"sparkles-component/test-support": [
"addon-test-support"
],
"sparkles-component/test-support/*": [
"addon-test-support/*"
],
"*": [
"types/*"
]
Expand All @@ -44,6 +50,8 @@
"app/**/*",
"addon/**/*",
"tests/**/*",
"types/**/*"
"types/**/*",
"test-support/**/*",
"addon-test-support/**/*"
]
}
14 changes: 14 additions & 0 deletions types/@ember-decorators/utils/decorator.d.ts
@@ -0,0 +1,14 @@
type MemberDescriptor = {
key: string;
kind: 'class' | 'method' | 'field' | 'initializer';
elements?: MemberDescriptor[];
key: string;
placement?: 'prototype' | 'static' | 'own';
extras?: MemberDescriptor[];
initializer?: () => any;
descriptor?: PropertyDescriptor;
};

export function decoratorWithParams(
fn: (desc: MemberDescriptor, params?: any[]) => MemberDescriptor
): any;
buschtoens marked this conversation as resolved.
Show resolved Hide resolved
16 changes: 16 additions & 0 deletions types/ember-compatibility-helpers.d.ts
@@ -0,0 +1,16 @@
export function gte(version: string): boolean;
export function lte(version: string): boolean;
export const HAS_UNDERSCORE_ACTIONS: boolean;
buschtoens marked this conversation as resolved.
Show resolved Hide resolved
export const HAS_MODERN_FACTORY_INJECTIONS: boolean;
export const HAS_DESCRIPTOR_TRAP: boolean;
export const HAS_NATIVE_COMPUTED_GETTERS: boolean;
export const IS_GLIMMER_2: boolean;
export const IS_RECORD_DATA: boolean;
export const SUPPORTS_FACTORY_FOR: boolean;
export const SUPPORTS_GET_OWNER: boolean;
export const SUPPORTS_SET_OWNER: boolean;
export const SUPPORTS_NEW_COMPUTED: boolean;
export const SUPPORTS_INVERSE_BLOCK: boolean;
export const SUPPORTS_CLOSURE_ACTIONS: boolean;
export const SUPPORTS_UNIQ_BY_COMPUTED: boolean;