Home Reference Source Test

source/Component.js

import 'dom-renderer/dist/polyfill';

import { stringifyDOM } from 'dom-renderer';

import {readFile, readdir, statSync, outputFile, remove} from 'fs-extra';

import {join, basename, dirname, extname} from 'path';

import Package from 'amd-bundle';

import * as Parser from './parser';

import { folderOf, metaOf } from './utility';

import { toDataURI } from '@tech_query/node-toolkit';


/**
 * Component packer
 */
export default  class Component {
    /**
     * @param {string} path - Component directory
     */
    constructor(path) {

        this.path = path;

        this.name = basename( path );

        this.entry = join(path, 'index');
    }

    /**
     * @param {string} path
     *
     * @return {DocumentFragment}
     */
    static async parseHTML(path) {

        const box = document.createElement('div'),
            fragment = document.createDocumentFragment();

        box.innerHTML = (await readFile( path ))  +  '';

        fragment.append(... box.childNodes);

        return fragment;
    }

    /**
     * @param {string}  source - File path or Style source code
     * @param {?string} type   - MIME type
     * @param {string}  [base] - Path of the file which `@import` located in
     *
     * @return {?Element} Style element
     */
    static async parseCSS(source, type, base) {

        var style;

        type = type  ?  type.split('/')[1]  :  extname( source ).slice(1);

        if (! source.includes('\n'))
            source = await readFile(base = source) + '';

        style = Parser[type]  &&  await Parser[type](source, dirname( base ));

        return  style && Object.assign(
            document.createElement('style'),  {textContent: style}
        );
    }

    /**
     * @param {DocumentFragment} fragment
     *
     * @return {Element[]}
     */
    static findStyle(fragment) {

        return  [ ].concat.apply([ ],  Array.from(
            fragment.querySelectorAll('link[rel="stylesheet"], template'),
            tag  =>  tag.content ?
                Array.from( tag.content.querySelectorAll('style') )  :  tag
        ));
    }

    /**
     * @param {string} tagName
     *
     * @return {string}
     */
    static identifierOf(tagName) {

        return  tagName[0].toUpperCase() +
            tagName.replace(/-(\w)/g,  (_, char) => char.toUpperCase()).slice(1);
    }

    /**
     * @param {String} path - Full name of a JS file
     *
     * @return {String} Packed JS source code
     */
    static packJS(path) {

        const single_entry = join(folderOf().lib || '',  'index.js');

        const name = (path === single_entry)  &&  metaOf().name;

        path = path.split('.').slice(0, -1).join('.');

        return  (new Package( path )).bundle(
            name  ||  basename( dirname( path ) )
        );
    }

    /**
     * @return {DocumentFragment} HTML version bundle of this component
     */
    async toHTML() {

        const fragment = await Component.parseHTML(this.entry + '.html'),
            CSS = [ ];

        for (let sheet  of  Component.findStyle( fragment )) {

            let style = await Component.parseCSS(
                (sheet.tagName === 'STYLE')  ?
                    sheet.textContent  :  join(this.path, sheet.getAttribute('href')),
                sheet.type,
                this.entry + '.css'
            );

            if (! style)  continue;

            sheet.replaceWith( style );

            if (style.parentNode === fragment)  CSS.push( style );
        }

        fragment.querySelector('template').content.prepend(... CSS);

        const script = fragment.querySelector('script');

        if ( script )
            script.replaceWith(Object.assign(document.createElement('script'), {
                text:  `\n${
                    Component.packJS( join(this.path, script.getAttribute('src')) )
                }\n`
            }));

        return fragment;
    }

    /**
     * @protected
     *
     * @param {String} file - File path
     *
     * @return {String} Legal ECMAScript source code
     */
    async assetOf(file) {

        switch ( extname( file ).slice(1) ) {
            case 'html':
            case 'htm':
                file = stringifyDOM(await this.toHTML());  break;
            case 'css':
            case 'less':
            case 'sass':
            case 'scss':
            case 'stylus':
                file = (await Component.parseCSS( file )).textContent;  break;
            case 'js':
                return;
            case 'json':
                return  (await readFile( file )) + '';
            case 'yaml':
            case 'yml':
                return  Parser.yaml((await readFile( file )) + '');
            default:
                file = toDataURI( file );
        }

        return  JSON.stringify( file );
    }

    /**
     * @return {string} JS version bundle of this component
     */
    async toJS() {

        const temp_file = [ ];

        for (let file  of  await readdir( this.path )) {

            file = join(this.path, file);

            if (! statSync( file ).isFile())  continue;

            let temp = `${file}.js`;

            file = await this.assetOf( file );

            if (!(file != null))  continue;

            temp_file.push( temp );

            await outputFile(temp,  `export default ${file}`);
        }

        const source = Component.packJS(this.entry + '.js');

        await Promise.all( temp_file.map(file => remove( file )) );

        return source;
    }
}