UNPKG

24.1 kBJavaScriptView Raw
1'use strict';
2
3const path = require('path');
4const gulp = require('gulp');
5const minify = require("babel-minify");
6const Handlebars = require('handlebars');
7const jsdom = require('jsdom');
8const glob = require('glob');
9const read = require('read-file');
10const reload = require('reload');
11const jsonfile = require('jsonfile');
12const fs = require('fs');
13const crypto = require("crypto");
14const copydir = require('copy-dir');
15const request = require('request-json');
16
17const regex = /{([^\/]*)}/gm;
18const whiteSpaceRegex = /\s{2,}/gm;
19const newLineRegex = /\n/gm;
20const subst = `:$1`;
21
22module.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 //todo:check routes
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; // api url variable replacement
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 /*import(mjs).then(t => {
67 console.log("component imported");
68 //console.log(t);
69 t.default();
70 }).catch(err => {
71 console.error(err)
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 // replace {param} with :param
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 // definitions
232 //Handlebars.registerPartial('definitions', `<script src="/${this.componentsId}.js"></script>`);
233
234 // Serve components
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 //next()
240 });
241
242 // serve self-contained components
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 //next()
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 = {}; // map of array of routes
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 // Language helper
276 Handlebars.registerHelper("i18n", (locale, k) => {
277 return this.resources.translations[locale][k]
278 });
279
280 // JSON helper
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 /*let resolvedApiUri = request.url;*/
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 /*let resolvedApiUri = request.url;*/
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 /*case "PUT":
381 case "OPTIONS":
382 case "DELETE":
383 case "POST":*/
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 // add locale
410 context['locale'] = this.locale;
411
412 // get api data
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 // remove pages from cache
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 // remove pages from cache
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 // copy static files
496 copydir.sync(this.workingDir + '/public', dist + '/public');
497 // build manifest.json file
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 // write template
512 const templatePath = 'public/' + name + '.template.html';
513 fs.writeFileSync(dist + "/" + templatePath, component.template);
514
515 // write self-contained component
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 // write singlefile packaged handlebars
529 const packagedHandlebars = 'public/' + componentsVersion + '.js';
530 fs.writeFileSync(dist + "/" + packagedHandlebars, this.packagedHandlebars());
531
532 // add definitions partial
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 // write file
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
559function 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
578const handlebarsScript = read.sync('./node_modules/handlebars/dist/handlebars.min.js');
\No newline at end of file