UNPKG

23.8 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
150 layout.compiled = Handlebars.compile(layout.template);
151
152 this.layouts[layout.name] = layout;
153 }
154
155 createResource(filePath) {
156 const resource = read.sync(filePath, 'utf8');
157 const jsonResource = JSON.parse(resource);
158 if (jsonResource) {
159 this.resources = Object.assign(this.resources || {}, jsonResource)
160 }
161 }
162
163 parseLayout(pattern) {
164 this.watchable.push({pattern: pattern, type: 'layout'});
165 const files = glob.sync(pattern);
166 for (let i = 0; i < files.length; i++) {
167 this.createLayout(files[i])
168 }
169 }
170
171 parse(pattern, skipWrite) {
172 this.watchable.push({pattern: pattern, type: 'component'});
173 const files = glob.sync(pattern);
174 for (let i = 0; i < files.length; i++) {
175 this.createComponent(files[i], skipWrite)
176 }
177 }
178
179 parseResource(pattern) {
180 this.watchable.push({pattern: pattern, type: 'resource'});
181 const files = glob.sync(pattern);
182 for (let i = 0; i < files.length; i++) {
183 this.createResource(files[i])
184 }
185 }
186
187 buildRoute(h, route, basePath, parents) {
188 this.routeCount += 1;
189 route.parents = parents;
190 route.id = this.routeCount;
191 let newPath = path.posix.join(basePath, route.path);
192
193 // replace {param} with :param
194 newPath = newPath.replace(regex, subst);
195
196 if (route.path === '/') {
197 newPath += '/'
198 }
199 if (!this.hasOwnProperty(newPath)) {
200 h[newPath] = [];
201 }
202 if (parents) {
203 h[newPath] = h[newPath].concat(parents)
204 }
205 if (!route.children || route.children.length === 0) {
206 h[newPath].push(route)
207 }
208 if (route.children && route.children.length > 0) {
209 let ps = parents || [];
210 ps.push(route);
211 for (let i = 0; i < route.children.length; i++) {
212 h = this.buildRoute(h, route.children[i], newPath, ps)
213 }
214 }
215 return h
216 }
217
218 changeDetected(watchable) {
219 return change => {
220 if (watchable.type === 'component') {
221 this.createComponent(change.path)
222 } else if (watchable.type === 'layout') {
223 this.createLayout(change.path)
224 } else if (watchable.type === 'resource') {
225 this.createResource(change.path)
226 }
227 if (this.reload) {
228 this.reload.reload()
229 }
230 }
231 }
232
233 setupExpress(app, opt) {
234 // definitions
235 //Handlebars.registerPartial('definitions', `<script src="/${this.componentsId}.js"></script>`);
236
237 Handlebars.registerPartial('definitions', `<script>(()=>{'use strict';window.context={{{contextObject}}} })()</script><script src="https://unpkg.com/@webcomponents/webcomponentsjs@2.1.3/webcomponents-bundle.js"></script><script src="/${this.componentsId}.js"></script>`);
238
239 // Serve components
240 app.use('/' + this.componentsId + '.js', (req, res, next) => {
241 const lang = req.query.lang || this.defaultLocale;
242 res.type('application/javascript');
243 res.send(this.packagedHandlebars(true));
244 //next()
245 });
246
247 // serve self-contained components
248 Object.keys(this.components).map(i => {
249 const component = this.components[i];
250 app.use('/' + component.name + '.component.js', (req, res, next) => {
251 res.type('application/javascript');
252 res.send(this.selfContainedComponent(component));
253 //next()
254 });
255 });
256
257 if (opt) {
258 if (opt.hasOwnProperty('liveReload') && opt.liveReload === true) {
259 this.reload = reload(app);
260 for (let i = 0; i < this.watchable.length; i++) {
261 const w = this.watchable[i];
262 gulp.watch(w.pattern, this.changeDetected(w))
263 }
264 }
265 }
266
267 this.routeCount = -1;
268 this.handle = {}; // map of array of routes
269 for (let i = 0; i < this.routes.length; i++) {
270 this.handle = this.buildRoute(this.handle, this.routes[i], "/", [])
271 }
272
273 for (const p in this.handle) {
274 if (this.handle.hasOwnProperty(p)) {
275 const rs = this.handle[p];
276 app.get(p, this.handleRoute(p, rs))
277 }
278 }
279
280 // Language helper
281 Handlebars.registerHelper("i18n", (locale, k) => {
282 return this.resources.translations[locale][k]
283 });
284
285 // JSON helper
286 Handlebars.registerHelper("json", k => {
287 return JSON.stringify(k)
288 });
289
290 }
291
292 handleRoute(p, rs) {
293 return (req, res) => {
294 return new Promise((resolve, reject) => {
295 let context = {
296 storage: this.resources
297 };
298 if (req.params) {
299 context.query = req.params
300 }
301 context.query = Object.assign(context.query || {}, req.query);
302 if (req.query.hasOwnProperty('lang')) {
303 this.locale = req.query.lang || this.defaultLocale
304 } else {
305 this.locale = this.defaultLocale
306 }
307 let layoutName = 'index';
308
309 if (rs && rs.length > 0) {
310 const r = rs[0];
311 if (r && r.hasOwnProperty('layout') && r.layout.length > 0) {
312 layoutName = r.layout
313 }
314 }
315
316 const layout = this.getLayout(layoutName);
317 const registeredPages = [];
318 const selfContainedWaiting = [];
319 const requests = [];
320
321 let outletName = 'router-outlet';
322 for (let i = 0; i < rs.length; i++) {
323 const r = rs[i];
324 if (r.hasOwnProperty('redirect')) {
325 res.redirect(308, r.redirect);
326 return resolve()
327 }
328 if (r.hasOwnProperty('outlet') && !!r.outlet && r.outlet.length > 0) {
329 outletName = r.outlet
330 }
331 registeredPages.push(outletName);
332 const component = this.components[r.component];
333 if (component.skipWrite) {
334 selfContainedWaiting.push(component)
335 }
336 Handlebars.registerPartial(outletName, "<" + component.name + ">" + (component.render ? component.template : "") + "</" + component.name + ">");
337 if (r.hasOwnProperty('requests')) {
338 r.requests.forEach(_request => {
339 const request = {
340 url: _request.url,
341 method: _request.method,
342 headers: _request.headers,
343 body: _request.body,
344 };
345
346 if (request.hasOwnProperty('url')) {
347 /*let resolvedApiUri = request.url;*/
348 request.url = request.url.replace(this.regex, function (match, param) {
349 return match.replace('$' + param, req.params[param]);
350 });
351
352 let split = request.url.split('?');
353 request.url = split[0];
354 let resolvedQuery = '';
355 if (split.length > 1) {
356 resolvedQuery = split[1]
357 }
358
359 for (let k in req.query) {
360 if (req.query.hasOwnProperty(k)) {
361 if (resolvedQuery.length > 0) {
362 resolvedQuery += '&'
363 }
364 resolvedQuery += k + '=' + req.query[k]
365 }
366 }
367
368 request.url += '?' + resolvedQuery;
369 }
370
371 if (request.hasOwnProperty('body') && typeof request.body === "object") {
372 /*let resolvedApiUri = request.url;*/
373 const body = JSON.stringify(request.body).replace(this.regex, function (match, param) {
374 return match.replace('$' + param, req.params[param]);
375 });
376
377 request.body = JSON.parse(body);
378 }
379
380 if (request.hasOwnProperty('method') && typeof request.method === "string") {
381 request.method = request.method.toUpperCase();
382 switch (request.method) {
383 case "GET":
384 case "POST":
385 /*case "PUT":
386 case "OPTIONS":
387 case "DELETE":
388 case "POST":*/
389 break;
390 default:
391 reject("method not allowed: " + request.method)
392 }
393 } else {
394 request.method = "GET"
395 }
396
397 requests.push(request)
398 });
399 }
400 if (r.hasOwnProperty('page')) {
401 context = Object.assign(context, r.page);
402 }
403 }
404
405 let selfContaining = "";
406 selfContainedWaiting.forEach(component => {
407 selfContaining += this.selfContainedComponent(component)
408 });
409
410
411 // add locale
412 context['locale'] = this.locale;
413
414 // get api data
415 if (requests.length > 0) {
416 const ps = [];
417 context.data = {};
418
419 requests.forEach((r, index) => {
420 const client = request.createClient(r.url);
421 if (r.headers) {
422 for (let key in r.headers) {
423 if (r.headers.hasOwnProperty(key)) {
424 client.headers[key] = r.headers[key]
425 }
426 }
427 }
428 switch (r.method) {
429 case "GET":
430 console.log('resolving GET ' + r.url);
431 ps.push(new Promise((resolve1, reject1) => {
432 client.get('', function (err, response, body) {
433 context.data[index.toString()] = body;
434 resolve1()
435 });
436 }));
437 break;
438 case "POST":
439 console.log('resolving POST ' + r.url);
440 ps.push(new Promise((resolve1, reject1) => {
441 client.post('', r.body, function (err, response, body) {
442 context.data[index.toString()] = body;
443 resolve1()
444 });
445 }));
446 break;
447 }
448 });
449
450 Promise.all(ps).then(() => {
451 context["contextObject"] = JSON.stringify(context);
452 res.send(layout.compiled(context));
453 // remove pages from cache
454 for (let i = 0; i < registeredPages.length; i++) {
455 Handlebars.unregisterPartial(registeredPages[i])
456 }
457 resolve()
458 })
459 } else {
460 context["contextObject"] = JSON.stringify(context);
461 res.send(layout.compiled(context));
462 // remove pages from cache
463 for (let i = 0; i < registeredPages.length; i++) {
464 Handlebars.unregisterPartial(registeredPages[i])
465 }
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';Handlebars.registerHelper("stringify",k=>{return JSON.stringify(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>(()=>{'use strict';window.context={{{contextObject}}} })()</script><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