Skip to content

Commit cf26502

Browse files
committed
[refactor] Simplify and generalize Module links
This changes the way that we link to modules in order to make it more generic. The main difference here is that we no longer assume that the project name has any relation to the module itself, instead we always use the fully-qualified module name as its id, its link, and title. There are some pros and cons to this approach: * Pros * More inline with JS/modules as a whole - Since we base everything on the full module name, we aren't basing it on arbitrary Ember-only concepts, like the Addon/App folders, etc. The build system now just receives a tree with a module structure, and assumes that it is the correct, canonical structure. * More self-documenting - users can know at a glance what the import path for an item is just by looking at the sidebar * Supports documenting multiple addons in a single project - this is important for libraries like ember-decorators that are made up of many small packages * Cons * Slightly more verbose - the name of the addon is generally included in titles, urls, and import paths, which can be redundant in the simple case. This could be special-cased in the future, so we at least don't include it in the title for the navigation index. * Could be problematic with Node/Browser overlap - users could try to have two sets of modules with identical import paths for both the Browser and Node (e.g. both have an `index.js`). This is problematic in general though, and moving forward Ember-CLI seems to be on a path to making sure addons don't do this because its confusing. Other changes: * Instead of nesting modules under a `/root` url, we're now nesting them under `/modules`, so we don't imply that it has anything to do with the structure of the addon. Everything is just a JS module. * Fixed routing to resolvable items, components with similar names should now work.
1 parent 7c4dbb8 commit cf26502

File tree

9 files changed

+66
-130
lines changed

9 files changed

+66
-130
lines changed
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
import Component from '@ember/component';
22
import layout from './template';
3-
import config from 'dummy/config/environment';
4-
5-
const projectName = config['ember-cli-addon-docs'].projectName;
63

74
export default Component.extend({
85
layout,
9-
classNames: ['import-path'],
10-
projectName
6+
classNames: ['import-path']
117
});

addon/components/api/x-import-path/template.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
{{/if}}
88

99
<span class="hljs-keyword">from</span>
10-
<span class="hljs-string">'{{projectName}}{{item.file}}'</span>;
10+
<span class="hljs-string">'{{item.file}}'</span>;

addon/components/docs-viewer/x-nav-item/component.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ export default Component.extend({
1111

1212
didInsertElement() {
1313
this._super(...arguments);
14+
let model = this.get('model');
15+
16+
if (typeof model === 'string' && model.includes('#')) {
17+
return;
18+
}
1419

1520
next(() => {
1621
this.get('docsRoutes.items').addObject(this);

addon/components/docs-viewer/x-nav/template.hbs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,11 @@
4949
{{/each}}
5050
{{else}}
5151
{{#each-in items as |module items|}}
52-
<li class="docs-viewer__module-heading">
53-
{{module}}
54-
</li>
52+
{{subnav.item module
53+
(concat root '.api.item')
54+
(concat 'modules/' module)
55+
class="docs-viewer__module-heading"
56+
}}
5557

5658
{{#docs-viewer/x-nav-list as |subnav|}}
5759
{{#each items as |item|}}

addon/routes/docs/api/item.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,24 @@ export default Route.extend({
55
model({ path }) {
66
let item;
77

8-
if (path.match(/^root\//)) {
8+
if (path.match(/^modules\//)) {
99
// Find by fully qualified id
10-
let itemId = path.replace(/^root\//, '');
11-
let [moduleId] = itemId.split('~');
10+
let itemId = path.replace(/^modules\//, '');
11+
let [moduleId] = itemId.split(/~|#/);
1212

1313
let module = this.store.peekRecord('module', moduleId);
1414

1515
item = module.get('components').findBy('id', itemId)
1616
|| module.get('classes').findBy('id', itemId)
1717
|| module;
1818
} else {
19+
// Create a regex that will match modules by either the path, or the
20+
// pod-path (/component, /route, etc)
21+
let type = path.match(/^(.*)s\//)[1];
22+
let pathRegex = new RegExp(`${path}(/${type})?$`);
23+
1924
let modules = this.store.peekAll('module');
20-
let matches = modules.filter(m => m.id.match(path));
25+
let matches = modules.filter(m => m.id.match(pathRegex));
2126
let module = matches[0];
2227

2328
assert(`no modules match the path '${path}'`, matches.length > 0);

ember-cli-build.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
const EmberAddon = require('ember-cli/lib/broccoli/ember-addon');
44
const Project = require('ember-cli/lib/models/project');
5+
const MergeTrees = require('broccoli-merge-trees');
6+
const Funnel = require('broccoli-funnel');
57

68
module.exports = function(defaults) {
79
let project = Project.closestSync(process.cwd());
@@ -21,9 +23,12 @@ module.exports = function(defaults) {
2123
},
2224
'ember-cli-addon-docs': {
2325
projects: {
24-
sandbox: {
25-
tree: 'sandbox'
26-
}
26+
sandbox: new MergeTrees([
27+
new Funnel('sandbox/app', { destDir: 'sandbox' }),
28+
new Funnel('sandbox', {
29+
include: ['package.json', 'README.md']
30+
})
31+
])
2732
}
2833
}
2934
});

index.js

Lines changed: 31 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,6 @@ const EmberApp = require('ember-cli/lib/broccoli/ember-app'); // eslint-disable-
1010
const Plugin = require('broccoli-plugin');
1111
const walkSync = require('walk-sync');
1212

13-
const DEFAULT_PROJECTS = {
14-
main: {
15-
tree: null,
16-
include: null,
17-
includeTrees: ['addon']
18-
}
19-
}
20-
2113
module.exports = {
2214
name: 'ember-cli-addon-docs',
2315

@@ -95,7 +87,7 @@ module.exports = {
9587
}
9688

9789
this.addonOptions = Object.assign({}, includer.options['ember-cli-addon-docs']);
98-
this.addonOptions.projects = Object.assign({}, DEFAULT_PROJECTS, this.addonOptions.projects);
90+
this.addonOptions.projects = Object.assign({}, this.addonOptions.projects);
9991

10092
includer.options.includeFileExtensionInSnippetNames = includer.options.includeFileExtensionInSnippetNames || false;
10193
includer.options.snippetSearchPaths = includer.options.snippetSearchPaths || ['tests/dummy/app'];
@@ -182,35 +174,10 @@ module.exports = {
182174
let project = this.project;
183175
let docsTrees = [];
184176

185-
for (let projectName in this.addonOptions.projects) {
186-
let docProject = this.addonOptions.projects[projectName];
187-
188-
let addonSourceTree;
189-
190-
if (docProject.tree) {
191-
addonSourceTree = docProject.tree;
192-
} else {
193-
let include = docProject.include || [];
194-
let includeTrees = docProject.includeTrees || [];
195-
196-
let includeTreePaths = includeTrees.map(t => parentAddon.treePaths[t]);
197-
let includeFunnels = [
198-
// We need to be very careful to avoid triggering a watch on the addon root here
199-
// because of https://github.com/nodejs/node/issues/15683
200-
new Funnel(new UnwatchedDir(parentAddon.root), {
201-
include: ['package.json', 'README.md']
202-
})
203-
];
204-
205-
for (let path of include.concat(includeTreePaths)) {
206-
let fullPath = `${parentAddon.root}/${path}`;
207-
if (fs.existsSync(fullPath)) {
208-
includeFunnels.push(new Funnel(fullPath, { destDir: path }));
209-
}
210-
}
177+
this.addonOptions.projects.main = this.addonOptions.projects.main || generateDefaultProject(parentAddon);
211178

212-
addonSourceTree = new MergeTrees(includeFunnels);
213-
}
179+
for (let projectName in this.addonOptions.projects) {
180+
let addonSourceTree = this.addonOptions.projects[projectName];
214181

215182
let pluginRegistry = new PluginRegistry(project);
216183

@@ -279,6 +246,33 @@ function findImporter(addon) {
279246
}
280247
}
281248

249+
function generateDefaultProject(parentAddon) {
250+
let includeFunnels = [
251+
// We need to be very careful to avoid triggering a watch on the addon root here
252+
// because of https://github.com/nodejs/node/issues/15683
253+
new Funnel(new UnwatchedDir(parentAddon.root), {
254+
include: ['package.json', 'README.md']
255+
})
256+
];
257+
258+
let addonTreePath = path.join(parentAddon.root, parentAddon.treePaths['addon']);
259+
let testSupportPath = path.join(parentAddon.root, parentAddon.treePaths['addon-test-support']);
260+
261+
if (fs.existsSync(addonTreePath)) {
262+
includeFunnels.push(new Funnel(addonTreePath, {
263+
destDir: parentAddon.name
264+
}));
265+
}
266+
267+
if (fs.existsSync(testSupportPath)) {
268+
includeFunnels.push(new Funnel(testSupportPath, {
269+
destDir: `${parentAddon.name}/test-support`
270+
}));
271+
}
272+
273+
return new MergeTrees(includeFunnels);
274+
}
275+
282276
class FindDummyAppFiles extends Plugin {
283277
build() {
284278
let addonPath = this.inputPaths[0];

lib/broccoli/docs-compiler.js

Lines changed: 5 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -72,76 +72,6 @@ function compileDocDescriptions(modules) {
7272
});
7373
}
7474

75-
function stripAppAddonDirs(modules) {
76-
77-
modules.forEach((module) => {
78-
module.file = module.file.replace(/^(app|addon)/, '');
79-
});
80-
81-
eachRelationship(modules, (relationship) => {
82-
relationship.forEach((item) => {
83-
if (item.file) item.file = item.file.replace(/^(app|addon)/, '');
84-
if (item.name) item.name = item.name.replace(/^(app|addon)/, '');
85-
});
86-
});
87-
}
88-
89-
/**
90-
* "Hoists" default exports from a given module. This is particularly useful
91-
* for class files, which generally only export a single default class and
92-
* would normally look like this:
93-
*
94-
* /classes/foo
95-
* FooClass
96-
* /classes/bar
97-
* BarClass
98-
*
99-
* Instead they become this:
100-
*
101-
* /classes
102-
* FooClass
103-
* BarClass
104-
*
105-
* Since these are the only exports of that type and the default, users can
106-
* generally infer that they can import the class from the file with the same
107-
* name as the class or component (in many cases this also doesn't matter
108-
* since the affected classes will be resolved).
109-
*
110-
* If a file has named exports they will continue to appear in that module:
111-
*
112-
* /classes
113-
* FooClass
114-
* /classes/foo
115-
* HelperClass
116-
*
117-
* @param {Object} modules
118-
*/
119-
function hoistDefaults(modules) {
120-
for (let m in modules) {
121-
let module = modules[m];
122-
let parentModulePath = path.dirname(m);
123-
124-
let value = _.remove(module, { isDefault: true });
125-
126-
if (value) {
127-
modules[parentModulePath] = (modules[parentModulePath] || []).concat(value);
128-
}
129-
}
130-
131-
// Remove any modules without exports now that hoisting is done
132-
for (let m in modules) {
133-
let module = modules[m];
134-
135-
if (module.length === 0) {
136-
delete modules[m];
137-
}
138-
}
139-
140-
return modules;
141-
}
142-
143-
144-
14575
const RESOLVED_TYPES = [
14676
'components',
14777
'helpers',
@@ -177,23 +107,23 @@ function generateResolvedTypeNavigationItems(modules, type) {
177107
}
178108

179109
function generateModuleNavigationItems(modules, type) {
180-
let navItems = modules.reduce((navItems, m) => {
110+
return modules.reduce((navItems, m) => {
181111
let items = m[type].map((item) => {
112+
let path = item.id ? item.id : `${m.id}#${item.name}`;
113+
182114
return {
183-
path: `root/${item.id || m.id}`,
115+
path: `modules/${path}`,
184116
name: item.name,
185117
isDefault: item.exportType === 'default'
186118
};
187119
});
188120

189121
if (items.length > 0) {
190-
navItems[m.file] = _.sortBy(items, ['name']);
122+
navItems[m.file] = _.sortBy(items, ['name']);
191123
}
192124

193125
return navItems;
194126
}, {});
195-
196-
return hoistDefaults(navItems);
197127
}
198128

199129
function generateNavigationIndex(modules, klasses) {
@@ -255,7 +185,6 @@ module.exports = class DocsCompiler extends CachingWriter {
255185
removeHiddenDocs(modules);
256186
removeEmptyModules(modules);
257187
compileDocDescriptions(modules);
258-
stripAppAddonDirs(modules);
259188

260189
let project = {
261190
id: name,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ module('Acceptance | API | helpers', function(hooks) {
3737

3838
assert.equal(
3939
helperItem.importPath,
40-
`import { ${helperName} } from 'ember-cli-addon-docs/helpers/${kebabName}';`,
40+
`import { ${helperName} } from 'sandbox/helpers/${kebabName}';`,
4141
'renders the import path correctly'
4242
);
4343

0 commit comments

Comments
 (0)