Skip to content

Commit ee27347

Browse files
committed
Initial commit for react-router-config-loader implementation
1 parent e6e2881 commit ee27347

21 files changed

+764
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,5 @@ typings/
5757
# dotenv environment variables file
5858
.env
5959

60+
# test Output
61+
test/dist

.npmignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.gitignore
2+
.vscode
3+
test

.vscode/launch.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
// Use IntelliSense to learn about possible Node.js debug attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"type": "node",
9+
"request": "launch",
10+
"name": "Mocha Tests",
11+
"program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
12+
"args": [
13+
"-u",
14+
"tdd",
15+
"--timeout",
16+
"999999",
17+
"--colors",
18+
"${workspaceRoot}/test"
19+
],
20+
"internalConsoleOptions": "openOnSessionStart"
21+
}
22+
]
23+
}

index.js

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
const path = require('path');
2+
const toSource = require('tosource');
3+
const deepCopy = require('deepcopy');
4+
const loaderUtils = require("loader-utils");
5+
6+
class Loader {
7+
constructor(loaderApi) {
8+
this.loaderApi = loaderApi;
9+
10+
const options = loaderUtils.getOptions(this.loaderApi) || {};
11+
this.componentsDir = options.componentsDir || '.';
12+
this.relativePath = options.relativePath || false;
13+
this.inheritProps = options.inheritProps || {};
14+
}
15+
16+
transform(source) {
17+
let [ imports, routes ] = this.transformRoutes(eval(source));
18+
return (
19+
`${this.generateImports(imports)}`
20+
+ `${this.generateRoutes(routes)}`
21+
);
22+
}
23+
24+
/**
25+
* Get name of component in the route
26+
*
27+
* @param {*} route route config object
28+
* @returns {string} component name
29+
*/
30+
componentName(route) {
31+
// use component name specified in route
32+
if (route.componentName) {
33+
return route.componentName;
34+
}
35+
36+
// default name equals base name of component path
37+
return path.basename(route.component, path.extname(route.component));
38+
}
39+
40+
/**
41+
* Get path to import component from
42+
*
43+
* @param {*} route route config object
44+
* @returns {string} component path
45+
*/
46+
componentPath(route) {
47+
if (path.isAbsolute(route.component) || !route.component.startsWith('.')) {
48+
// load for absolute path or node_modules, no change
49+
return route.component.replace(/\\/g, '\\\\');
50+
}
51+
else {
52+
// relative path, join with base path
53+
// NB: prefix ./ to not load from node_modules
54+
return `./${path.join(this.componentsDir, route.component)}`.replace(/\\/g, '\\\\');
55+
}
56+
}
57+
58+
/**
59+
* Transform routes config into react router config by collecting component imports
60+
*
61+
* @param {Array} routes routes config
62+
* @returns { [ Array, Array ] } [ component path to import, react routes config ]
63+
*/
64+
transformRoutes(routes) {
65+
let imports = new Set();
66+
routes.forEach(route => this.transformRouteIter(route, imports, '', this.inheritProps));
67+
68+
return [ Array.from(imports), routes ];
69+
}
70+
71+
transformRouteIter(route, imports, cwd, inheritProps) {
72+
// transform component configuration into name and import
73+
if (route.component) {
74+
imports.add(`${this.componentName(route)}|${this.componentPath(route)}`);
75+
route.component = this.componentName(route);
76+
delete route.componentName;
77+
}
78+
79+
// transform relative path to absolute
80+
if (route.path && this.relativePath) {
81+
cwd = route.path = path.posix.join(cwd, route.path);
82+
}
83+
84+
// merge inherit props to route
85+
if (route.inheritProps) {
86+
inheritProps = Object.assign(inheritProps, route.inheritProps);
87+
delete route.inheritProps;
88+
}
89+
Object.assign(route, inheritProps);
90+
91+
// recursive transform child routes
92+
if (route.routes) {
93+
route.routes.forEach(route => this.transformRouteIter(route, imports, cwd, inheritProps));
94+
}
95+
}
96+
97+
/**
98+
* Generate js import expression for components
99+
*
100+
* @param {string} imports import information for react components
101+
* @returns {string} js source code that import components
102+
*/
103+
generateImports(imports) {
104+
return imports.map(kv => {
105+
let [ name, file ] = kv.split('|');
106+
return `import ${name} from '${file}';\n`;
107+
}).join('');
108+
}
109+
110+
/**
111+
* Generate js export expression for react-router-config
112+
*
113+
* @param {*} routes react route configs
114+
* @returns {string} js source code that export react router config
115+
*/
116+
generateRoutes(routes) {
117+
return `export default [${routes.map(route => this.generateRouteIter(route, '')).join(',\n')}];`;
118+
}
119+
120+
generateRouteIter(route, indent) {
121+
let component = deepCopy(route.component);
122+
let routes = deepCopy(route.routes);
123+
delete route.component;
124+
delete route.routes;
125+
126+
let source = toSource(route, null, ' ', indent);
127+
let nextIndent = indent + ' ';
128+
129+
return [
130+
source === '{}' ? '{' : `${source.substring(0, source.length - 2)},`,
131+
component ? `${nextIndent}component:${component},` : '',
132+
routes ? `${nextIndent}routes:[${routes.map(route => `${this.generateRouteIter(route, `${nextIndent}`)}`).join(`,\n${nextIndent}`)}]` : '',
133+
`${indent}}`
134+
].filter(str => str).join('\n');
135+
}
136+
}
137+
138+
module.exports= exports = function(source) {
139+
let loader = new Loader(this);
140+
return loader.transform(source);
141+
};
142+
143+
exports.Loader = Loader;

package.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "react-router-config-loader",
3+
"version": "0.1.0",
4+
"description": "Webpack loader to load react-router-config",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "nyc mocha"
8+
},
9+
"author": {
10+
"name": "grayyang",
11+
"email": "[email protected]"
12+
},
13+
"license": "MIT",
14+
"dependencies": {
15+
"deepcopy": "^0.6.3",
16+
"tosource": "^1.0.0"
17+
},
18+
"devDependencies": {
19+
"chai": "^4.1.1",
20+
"chai-as-promised": "^7.1.1",
21+
"js-promisify": "^1.1.0",
22+
"mocha": "^3.5.0",
23+
"nyc": "^11.1.0",
24+
"rimraf": "^2.6.1",
25+
"webpack": "^3.5.4",
26+
"yaml-loader": "^0.5.0"
27+
}
28+
}

test/componentName.spec.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const Loader = require('../index.js').Loader;
2+
const chai = require('chai');
3+
chai.should();
4+
5+
describe('Loader', () => {
6+
describe('#componentName', () => {
7+
it('should return path name by default', () => {
8+
let loader = new Loader({});
9+
let route = {
10+
component: './src/Componnet.js',
11+
};
12+
13+
let name = loader.componentName(route);
14+
name.should.equal('Componnet');
15+
});
16+
17+
it('should return componentName if it is specified', () => {
18+
let loader = new Loader({});
19+
let route = {
20+
component: './Componnet',
21+
componentName: 'TestComponent',
22+
};
23+
24+
let name = loader.componentName(route);
25+
name.should.equal('TestComponent');
26+
});
27+
});
28+
});

test/componentPath.spec.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
const Loader = require('../index.js').Loader;
2+
const path = require('path');
3+
const chai = require('chai');
4+
chai.should();
5+
6+
describe('Loader', () => {
7+
describe('#componentPath', () => {
8+
it('should return relative path if one relative path specified', () => {
9+
let loader = new Loader({});
10+
let route = {
11+
component: './src/Component',
12+
}
13+
14+
let filepath = loader.componentPath(route);
15+
path.resolve(filepath).should.equal(path.resolve('./src/Component'));
16+
filepath.startsWith('.').should.be.true;
17+
});
18+
19+
it('should return absolute path if one absolute path specified', () => {
20+
let loader = new Loader({});
21+
let route = {
22+
component: path.resolve('./src/Component'),
23+
};
24+
25+
let filepath = loader.componentPath(route);
26+
path.isAbsolute(filepath).should.be.true;
27+
path.resolve(filepath).should.equal(path.resolve('./src/Component'));
28+
});
29+
30+
it('should return module path if one npm module specified', () => {
31+
let loader = new Loader({});
32+
let route = {
33+
component: 'Module/Component',
34+
};
35+
36+
let filepath = loader.componentPath(route);
37+
filepath.should.equal('Module/Component');
38+
});
39+
40+
it('should return path relative to componentsDir if it is specified', () => {
41+
let loader = new Loader({ query: { componentsDir: './src' } });
42+
let route = {
43+
component: './Component',
44+
};
45+
46+
let filepath = loader.componentPath(route);
47+
path.resolve(filepath).should.equal(path.resolve('./src/Component'));
48+
filepath.startsWith('.').should.be.true;
49+
});
50+
51+
it('should return escape character \\\\ if including \\ in path', () => {
52+
let loader = new Loader({});
53+
let route = {
54+
component: 'Module\\Component',
55+
};
56+
57+
let filepath = loader.componentPath(route);
58+
filepath.should.equal('Module\\\\Component');
59+
});
60+
});
61+
});

test/data/components/Child.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class Child {
2+
get whoami() {
3+
return 'Child';
4+
}
5+
}
6+
7+
export default Child;

test/data/components/GrandChild.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class GrandChild {
2+
get whoami() {
3+
return 'GrandChild';
4+
}
5+
}
6+
7+
export default GrandChild;

test/data/components/Home.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class Home {
2+
get whoami() {
3+
return 'Home';
4+
}
5+
}
6+
7+
export default Home;

0 commit comments

Comments
 (0)