Skip to content

Commit f80e066

Browse files
committed
[api] Remove helper items, rework navigation index
It turns out that helpers can actually be functions _or_ classes, which was not something which the original data model took into account. This PR removes the notion of a unique "helper" type from the data model in favor of deferring to whatever construct is ultimately resolved: Either a class or a function. The navigation index code has also been simplified a bit - it was pretty hacked together after EmberConf, so it needed some straightening out. For now, the rule is that every resolvable file gets an entry, and the the last segment of the file path becomes the entry's name. Helpers and components are {{curlied}} and everything else is CapitalCased. We are also no longer making assumptions about what a file is exporting. This means that if helper function or class is exported from resolved directory, it will _not show up_ in the navigation at all. We can work on solving this use case later on, but I think it will be sufficiently rare that we can punt on it until later. `utils/` folders are purposefully excluded from this and are placed instead in the module structure, since that is the most common directory for helper functions. We also need to consider how this structure may change with module unification and nesting, but that's a problem for the future.
1 parent 335c0c2 commit f80e066

File tree

11 files changed

+188
-105
lines changed

11 files changed

+188
-105
lines changed

addon/components/api/x-class/template.hbs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
<h1 class='docs-h1'>{{class.name}}</h1>
1+
<h1 class='docs-h1' data-test-class-name>{{class.name}}</h1>
22

33
{{! wrapping in a div seems to work around https://github.com/ember-learn/ember-cli-addon-docs/issues/7 }}
4-
<div>{{{class.description}}}</div>
4+
<div data-test-class-description>{{{class.description}}}</div>
55

66
{{#if hasContents}}
77
<div class="flex flex-row-reverse">

addon/components/api/x-component/template.hbs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
<h1 class='docs-h1'>{{component.name}}</h1>
1+
<h1 class='docs-h1' data-test-component-name>{{component.name}}</h1>
22

33
{{! wrapping in a div seems to work around https://github.com/ember-learn/ember-cli-addon-docs/issues/7 }}
4-
<div>{{{component.description}}}</div>
4+
<div data-test-component-name>{{{component.description}}}</div>
55

66
{{#if hasContents}}
77
<div class="flex flex-row-reverse">

addon/components/api/x-module/template.hbs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
classes=module.classes
55
components=module.components
66
functions=module.functions
7-
helpers=module.helpers
87
variables=module.variables
98
)
109
}}
@@ -14,7 +13,6 @@
1413
classes=module.classes
1514
components=module.components
1615
functions=module.functions
17-
helpers=module.helpers
1816
variables=module.variables
1917
)
2018
}}

addon/models/module.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ export default DS.Model.extend({
66
file: attr(),
77
variables: attr(),
88
functions: attr(),
9-
helpers: attr(),
109

1110
classes: hasMany('class', { async: false, }),
1211
components: hasMany('class', { async: false, })

lib/broccoli/docs-compiler.js

Lines changed: 50 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ function removeEmptyModules(modules) {
5757
module.classes.length === 0
5858
&& module.components.length === 0
5959
&& module.functions.length === 0
60-
&& module.helpers.length === 0
6160
&& module.variables.length === 0
6261
);
6362
});
@@ -137,99 +136,82 @@ function hoistDefaults(modules) {
137136
delete modules[m];
138137
}
139138
}
139+
140+
return modules;
140141
}
141142

142-
function generateResolvedTypedNavigationItems(collection, file, type) {
143-
return collection.filter(c => c.file.includes(`${type}s/`)).map((c) => {
144-
let segments = file.split('/');
143+
144+
145+
const RESOLVED_TYPES = [
146+
'components',
147+
'helpers',
148+
'controllers',
149+
'mixins',
150+
'models',
151+
'services'
152+
];
153+
154+
function generateResolvedTypeNavigationItems(modules, type) {
155+
let items = modules.map(m => {
156+
let segments = m.file.split('/');
145157
let fileName = segments.pop();
146158

147-
if (fileName === type) {
159+
if (type.match(fileName)) {
148160
fileName = segments.pop();
149161
}
150162

151163
let name;
152-
if (['component', 'helper'].includes(type)) {
164+
if (['components', 'helpers'].includes(type)) {
153165
name = `{{${fileName}}}`;
154166
} else {
155167
name = _.upperFirst(_.camelCase(fileName));
156168
}
157169

158170
return {
159-
path: `${type}s/${fileName}`,
171+
path: `${type}/${fileName}`,
160172
name
161173
};
162174
});
175+
176+
return _.sortBy(items, ['name']);
163177
}
164178

165-
function generateModuleNavigationItems(module, collection) {
166-
return collection.map((item) => {
167-
return {
168-
path: `root/${item.id || module.id}`,
169-
name: item.name,
170-
isDefault: item.exportType === 'default'
171-
};
172-
});
179+
function generateModuleNavigationItems(modules, type) {
180+
let navItems = modules.reduce((navItems, m) => {
181+
let items = m[type].map((item) => {
182+
return {
183+
path: `root/${item.id || module.id}`,
184+
name: item.name,
185+
isDefault: item.exportType === 'default'
186+
};
187+
});
188+
189+
if (items.length > 0) {
190+
navItems[m.file] = _.sortBy(items, ['name']);
191+
}
192+
193+
return navItems;
194+
}, {});
195+
196+
return hoistDefaults(navItems);
173197
}
174198

175199
function generateNavigationIndex(modules, klasses) {
176-
let components = [];
177-
let controllers = [];
178-
let helpers = [];
179-
let mixins = [];
180-
let models = [];
181-
let services = [];
182-
183-
let classes = {};
184-
let functions = {};
185-
let variables = {};
186-
187-
modules.forEach((m) => {
188-
let file = m.file;
189-
190-
let componentItems = generateResolvedTypedNavigationItems(m.components, file, 'component');
191-
let helperItems = generateResolvedTypedNavigationItems(m.helpers, file, 'helper');
192-
193-
let controllerItems = generateResolvedTypedNavigationItems(m.classes, file, 'controller');
194-
let mixinItems = generateResolvedTypedNavigationItems(m.classes, file, 'mixin');
195-
let modelItems = generateResolvedTypedNavigationItems(m.classes, file, 'model');
196-
let serviceItems = generateResolvedTypedNavigationItems(m.classes, file, 'service');
197-
198-
let classItems = generateModuleNavigationItems(
199-
m, m.classes.filter(c => !c.file.match(/controllers\/|mixins\/|models\/|services\//))
200-
);
200+
let navigationIndex = {};
201201

202-
let functionItems = generateModuleNavigationItems(m, m.functions);
203-
let variableItems = generateModuleNavigationItems(m, m.variables);
202+
for (let type of RESOLVED_TYPES) {
203+
let resolvedModules = modules.filter(m => m.file.match(`${type}/`) && !m.file.match('utils/'));
204204

205-
components = components.concat(componentItems);
206-
controllers = controllers.concat(controllerItems);
207-
helpers = helpers.concat(helperItems);
208-
mixins = mixins.concat(mixinItems);
209-
models = models.concat(modelItems);
210-
services = services.concat(serviceItems);
205+
navigationIndex[type] = generateResolvedTypeNavigationItems(resolvedModules, type);
206+
}
211207

212-
classes[file] = _.sortBy(classItems, ['name']);
213-
functions[file] = _.sortBy(functionItems, ['name']);
214-
variables[file] = _.sortBy(variableItems, ['name']);
215-
});
208+
let nonResolvedModules = modules.filter(m => {
209+
return !m.file.match(new RegExp(`(${RESOLVED_TYPES.join('|')})/`)) || m.file.match('utils/');
210+
})
216211

217-
hoistDefaults(classes);
218-
hoistDefaults(functions);
219-
hoistDefaults(variables);
220-
221-
let navigationIndex = {
222-
components: _.sortBy(components, ['path']),
223-
controllers: _.sortBy(controllers, ['name']),
224-
helpers: _.sortBy(helpers, ['path']),
225-
mixins: _.sortBy(mixins, ['name']),
226-
models: _.sortBy(models, ['name']),
227-
services: _.sortBy(services, ['name']),
228-
229-
classes,
230-
functions,
231-
variables
232-
};
212+
navigationIndex.classes = generateModuleNavigationItems(nonResolvedModules, 'classes');
213+
navigationIndex.functions = generateModuleNavigationItems(nonResolvedModules, 'functions');
214+
navigationIndex.variables = generateModuleNavigationItems(nonResolvedModules, 'variables');
233215

234216
for (let type in navigationIndex) {
235217
let items = navigationIndex[type];

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@
7979
"common-tags": "^1.7.2",
8080
"ember-classy-page-object": "^0.4.6",
8181
"ember-cli": "~3.0.0",
82-
"ember-cli-addon-docs-esdoc": "^0.1.1",
83-
"ember-cli-addon-docs-yuidoc": "^0.1.1",
82+
"ember-cli-addon-docs-esdoc": "^0.2.0",
83+
"ember-cli-addon-docs-yuidoc": "^0.2.0",
8484
"ember-cli-dependency-checker": "^2.1.0",
8585
"ember-cli-deploy": "^1.0.2",
8686
"ember-cli-deploy-build": "^1.1.1",
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/** @documenter esdoc */
2+
3+
import Helper from '@ember/component/helper';
4+
5+
/**
6+
A class based ESDoc helper
7+
*/
8+
export default class ESDocClassHelper extends Helper {
9+
/**
10+
returns the absolute value of a number
11+
12+
@param {number} [number] the passed number
13+
@return {number}
14+
*/
15+
compute([number]) {
16+
return Math.abs(number);
17+
}
18+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/** @documenter yuidoc */
2+
3+
import Helper from '@ember/component/helper';
4+
5+
/**
6+
A class based YUIDoc helper
7+
8+
@class YUIDocClassHelper
9+
*/
10+
const YUIDocClassHelper = Helper.extend({
11+
/**
12+
returns the absolute value of a number
13+
14+
@method compute
15+
@param {number} [number] the passed number
16+
@return {number}
17+
*/
18+
compute([number]) {
19+
return Math.abs(number);
20+
}
21+
});
22+
23+
export default YUIDocClassHelper;

tests/acceptance/sandbox/api/helpers-test.js

Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,43 +3,78 @@ import { setupApplicationTest } from 'ember-qunit';
33
import { currentURL, visit } from '@ember/test-helpers';
44

55
import modulePage from '../../../pages/api/module';
6+
import classPage from '../../../pages/api/class';
67

78
module('Acceptance | API | helpers', function(hooks) {
89
setupApplicationTest(hooks);
910

10-
for (let documenter of ['esdoc', 'yuidoc']) {
11-
let helperName = `${documenter}Helper`;
12-
let kebabName = `${documenter}-helper`;
11+
module('standard helpers', function() {
12+
for (let documenter of ['esdoc', 'yuidoc']) {
13+
let helperName = `${documenter}Helper`;
14+
let kebabName = `${documenter}-helper`;
1315

14-
test('{{esdoc-helper}}', async function(assert) {
15-
await visit('/sandbox');
16-
await modulePage.navItems.findOne({ text: `{{${kebabName}}}` }).click();
16+
test(`{{${kebabName}}}`, async function(assert) {
17+
await visit('/sandbox');
18+
await modulePage.navItems.findOne({ text: `{{${kebabName}}}` }).click();
1719

18-
assert.equal(currentURL(), `/sandbox/api/helpers/${kebabName}`, 'correct url');
20+
assert.equal(currentURL(), `/sandbox/api/helpers/${kebabName}`, 'correct url');
1921

20-
let helpersSection = modulePage.sections.findOne({ header: 'Helpers' });
22+
let functionsSection = modulePage.sections.findOne({ header: 'Functions' });
2123

22-
assert.ok(helpersSection.isPresent, 'Renders the helpers section');
24+
assert.ok(functionsSection.isPresent, 'Renders the functions section');
2325

24-
let helperItem = helpersSection.items.findOne(i => i.header.includes(helperName));
26+
let helperItem = functionsSection.items.findOne(i => i.header.includes(helperName));
2527

26-
assert.ok(helperItem.isPresent, 'Renders the helper item');
28+
assert.ok(helperItem.isPresent, 'Renders the helper item');
2729

28-
assert.equal(
29-
helperItem.header,
30-
`${helperName}(number: number): number`,
31-
'renders the type signature of the helper correctly'
32-
);
30+
assert.equal(
31+
helperItem.header,
32+
`${helperName}(number: number): number`,
33+
'renders the type signature of the helper correctly'
34+
);
3335

34-
assert.equal(
35-
helperItem.importPath,
36-
`import { ${helperName} } from 'ember-cli-addon-docsapp/helpers/${kebabName}';`,
37-
'renders the import path correctly'
38-
);
36+
assert.equal(
37+
helperItem.importPath,
38+
`import { ${helperName} } from 'ember-cli-addon-docs/helpers/${kebabName}';`,
39+
'renders the import path correctly'
40+
);
3941

40-
assert.equal(helperItem.params.length, 1, 'renders the item parameter');
41-
});
42-
}
42+
assert.equal(helperItem.params.length, 1, 'renders the item parameter');
43+
});
44+
}
45+
});
46+
47+
module('class helpers', function() {
48+
for (let documenter of ['ESDoc', 'YUIDoc']) {
49+
let helperName = `${documenter}ClassHelper`;
50+
let kebabName = `${documenter.toLowerCase()}-class-helper`;
51+
52+
test(`{{${kebabName}}}`, async function(assert) {
53+
await visit('/sandbox');
54+
await classPage.navItems.findOne({ text: `{{${kebabName}}}` }).click();
55+
56+
assert.equal(currentURL(), `/sandbox/api/helpers/${kebabName}`, 'correct url');
57+
58+
assert.equal(classPage.title, helperName, 'Renders the class title correctly');
59+
60+
let methodsSection = modulePage.sections.findOne({ header: 'Methods' });
61+
62+
assert.ok(methodsSection.isPresent, 'Renders the methods section');
63+
64+
let computeItem = methodsSection.items.findOne(i => i.header.includes('compute'));
65+
66+
assert.ok(computeItem.isPresent, 'Renders the helper item');
67+
68+
assert.equal(
69+
computeItem.header,
70+
'compute(number: number): number',
71+
'renders the type signature of the helper correctly'
72+
);
73+
74+
assert.equal(computeItem.params.length, 1, 'renders the item parameter');
75+
});
76+
}
77+
});
4378
});
4479

4580

tests/pages/api/class.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import PageObject, { collection, text } from 'ember-classy-page-object';
2+
3+
const ClassPage = PageObject.extend({
4+
navItems: collection({ scope: '[data-test-id="nav-item"]' }),
5+
6+
title: text('[data-test-class-name]'),
7+
description: text('[data-test-class-description]'),
8+
9+
sections: collection({
10+
scope: '[data-test-api-section]',
11+
12+
header: text('[data-test-section-header]'),
13+
14+
items: collection({
15+
scope: '[data-test-item]',
16+
17+
header: text('[data-test-item-header]'),
18+
importPath: text('[data-test-import-path]'),
19+
description: text('[data-test-item-description]'),
20+
21+
params: collection({
22+
scope: '[data-test-item-params] [data-test-item-param]'
23+
})
24+
})
25+
})
26+
});
27+
28+
export default ClassPage.create();

0 commit comments

Comments
 (0)