1 | 'use strict';
|
2 |
|
3 | const path = require('path');
|
4 | const gulp = require('gulp');
|
5 | const minify = require("babel-minify");
|
6 | const Handlebars = require('handlebars');
|
7 | const jsdom = require('jsdom');
|
8 | const glob = require('glob');
|
9 | const read = require('read-file');
|
10 | const reload = require('reload');
|
11 | const jsonfile = require('jsonfile');
|
12 | const fs = require('fs');
|
13 | const crypto = require("crypto");
|
14 | const copydir = require('copy-dir');
|
15 | const request = require('request-json');
|
16 |
|
17 | const regex = /{(.*)}/gm;
|
18 | const whiteSpaceRegex = /\s{2,}/gm;
|
19 | const newLineRegex = /\n/gm;
|
20 | const subst = `:$1`;
|
21 |
|
22 | module.exports.Helper = class Helper {
|
23 | constructor(config) {
|
24 | this.defaultLocale = config.defaultLocale || '';
|
25 | this.locales = [];
|
26 | this.watchable = [];
|
27 | this.workingDir = config.workingDir || '.';
|
28 | this.components = config.components || {};
|
29 | this.routes = config.routes || {};
|
30 |
|
31 |
|
32 |
|
33 | this.layouts = config.layouts || {};
|
34 | this.resources = {
|
35 | translations: {},
|
36 | };
|
37 |
|
38 | for (let locale in this.resources.translations) {
|
39 | if (this.resources.translations.hasOwnProperty(locale)) {
|
40 | this.locales.push(locale)
|
41 | }
|
42 | }
|
43 |
|
44 | this.componentsId = crypto.randomBytes(12).toString('hex');
|
45 | this.regex = /\$(\w+)/g;
|
46 | }
|
47 |
|
48 | getLayout(name, reload) {
|
49 | if (this.layouts.hasOwnProperty(name)) {
|
50 | const layout = this.layouts[name];
|
51 | if (reload) {
|
52 | layout.template = read.sync(layout.file, 'utf8');
|
53 | }
|
54 | return layout
|
55 | }
|
56 | }
|
57 |
|
58 | getComponent(name, reload) {
|
59 | if (this.components.hasOwnProperty(name)) {
|
60 | const component = this.components[name];
|
61 | if (reload) {
|
62 | this.createComponent(component.file)
|
63 | }
|
64 | return this.components[name]
|
65 | }
|
66 | }
|
67 |
|
68 | createComponent(filePath, skipWrite) {
|
69 | const component = {
|
70 | file: filePath,
|
71 | name: path.basename(filePath, '.html'),
|
72 | template: '',
|
73 | skipWrite: skipWrite,
|
74 | render: true,
|
75 | };
|
76 | const template = read.sync(component.file, 'utf8');
|
77 | const dom = new jsdom.JSDOM(template, {
|
78 | includeNodeLocations: true
|
79 | });
|
80 | const body = dom.window.document.head;
|
81 | for (let i = 0; i < body.children.length; i++) {
|
82 | let child = body.children[i];
|
83 | if (child.nodeName === "TEMPLATE") {
|
84 | const loc = dom.nodeLocation(child);
|
85 | const start = loc.startTag.endOffset;
|
86 | const end = loc.endOffset - start - 11;
|
87 | component.template = template.substr(start, end);
|
88 | component.template = component.template.replace(newLineRegex, ``);
|
89 | component.template = component.template.replace(whiteSpaceRegex, ` `);
|
90 | component.render = child.getAttribute('render') !== 'false'
|
91 | } else if (child.nodeName === "SCRIPT") {
|
92 | const {code, map} = minify(child.innerHTML, {
|
93 | mangle: {
|
94 | keepClassName: true
|
95 | }
|
96 | });
|
97 | component.minifiedScript = code;
|
98 | component.script = child.innerHTML
|
99 | }
|
100 | }
|
101 |
|
102 | Handlebars.registerPartial(component.name, "<" + component.name + ">" + (component.render ? component.template : "") + "</" + component.name + ">");
|
103 | component.precompiledTemplate = Handlebars.precompile(component.template);
|
104 | component.precompiled = Handlebars.precompile("<" + component.name + ">{{> _" + component.name + "}}</" + component.name + ">");
|
105 | this.components[component.name] = component;
|
106 | }
|
107 |
|
108 | createLayout(filePath) {
|
109 | const layout = {
|
110 | file: filePath,
|
111 | name: path.basename(filePath, '.html'),
|
112 | };
|
113 | layout.template = read.sync(layout.file, 'utf8');
|
114 | layout.template = layout.template.replace(newLineRegex, ``);
|
115 | layout.template = layout.template.replace(whiteSpaceRegex, ` `);
|
116 | this.layouts[layout.name] = layout;
|
117 | }
|
118 |
|
119 | createResource(filePath) {
|
120 | const resource = read.sync(filePath, 'utf8');
|
121 | const jsonResource = JSON.parse(resource);
|
122 | if (jsonResource) {
|
123 | this.resources = Object.assign(this.resources || {}, jsonResource)
|
124 | }
|
125 | }
|
126 |
|
127 | parseLayout(pattern) {
|
128 | this.watchable.push({pattern: pattern, type: 'layout'});
|
129 | const files = glob.sync(pattern);
|
130 | for (let i = 0; i < files.length; i++) {
|
131 | this.createLayout(files[i])
|
132 | }
|
133 | }
|
134 |
|
135 | parse(pattern, skipWrite) {
|
136 | this.watchable.push({pattern: pattern, type: 'component'});
|
137 | const files = glob.sync(pattern);
|
138 | for (let i = 0; i < files.length; i++) {
|
139 | this.createComponent(files[i], skipWrite)
|
140 | }
|
141 | }
|
142 |
|
143 | parseResource(pattern) {
|
144 | this.watchable.push({pattern: pattern, type: 'resource'});
|
145 | const files = glob.sync(pattern);
|
146 | for (let i = 0; i < files.length; i++) {
|
147 | this.createResource(files[i])
|
148 | }
|
149 | }
|
150 |
|
151 | buildRoute(h, route, basePath, parents) {
|
152 | this.routeCount += 1;
|
153 | route.parents = parents;
|
154 | route.id = this.routeCount;
|
155 | let newPath = path.posix.join(basePath, route.path);
|
156 |
|
157 |
|
158 | newPath = newPath.replace(regex, subst);
|
159 |
|
160 | if (route.path === '/') {
|
161 | newPath += '/'
|
162 | }
|
163 | if (!this.hasOwnProperty(newPath)) {
|
164 | h[newPath] = [];
|
165 | }
|
166 | if (parents) {
|
167 | h[newPath] = h[newPath].concat(parents)
|
168 | }
|
169 | if (!route.children || route.children.length === 0) {
|
170 | h[newPath].push(route)
|
171 | }
|
172 | if (route.children && route.children.length > 0) {
|
173 | let ps = parents || [];
|
174 | ps.push(route);
|
175 | for (let i = 0; i < route.children.length; i++) {
|
176 | h = this.buildRoute(h, route.children[i], newPath, ps)
|
177 | }
|
178 | }
|
179 | return h
|
180 | }
|
181 |
|
182 | changeDetected(watchable) {
|
183 | return change => {
|
184 | if (watchable.type === 'component') {
|
185 | this.createComponent(change.path)
|
186 | } else if (watchable.type === 'layout') {
|
187 | this.createLayout(change.path)
|
188 | } else if (watchable.type === 'resource') {
|
189 | this.createResource(change.path)
|
190 | }
|
191 | if (this.reload) {
|
192 | this.reload.reload()
|
193 | }
|
194 | }
|
195 | }
|
196 |
|
197 | setupExpress(app, opt) {
|
198 |
|
199 |
|
200 |
|
201 |
|
202 | app.use('/' + this.componentsId + '.js', (req, res, next) => {
|
203 | const lang = req.query.lang || this.defaultLocale;
|
204 | res.type('application/javascript');
|
205 | res.send(this.packagedHandlebars(true));
|
206 |
|
207 | });
|
208 |
|
209 |
|
210 | Object.keys(this.components).map(i => {
|
211 | const component = this.components[i];
|
212 | app.use('/' + component.name + '.component.js', (req, res, next) => {
|
213 | res.type('application/javascript');
|
214 | res.send(this.selfContainedComponent(component));
|
215 |
|
216 | });
|
217 | });
|
218 |
|
219 | if (opt) {
|
220 | if (opt.hasOwnProperty('liveReload') && opt.liveReload === true) {
|
221 | this.reload = reload(app);
|
222 | for (let i = 0; i < this.watchable.length; i++) {
|
223 | const w = this.watchable[i];
|
224 | gulp.watch(w.pattern, this.changeDetected(w))
|
225 | }
|
226 | }
|
227 | }
|
228 |
|
229 | this.routeCount = -1;
|
230 | this.handle = {};
|
231 | for (let i = 0; i < this.routes.length; i++) {
|
232 | this.handle = this.buildRoute(this.handle, this.routes[i], "/", [])
|
233 | }
|
234 |
|
235 | for (const p in this.handle) {
|
236 | if (this.handle.hasOwnProperty(p)) {
|
237 | const rs = this.handle[p];
|
238 | app.get(p, this.handleRoute(p, rs))
|
239 | }
|
240 | }
|
241 |
|
242 |
|
243 | Handlebars.registerHelper("i18n", (locale, k) => {
|
244 | return this.resources.translations[locale][k]
|
245 | });
|
246 |
|
247 |
|
248 | Handlebars.registerHelper("json", k => {
|
249 | return JSON.stringify(k)
|
250 | });
|
251 |
|
252 | }
|
253 |
|
254 | handleRoute(p, rs) {
|
255 | return (req, res) => {
|
256 | return new Promise((resolve, reject) => {
|
257 | let context = {};
|
258 | if (req.params) {
|
259 | context.query = req.params
|
260 | }
|
261 | context.query = Object.assign(context.query || {}, req.query);
|
262 | if (req.query.hasOwnProperty('lang')) {
|
263 | this.locale = req.query.lang || this.defaultLocale
|
264 | } else {
|
265 | this.locale = this.defaultLocale
|
266 | }
|
267 | let layoutName = 'index';
|
268 |
|
269 | if (rs && rs.length > 0) {
|
270 | const r = rs[0];
|
271 | if (r && r.hasOwnProperty('layout') && r.layout.length > 0) {
|
272 | layoutName = r.layout
|
273 | }
|
274 | }
|
275 |
|
276 | const layout = this.getLayout(layoutName);
|
277 | const registeredPages = [];
|
278 | const selfContainedWaiting = [];
|
279 | const requests = [];
|
280 |
|
281 | let outletName = 'router-outlet';
|
282 | for (let i = 0; i < rs.length; i++) {
|
283 | const r = rs[i];
|
284 | if (r.hasOwnProperty('redirect')) {
|
285 | res.redirect(308, r.redirect);
|
286 | return resolve()
|
287 | }
|
288 | if (r.hasOwnProperty('outlet') && !!r.outlet && r.outlet.length > 0) {
|
289 | outletName = r.outlet
|
290 | }
|
291 | registeredPages.push(outletName);
|
292 | const component = this.components[r.component];
|
293 | if (component.skipWrite) {
|
294 | selfContainedWaiting.push(component)
|
295 | }
|
296 | Handlebars.registerPartial(outletName, "<" + component.name + ">" + (component.render ? component.template : "") + "</" + component.name + ">");
|
297 | if (r.hasOwnProperty('requests')) {
|
298 | r.requests.forEach(_request => {
|
299 | const request = {
|
300 | url: _request.url,
|
301 | method: _request.method,
|
302 | headers: _request.headers,
|
303 | body: _request.body,
|
304 | };
|
305 |
|
306 | if (request.hasOwnProperty('url')) {
|
307 |
|
308 | request.url = request.url.replace(this.regex, function (match, param) {
|
309 | return match.replace('$' + param, req.params[param]);
|
310 | });
|
311 |
|
312 | let split = request.url.split('?');
|
313 | request.url = split[0];
|
314 | let resolvedQuery = '';
|
315 | if (split.length > 1) {
|
316 | resolvedQuery = split[1]
|
317 | }
|
318 |
|
319 | for (let k in req.query) {
|
320 | if (req.query.hasOwnProperty(k)) {
|
321 | if (resolvedQuery.length > 0) {
|
322 | resolvedQuery += '&'
|
323 | }
|
324 | resolvedQuery += k + '=' + req.query[k]
|
325 | }
|
326 | }
|
327 |
|
328 | request.url += '?' + resolvedQuery;
|
329 | }
|
330 | if (request.hasOwnProperty('method') && typeof request.method === "string") {
|
331 | request.method = request.method.toUpperCase();
|
332 | switch (request.method) {
|
333 | case "GET":
|
334 | case "POST":
|
335 | |
336 |
|
337 |
|
338 |
|
339 | break;
|
340 | default:
|
341 | reject("method not allowed: " + request.method)
|
342 | }
|
343 | } else {
|
344 | request.method = "GET"
|
345 | }
|
346 |
|
347 | requests.push(request)
|
348 | });
|
349 | }
|
350 | if (r.hasOwnProperty('page')) {
|
351 | context = Object.assign(context, r.page);
|
352 | }
|
353 | }
|
354 |
|
355 | let selfContaining = "";
|
356 | selfContainedWaiting.forEach(component => {
|
357 | selfContaining += this.selfContainedComponent(component)
|
358 | });
|
359 |
|
360 | Handlebars.registerPartial('definitions', `<script id="dynamic-helper" type="application/json">${JSON.stringify(this.routes)}</script><script src="https://unpkg.com/@webcomponents/webcomponentsjs@2.1.3/webcomponents-bundle.js"></script><script src="/${this.componentsId}.js"></script>${selfContaining.length > 0 ? `<script>${selfContaining}</script>` : ''}`);
|
361 |
|
362 | const fn = Handlebars.compile(layout.template);
|
363 |
|
364 |
|
365 | context['locale'] = this.locale;
|
366 |
|
367 |
|
368 | if (requests.length > 0) {
|
369 | const ps = [];
|
370 | context.data = {};
|
371 |
|
372 | requests.forEach((r, index) => {
|
373 | const client = request.createClient(r.url);
|
374 | if (r.headers) {
|
375 | for (let key in r.headers) {
|
376 | if (r.headers.hasOwnProperty(key)) {
|
377 | client.headers[key] = r.headers[key]
|
378 | }
|
379 | }
|
380 | }
|
381 | switch (r.method) {
|
382 | case "GET":
|
383 | console.log('resolving GET ' + r.url);
|
384 | ps.push(new Promise((resolve1, reject1) => {
|
385 | client.get('', function (err, response, body) {
|
386 | context.data[index] = body;
|
387 | resolve1()
|
388 | });
|
389 | }));
|
390 | break;
|
391 | case "POST":
|
392 | console.log('resolving POST ' + r.url);
|
393 | ps.push(new Promise((resolve1, reject1) => {
|
394 | client.post('', r.body, function (err, response, body) {
|
395 | context.data[index] = body;
|
396 | resolve1()
|
397 | });
|
398 | }));
|
399 | break;
|
400 | }
|
401 | });
|
402 |
|
403 | Promise.all(ps).then(() => {
|
404 | context.json = JSON.stringify(context);
|
405 | res.send(fn(context));
|
406 |
|
407 | for (let i = 0; i < registeredPages.length; i++) {
|
408 | Handlebars.unregisterPartial(registeredPages[i])
|
409 | }
|
410 | Handlebars.unregisterPartial('definitions');
|
411 | resolve()
|
412 | })
|
413 | } else {
|
414 | context.json = JSON.stringify(context);
|
415 | res.send(fn(context));
|
416 |
|
417 | for (let i = 0; i < registeredPages.length; i++) {
|
418 | Handlebars.unregisterPartial(registeredPages[i])
|
419 | }
|
420 | Handlebars.unregisterPartial('definitions');
|
421 | resolve()
|
422 | }
|
423 | })
|
424 | }
|
425 | }
|
426 |
|
427 | packagedHandlebars(isLocal) {
|
428 | const cArr = Object.keys(this.components).filter(i => {
|
429 | if (!this.components[i].skipWrite) {
|
430 | return this.components[i]
|
431 | }
|
432 | }).map(i => this.components[i]);
|
433 |
|
434 | const resources = Object.assign({}, this.resources || {});
|
435 | return `${handlebarsScript}(function(){'use strict';const R=${JSON.stringify(!!resources ? resources : {})};window.translations=R.translations;window.storage=R.storage;Handlebars.registerHelper("json",k=>{return JSON.stringify(k)});Handlebars.registerHelper('i18n',(l,k)=>{return R.translations[l][k]});var template=Handlebars.template,templates=Handlebars.templates=Handlebars.templates || {};${cArr.map((component) => {
|
436 | return `Handlebars.partials['${component.name}'] = template(${component.precompiled});Handlebars.partials['_${component.name}'] = template(${component.precompiledTemplate});`
|
437 | }).join('')};WebComponents.waitFor=WebComponents.waitFor||function(p){return Promise.all([p].map(function(fn){return fn instanceof Function ? fn() : fn;}))};WebComponents.waitFor(()=>{return new Promise((res)=>{var cc=new function(){this.define=function(name,module){if(module&&module.hasOwnProperty('exports')){module.exports.prototype.template=function(c,o){return Handlebars.partials["_"+name](c,o)};window.customElements.define(name,module.exports)}}};window['customComponents']=cc;${cArr.map((component) => {
|
438 | return component.script ? `cc.define('${component.name}',(function(){var module={};${component.script};return module})());` : ''
|
439 | }).join('')};res()})})${isLocal ? `;var r=document.createElement('script');r.setAttribute('src','/reload/reload.js');document.head.appendChild(r)` : ``}})();`
|
440 | }
|
441 |
|
442 | selfContainedComponent(component) {
|
443 | return `(function(){'use strict';WebComponents.waitFor(()=>new Promise(n=>{var cc=new function(){this.define=function(n,e){e&&e.hasOwnProperty("exports")&&(e.exports.prototype.template=function(e,t){return Handlebars.partials[n](e,t)},window.customElements.define(n,e.exports))}};Handlebars.partials['${component.name}']=Handlebars.template(${component.precompiled});cc.define('${component.name}',(function(){const module={};${component.minifiedScript ? component.minifiedScript : ''};return module})());n()}));})();`
|
444 | }
|
445 |
|
446 | build(dist) {
|
447 | mkDirByPathSync(dist);
|
448 | mkDirByPathSync(dist + '/public');
|
449 |
|
450 |
|
451 | copydir.sync(this.workingDir + '/public', dist + '/public');
|
452 |
|
453 |
|
454 | const componentsVersion = crypto.randomBytes(6).toString('hex');
|
455 | const manifest = {
|
456 | 'imports': [],
|
457 | 'routes': this.routes,
|
458 | 'defaultLocale': this.defaultLocale,
|
459 | 'resources': this.resources,
|
460 | 'componentsVersion': componentsVersion,
|
461 | };
|
462 | for (let name in this.components) {
|
463 | if (this.components.hasOwnProperty(name)) {
|
464 | const component = this.components[name];
|
465 |
|
466 |
|
467 | const templatePath = 'public/' + name + '.template.html';
|
468 | fs.writeFileSync(dist + "/" + templatePath, component.template);
|
469 |
|
470 |
|
471 | const componentPath = 'public/' + name + '.component.js';
|
472 | fs.writeFileSync(dist + "/" + componentPath, this.selfContainedComponent(component));
|
473 |
|
474 | manifest.imports.push({
|
475 | 'templatePath': templatePath,
|
476 | 'componentPath': componentPath,
|
477 | 'name': name,
|
478 | 'render': component.render,
|
479 | })
|
480 | }
|
481 | }
|
482 |
|
483 |
|
484 | const packagedHandlebars = 'public/' + componentsVersion + '.js';
|
485 | fs.writeFileSync(dist + "/" + packagedHandlebars, this.packagedHandlebars());
|
486 |
|
487 |
|
488 | fs.writeFileSync(dist + "/definitions.html", `<script src="https://unpkg.com/@webcomponents/webcomponentsjs@2.0.0/webcomponents-bundle.js"></script><script src="/${componentsVersion}.js"></script>`);
|
489 | manifest.imports.push({
|
490 | 'templatePath': "definitions.html",
|
491 | 'name': "definitions",
|
492 | 'render': true
|
493 | });
|
494 |
|
495 | for (let name in this.layouts) {
|
496 | if (this.layouts.hasOwnProperty(name)) {
|
497 | const newFilePath = 'public/' + name + '.layout.html';
|
498 |
|
499 |
|
500 | fs.writeFileSync(dist + '/' + newFilePath, this.layouts[name].template);
|
501 |
|
502 | manifest.imports.push({
|
503 | 'templatePath': newFilePath,
|
504 | 'name': name,
|
505 | 'layout': true,
|
506 | })
|
507 | }
|
508 | }
|
509 |
|
510 | jsonfile.writeFileSync(dist + '/manifest.json', manifest)
|
511 | }
|
512 | };
|
513 |
|
514 | function mkDirByPathSync(targetDir, {isRelativeToScript = false} = {}) {
|
515 | const sep = path.sep;
|
516 | const initDir = path.isAbsolute(targetDir) ? sep : '';
|
517 | const baseDir = isRelativeToScript ? __dirname : '.';
|
518 | targetDir.split(sep).reduce((parentDir, childDir) => {
|
519 | const curDir = path.resolve(baseDir, parentDir, childDir);
|
520 | try {
|
521 | fs.mkdirSync(curDir);
|
522 | } catch (err) {
|
523 | if (err.code !== 'EEXIST') {
|
524 | throw err;
|
525 | }
|
526 | }
|
527 | return curDir;
|
528 | },
|
529 | initDir
|
530 | )
|
531 | }
|
532 |
|
533 | const handlebarsScript = read.sync('./node_modules/handlebars/dist/handlebars.min.js'); |
\ | No newline at end of file |