Skip to content

Commit

Permalink
WIP implementation of the Decorators RFC
Browse files Browse the repository at this point in the history
  • Loading branch information
NullVoxPopuli committed Jan 15, 2019
1 parent 8c65372 commit a252110
Show file tree
Hide file tree
Showing 12 changed files with 1,241 additions and 27 deletions.
10 changes: 9 additions & 1 deletion broccoli/packages.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const WriteFile = require('broccoli-file-creator');
const StringReplace = require('broccoli-string-replace');
const GlimmerTemplatePrecompiler = require('./glimmer-template-compiler');
const VERSION_PLACEHOLDER = /VERSION_STRING_PLACEHOLDER/g;
const transfromBabelPlugins = require('./transforms/transform-babel-plugins');

const debugTree = BroccoliDebug.buildDebugCallback('ember-source');

Expand Down Expand Up @@ -83,6 +84,13 @@ module.exports.getPackagesES = function getPackagesES() {
exclude: ['**/*.ts'],
});

// tsc / typescript handles decorators and class properties on its own
// so for non ts, transpile the proposal features (decorators, etc)
let transpiledProposals = debugTree(
transfromBabelPlugins(debugTree(nonTypeScriptContents, `get-packages-es:babel-plugins:input`)),
`get-packages-es:babel-plugins:output`
);

let typescriptContents = new Funnel(debuggedCompiledTemplatesAndTypeScript, {
include: ['**/*.ts'],
});
Expand All @@ -95,7 +103,7 @@ module.exports.getPackagesES = function getPackagesES() {

let debuggedCompiledTypescript = debugTree(typescriptCompiled, `get-packages-es:ts:output`);

let mergedFinalOutput = new MergeTrees([nonTypeScriptContents, debuggedCompiledTypescript], {
let mergedFinalOutput = new MergeTrees([transpiledProposals, debuggedCompiledTypescript], {
overwrite: true,
});

Expand Down
13 changes: 13 additions & 0 deletions broccoli/transforms/transform-babel-plugins.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const Babel = require('broccoli-babel-transpiler');

module.exports = function(tree) {
let options = {
sourceMaps: true,
plugins: [
['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true, legacy: false }],
['@babel/plugin-proposal-class-properties'],
],
};

return new Babel(tree, options);
};
11 changes: 11 additions & 0 deletions jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2016",
"experimentalDecorators": true
},
"exclude": [
"node_modules",
"**/node_modules/*"
]
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@
"@babel/plugin-transform-arrow-functions": "^7.2.0",
"@babel/plugin-transform-block-scoping": "^7.2.0",
"@babel/plugin-transform-classes": "^7.2.2",
"@babel/plugin-proposal-class-properties": "^7.2.1",
"@babel/plugin-proposal-decorators": "^7.2.0",
"@babel/plugin-transform-computed-properties": "^7.2.0",
"@babel/plugin-transform-destructuring": "^7.2.0",
"@babel/plugin-transform-literals": "^7.2.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// import { NEEDS_STAGE_1_DECORATORS } from 'ember-decorators-flags';

// let isStage1FieldDescriptor;

// if (NEEDS_STAGE_1_DECORATORS) {
// isStage1FieldDescriptor = function isStage1FieldDescriptor(possibleDesc) {
// if (possibleDesc.length === 3) {
// let [target, key, desc] = possibleDesc;

// return (
// typeof target === 'object' &&
// target !== null &&
// typeof key === 'string' &&
// ((typeof desc === 'object' &&
// desc !== null &&
// 'enumerable' in desc &&
// 'configurable' in desc) ||
// desc === undefined) // TS compatibility
// );
// } else if (possibleDesc.length === 1) {
// let [target] = possibleDesc;

// return typeof target === 'function' && 'prototype' in target;
// }

// return false;
// }
// }

export function isFieldDescriptor(possibleDesc) {
let isDescriptor = isStage2FieldDescriptor(possibleDesc);

// if (NEEDS_STAGE_1_DECORATORS) {
// isDescriptor = isDescriptor || isStage1FieldDescriptor(possibleDesc);
// }

return isDescriptor;
}



export function isStage2FieldDescriptor(possibleDesc) {
return possibleDesc && possibleDesc.toString() === '[object Descriptor]';
}
141 changes: 141 additions & 0 deletions packages/@ember/-internals/metal/lib/-private/decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { assert } from '@ember/debug';

// import { NEEDS_STAGE_1_DECORATORS } from 'ember-decorators-flags';
// import { deprecate } from '@ember/application/deprecations';

import { isFieldDescriptor, isStage2FieldDescriptor } from './class-field-descriptor';

function kindForDesc(desc) {
if ('value' in desc && desc.enumerable === true) {
return 'field';
} else {
return 'method';
}
}

function placementForKind(kind) {
return kind === 'method' ? 'prototype' : 'own';
}

function convertStage1ToStage2(desc) {
if (desc.length === 3) {
// Class element decorator
let [, key, descriptor] = desc;

let kind = kindForDesc(desc);
let placement = placementForKind(kind);

let initializer = descriptor !== undefined ? descriptor.initializer : undefined;

return {
descriptor,
key,
kind,
placement,
initializer,
toString: () => '[object Descriptor]',
};
} else {
// Class decorator
return {
kind: 'class',
elements: [],
};
}
}

export function decorator(fn) {
// if (NEEDS_STAGE_1_DECORATORS) {
// return function(...params) {
// if (isStage2FieldDescriptor(params)) {
// let desc = params[0];

// return deprecateDirectDescriptorMutation(fn, desc);
// } else {
// let desc = convertStage1ToStage2(params);

// desc = deprecateDirectDescriptorMutation(fn, desc);

// if (typeof desc.finisher === 'function') {
// // Finishers are supposed to run at the end of class finalization,
// // but we don't get that with stage 1 transforms. We have to be careful
// // to make sure that we aren't doing any operations which would change
// // due to timing.
// let [target] = params;

// desc.finisher(target.prototype ? target : target.constructor);
// }

// if (typeof desc.initializer === 'function') {
// // Babel 6 / the legacy decorator transform needs the initializer back
// // on the property descriptor/ In case the user has set a new
// // initializer on the member descriptor, we transfer it back to
// // original descriptor.
// desc.descriptor.initializer = desc.initializer;
// }

// return desc.descriptor;
// }
// };
// } else {
return fn;
// }
}

/**
* A macro that takes a decorator function and allows it to optionally
* receive parameters
*
* ```js
* let foo = decoratorWithParams((target, desc, key, params) => {
* console.log(params);
* });
*
* class {
* @foo bar; // undefined
* @foo('bar') baz; // ['bar']
* }
* ```
*
* @param {Function} fn - decorator function
*/
export function decoratorWithParams(fn) {
return function(...params) {
// determine if user called as @computed('blah', 'blah') or @computed
if (isFieldDescriptor(params)) {
return decorator(fn)(...params);
} else {
return decorator(desc => fn(desc, params));
}
};
}

/**
* A macro that takes a decorator function and requires it to receive
* parameters:
*
* ```js
* let foo = decoratorWithRequiredParams((target, desc, key, params) => {
* console.log(params);
* });
*
* class {
* @foo('bar') baz; // ['bar']
* @foo bar; // Error
* }
* ```
*
* @param {Function} fn - decorator function
*/
export function decoratorWithRequiredParams(fn, name) {
return function(...params) {
assert(
`The @${name || fn.name} decorator requires parameters`,
!isFieldDescriptor(params) && params.length > 0
);

return decorator(desc => {
return fn(desc, params);
});
};
}
124 changes: 124 additions & 0 deletions packages/@ember/-internals/metal/lib/-private/decorator_utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { assert } from '@ember/debug';

import { defineProperty } from '@ember/-internals/metal';
import { DecoratorDescriptor } from '../computed';
import { isFieldDescriptor } from './class-field-descriptor';
import { decorator } from './decorator';
import { computedDescriptorFor, isComputedDescriptor } from './descriptor';

export const DECORATOR_COMPUTED_FN = new WeakMap();
export const DECORATOR_PARAMS = new WeakMap();
export const DECORATOR_MODIFIERS = new WeakMap();

export function collapseProto(target) {
// We must collapse the superclass prototype to make sure that the `actions`
// object will exist. Since collapsing doesn't generally happen until a class is
// instantiated, we have to do it manually.
if (typeof target.constructor.proto === 'function') {
target.constructor.proto();
}
}

export function buildComputedDesc(dec, desc) {
let fn = DECORATOR_COMPUTED_FN.get(dec);
let params = DECORATOR_PARAMS.get(dec);
let modifiers = DECORATOR_MODIFIERS.get(dec);

let computedDesc = fn(desc, params);

assert(`computed decorators must return an instance of an Ember ComputedProperty descriptor, received ${computedDesc}`, isComputedDescriptor(computedDesc));

if (modifiers) {
modifiers.forEach(m => {
if (Array.isArray(m)) {
computedDesc[m[0]](...m[1]);
} else {
computedDesc[m]();
}
});
}

return computedDesc;
}

export function computedDecoratorWithParams(fn) {
return function(...params) {
if (isFieldDescriptor(params)) {
// Funkiness of application call here is due to `...params` transpiling to
// use `apply`, which is no longer on the prototype of the computedDecorator
// since it has had it's prototype changed :upside_down_face:
return Function.apply.call(computedDecorator(fn), undefined, params);
} else {
return computedDecorator(fn, params);
}
}
}

export function computedDecoratorWithRequiredParams(fn, name) {
return function(...params) {
assert(
`The @${name || fn.name} decorator requires parameters`,
!isFieldDescriptor(params) && params.length > 0
);

return computedDecorator(fn, params);
};
}


/**
* A macro that receives a decorator function which returns a ComputedProperty,
* and defines that property using `Ember.defineProperty`. Conceptually, CPs
* are custom property descriptors that require Ember's intervention to apply
* correctly. In the future, we will use finishers to define the CPs rather than
* directly defining them in the decorator function.
*
* @param {Function} fn - decorator function
*/
export function computedDecorator(fn, params) {
let dec = decorator((desc) => {
// All computeds are methods
desc.kind = 'method';
desc.placement = 'prototype';

desc.finisher = function initializeComputedProperty(target) {
let { prototype } = target;
let { key } = desc;

assert(`ES6 property getters/setters only need to be decorated once, '${key}' was decorated on both the getter and the setter`, !computedDescriptorFor(prototype, key));

let computedDesc = buildComputedDesc(dec, desc);

// if (!HAS_NATIVE_COMPUTED_GETTERS) {
// // Until recent versions of Ember, computed properties would be defined
// // by just setting them. We need to blow away any predefined properties
// // (getters/setters, etc.) to allow Ember.defineProperty to work correctly.
// Object.defineProperty(prototype, key, {
// configurable: true,
// writable: true,
// enumerable: true,
// value: undefined
// });
// }

defineProperty(prototype, key, computedDesc);

// if (NEEDS_STAGE_1_DECORATORS) {
// // There's currently no way to disable redefining the property when decorators
// // are run, so return the property descriptor we just assigned
// desc.descriptor = Object.getOwnPropertyDescriptor(prototype, key);
// }

return target;
}

return desc;
});

Object.setPrototypeOf(dec, DecoratorDescriptor.prototype);

DECORATOR_COMPUTED_FN.set(dec, fn);
DECORATOR_PARAMS.set(dec, params);

return dec;
}
Loading

0 comments on commit a252110

Please sign in to comment.