<commit_before>/* jshint asi:true */
//先等图片都加载完成
//再执行布局函数

/**
 * 执行主函数
 * @param  {[type]} function( [description]
 * @return {[type]}           [description]
 */
(function() {

    /**
     * 内容JSON
     */
    var demoContent = [
        {
            demo_link: 'https://codepen.io/haoyang/pen/jrvrQq',
            img_link: 'https://ooo.0o0.ooo/2016/11/24/5836d81f48cd2.png',
            code_link: 'https://codepen.io/haoyang/pen/jrvrQq',
            title: 'Fisher-Yates 洗牌算法动画',
            core_tech: 'JavaScript',
            description: 'Fisher-Yates 洗牌算法动画。算法详情见 这里。'
        },{
        demo_link: 'http://gaohaoyang.github.io/test/headerTransition/',
        img_link: 'https://ooo.0o0.ooo/2016/06/20/5768c1597d1fe.png',
        code_link: 'https://github.com/Gaohaoyang/test/tree/master/headerTransition',
        title: 'Header Transition 渐变动画',
        core_tech: 'jQuery BootStrap CSS3',
        description: '花费不到半小时帮师兄做了一个简单的 CSS3 动画效果，当页面滚动到指定距离时，header 区的背景色由透明变为蓝色。仿照了网站 https://quorrajs.org/ 的 Header 区动画效果。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/mask-fade-out/',
        img_link: 'http://7q5cdt.com1.z0.glb.clouddn.com/demo-fade-out.png',
        code_link: 'https://github.com/Gaohaoyang/mask-fade-out',
        title: '遮罩层按指定路径缩小消失',
        core_tech: 'jQuery CSS',
        description: '使用 animate 方法，做到兼容 IE8。曾在联想服务官网上线3个月。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/ToDo-WebApp/',
        img_link: 'http://7q5cdt.com1.z0.glb.clouddn.com/blog-todoWebApp.png',
        code_link: 'https://github.com/Gaohaoyang/ToDo-WebApp',
        title: '百度前端学院 task0004 ToDo 应用(移动端)',
        core_tech: 'JavaScript LocalStorage requireJS Sass Gulp XSS',
        description: '在任务三中，做了一个 PC 端的 ToDo 应用。任务四是将它优化，以适应移动端设备。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/baidu-ife-practice/task0003/',
        img_link: 'http://7q5cdt.com1.z0.glb.clouddn.com/demo-todo.png',
        code_link: 'https://github.com/Gaohaoyang/baidu-ife-practice/tree/master/task0003',
        title: '百度前端学院 task0003 ToDo 应用',
        core_tech: 'JavaScript LocalStorage',
        description: '任务三，ToDo 单页应用，主要使用了 LocalStorage 存储数据，使用 JSON 模拟了 3 张数据表。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/ife/task/task0002/work/Gaohaoyang/task0002_5.html',
        img_link: 'http://7q5cdt.com1.z0.glb.clouddn.com/demo-drag.png',
        code_link: 'https://github.com/Gaohaoyang/ife/tree/master/task/task0002/work/Gaohaoyang',
        title: '拖拽交互',
        core_tech: 'JavaScript',
        description: '对鼠标事件应用，以及判断定位的方法等。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/ife/task/task0002/work/Gaohaoyang/task0002_4.html',
        img_link: 'http://ww2.sinaimg.cn/large/7011d6cfjw1f3ba2krzs0j207005y0sv.jpg',
        code_link: 'https://github.com/Gaohaoyang/ife/tree/master/task/task0002/work/Gaohaoyang',
        title: '输入框即时提示',
        core_tech: 'JavaScript',
        description: '对input监听，使用正在表达式高亮匹配，实现输入联想效果。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/ife/task/task0002/work/Gaohaoyang/task0002_3.html',
        img_link: 'http://ww2.sinaimg.cn/large/7011d6cfjw1f3ba04okoqj20eq093wh1.jpg',
        code_link: 'https://github.com/Gaohaoyang/ife/tree/master/task/task0002/work/Gaohaoyang',
        title: '轮播图',
        core_tech: 'JavaScript',
        description: '变速运动，运动终止条件的应用。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/ife/task/task0002/work/Gaohaoyang/task0002_2.html',
        img_link: 'http://ww4.sinaimg.cn/large/7011d6cfjw1f3b9w6xpz5j20ae02pgm3.jpg',
        code_link: 'https://github.com/Gaohaoyang/ife/tree/master/task/task0002/work/Gaohaoyang',
        title: '倒计时',
        core_tech: 'JavaScript',
        description: 'setInterval()，Date 对象的学习和使用。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/ife/task/task0002/work/Gaohaoyang/task0002_1.html',
        img_link: 'http://ww3.sinaimg.cn/large/7011d6cfjw1f3b9tmyuh2j20au0aaaar.jpg',
        code_link: 'https://github.com/Gaohaoyang/ife/tree/master/task/task0002/work/Gaohaoyang',
        title: '处理兴趣爱好列表',
        core_tech: 'JavaScript',
        description: '对JavaScript正则表达式和字符串的练习。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/ife/task/task0002/work/Gaohaoyang/index.html',
        img_link: 'http://7q5cdt.com1.z0.glb.clouddn.com/demo-demo-index.png',
        code_link: 'https://github.com/Gaohaoyang/ife/tree/master/task/task0002/work/Gaohaoyang',
        title: '百度前端学院 task0002 展示主页',
        core_tech: 'HTML CSS',
        description: '任务二的展示主页，里面包含五个小 demo。还有一篇相关的日志。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/ife/task/task0001/work/Gaohaoyang/index.html',
        img_link: 'http://7q5cdt.com1.z0.glb.clouddn.com/Demo-blog-ife.jpg',
        code_link: 'https://github.com/Gaohaoyang/ife/tree/master/task/task0001/work/Gaohaoyang',
        title: '百度前端学院 task0001 个人博客',
        core_tech: 'HTML CSS',
        description: '完成百度前端学院的任务。深刻的理解了BFC、浮动、inline-block间距，多列布局等技术。还有一篇相关的日志。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/ghost-button-css3/',
        img_link: 'http://7q5cdt.com1.z0.glb.clouddn.com/DemoGhost-Button.png',
        code_link: 'https://github.com/Gaohaoyang/ghost-button-css3',
        title: 'Ghost Button 幽灵按钮',
        core_tech: 'CSS3',
        description: '使用 CSS3 中的旋转、缩放、过渡、动画等效果，制作出很酷的按钮效果。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/shadow-demo-css3',
        img_link: 'http://7q5cdt.com1.z0.glb.clouddn.com/Demoshadow.png',
        code_link: 'https://github.com/Gaohaoyang/shadow-demo-css3',
        title: 'CSS3 阴影特效',
        core_tech: 'CSS3',
        description: 'CSS3 中的阴影、旋转等效果，制作出曲边阴影和翘边阴影。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/learning-AngularJS/2-3-bookstore-add-sth-by-myself/',
        img_link: 'http://7q5cdt.com1.z0.glb.clouddn.com/DemoAngularJS.png',
        code_link: 'https://github.com/Gaohaoyang/learning-AngularJS/tree/master/2-3-bookstore-add-sth-by-myself',
        title: 'AngularJS 结合动画效果',
        core_tech: 'AngularJS CSS3',
        description: '使用 AngularJS 中的 ngAnimate 加 CSS3里面的一些旋转效果，做出页面切换的效果。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/learning-AngularJS/2-4-UiRouterPractice',
        img_link: 'http://ww2.sinaimg.cn/large/7011d6cfjw1f3b8zumfqij20q40nh76x.jpg',
        code_link: 'https://github.com/Gaohaoyang/learning-AngularJS/tree/master/2-4-UiRouterPractice',
        title: 'AngularJS UI-router 练习',
        core_tech: 'AngularJS UI-router',
        description: 'UI-router 作为 AngularJS 中路由的扩充，实现了多级路由的效果。使得前端界面跳转更加方便。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/test/bootstrap-zhihu/',
        img_link: 'http://7q5cdt.com1.z0.glb.clouddn.com/teach-girlfriend-html-CopyZhihu.jpg',
        code_link: 'https://github.com/Gaohaoyang/test/tree/master/bootstrap-zhihu',
        title: '仿知乎页面',
        core_tech: 'HTML BootStrap',
        description: '使用BootStrap仿照知乎做了一个静态页面。'
    }];

    contentInit(demoContent) //内容初始化
    waitImgsLoad() //等待图片加载，并执行布局初始化
}());

/**
 * 内容初始化
 * @return {[type]} [description]
 */
function contentInit(content) {
    // var htmlArr = [];
    // for (var i = 0; i < content.length; i++) {
    //     htmlArr.push('
')
    //     htmlArr.push('')
    //     htmlArr.push('')
    //     htmlArr.push('')
    //     htmlArr.push('
')
    //     htmlArr.push(''+content[i].title+'')
    //     htmlArr.push('
')
    //     htmlArr.push('
主要技术：'+content[i].core_tech+'
')
    //     htmlArr.push('
'+content[i].description)
    //     htmlArr.push('源代码 ')
    //     htmlArr.push('
')
    //     htmlArr.push('
')
    // }
    // var htmlStr = htmlArr.join('')
    var htmlStr = ''
    for (var i = 0; i < content.length; i++) {
        htmlStr +=
            '
' +
            '   ' +
            '       ' +
            '   ' +
            '
' +
            '       ' + content[i].title + '' +
            '
' +
            '
主要技术：' + content[i].core_tech + '
' +
            '
' + content[i].description +
            '       源代码 ' +
            '
' +
            '
'
    }
    var grid = document.querySelector('.grid')
    grid.insertAdjacentHTML('afterbegin', htmlStr)
}

/**
 * 等待图片加载
 * @return {[type]} [description]
 */
function waitImgsLoad() {
    var imgs = document.querySelectorAll('.grid img')
    var totalImgs = imgs.length
    var count = 0
        //console.log(imgs)
    for (var i = 0; i < totalImgs; i++) {
        if (imgs[i].complete) {
            //console.log('complete');
            count++
        } else {
            imgs[i].onload = function() {
                // alert('onload')
                count++
                //console.log('onload' + count)
                if (count == totalImgs) {
                    //console.log('onload---bbbbbbbb')
                    initGrid()
                }
            }
        }
    }
    if (count == totalImgs) {
        //console.log('---bbbbbbbb')
        initGrid()
    }
}

/**
 * 初始化栅格布局
 * @return {[type]} [description]
 */
function initGrid() {
    var msnry = new Masonry('.grid', {
        // options
        itemSelector: '.grid-item',
        columnWidth: 250,
        isFitWidth: true,
        gutter: 20,
    })
}
<commit_msg>demo1 updata<commit_after>/* jshint asi:true */
//先等图片都加载完成
//再执行布局函数

/**
 * 执行主函数
 * @param  {[type]} function( [description]
 * @return {[type]}           [description]
 */
(function() {

    /**
     * 内容JSON
     */
    var demoContent = [
        {
            demo_link: 'https://create.arduino.cc/projecthub/otto/otto-build-your-own-robot-in-two-hours-5f2a1c',
            img_link: 'https://hackster.imgix.net/uploads/image/file/152711/FL32WPLIO8U35NB.LARGE.jpg?w=680&h=510&fit=max&fm=jpg',
            code_link: 'https://create.arduino.cc/projecthub/otto/otto-build-your-own-robot-in-two-hours-5f2a1c',
            title: 'Otto 做你自己的小机器人',
            core_tech: 'Arduino',
            description: 'Otto小机器人制作教程，详见 这里。'
        },{
        demo_link: 'http://gaohaoyang.github.io/test/headerTransition/',
        img_link: 'https://ooo.0o0.ooo/2016/06/20/5768c1597d1fe.png',
        code_link: 'https://github.com/Gaohaoyang/test/tree/master/headerTransition',
        title: 'Header Transition 渐变动画',
        core_tech: 'jQuery BootStrap CSS3',
        description: '花费不到半小时帮师兄做了一个简单的 CSS3 动画效果，当页面滚动到指定距离时，header 区的背景色由透明变为蓝色。仿照了网站 https://quorrajs.org/ 的 Header 区动画效果。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/mask-fade-out/',
        img_link: 'http://7q5cdt.com1.z0.glb.clouddn.com/demo-fade-out.png',
        code_link: 'https://github.com/Gaohaoyang/mask-fade-out',
        title: '遮罩层按指定路径缩小消失',
        core_tech: 'jQuery CSS',
        description: '使用 animate 方法，做到兼容 IE8。曾在联想服务官网上线3个月。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/ToDo-WebApp/',
        img_link: 'http://7q5cdt.com1.z0.glb.clouddn.com/blog-todoWebApp.png',
        code_link: 'https://github.com/Gaohaoyang/ToDo-WebApp',
        title: '百度前端学院 task0004 ToDo 应用(移动端)',
        core_tech: 'JavaScript LocalStorage requireJS Sass Gulp XSS',
        description: '在任务三中，做了一个 PC 端的 ToDo 应用。任务四是将它优化，以适应移动端设备。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/baidu-ife-practice/task0003/',
        img_link: 'http://7q5cdt.com1.z0.glb.clouddn.com/demo-todo.png',
        code_link: 'https://github.com/Gaohaoyang/baidu-ife-practice/tree/master/task0003',
        title: '百度前端学院 task0003 ToDo 应用',
        core_tech: 'JavaScript LocalStorage',
        description: '任务三，ToDo 单页应用，主要使用了 LocalStorage 存储数据，使用 JSON 模拟了 3 张数据表。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/ife/task/task0002/work/Gaohaoyang/task0002_5.html',
        img_link: 'http://7q5cdt.com1.z0.glb.clouddn.com/demo-drag.png',
        code_link: 'https://github.com/Gaohaoyang/ife/tree/master/task/task0002/work/Gaohaoyang',
        title: '拖拽交互',
        core_tech: 'JavaScript',
        description: '对鼠标事件应用，以及判断定位的方法等。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/ife/task/task0002/work/Gaohaoyang/task0002_4.html',
        img_link: 'http://ww2.sinaimg.cn/large/7011d6cfjw1f3ba2krzs0j207005y0sv.jpg',
        code_link: 'https://github.com/Gaohaoyang/ife/tree/master/task/task0002/work/Gaohaoyang',
        title: '输入框即时提示',
        core_tech: 'JavaScript',
        description: '对input监听，使用正在表达式高亮匹配，实现输入联想效果。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/ife/task/task0002/work/Gaohaoyang/task0002_3.html',
        img_link: 'http://ww2.sinaimg.cn/large/7011d6cfjw1f3ba04okoqj20eq093wh1.jpg',
        code_link: 'https://github.com/Gaohaoyang/ife/tree/master/task/task0002/work/Gaohaoyang',
        title: '轮播图',
        core_tech: 'JavaScript',
        description: '变速运动，运动终止条件的应用。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/ife/task/task0002/work/Gaohaoyang/task0002_2.html',
        img_link: 'http://ww4.sinaimg.cn/large/7011d6cfjw1f3b9w6xpz5j20ae02pgm3.jpg',
        code_link: 'https://github.com/Gaohaoyang/ife/tree/master/task/task0002/work/Gaohaoyang',
        title: '倒计时',
        core_tech: 'JavaScript',
        description: 'setInterval()，Date 对象的学习和使用。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/ife/task/task0002/work/Gaohaoyang/task0002_1.html',
        img_link: 'http://ww3.sinaimg.cn/large/7011d6cfjw1f3b9tmyuh2j20au0aaaar.jpg',
        code_link: 'https://github.com/Gaohaoyang/ife/tree/master/task/task0002/work/Gaohaoyang',
        title: '处理兴趣爱好列表',
        core_tech: 'JavaScript',
        description: '对JavaScript正则表达式和字符串的练习。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/ife/task/task0002/work/Gaohaoyang/index.html',
        img_link: 'http://7q5cdt.com1.z0.glb.clouddn.com/demo-demo-index.png',
        code_link: 'https://github.com/Gaohaoyang/ife/tree/master/task/task0002/work/Gaohaoyang',
        title: '百度前端学院 task0002 展示主页',
        core_tech: 'HTML CSS',
        description: '任务二的展示主页，里面包含五个小 demo。还有一篇相关的日志。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/ife/task/task0001/work/Gaohaoyang/index.html',
        img_link: 'http://7q5cdt.com1.z0.glb.clouddn.com/Demo-blog-ife.jpg',
        code_link: 'https://github.com/Gaohaoyang/ife/tree/master/task/task0001/work/Gaohaoyang',
        title: '百度前端学院 task0001 个人博客',
        core_tech: 'HTML CSS',
        description: '完成百度前端学院的任务。深刻的理解了BFC、浮动、inline-block间距，多列布局等技术。还有一篇相关的日志。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/ghost-button-css3/',
        img_link: 'http://7q5cdt.com1.z0.glb.clouddn.com/DemoGhost-Button.png',
        code_link: 'https://github.com/Gaohaoyang/ghost-button-css3',
        title: 'Ghost Button 幽灵按钮',
        core_tech: 'CSS3',
        description: '使用 CSS3 中的旋转、缩放、过渡、动画等效果，制作出很酷的按钮效果。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/shadow-demo-css3',
        img_link: 'http://7q5cdt.com1.z0.glb.clouddn.com/Demoshadow.png',
        code_link: 'https://github.com/Gaohaoyang/shadow-demo-css3',
        title: 'CSS3 阴影特效',
        core_tech: 'CSS3',
        description: 'CSS3 中的阴影、旋转等效果，制作出曲边阴影和翘边阴影。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/learning-AngularJS/2-3-bookstore-add-sth-by-myself/',
        img_link: 'http://7q5cdt.com1.z0.glb.clouddn.com/DemoAngularJS.png',
        code_link: 'https://github.com/Gaohaoyang/learning-AngularJS/tree/master/2-3-bookstore-add-sth-by-myself',
        title: 'AngularJS 结合动画效果',
        core_tech: 'AngularJS CSS3',
        description: '使用 AngularJS 中的 ngAnimate 加 CSS3里面的一些旋转效果，做出页面切换的效果。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/learning-AngularJS/2-4-UiRouterPractice',
        img_link: 'http://ww2.sinaimg.cn/large/7011d6cfjw1f3b8zumfqij20q40nh76x.jpg',
        code_link: 'https://github.com/Gaohaoyang/learning-AngularJS/tree/master/2-4-UiRouterPractice',
        title: 'AngularJS UI-router 练习',
        core_tech: 'AngularJS UI-router',
        description: 'UI-router 作为 AngularJS 中路由的扩充，实现了多级路由的效果。使得前端界面跳转更加方便。'
    }, {
        demo_link: 'http://gaohaoyang.github.io/test/bootstrap-zhihu/',
        img_link: 'http://7q5cdt.com1.z0.glb.clouddn.com/teach-girlfriend-html-CopyZhihu.jpg',
        code_link: 'https://github.com/Gaohaoyang/test/tree/master/bootstrap-zhihu',
        title: '仿知乎页面',
        core_tech: 'HTML BootStrap',
        description: '使用BootStrap仿照知乎做了一个静态页面。'
    }];

    contentInit(demoContent) //内容初始化
    waitImgsLoad() //等待图片加载，并执行布局初始化
}());

/**
 * 内容初始化
 * @return {[type]} [description]
 */
function contentInit(content) {
    // var htmlArr = [];
    // for (var i = 0; i < content.length; i++) {
    //     htmlArr.push('
')
    //     htmlArr.push('')
    //     htmlArr.push('')
    //     htmlArr.push('')
    //     htmlArr.push('
')
    //     htmlArr.push(''+content[i].title+'')
    //     htmlArr.push('
')
    //     htmlArr.push('
主要技术：'+content[i].core_tech+'
')
    //     htmlArr.push('
'+content[i].description)
    //     htmlArr.push('源代码 ')
    //     htmlArr.push('
')
    //     htmlArr.push('
')
    // }
    // var htmlStr = htmlArr.join('')
    var htmlStr = ''
    for (var i = 0; i < content.length; i++) {
        htmlStr +=
            '
' +
            '   ' +
            '       ' +
            '   ' +
            '
' +
            '       ' + content[i].title + '' +
            '
' +
            '
主要技术：' + content[i].core_tech + '
' +
            '
' + content[i].description +
            '       源代码 ' +
            '
' +
            '
'
    }
    var grid = document.querySelector('.grid')
    grid.insertAdjacentHTML('afterbegin', htmlStr)
}

/**
 * 等待图片加载
 * @return {[type]} [description]
 */
function waitImgsLoad() {
    var imgs = document.querySelectorAll('.grid img')
    var totalImgs = imgs.length
    var count = 0
        //console.log(imgs)
    for (var i = 0; i < totalImgs; i++) {
        if (imgs[i].complete) {
            //console.log('complete');
            count++
        } else {
            imgs[i].onload = function() {
                // alert('onload')
                count++
                //console.log('onload' + count)
                if (count == totalImgs) {
                    //console.log('onload---bbbbbbbb')
                    initGrid()
                }
            }
        }
    }
    if (count == totalImgs) {
        //console.log('---bbbbbbbb')
        initGrid()
    }
}

/**
 * 初始化栅格布局
 * @return {[type]} [description]
 */
function initGrid() {
    var msnry = new Masonry('.grid', {
        // options
        itemSelector: '.grid-item',
        columnWidth: 250,
        isFitWidth: true,
        gutter: 20,
    })
}
<|endoftext|>


<commit_before>const fs = require('fs')
const argv = require('yargs').argv

writeConfig()

module.exports = function(config) {
  config.set({

    // base path, that will be used to resolve files and exclude
    basePath: '',

    // frameworks to use
    frameworks: [ 'jasmine' ],

    // list of files / patterns to load in the browser
    files: [

      // dependencies for main lib AND plugin
      'node_modules/superagent/superagent.js',
      'node_modules/moment/moment.js',
      'node_modules/moment/locales/es.js', // only spanish for testing
      'node_modules/moment-timezone/builds/moment-timezone-with-data.js',
      'node_modules/rrule/dist/es5/rrule.js',
      'node_modules/jquery/dist/jquery.js',
      'node_modules/components-jqueryui/jquery-ui.js',
      'node_modules/components-jqueryui/themes/cupertino/jquery-ui.css',

      // dependencies for tests
      'node_modules/native-promise-only/lib/npo.src.js', // Promises needed by xhr-mock
      'node_modules/xhr-mock/dist/xhr-mock.js', // TODO: should include this via require(), but .d.ts problems
      'node_modules/jasmine-jquery/lib/jasmine-jquery.js',
      'node_modules/jquery-simulate/jquery.simulate.js',

      // main lib AND plugin files
      'dist/fullcalendar.js',
      'dist/fullcalendar.css',
      'dist/plugins/gcal.js',
      'dist/plugins/moment.js',
      'dist/plugins/moment-timezone.js',
      'dist/plugins/rrule.js',
      'dist/locales-all.js',

      // COMMENT-OUT THESE LINES when testing IE11
      'node_modules/luxon/build/global/luxon.js',
      'dist/fullcalendar-luxon.js',

      // a way to dump variables into the test environment
      'tmp/automated-test-config.js',

      // so plugins can dump files into here and test side effects
      'tmp/test-side-effects/*.js',

      'tests/automated/base.css',
      'tmp/automated-tests.js',

      { // serve all other files
        pattern: '**/*',
        included: false, // don't immediately execute
        nocache: true, // don't let the webserver cache
        watched: false // don't let changes trigger tests to restart
      }
    ],

    preprocessors: {
      '**/*.js': [ 'sourcemap' ]
    },

    // test results reporter to use
    // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage', 'verbose'
    reporters: [ 'dots' ],

    // web server port
    port: 9876,

    // enable / disable colors in the output (reporters and logs)
    colors: true,

    // level of logging
    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
    logLevel: config.LOG_INFO,

    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: true,

    // If browser does not capture in given timeout [ms], kill it
    captureTimeout: 60000,

    customLaunchers: {
      ChromeHeadless_custom: {
        base: 'ChromeHeadless',
        flags: [
          '--no-sandbox', // needed for TravisCI: https://docs.travis-ci.com/user/chrome#Sandboxing
          '--window-size=1280,1696' // some tests only work with larger window (w?, h?)
        ]
      }
    }
  })
}

function writeConfig() {
  let config = {
    isTravis: ('travis' in argv)
  }

  if (!fs.existsSync('tmp')) {
    fs.mkdirSync('tmp')
  }
  fs.writeFileSync(
    'tmp/automated-test-config.js',
    'window.karmaConfig = ' + JSON.stringify(config),
    { encoding: 'utf8' }
  )
}
<commit_msg>fix some rename problems<commit_after>const fs = require('fs')
const argv = require('yargs').argv

writeConfig()

module.exports = function(config) {
  config.set({

    // base path, that will be used to resolve files and exclude
    basePath: '',

    // frameworks to use
    frameworks: [ 'jasmine' ],

    // list of files / patterns to load in the browser
    files: [

      // dependencies for main lib AND plugin
      'node_modules/superagent/superagent.js',
      'node_modules/moment/moment.js',
      'node_modules/moment/locale/es.js', // only spanish for testing
      'node_modules/moment-timezone/builds/moment-timezone-with-data.js',
      'node_modules/rrule/dist/es5/rrule.js',
      'node_modules/jquery/dist/jquery.js',
      'node_modules/components-jqueryui/jquery-ui.js',
      'node_modules/components-jqueryui/themes/cupertino/jquery-ui.css',

      // dependencies for tests
      'node_modules/native-promise-only/lib/npo.src.js', // Promises needed by xhr-mock
      'node_modules/xhr-mock/dist/xhr-mock.js', // TODO: should include this via require(), but .d.ts problems
      'node_modules/jasmine-jquery/lib/jasmine-jquery.js',
      'node_modules/jquery-simulate/jquery.simulate.js',

      // main lib AND plugin files
      'dist/fullcalendar.js',
      'dist/fullcalendar.css',
      'dist/plugins/gcal.js',
      'dist/plugins/moment.js',
      'dist/plugins/moment-timezone.js',
      'dist/plugins/rrule.js',
      'dist/locales-all.js',

      // COMMENT-OUT THESE LINES when testing IE11
      'node_modules/luxon/build/global/luxon.js',
      'dist/plugins/luxon.js',

      // a way to dump variables into the test environment
      'tmp/automated-test-config.js',

      // so plugins can dump files into here and test side effects
      'tmp/test-side-effects/*.js',

      'tests/automated/base.css',
      'tmp/automated-tests.js',

      { // serve all other files
        pattern: '**/*',
        included: false, // don't immediately execute
        nocache: true, // don't let the webserver cache
        watched: false // don't let changes trigger tests to restart
      }
    ],

    preprocessors: {
      '**/*.js': [ 'sourcemap' ]
    },

    // test results reporter to use
    // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage', 'verbose'
    reporters: [ 'dots' ],

    // web server port
    port: 9876,

    // enable / disable colors in the output (reporters and logs)
    colors: true,

    // level of logging
    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
    logLevel: config.LOG_INFO,

    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: true,

    // If browser does not capture in given timeout [ms], kill it
    captureTimeout: 60000,

    customLaunchers: {
      ChromeHeadless_custom: {
        base: 'ChromeHeadless',
        flags: [
          '--no-sandbox', // needed for TravisCI: https://docs.travis-ci.com/user/chrome#Sandboxing
          '--window-size=1280,1696' // some tests only work with larger window (w?, h?)
        ]
      }
    }
  })
}

function writeConfig() {
  let config = {
    isTravis: ('travis' in argv)
  }

  if (!fs.existsSync('tmp')) {
    fs.mkdirSync('tmp')
  }
  fs.writeFileSync(
    'tmp/automated-test-config.js',
    'window.karmaConfig = ' + JSON.stringify(config),
    { encoding: 'utf8' }
  )
}
<|endoftext|>


<commit_before>import fs from 'fs-extra';
import path from 'path';
import {taffy} from 'taffydb';
import IceCap from 'ice-cap';
import StaticFileBuilder from './Builder/StaticFileBuilder.js';
import IdentifiersDocBuilder from './Builder/IdentifiersDocBuilder.js';
import IndexDocBuilder from './Builder/IndexDocBuilder.js';
import ClassDocBuilder from './Builder/ClassDocBuilder.js';
import SingleDocBuilder from './Builder/SingleDocBuilder.js';
import FileDocBuilder from './Builder/FileDocBuilder.js';
import SearchIndexBuilder from './Builder/SearchIndexBuilder.js';
import CoverageBuilder from './Builder/CoverageBuilder.js';
import ASTDocBuilder from './Builder/ASTDocBuilder.js';
import SourceDocBuilder from './Builder/SourceDocBuilder.js';
import TestDocBuilder from './Builder/TestDocBuilder.js';
import TestFileDocBuilder from './Builder/TestFileDocBuilder.js';

export default function publish(values, asts, config) {
  IceCap.debug = !!config.debug;

  let dumpPath = path.resolve(config.destination, 'dump.json');
  fs.outputFileSync(dumpPath, JSON.stringify(values, null, 2));

  let data = taffy(values);
  let _coverage = null;

  function log(text) {
    console.log(text);
  }

  function writeHTML(html, fileName) {
    log(fileName);
    let filePath = path.resolve(config.destination, fileName);
    fs.outputFileSync(filePath, html, {encoding: 'utf8'});
  }

  function writeCoverage(coverage, fileName) {
    _coverage = coverage;
    let json = JSON.stringify(coverage, null, 2);
    let filePath = path.resolve(config.destination, fileName);
    fs.outputFileSync(filePath, json, {encoding: 'utf8'});
  }

  function writeAST(astJSON, fileName) {
    let filePath = path.resolve(config.destination, fileName);
    fs.outputFileSync(filePath, astJSON, {encoding: 'utf8'});
  }

  function copy(srcPath, destPath) {
    log(destPath);
    fs.copySync(srcPath, path.resolve(config.destination, destPath));
  }

  if (config.coverage) {
    new CoverageBuilder(data, config).exec(writeCoverage);
  }

  new IdentifiersDocBuilder(data, config).exec(writeHTML);
  new IndexDocBuilder(data, config, _coverage).exec(writeHTML);
  new ClassDocBuilder(data, config).exec(writeHTML);
  new SingleDocBuilder(data, config).exec(writeHTML);
  new FileDocBuilder(data, config).exec(writeHTML);
  new StaticFileBuilder(data, config).exec(copy);
  new SearchIndexBuilder(data, config).exec(writeHTML);
  new ASTDocBuilder(data, asts, config).exec(writeAST);
  new SourceDocBuilder(data, config, _coverage).exec(writeHTML);

  if (config.test) {
    new TestDocBuilder(data, config).exec(writeHTML);
    new TestFileDocBuilder(data, config).exec(writeHTML);
  }

  if (config.coverage) {
    console.log('==================================');
    console.log(`Coverage: ${_coverage.coverage} (${_coverage.actualCount}/${_coverage.expectCount})`);
    console.log('==================================');
  }
};
<commit_msg>doc: write doc comment.<commit_after>import fs from 'fs-extra';
import path from 'path';
import {taffy} from 'taffydb';
import IceCap from 'ice-cap';
import StaticFileBuilder from './Builder/StaticFileBuilder.js';
import IdentifiersDocBuilder from './Builder/IdentifiersDocBuilder.js';
import IndexDocBuilder from './Builder/IndexDocBuilder.js';
import ClassDocBuilder from './Builder/ClassDocBuilder.js';
import SingleDocBuilder from './Builder/SingleDocBuilder.js';
import FileDocBuilder from './Builder/FileDocBuilder.js';
import SearchIndexBuilder from './Builder/SearchIndexBuilder.js';
import CoverageBuilder from './Builder/CoverageBuilder.js';
import ASTDocBuilder from './Builder/ASTDocBuilder.js';
import SourceDocBuilder from './Builder/SourceDocBuilder.js';
import TestDocBuilder from './Builder/TestDocBuilder.js';
import TestFileDocBuilder from './Builder/TestFileDocBuilder.js';

/**
 * publish document as HTML.
 * @param {DocObject[]} values - all doc objects.
 * @param {AST[]} asts - all ASTs.
 * @param {ESDocConfig} config - ESDoc config object.
 */
export default function publish(values, asts, config) {
  IceCap.debug = !!config.debug;

  let dumpPath = path.resolve(config.destination, 'dump.json');
  fs.outputFileSync(dumpPath, JSON.stringify(values, null, 2));

  let data = taffy(values);
  let _coverage = null;

  function log(text) {
    console.log(text);
  }

  function writeHTML(html, fileName) {
    log(fileName);
    let filePath = path.resolve(config.destination, fileName);
    fs.outputFileSync(filePath, html, {encoding: 'utf8'});
  }

  function writeCoverage(coverage, fileName) {
    _coverage = coverage;
    let json = JSON.stringify(coverage, null, 2);
    let filePath = path.resolve(config.destination, fileName);
    fs.outputFileSync(filePath, json, {encoding: 'utf8'});
  }

  function writeAST(astJSON, fileName) {
    let filePath = path.resolve(config.destination, fileName);
    fs.outputFileSync(filePath, astJSON, {encoding: 'utf8'});
  }

  function copy(srcPath, destPath) {
    log(destPath);
    fs.copySync(srcPath, path.resolve(config.destination, destPath));
  }

  if (config.coverage) {
    new CoverageBuilder(data, config).exec(writeCoverage);
  }

  new IdentifiersDocBuilder(data, config).exec(writeHTML);
  new IndexDocBuilder(data, config, _coverage).exec(writeHTML);
  new ClassDocBuilder(data, config).exec(writeHTML);
  new SingleDocBuilder(data, config).exec(writeHTML);
  new FileDocBuilder(data, config).exec(writeHTML);
  new StaticFileBuilder(data, config).exec(copy);
  new SearchIndexBuilder(data, config).exec(writeHTML);
  new ASTDocBuilder(data, asts, config).exec(writeAST);
  new SourceDocBuilder(data, config, _coverage).exec(writeHTML);

  if (config.test) {
    new TestDocBuilder(data, config).exec(writeHTML);
    new TestFileDocBuilder(data, config).exec(writeHTML);
  }

  if (config.coverage) {
    console.log('==================================');
    console.log(`Coverage: ${_coverage.coverage} (${_coverage.actualCount}/${_coverage.expectCount})`);
    console.log('==================================');
  }
};
<|endoftext|>


<commit_before>'use strict';

import React from 'react';
import ReactDOM from 'react-dom';
import ResizeSensor from './vendor/ResizeSensor/ResizeSensor.js';

import {shouldComponentUpdate} from 'react/lib/ReactComponentWithPureRenderMixin';

class SizeReporter extends React.Component {
  static propTypes = {
    children: React.PropTypes.node.isRequired,
    onSizeChange: React.PropTypes.func.isRequired
  }

  constructor(props){
    super(props);
    this.ResizeSensor = null;
    this.offsetHeight = null;
    this.offsetWidth = null;
  }

  componentDidMount(){
    this.node = ReactDOM.findDOMNode(this);
    this.attachListener();
    this.props.onSizeChange({height: this.node.offsetHeight, width: this.node.offsetWidth});
  }

  shouldComponentUpdate = shouldComponentUpdate

  componentWillUnmount(){
    if (this.ResizeSensor){
      this.ResizeSensor.detach();
    }
  }

  componentDidUpdate(){
    if (
      this.node &&
      (
        this.node.offsetHeight !== this.offsetHeight ||
        this.node.offsetWidth !== this.offsetWidth
      )
    ){
      this.props.onSizeChange({height: this.node.offsetHeight, width: this.node.offsetWidth});
    }
  }

  attachListener = () => {
    if (this.node){
      this.ResizeSensor = new ResizeSensor(
        this.node,
        () => this.props.onSizeChange({height: this.node.offsetHeight, width: this.node.offsetWidth})
      );
    }
  }

  reattachResizeListener = () => {
    this.ResizeSensor.detach();
    this.attachListener();
  }

  render(){
    const { children, ...props } = this.props;

    if (this.node){
      this.offsetHeight = this.node.offsetHeight;
      this.offsetWidth = this.node.offsetWidth;
    }

    return (
      <div {...props}>
        {children || null}
      </div>
    )
  }
}

export default SizeReporter;
<commit_msg>fix unknown attribute<commit_after>'use strict';

import React from 'react';
import ReactDOM from 'react-dom';
import ResizeSensor from './vendor/ResizeSensor/ResizeSensor.js';

import {shouldComponentUpdate} from 'react/lib/ReactComponentWithPureRenderMixin';

class SizeReporter extends React.Component {
  static propTypes = {
    children: React.PropTypes.node.isRequired,
    onSizeChange: React.PropTypes.func.isRequired
  }

  constructor(props){
    super(props);
    this.ResizeSensor = null;
    this.offsetHeight = null;
    this.offsetWidth = null;
  }

  componentDidMount(){
    this.node = ReactDOM.findDOMNode(this);
    this.attachListener();
    this.props.onSizeChange({height: this.node.offsetHeight, width: this.node.offsetWidth});
  }

  shouldComponentUpdate = shouldComponentUpdate

  componentWillUnmount(){
    if (this.ResizeSensor){
      this.ResizeSensor.detach();
    }
  }

  componentDidUpdate(){
    if (
      this.node &&
      (
        this.node.offsetHeight !== this.offsetHeight ||
        this.node.offsetWidth !== this.offsetWidth
      )
    ){
      this.props.onSizeChange({height: this.node.offsetHeight, width: this.node.offsetWidth});
    }
  }

  attachListener = () => {
    if (this.node){
      this.ResizeSensor = new ResizeSensor(
        this.node,
        () => this.props.onSizeChange({height: this.node.offsetHeight, width: this.node.offsetWidth})
      );
    }
  }

  reattachResizeListener = () => {
    this.ResizeSensor.detach();
    this.attachListener();
  }

  render(){
    const { children, onSizeChange, ...props } = this.props;

    if (this.node){
      this.offsetHeight = this.node.offsetHeight;
      this.offsetWidth = this.node.offsetWidth;
    }

    return (
      <div {...props}>
        {children || null}
      </div>
    )
  }
}

export default SizeReporter;
<|endoftext|>


<commit_before>/*
 * BSD 3-Clause License
 *
 * Copyright (c) 2017, MapCreator
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 *  Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 *  Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import {camel as camelCase, pascal as pascalCase, snake as snakeCase} from 'case';
import {EventEmitter} from 'events';
import {DeletedState} from './enums';
import {hashObject} from './utils/hash';
import {getTypeName} from './utils/reflection';
import {encodeQueryString} from './utils/requests';

/**
 * Used for keeping track of the request parameters
 *
 * @fires RequestParameters#change
 * @fires RequestParameters#change:page
 * @fires RequestParameters#change:perPage
 * @fires RequestParameters#change:search
 * @fires RequestParameters#change:sort
 * @fires RequestParameters#change:deleted
 * @fires RequestParameters#change:extra
 */
export default class RequestParameters extends EventEmitter {
  /**
   * RequestParameters constructor
   * @param {Object} object - properties
   */
  constructor(object = {}) {
    super();

    // Apply defaults
    RequestParameters.keys().forEach(x => this._resolve(x));

    // Apply properties
    this.apply(object);
  }

  // region instance
  // region instance getters
  /**
   * Get page number
   * @returns {Number} - Page number
   * @throws TypeError
   */
  get page() {
    return this._resolve('page');
  }

  /**
   * Get rows per page
   * @returns {Number} - Per page
   * @throws TypeError
   */
  get perPage() {
    return this._resolve('perPage');
  }

  /**
   * Search query
   * @returns {Object<String, String|Array<String>>} - Query
   * @throws TypeError
   */
  get search() {
    return this._resolve('search');
  }

  /**
   * Get sort options
   * @returns {Array<String>} - Per page
   * @throws TypeError
   */
  get sort() {
    return this._resolve('sort');
  }

  /**
   * If deleted items should be shown
   * @returns {String} - Deleted items filter state
   * @see {@link DeletedState}
   */
  get deleted() {
    return this._resolve('deleted');
  }

  /**
   * Extra parameters
   * @returns {Object} - Extra parameters
   */
  get extra() {
    return this._resolve('extra');
  }

  // endregion instance getters

  // region instance setters
  /**
   * Page number
   * @param {Number} value - Page number
   */
  set page(value) {
    this._update('page', value);
  }

  /**
   * Rows per page number
   * @param {Number} value - Per page
   */
  set perPage(value) {
    this._update('perPage', value);
  }

  /**
   * Search query
   * @param {Object<String, String|Array<String>>} value - Search query
   */
  set search(value) {
    this._update('search', value);
  }

  /**
   * Sort query
   * @param {Array<String>} value - Sort query
   */
  set sort(value) {
    this._update('sort', value);
  }

  /**
   * Deleted items filter state
   * @param {String} value - Deleted items filter state
   * @see {@link DeletedState}
   */
  set deleted(value) {
    this._update('deleted', value);
  }

  /**
   * Extra request parameters
   * @param {Object} value - Extra request parameters
   */
  set extra(value) {
    this._update('extra', value);
  }

  // endregion instance setters
  // endregion instance

  // region static
  // region getters
  /**
   * Default page number
   * @returns {Number} - Page number
   */
  static get page() {
    return RequestParameters._page || 1;
  }

  /**
   * Default per page
   * @returns {Number} - Per page
   */
  static get perPage() {
    return RequestParameters._perPage || Number(process.env.PER_PAGE) || 12;
  }

  /**
   * Gets the maximum allowed value for perPage
   * Some users will have a special permission that allows them to fetch more than 50 resources at once
   * @returns {Number} - Maximum amount of resources per page
   */
  static get maxPerPage() {
    return RequestParameters._maxPerPage || 50;
  }

  /**
   * Default search query
   * @returns {Object<String, String|Array<String>>} - Search query
   */
  static get search() {
    return RequestParameters._search || {};
  }

  /**
   * Default sort query
   * @returns {Array<String>} - Sort query
   */
  static get sort() {
    return RequestParameters._sort || [];
  }

  /**
   * Default deleted items filter state
   * @returns {String} -  Deleted items filter state
   */
  static get deleted() {
    return RequestParameters._deleted || DeletedState.NONE;
  }

  /**
   * Default extra request parameters
   * @returns {Object} - Extra request parameters
   */
  static get extra() {
    return RequestParameters._extra || {};
  }

  // endregion getters

  // region setters
  /**
   * Default page number
   * @param {Number} value - Page number
   */
  static set page(value) {
    RequestParameters._page = RequestParameters._validatePage(value);
  }

  /**
   * Default per page
   * @param {Number} value - Per page
   */
  static set perPage(value) {
    RequestParameters._perPage = RequestParameters._validatePerPage(value);
  }

  /**
   * Sets the maximum allowed value for perPage
   * Some users will have a special permission that allows them to fetch more than 50 resources at once
   * @param {Number} value - Maximum amount of resources per page
   */
  static set maxPerPage(value) {
    RequestParameters._maxPerPage = RequestParameters._validateMaxPerPage(value);
  }

  /**
   * Default search query
   * @param {Object<String, String|Array<String>>} value - Search query
   */
  static set search(value) {
    RequestParameters._search = RequestParameters._validateSearch(value);
  }

  /**
   * Default sort query
   * @param {Array<String>} value - Sort query
   */
  static set sort(value) {
    RequestParameters._sort = RequestParameters._validateSort(value);
  }

  /**
   * Default deleted items filter state
   * @param {String} value -  Deleted items filter state
   */
  static set deleted(value) {
    RequestParameters._deleted = RequestParameters._validateDeleted(value);
  }

  /**
   * Default extra request parameters
   * @param {Object} value - Extra request parameters
   */
  static set extra(value) {
    RequestParameters._extra = RequestParameters._validateExtra(value);
  }

  // endregion setters
  // endregion static

  // region validators
  /**
   * Validators should work the same as laravel's ::validate method. This means
   * this means that they will throw a TypeError or return a normalized result.
   */

  static _validatePage(value) {
    if (typeof value !== 'number') {
      throw new TypeError(`Expected page to be of type 'number' instead got '${typeof value}'`);
    }

    return Math.round(value);
  }

  static _validatePerPage(value) {
    if (typeof value !== 'number') {
      throw new TypeError(`Expected page to be of type 'Number' instead got '${getTypeName(value)}'`);
    }

    value = Math.round(value);
    value = Math.min(RequestParameters.maxPerPage, value); // Upper limit is 50 by default
    value = Math.max(1, value); // Lower limit is 1

    return value;
  }

  static _validateMaxPerPage(value) {
    if (typeof value !== 'number') {
      throw new TypeError(`Expected page to be of type 'Number' instead got '${getTypeName(value)}'`);
    }

    if (value < 1) {
      throw new TypeError('Value must be greater or equal to 1');
    }

    return value;
  }

  static _validateSearch(value) {
    if (typeof value !== 'object' || Array.isArray(value)) {
      throw new TypeError(`Expected value to be of type "Object" got "${getTypeName(value)}"`);
    }

    // Normalization macro
    const normalize = x => typeof x === 'number' ? x.toString() : x;

    for (let key of Object.keys(value)) {
      key = normalize(key);
      value[key] = normalize(value[key]);

      if (typeof key !== 'string') {
        throw new TypeError(`Expected key to be of type "String" got "${getTypeName(key)}"`);
      }

      if (Array.isArray(value[key])) {
        if (value[key].length > 0) {
          for (const query of value[key]) {
            if (typeof query !== 'string') {
              throw new TypeError(`Expected query for "${key}" to be of type "String" got "${getTypeName(query)}"`);
            }
          }
        } else {
          // Drop empty nodes
          delete value[key];
        }
      } else if (value[key] === null) {
        delete value[key];
      } else if (typeof value[key] !== 'string') {
        throw new TypeError(`Expected query value to be of type "String" or "Array" got "${getTypeName(value[key])}"`);
      }
    }

    return value;
  }

  static _validateSort(value) {
    if (typeof value === 'string') {
      return this._validateSort(value.split(','));
    }

    if (!(value instanceof Array)) {
      throw new TypeError(`Expected sort value to be of type "Array" got "${getTypeName(value)}"`);
    }

    // Array keys type checking
    value
      .filter(x => typeof x !== 'string')
      .forEach(x => {
        throw new TypeError(`Expected sort array values to be of type "String" got "${getTypeName(x)}"`);
      });

    // Don't do regex matching because it's something
    // we can just let the server do for us.

    return value;
  }

  static _validateDeleted(value) {
    if (typeof value !== 'string') {
      throw new TypeError(`Expected deleted to be of type "string" got "${getTypeName(value)}". See: DeletedState`);
    }

    value = value.toLowerCase();

    const possible = DeletedState.values();

    if (!possible.includes(value)) {
      throw new TypeError(`Expected deleted to be one of ${possible.join(', ')}, got ${value}`);
    }

    return value;
  }

  static _validateExtra(value) {
    if (typeof value !== 'object') {
      throw new TypeError(`Expected extra to be of type 'object', got '${getTypeName(value)}'`);
    }

    return value;
  }

  // endregion validators

  _resolve(name) {
    const _name = '_' + name;

    if (!this[_name]) {
      // Confuse esdoc
      (this || {})[_name] = RequestParameters[name];
    }

    return this[_name];
  }

  _update(name, value) {
    const _name = '_' + name;

    value = RequestParameters['_validate' + pascalCase(name)](value);
    (this || {})[_name] = value; // Weird syntax confuses esdoc

    /**
     * Change event.
     *
     * @event RequestParameters#change
     * @type {object}
     * @property {string} name - Parameter name
     * @property {*} value - New value
     */
    this.emit('change', {name, value});
    this.emit('change:' + name, value);
  }

  // region utils
  /**
   * Urlencode parameters
   * @returns {string} - HTTP query
   */
  encode() {
    return encodeQueryString(this.toParameterObject());
  }

  /**
   * Convert to object
   * @returns {Object} - Object
   */
  toObject() {
    return RequestParameters
      .keys()
      .reduce((obj, key) => {
        obj[snakeCase(key)] = this._resolve(key);
        return obj;
      }, {});
  }

  /**
   * Convert to object
   * @returns {Object} - Object
   */
  toParameterObject() {
    const data = {};

    RequestParameters
      .keys()
      .forEach(key => {
        // Skip extra key
        if (key === 'extra') {
          return;
        }

        data[snakeCase(key)] = this._resolve(key);
      });

    // Fix column names for sort
    data.sort = data.sort.map(x =>
      snakeCase(x).replace(/^_/, '-'),
    ).join(',');

    // Fix column names for search
    for (const key of Object.keys(data.search)) {
      const snakeKey = key.split(',').map(snakeCase).join(',');

      if (key !== snakeKey) {
        data.search[snakeKey] = data.search[key];
        delete data.search[key];
      }
    }

    // Overwrite using extra properties
    const extra = this._resolve('extra');

    for (const key of Object.keys(extra)) {
      data[key] = extra[key];
    }

    return data;
  }

  /**
   * Copy object
   * @returns {RequestParameters} - Copy
   */
  copy() {
    return new RequestParameters(this.toObject());
  }

  /**
   * Different parameters
   * @returns {Array<String>} - keys
   */
  static keys() {
    // enumeration is disabled for properties
    return [
      'page',
      'perPage',
      'search',
      'sort',
      'deleted',
      'extra',
    ];
  }

  /**
   * Generates a cache token
   * @returns {string} - Cache token
   */
  token() {
    const data = this.toObject();

    delete data['page'];
    delete data['per_page'];

    return hashObject(data);
  }

  /**
   * Resets all parameters back to default
   * @returns {void}
   */
  static resetDefaults() {
    for (const key of RequestParameters.keys()) {
      delete RequestParameters['_' + key];
    }
  }

  /**
   * Apply parameters from object
   * @param {object|RequestParameters} params - parameters
   * @returns {void}
   */
  apply(params) {
    if (params instanceof RequestParameters) {
      params = params.toObject();
    }

    for (const key of Object.keys(params)) {
      const Key = camelCase(key);

      if (key[0] === '_' || !RequestParameters.keys().includes(Key)) {
        continue;
      }

      this._update(Key, params[key]);
    }
  }
  // endregion utils
}
<commit_msg>[API-682] Allow numeric values in search array<commit_after>/*
 * BSD 3-Clause License
 *
 * Copyright (c) 2017, MapCreator
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 *  Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 *  Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import {camel as camelCase, pascal as pascalCase, snake as snakeCase} from 'case';
import {EventEmitter} from 'events';
import {DeletedState} from './enums';
import {hashObject} from './utils/hash';
import {getTypeName} from './utils/reflection';
import {encodeQueryString} from './utils/requests';

/**
 * Used for keeping track of the request parameters
 *
 * @fires RequestParameters#change
 * @fires RequestParameters#change:page
 * @fires RequestParameters#change:perPage
 * @fires RequestParameters#change:search
 * @fires RequestParameters#change:sort
 * @fires RequestParameters#change:deleted
 * @fires RequestParameters#change:extra
 */
export default class RequestParameters extends EventEmitter {
  /**
   * RequestParameters constructor
   * @param {Object} object - properties
   */
  constructor(object = {}) {
    super();

    // Apply defaults
    RequestParameters.keys().forEach(x => this._resolve(x));

    // Apply properties
    this.apply(object);
  }

  // region instance
  // region instance getters
  /**
   * Get page number
   * @returns {Number} - Page number
   * @throws TypeError
   */
  get page() {
    return this._resolve('page');
  }

  /**
   * Get rows per page
   * @returns {Number} - Per page
   * @throws TypeError
   */
  get perPage() {
    return this._resolve('perPage');
  }

  /**
   * Search query
   * @returns {Object<String, String|Array<String>>} - Query
   * @throws TypeError
   */
  get search() {
    return this._resolve('search');
  }

  /**
   * Get sort options
   * @returns {Array<String>} - Per page
   * @throws TypeError
   */
  get sort() {
    return this._resolve('sort');
  }

  /**
   * If deleted items should be shown
   * @returns {String} - Deleted items filter state
   * @see {@link DeletedState}
   */
  get deleted() {
    return this._resolve('deleted');
  }

  /**
   * Extra parameters
   * @returns {Object} - Extra parameters
   */
  get extra() {
    return this._resolve('extra');
  }

  // endregion instance getters

  // region instance setters
  /**
   * Page number
   * @param {Number} value - Page number
   */
  set page(value) {
    this._update('page', value);
  }

  /**
   * Rows per page number
   * @param {Number} value - Per page
   */
  set perPage(value) {
    this._update('perPage', value);
  }

  /**
   * Search query
   * @param {Object<String, String|Array<String>>} value - Search query
   */
  set search(value) {
    this._update('search', value);
  }

  /**
   * Sort query
   * @param {Array<String>} value - Sort query
   */
  set sort(value) {
    this._update('sort', value);
  }

  /**
   * Deleted items filter state
   * @param {String} value - Deleted items filter state
   * @see {@link DeletedState}
   */
  set deleted(value) {
    this._update('deleted', value);
  }

  /**
   * Extra request parameters
   * @param {Object} value - Extra request parameters
   */
  set extra(value) {
    this._update('extra', value);
  }

  // endregion instance setters
  // endregion instance

  // region static
  // region getters
  /**
   * Default page number
   * @returns {Number} - Page number
   */
  static get page() {
    return RequestParameters._page || 1;
  }

  /**
   * Default per page
   * @returns {Number} - Per page
   */
  static get perPage() {
    return RequestParameters._perPage || Number(process.env.PER_PAGE) || 12;
  }

  /**
   * Gets the maximum allowed value for perPage
   * Some users will have a special permission that allows them to fetch more than 50 resources at once
   * @returns {Number} - Maximum amount of resources per page
   */
  static get maxPerPage() {
    return RequestParameters._maxPerPage || 50;
  }

  /**
   * Default search query
   * @returns {Object<String, String|Array<String>>} - Search query
   */
  static get search() {
    return RequestParameters._search || {};
  }

  /**
   * Default sort query
   * @returns {Array<String>} - Sort query
   */
  static get sort() {
    return RequestParameters._sort || [];
  }

  /**
   * Default deleted items filter state
   * @returns {String} -  Deleted items filter state
   */
  static get deleted() {
    return RequestParameters._deleted || DeletedState.NONE;
  }

  /**
   * Default extra request parameters
   * @returns {Object} - Extra request parameters
   */
  static get extra() {
    return RequestParameters._extra || {};
  }

  // endregion getters

  // region setters
  /**
   * Default page number
   * @param {Number} value - Page number
   */
  static set page(value) {
    RequestParameters._page = RequestParameters._validatePage(value);
  }

  /**
   * Default per page
   * @param {Number} value - Per page
   */
  static set perPage(value) {
    RequestParameters._perPage = RequestParameters._validatePerPage(value);
  }

  /**
   * Sets the maximum allowed value for perPage
   * Some users will have a special permission that allows them to fetch more than 50 resources at once
   * @param {Number} value - Maximum amount of resources per page
   */
  static set maxPerPage(value) {
    RequestParameters._maxPerPage = RequestParameters._validateMaxPerPage(value);
  }

  /**
   * Default search query
   * @param {Object<String, String|Array<String>>} value - Search query
   */
  static set search(value) {
    RequestParameters._search = RequestParameters._validateSearch(value);
  }

  /**
   * Default sort query
   * @param {Array<String>} value - Sort query
   */
  static set sort(value) {
    RequestParameters._sort = RequestParameters._validateSort(value);
  }

  /**
   * Default deleted items filter state
   * @param {String} value -  Deleted items filter state
   */
  static set deleted(value) {
    RequestParameters._deleted = RequestParameters._validateDeleted(value);
  }

  /**
   * Default extra request parameters
   * @param {Object} value - Extra request parameters
   */
  static set extra(value) {
    RequestParameters._extra = RequestParameters._validateExtra(value);
  }

  // endregion setters
  // endregion static

  // region validators
  /**
   * Validators should work the same as laravel's ::validate method. This means
   * this means that they will throw a TypeError or return a normalized result.
   */

  static _validatePage(value) {
    if (typeof value !== 'number') {
      throw new TypeError(`Expected page to be of type 'number' instead got '${typeof value}'`);
    }

    return Math.round(value);
  }

  static _validatePerPage(value) {
    if (typeof value !== 'number') {
      throw new TypeError(`Expected page to be of type 'Number' instead got '${getTypeName(value)}'`);
    }

    value = Math.round(value);
    value = Math.min(RequestParameters.maxPerPage, value); // Upper limit is 50 by default
    value = Math.max(1, value); // Lower limit is 1

    return value;
  }

  static _validateMaxPerPage(value) {
    if (typeof value !== 'number') {
      throw new TypeError(`Expected page to be of type 'Number' instead got '${getTypeName(value)}'`);
    }

    if (value < 1) {
      throw new TypeError('Value must be greater or equal to 1');
    }

    return value;
  }

  static _validateSearch(value) {
    if (typeof value !== 'object' || Array.isArray(value)) {
      throw new TypeError(`Expected value to be of type "Object" got "${getTypeName(value)}"`);
    }

    // Normalization macro
    const normalize = x => typeof x === 'number' ? x.toString() : x;

    for (let key of Object.keys(value)) {
      key = normalize(key);
      value[key] = normalize(value[key]);

      if (typeof key !== 'string') {
        throw new TypeError(`Expected key to be of type "String" got "${getTypeName(key)}"`);
      }

      if (Array.isArray(value[key])) {
        if (value[key].length > 0) {
          for (const query of value[key]) {
            if (typeof query !== 'string' && typeof query !== 'number') {
              throw new TypeError(`Expected query for "${key}" to be of type "String" or "Number" got "${getTypeName(query)}"`);
            }
          }
        } else {
          // Drop empty nodes
          delete value[key];
        }
      } else if (value[key] === null) {
        delete value[key];
      } else if (typeof value[key] !== 'string') {
        throw new TypeError(`Expected query value to be of type "String" or "Array" got "${getTypeName(value[key])}"`);
      }
    }

    return value;
  }

  static _validateSort(value) {
    if (typeof value === 'string') {
      return this._validateSort(value.split(','));
    }

    if (!(value instanceof Array)) {
      throw new TypeError(`Expected sort value to be of type "Array" got "${getTypeName(value)}"`);
    }

    // Array keys type checking
    value
      .filter(x => typeof x !== 'string')
      .forEach(x => {
        throw new TypeError(`Expected sort array values to be of type "String" got "${getTypeName(x)}"`);
      });

    // Don't do regex matching because it's something
    // we can just let the server do for us.

    return value;
  }

  static _validateDeleted(value) {
    if (typeof value !== 'string') {
      throw new TypeError(`Expected deleted to be of type "string" got "${getTypeName(value)}". See: DeletedState`);
    }

    value = value.toLowerCase();

    const possible = DeletedState.values();

    if (!possible.includes(value)) {
      throw new TypeError(`Expected deleted to be one of ${possible.join(', ')}, got ${value}`);
    }

    return value;
  }

  static _validateExtra(value) {
    if (typeof value !== 'object') {
      throw new TypeError(`Expected extra to be of type 'object', got '${getTypeName(value)}'`);
    }

    return value;
  }

  // endregion validators

  _resolve(name) {
    const _name = '_' + name;

    if (!this[_name]) {
      // Confuse esdoc
      (this || {})[_name] = RequestParameters[name];
    }

    return this[_name];
  }

  _update(name, value) {
    const _name = '_' + name;

    value = RequestParameters['_validate' + pascalCase(name)](value);
    (this || {})[_name] = value; // Weird syntax confuses esdoc

    /**
     * Change event.
     *
     * @event RequestParameters#change
     * @type {object}
     * @property {string} name - Parameter name
     * @property {*} value - New value
     */
    this.emit('change', {name, value});
    this.emit('change:' + name, value);
  }

  // region utils
  /**
   * Urlencode parameters
   * @returns {string} - HTTP query
   */
  encode() {
    return encodeQueryString(this.toParameterObject());
  }

  /**
   * Convert to object
   * @returns {Object} - Object
   */
  toObject() {
    return RequestParameters
      .keys()
      .reduce((obj, key) => {
        obj[snakeCase(key)] = this._resolve(key);
        return obj;
      }, {});
  }

  /**
   * Convert to object
   * @returns {Object} - Object
   */
  toParameterObject() {
    const data = {};

    RequestParameters
      .keys()
      .forEach(key => {
        // Skip extra key
        if (key === 'extra') {
          return;
        }

        data[snakeCase(key)] = this._resolve(key);
      });

    // Fix column names for sort
    data.sort = data.sort.map(x =>
      snakeCase(x).replace(/^_/, '-'),
    ).join(',');

    // Fix column names for search
    for (const key of Object.keys(data.search)) {
      const snakeKey = key.split(',').map(snakeCase).join(',');

      if (key !== snakeKey) {
        data.search[snakeKey] = data.search[key];
        delete data.search[key];
      }
    }

    // Overwrite using extra properties
    const extra = this._resolve('extra');

    for (const key of Object.keys(extra)) {
      data[key] = extra[key];
    }

    return data;
  }

  /**
   * Copy object
   * @returns {RequestParameters} - Copy
   */
  copy() {
    return new RequestParameters(this.toObject());
  }

  /**
   * Different parameters
   * @returns {Array<String>} - keys
   */
  static keys() {
    // enumeration is disabled for properties
    return [
      'page',
      'perPage',
      'search',
      'sort',
      'deleted',
      'extra',
    ];
  }

  /**
   * Generates a cache token
   * @returns {string} - Cache token
   */
  token() {
    const data = this.toObject();

    delete data['page'];
    delete data['per_page'];

    return hashObject(data);
  }

  /**
   * Resets all parameters back to default
   * @returns {void}
   */
  static resetDefaults() {
    for (const key of RequestParameters.keys()) {
      delete RequestParameters['_' + key];
    }
  }

  /**
   * Apply parameters from object
   * @param {object|RequestParameters} params - parameters
   * @returns {void}
   */
  apply(params) {
    if (params instanceof RequestParameters) {
      params = params.toObject();
    }

    for (const key of Object.keys(params)) {
      const Key = camelCase(key);

      if (key[0] === '_' || !RequestParameters.keys().includes(Key)) {
        continue;
      }

      this._update(Key, params[key]);
    }
  }
  // endregion utils
}
<|endoftext|>


<commit_before>'use strict';

const { exec } = require('child_process');
const path = require('path');
const ApiGateway = require('./ApiGateway');
const ApiGatewayWebSocket = require('./ApiGatewayWebSocket');
const debugLog = require('./debugLog');
const {
  functionCacheCleanup,
  getFunctionOptions,
} = require('./functionHelper');
const { satisfiesVersionRange } = require('./utils');
const {
  defaults,
  commands: commandOptions,
  CUSTOM_OPTION,
  SERVER_SHUTDOWN_TIMEOUT,
  supportedRuntimes,
} = require('./config/index.js');
const { peerDependencies } = require('../package.json');

const { stringify } = JSON;

module.exports = class ServerlessOffline {
  constructor(serverless, options) {
    this.serverless = serverless;
    this.service = serverless.service;
    this.log = serverless.cli.log.bind(serverless.cli);
    this.options = options;
    this.exitCode = 0;
    this._experimentalWarningNotified = false;
    this.commands = {
      offline: {
        // add start nested options
        commands: {
          start: {
            lifecycleEvents: ['init', 'end'],
            options: commandOptions,
            usage:
              'Simulates API Gateway to call your lambda functions offline using backward compatible initialization.',
          },
        },
        lifecycleEvents: ['start'],
        options: commandOptions,
        usage: 'Simulates API Gateway to call your lambda functions offline.',
      },
    };

    this.hooks = {
      'offline:start:init': this.start.bind(this),
      'offline:start': this.startWithExplicitEnd.bind(this),
      'offline:start:end': this.end.bind(this),
    };
  }

  printBlankLine() {
    console.log();
  }

  logPluginIssue() {
    this.log(
      'If you think this is an issue with the plugin please submit it, thanks!',
    );
    this.log('https://github.com/dherault/serverless-offline/issues');
  }

  // Entry point for the plugin (sls offline) when running 'sls offline start'
  async start() {
    this._verifyServerlessVersionCompatibility();

    // Some users would like to know their environment outside of the handler
    process.env.IS_OFFLINE = true;

    await this._buildServer();
    await this.apiGateway._listen();

    if (this.hasWebsocketRoutes) {
      await this.apiGatewayWebSocket._listen();
    }

    if (process.env.NODE_ENV === 'test') {
      return;
    }

    if (this.options.exec) {
      await this._executeShellScript();
    } else {
      await this._listenForTermination();
    }
  }

  /**
   * Entry point for the plugin (sls offline) when running 'sls offline'
   * The call to this.end() would terminate the process before 'offline:start:end' could be consumed
   * by downstream plugins. When running sls offline that can be expected, but docs say that
   * 'sls offline start' will provide the init and end hooks for other plugins to consume
   * */
  async startWithExplicitEnd() {
    await this.start();
    this.end();
  }

  async _listenForTermination() {
    const command = await new Promise((resolve) => {
      process
        // SIGINT will be usually sent when user presses ctrl+c
        .on('SIGINT', () => resolve('SIGINT'))
        // SIGTERM is a default termination signal in many cases,
        // for example when "killing" a subprocess spawned in node
        // with child_process methods
        .on('SIGTERM', () => resolve('SIGTERM'));
    });

    this.log(`Got ${command} signal. Offline Halting...`);
  }

  _executeShellScript() {
    const command = this.options.exec;
    const options = {
      env: {
        IS_OFFLINE: true,
        ...this.service.provider.environment,
        ...this.originalEnvironment,
      },
    };

    this.log(`Offline executing script [${command}]`);

    return new Promise((resolve) => {
      exec(command, options, (error, stdout, stderr) => {
        this.log(`exec stdout: [${stdout}]`);
        this.log(`exec stderr: [${stderr}]`);

        if (error) {
          // Use the failed command's exit code, proceed as normal so that shutdown can occur gracefully
          this.log(`Offline error executing script [${error}]`);
          this.exitCode = error.code || 1;
        }
        resolve();
      });
    });
  }

  async _buildServer() {
    // Methods
    this._setOptions(); // Will create meaningful options from cli options
    this._storeOriginalEnvironment(); // stores the original process.env for assigning upon invoking the handlers

    this.apiGateway = new ApiGateway(
      this.serverless,
      this.options,
      this.velocityContextOptions,
    );

    await this.apiGateway._createServer();

    this.hasWebsocketRoutes = false;
    this.apiGatewayWebSocket = new ApiGatewayWebSocket(
      this.serverless,
      this.options,
    );
    await this.apiGatewayWebSocket._createWebSocket();

    this._setupEvents();
    this.apiGateway._createResourceRoutes(); // HTTP Proxy defined in Resource
    this.apiGateway._create404Route(); // Not found handling
  }

  _storeOriginalEnvironment() {
    this.originalEnvironment = { ...process.env };
  }

  _setOptions() {
    // In the constructor, stage and regions are set to undefined
    if (this.options.region === undefined) delete this.options.region;
    if (this.options.stage === undefined) delete this.options.stage;

    // TODO FIXME remove, leftover from default options
    const defaultsTEMP = {
      region: this.service.provider.region,
      stage: this.service.provider.stage,
    };

    // custom options
    const { [CUSTOM_OPTION]: customOptions } = this.service.custom || {};

    // merge options
    // order of Precedence: command line options, custom options, defaults.
    this.options = {
      ...defaults,
      ...defaultsTEMP, // TODO FIXME, see above
      ...customOptions,
      ...this.options,
    };

    // Prefix must start and end with '/'
    if (!this.options.prefix.startsWith('/')) {
      this.options.prefix = `/${this.options.prefix}`;
    }
    if (!this.options.prefix.endsWith('/')) this.options.prefix += '/';

    this.velocityContextOptions = {
      stage: this.options.stage,
      stageVariables: {}, // this.service.environment.stages[this.options.stage].vars,
    };

    // Parse CORS options
    this.options.corsAllowHeaders = this.options.corsAllowHeaders
      .replace(/\s/g, '')
      .split(',');
    this.options.corsAllowOrigin = this.options.corsAllowOrigin
      .replace(/\s/g, '')
      .split(',');
    this.options.corsExposedHeaders = this.options.corsExposedHeaders
      .replace(/\s/g, '')
      .split(',');

    if (this.options.corsDisallowCredentials) {
      this.options.corsAllowCredentials = false;
    }

    this.options.corsConfig = {
      credentials: this.options.corsAllowCredentials,
      exposedHeaders: this.options.corsExposedHeaders,
      headers: this.options.corsAllowHeaders,
      origin: this.options.corsAllowOrigin,
    };

    this.options.cacheInvalidationRegex = new RegExp(
      this.options.cacheInvalidationRegex,
    );

    this.log(`Starting Offline: ${this.options.stage}/${this.options.region}.`);
    debugLog('options:', this.options);
  }

  async end() {
    this.log('Halting offline server');
    functionCacheCleanup();
    await this.apiGateway.stop(SERVER_SHUTDOWN_TIMEOUT);

    if (process.env.NODE_ENV === 'test') {
      return;
    }

    process.exit(this.exitCode);
  }

  _setupEvents() {
    this._verifySupportedRuntime();

    const defaultContentType = 'application/json';
    const { apiKeys } = this.service.provider;
    const protectedRoutes = [];

    // for simple API Key authentication model
    if (apiKeys) {
      this.log(`Key with token: ${this.options.apiKey}`);

      if (this.options.noAuth) {
        this.log(
          'Authorizers are turned off. You do not need to use x-api-key header.',
        );
      } else {
        this.log('Remember to use x-api-key on the request headers');
      }
    }

    let { runtime } = this.service.provider;

    if (runtime === 'provided') {
      runtime = this.options.providedRuntime;
    }

    Object.entries(this.service.functions).forEach(([key, functionObj]) => {
      const funName = key;

      const servicePath = path.join(
        this.serverless.config.servicePath,
        this.options.location,
      );

      const funOptions = getFunctionOptions(
        functionObj,
        key,
        servicePath,
        runtime,
      );

      debugLog(`funOptions ${stringify(funOptions, null, 2)} `);
      this.printBlankLine();
      debugLog(funName, 'runtime', runtime);
      this.log(`Routes for ${funName}:`);

      if (!functionObj.events) {
        functionObj.events = [];
      }

      // TODO `fun.name` is not set in the jest test run
      // possible serverless BUG?
      if (process.env.NODE_ENV !== 'test') {
        // Add proxy for lamda invoke
        functionObj.events.push({
          http: {
            integration: 'lambda',
            method: 'POST',
            path: `{apiVersion}/functions/${functionObj.name}/invocations`,
            request: {
              template: {
                // AWS SDK for NodeJS specifies as 'binary/octet-stream' not 'application/json'
                'binary/octet-stream': '$input.body',
              },
            },
            response: {
              headers: {
                'Content-Type': 'application/json',
              },
            },
          },
        });
      }

      // Adds a route for each http endpoint
      functionObj.events.forEach((event) => {
        if (event.websocket) {
          this.hasWebsocketRoutes = true;

          this._experimentalWebSocketSupportWarning();

          this.apiGatewayWebSocket._createWsAction(
            functionObj,
            funName,
            servicePath,
            funOptions,
            event,
          );

          return;
        }

        if (event.http) {
          this.apiGateway._createRoutes(
            event,
            funOptions,
            protectedRoutes,
            funName,
            servicePath,
            runtime,
            defaultContentType,
            key,
            functionObj,
          );
        }
      });
    });
  }

  _verifySupportedRuntime() {
    let { runtime } = this.service.provider;

    if (runtime === 'provided') {
      runtime = this.options.providedRuntime;

      if (!runtime) {
        throw new Error(
          `Runtime "provided" is not supported by "Serverless-Offline".
           Please specify the additional "providedRuntime" option.
          `,
        );
      }
    }

    // print message but keep working (don't error out or exit process)
    if (!supportedRuntimes.has(runtime)) {
      this.printBlankLine();
      this.log(`Warning: found unsupported runtime '${runtime}'`);
    }
  }

  // TODO: missing tests
  _verifyServerlessVersionCompatibility() {
    const { version: currentVersion } = this.serverless;
    const { serverless: requiredVersion } = peerDependencies;

    const versionIsSatisfied = satisfiesVersionRange(
      requiredVersion,
      currentVersion,
    );

    if (!versionIsSatisfied) {
      this.log(
        `"Serverless-Offline" requires "Serverless" version ${requiredVersion} but found version ${currentVersion}.
         Be aware that functionality might be limited or has serious bugs.
         To avoid any issues update "Serverless" to a later version.
        `,
        'serverless-offline',
        { color: 'red' },
      );
    }
  }

  // TODO: eventually remove WARNING after release has been deemed stable
  _experimentalWebSocketSupportWarning() {
    // notify only once
    if (this._experimentalWarningNotified) {
      return;
    }

    this.log(
      `WebSocket support in "Serverless-Offline is experimental.
       For any bugs, missing features or other feedback file an issue at https://github.com/dherault/serverless-offline/issues
      `,
      'serverless-offline',
      { color: 'magenta' },
    );

    this._experimentalWarningNotified = true;
  }
};
<commit_msg>Rename "key" and "funName" variables to functionName (same thing)<commit_after>'use strict';

const { exec } = require('child_process');
const path = require('path');
const ApiGateway = require('./ApiGateway');
const ApiGatewayWebSocket = require('./ApiGatewayWebSocket');
const debugLog = require('./debugLog');
const {
  functionCacheCleanup,
  getFunctionOptions,
} = require('./functionHelper');
const { satisfiesVersionRange } = require('./utils');
const {
  defaults,
  commands: commandOptions,
  CUSTOM_OPTION,
  SERVER_SHUTDOWN_TIMEOUT,
  supportedRuntimes,
} = require('./config/index.js');
const { peerDependencies } = require('../package.json');

const { stringify } = JSON;

module.exports = class ServerlessOffline {
  constructor(serverless, options) {
    this.serverless = serverless;
    this.service = serverless.service;
    this.log = serverless.cli.log.bind(serverless.cli);
    this.options = options;
    this.exitCode = 0;
    this._experimentalWarningNotified = false;
    this.commands = {
      offline: {
        // add start nested options
        commands: {
          start: {
            lifecycleEvents: ['init', 'end'],
            options: commandOptions,
            usage:
              'Simulates API Gateway to call your lambda functions offline using backward compatible initialization.',
          },
        },
        lifecycleEvents: ['start'],
        options: commandOptions,
        usage: 'Simulates API Gateway to call your lambda functions offline.',
      },
    };

    this.hooks = {
      'offline:start:init': this.start.bind(this),
      'offline:start': this.startWithExplicitEnd.bind(this),
      'offline:start:end': this.end.bind(this),
    };
  }

  printBlankLine() {
    console.log();
  }

  logPluginIssue() {
    this.log(
      'If you think this is an issue with the plugin please submit it, thanks!',
    );
    this.log('https://github.com/dherault/serverless-offline/issues');
  }

  // Entry point for the plugin (sls offline) when running 'sls offline start'
  async start() {
    this._verifyServerlessVersionCompatibility();

    // Some users would like to know their environment outside of the handler
    process.env.IS_OFFLINE = true;

    await this._buildServer();
    await this.apiGateway._listen();

    if (this.hasWebsocketRoutes) {
      await this.apiGatewayWebSocket._listen();
    }

    if (process.env.NODE_ENV === 'test') {
      return;
    }

    if (this.options.exec) {
      await this._executeShellScript();
    } else {
      await this._listenForTermination();
    }
  }

  /**
   * Entry point for the plugin (sls offline) when running 'sls offline'
   * The call to this.end() would terminate the process before 'offline:start:end' could be consumed
   * by downstream plugins. When running sls offline that can be expected, but docs say that
   * 'sls offline start' will provide the init and end hooks for other plugins to consume
   * */
  async startWithExplicitEnd() {
    await this.start();
    this.end();
  }

  async _listenForTermination() {
    const command = await new Promise((resolve) => {
      process
        // SIGINT will be usually sent when user presses ctrl+c
        .on('SIGINT', () => resolve('SIGINT'))
        // SIGTERM is a default termination signal in many cases,
        // for example when "killing" a subprocess spawned in node
        // with child_process methods
        .on('SIGTERM', () => resolve('SIGTERM'));
    });

    this.log(`Got ${command} signal. Offline Halting...`);
  }

  _executeShellScript() {
    const command = this.options.exec;
    const options = {
      env: {
        IS_OFFLINE: true,
        ...this.service.provider.environment,
        ...this.originalEnvironment,
      },
    };

    this.log(`Offline executing script [${command}]`);

    return new Promise((resolve) => {
      exec(command, options, (error, stdout, stderr) => {
        this.log(`exec stdout: [${stdout}]`);
        this.log(`exec stderr: [${stderr}]`);

        if (error) {
          // Use the failed command's exit code, proceed as normal so that shutdown can occur gracefully
          this.log(`Offline error executing script [${error}]`);
          this.exitCode = error.code || 1;
        }
        resolve();
      });
    });
  }

  async _buildServer() {
    // Methods
    this._setOptions(); // Will create meaningful options from cli options
    this._storeOriginalEnvironment(); // stores the original process.env for assigning upon invoking the handlers

    this.apiGateway = new ApiGateway(
      this.serverless,
      this.options,
      this.velocityContextOptions,
    );

    await this.apiGateway._createServer();

    this.hasWebsocketRoutes = false;
    this.apiGatewayWebSocket = new ApiGatewayWebSocket(
      this.serverless,
      this.options,
    );
    await this.apiGatewayWebSocket._createWebSocket();

    this._setupEvents();
    this.apiGateway._createResourceRoutes(); // HTTP Proxy defined in Resource
    this.apiGateway._create404Route(); // Not found handling
  }

  _storeOriginalEnvironment() {
    this.originalEnvironment = { ...process.env };
  }

  _setOptions() {
    // In the constructor, stage and regions are set to undefined
    if (this.options.region === undefined) delete this.options.region;
    if (this.options.stage === undefined) delete this.options.stage;

    // TODO FIXME remove, leftover from default options
    const defaultsTEMP = {
      region: this.service.provider.region,
      stage: this.service.provider.stage,
    };

    // custom options
    const { [CUSTOM_OPTION]: customOptions } = this.service.custom || {};

    // merge options
    // order of Precedence: command line options, custom options, defaults.
    this.options = {
      ...defaults,
      ...defaultsTEMP, // TODO FIXME, see above
      ...customOptions,
      ...this.options,
    };

    // Prefix must start and end with '/'
    if (!this.options.prefix.startsWith('/')) {
      this.options.prefix = `/${this.options.prefix}`;
    }
    if (!this.options.prefix.endsWith('/')) this.options.prefix += '/';

    this.velocityContextOptions = {
      stage: this.options.stage,
      stageVariables: {}, // this.service.environment.stages[this.options.stage].vars,
    };

    // Parse CORS options
    this.options.corsAllowHeaders = this.options.corsAllowHeaders
      .replace(/\s/g, '')
      .split(',');
    this.options.corsAllowOrigin = this.options.corsAllowOrigin
      .replace(/\s/g, '')
      .split(',');
    this.options.corsExposedHeaders = this.options.corsExposedHeaders
      .replace(/\s/g, '')
      .split(',');

    if (this.options.corsDisallowCredentials) {
      this.options.corsAllowCredentials = false;
    }

    this.options.corsConfig = {
      credentials: this.options.corsAllowCredentials,
      exposedHeaders: this.options.corsExposedHeaders,
      headers: this.options.corsAllowHeaders,
      origin: this.options.corsAllowOrigin,
    };

    this.options.cacheInvalidationRegex = new RegExp(
      this.options.cacheInvalidationRegex,
    );

    this.log(`Starting Offline: ${this.options.stage}/${this.options.region}.`);
    debugLog('options:', this.options);
  }

  async end() {
    this.log('Halting offline server');
    functionCacheCleanup();
    await this.apiGateway.stop(SERVER_SHUTDOWN_TIMEOUT);

    if (process.env.NODE_ENV === 'test') {
      return;
    }

    process.exit(this.exitCode);
  }

  _setupEvents() {
    this._verifySupportedRuntime();

    const defaultContentType = 'application/json';
    const { apiKeys } = this.service.provider;
    const protectedRoutes = [];

    // for simple API Key authentication model
    if (apiKeys) {
      this.log(`Key with token: ${this.options.apiKey}`);

      if (this.options.noAuth) {
        this.log(
          'Authorizers are turned off. You do not need to use x-api-key header.',
        );
      } else {
        this.log('Remember to use x-api-key on the request headers');
      }
    }

    let { runtime } = this.service.provider;

    if (runtime === 'provided') {
      runtime = this.options.providedRuntime;
    }

    Object.entries(this.service.functions).forEach(
      ([functionName, functionObj]) => {
        const servicePath = path.join(
          this.serverless.config.servicePath,
          this.options.location,
        );

        const funOptions = getFunctionOptions(
          functionObj,
          functionName,
          servicePath,
          runtime,
        );

        debugLog(`funOptions ${stringify(funOptions, null, 2)} `);
        this.printBlankLine();
        debugLog(functionName, 'runtime', runtime);
        this.log(`Routes for ${functionName}:`);

        if (!functionObj.events) {
          functionObj.events = [];
        }

        // TODO `fun.name` is not set in the jest test run
        // possible serverless BUG?
        if (process.env.NODE_ENV !== 'test') {
          // Add proxy for lamda invoke
          functionObj.events.push({
            http: {
              integration: 'lambda',
              method: 'POST',
              path: `{apiVersion}/functions/${functionObj.name}/invocations`,
              request: {
                template: {
                  // AWS SDK for NodeJS specifies as 'binary/octet-stream' not 'application/json'
                  'binary/octet-stream': '$input.body',
                },
              },
              response: {
                headers: {
                  'Content-Type': 'application/json',
                },
              },
            },
          });
        }

        // Adds a route for each http endpoint
        functionObj.events.forEach((event) => {
          if (event.websocket) {
            this.hasWebsocketRoutes = true;

            this._experimentalWebSocketSupportWarning();

            this.apiGatewayWebSocket._createWsAction(
              functionObj,
              functionName,
              servicePath,
              funOptions,
              event,
            );

            return;
          }

          if (event.http) {
            this.apiGateway._createRoutes(
              event,
              funOptions,
              protectedRoutes,
              functionName,
              servicePath,
              runtime,
              defaultContentType,
              functionName,
              functionObj,
            );
          }
        });
      },
    );
  }

  _verifySupportedRuntime() {
    let { runtime } = this.service.provider;

    if (runtime === 'provided') {
      runtime = this.options.providedRuntime;

      if (!runtime) {
        throw new Error(
          `Runtime "provided" is not supported by "Serverless-Offline".
           Please specify the additional "providedRuntime" option.
          `,
        );
      }
    }

    // print message but keep working (don't error out or exit process)
    if (!supportedRuntimes.has(runtime)) {
      this.printBlankLine();
      this.log(`Warning: found unsupported runtime '${runtime}'`);
    }
  }

  // TODO: missing tests
  _verifyServerlessVersionCompatibility() {
    const { version: currentVersion } = this.serverless;
    const { serverless: requiredVersion } = peerDependencies;

    const versionIsSatisfied = satisfiesVersionRange(
      requiredVersion,
      currentVersion,
    );

    if (!versionIsSatisfied) {
      this.log(
        `"Serverless-Offline" requires "Serverless" version ${requiredVersion} but found version ${currentVersion}.
         Be aware that functionality might be limited or has serious bugs.
         To avoid any issues update "Serverless" to a later version.
        `,
        'serverless-offline',
        { color: 'red' },
      );
    }
  }

  // TODO: eventually remove WARNING after release has been deemed stable
  _experimentalWebSocketSupportWarning() {
    // notify only once
    if (this._experimentalWarningNotified) {
      return;
    }

    this.log(
      `WebSocket support in "Serverless-Offline is experimental.
       For any bugs, missing features or other feedback file an issue at https://github.com/dherault/serverless-offline/issues
      `,
      'serverless-offline',
      { color: 'magenta' },
    );

    this._experimentalWarningNotified = true;
  }
};
<|endoftext|>