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