├── .editorconfig
├── .gitignore
├── LICENSE
├── README.md
├── assets
├── 404.html
├── __stencil-dev-server__
│ └── favicon.ico
└── index.html
├── bin
└── stencil-dev-server
├── circle.yml
├── package-lock.json
├── package.json
├── preprocessor.js
├── src
├── __tests__
│ ├── empty-html5.ts
│ ├── empty-html5
│ │ └── stencil.config.js
│ ├── empty.ts
│ ├── empty
│ │ └── stencil.config.js
│ ├── no-html5.ts
│ ├── no-html5
│ │ ├── alert.js
│ │ ├── index.html
│ │ └── stencil.config.js
│ ├── simple.ts
│ └── simple
│ │ ├── alert.js
│ │ ├── index.html
│ │ ├── stencil.config.js
│ │ └── stencil.config.ssl.js
├── definitions.ts
├── index.ts
├── middlewares.ts
├── promisify.ts
└── utils.ts
├── tsconfig.json
└── types
├── devcert-san.d.ts
├── ecstatic.d.ts
└── tiny-lr.d.ts
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | indent_style = space
8 | indent_size = 2
9 | end_of_line = lf
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
13 | [*.md]
14 | insert_final_newline = false
15 | trim_trailing_whitespace = false
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | dist/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Josh Thomas
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [![npm][npm-badge]][npm-badge-url]
2 | [![Build Status][circle-badge]][circle-badge-url]
3 |
4 | ## Deprecated: The stencil-dev-server is now integrated into @stencil/core.
5 |
6 | As of Stencil `0.10.0`, please use the `--serve` argument to enable Stencil's integrated dev-server, which replaces this project.
7 |
8 | # Stencil Dev Server
9 |
10 | This is a very simple http-server with a filewatcher and livereload built in. This server
11 | was built with the purpose of making it easy to develop stencil apps and components, but it will work
12 | with about any dev workflow.
13 |
14 | Just provide a directory.
15 |
16 | ```
17 | stencil-dev-server --root public
18 | ```
19 |
20 | There are a number of options available, but all have sane defaults.
21 |
22 | - **--root**
23 | - The directory that should be watched and served
24 | - It defaults to the current directory that the command was executed from.
25 | - **--watchGlob**
26 | - The pattern of files to watch in the root directory for changes.
27 | - The glob defaults to **\*\*/\*\***.
28 | - **--address**
29 | - The ip address that the server should listen to.
30 | - Defaults to *0.0.0.0*. Point your browser to localhost.
31 | - **--httpPort**
32 | - The port that the http server should use. If the number provided is in use it will choose another.
33 | - Defaults to *3333*.
34 | - **--liveReloadPort**
35 | - The port that the live-reload server should use. If the number provided is in use it will choose another.
36 | - Defaults to *35729*.
37 | - **--additionalJsScripts**
38 | - A comma separated list of javascript files that you would like appended to all html page body tags. This allows you to expand the dev server to do additional behaviors.
39 | - **--config**
40 | - The path to a config file for the dev server. This allows you to keep a specific set of default parameters in a configuration file.
41 | - Defaults to *./stencil.config.js*
42 | - **--ssl**
43 | - Enables https for development server with self signed SSL certificate
44 | - Defaults to false
45 | - **--no-open**
46 | - Disables automatically opening a browser.
47 |
48 | Config File Structure
49 |
50 | ```js
51 | exports.devServer = {
52 | root: './',
53 | additionalJSScripts: [
54 | 'http://localhost:3529/debug.js',
55 | './scripts/additionalDebug.js'
56 | ],
57 | watchGlob: '**/*'
58 | };
59 | ```
60 |
61 | [npm-badge]: https://img.shields.io/npm/v/@stencil/dev-server.svg?style=flat-square
62 | [npm-badge-url]: https://www.npmjs.com/package/@stencil/dev-server
63 | [circle-badge]: https://circleci.com/gh/ionic-team/stencil-dev-server.svg?style=shield
64 | [circle-badge-url]: https://circleci.com/gh/ionic-team/stencil-dev-server
65 |
--------------------------------------------------------------------------------
/assets/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 404: Resource not found
7 |
8 |
9 |
10 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/assets/__stencil-dev-server__/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ionic-team/stencil-dev-server/a85c67d356efe3403ab778bd934bfa36d4b0f84d/assets/__stencil-dev-server__/favicon.ico
--------------------------------------------------------------------------------
/assets/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | listing directory {directory}
7 |
8 |
32 |
33 |
34 |
35 |
~ {linked-path}
36 | {files}
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/bin/stencil-dev-server:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env node
2 | 'use strict';
3 |
4 | process.title = 'stencil-dev-server';
5 | process.on('unhandledRejection', function(r) { console.error(r) });
6 | process.env.TINY_STATIC_LR_BIN = __filename;
7 |
8 | let cmdArgs = process.argv;
9 | const npmRunArgs = process.env.npm_config_argv;
10 | if (npmRunArgs) {
11 | cmdArgs = cmdArgs.concat(JSON.parse(npmRunArgs).original);
12 | }
13 |
14 | const server = require('../dist');
15 |
16 | server.run(cmdArgs);
17 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | machine:
2 | node:
3 | version: 6.9.0
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@stencil/dev-server",
3 | "version": "0.0.19-1",
4 | "description": "Tiny LiveReload server that watches a single directory",
5 | "main": "dist/index.js",
6 | "bin": {
7 | "stencil-dev-server": "./bin/stencil-dev-server"
8 | },
9 | "scripts": {
10 | "dev": "tsc -w -p .",
11 | "build": "tsc -p .",
12 | "test": "jest --runInBand",
13 | "deploy": "npm run build && np"
14 | },
15 | "files": [
16 | "assets/",
17 | "bin/",
18 | "dist/",
19 | "LICENSE",
20 | "README.md"
21 | ],
22 | "repository": {
23 | "type": "git",
24 | "url": "git+https://github.com/ionic-team/stencil-dev-server.git"
25 | },
26 | "author": "Ionic Team",
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/ionic-team/stencil-dev-server/issues"
30 | },
31 | "homepage": "https://github.com/ionic-team/stencil-dev-server#readme",
32 | "dependencies": {
33 | "@ionic/discover": "^0.3.3",
34 | "chokidar": "^1.7.0",
35 | "devcert-san": "^0.3.3",
36 | "ecstatic": "^2.2.1",
37 | "opn": "^5.1.0",
38 | "tiny-lr": "^1.0.5"
39 | },
40 | "devDependencies": {
41 | "@types/chokidar": "^1.7.0",
42 | "@types/jest": "^21.1.2",
43 | "@types/opn": "^3.0.28",
44 | "@types/supertest": "^2.0.3",
45 | "jest": "^21.2.1",
46 | "np": "^2.16.0",
47 | "supertest": "^3.0.0",
48 | "typescript": "^2.4.1"
49 | },
50 | "jest": {
51 | "moduleFileExtensions": [
52 | "ts",
53 | "tsx",
54 | "js"
55 | ],
56 | "transform": {
57 | "^.+\\.(ts|tsx)$": "/preprocessor.js"
58 | },
59 | "testMatch": [
60 | "**/__tests__/*.(ts|tsx|js)"
61 | ]
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/preprocessor.js:
--------------------------------------------------------------------------------
1 | const tsc = require('typescript');
2 | const tsConfig = require('./tsconfig.json');
3 |
4 | module.exports = {
5 | process: function (src, path) {
6 | if (path.endsWith('.ts') || path.endsWith('.tsx')) {
7 | return tsc.transpile(src, tsConfig.compilerOptions, path, []);
8 | }
9 | return src;
10 | },
11 | };
12 |
--------------------------------------------------------------------------------
/src/__tests__/empty-html5.ts:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import * as request from 'supertest';
4 | import * as path from 'path';
5 | import { run as createDevServer, StencilDevServer } from '..';
6 |
7 | describe('GET Collection', async function () {
8 | let cmdArgs: string[];
9 | let devServer: StencilDevServer;
10 |
11 | beforeAll(async function () {
12 | cmdArgs = [
13 | '--config', path.join(__dirname, './empty-html5/stencil.config.js'),
14 | '--no-open',
15 | ];
16 | devServer = await createDevServer(cmdArgs);
17 | });
18 |
19 | afterAll(async function () {
20 | await devServer.close();
21 | });
22 |
23 | describe('Scenarios for an empty directory with html5mode true', function () {
24 | it('should return 200 and directory contents', async function () {
25 | const response = await request(devServer.httpServer).get('/');
26 | expect(response.status).toBe(200);
27 | expect(response.text).toContain('~ ');
28 | });
29 |
30 | it('should return 404 for files that do not exist', async function () {
31 | const response = await request(devServer.httpServer).get('/red.jpg');
32 | expect(response.status).toBe(404);
33 | });
34 |
35 | it('should return 404 for html files that do not exist', async function () {
36 | const response = await request(devServer.httpServer).get('/red.html');
37 | expect(response.status).toBe(404);
38 | });
39 |
40 | it('should return 404 for folders that do not exist', async function () {
41 | const response = await request(devServer.httpServer).get('/red');
42 | expect(response.status).toBe(404);
43 | });
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/src/__tests__/empty-html5/stencil.config.js:
--------------------------------------------------------------------------------
1 | exports.devServer = {
2 | watchGlob: '**/*.js',
3 | html5mode: true,
4 | root: __dirname,
5 | };
6 |
--------------------------------------------------------------------------------
/src/__tests__/empty.ts:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import * as request from 'supertest';
4 | import * as path from 'path';
5 | import { run as createDevServer, StencilDevServer } from '..';
6 |
7 | describe('GET Collection', async function () {
8 | let cmdArgs: string[];
9 | let devServer: StencilDevServer;
10 |
11 | beforeAll(async function () {
12 | cmdArgs = [
13 | '--config', path.join(__dirname, './empty/stencil.config.js'),
14 | '--no-open',
15 | ];
16 | devServer = await createDevServer(cmdArgs);
17 | });
18 |
19 | afterAll(async function () {
20 | await devServer.close();
21 | });
22 |
23 | describe('Scenarios for an empty directory with html5mode false', function () {
24 | it('should return 200 and directory contents', async function () {
25 | const response = await request(devServer.httpServer).get('/');
26 | expect(response.status).toBe(200);
27 | expect(response.text).toContain('~ ');
28 | });
29 |
30 | it('should return 404 for files that do not exist', async function () {
31 | const response = await request(devServer.httpServer).get('/red.jpg');
32 | expect(response.status).toBe(404);
33 | });
34 |
35 | it('should return 404 for html files that do not exist', async function () {
36 | const response = await request(devServer.httpServer).get('/red.html');
37 | expect(response.status).toBe(404);
38 | });
39 |
40 | it('should return 404 for folders that do not exist', async function () {
41 | const response = await request(devServer.httpServer).get('/red');
42 | expect(response.status).toBe(404);
43 | });
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/src/__tests__/empty/stencil.config.js:
--------------------------------------------------------------------------------
1 | exports.devServer = {
2 | watchGlob: '**/*.js',
3 | html5mode: false,
4 | root: __dirname,
5 | };
6 |
--------------------------------------------------------------------------------
/src/__tests__/no-html5.ts:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import * as request from 'supertest';
4 | import * as path from 'path';
5 | import { run as createDevServer, StencilDevServer } from '..';
6 |
7 | describe('GET Collection', async function () {
8 | let cmdArgs: string[];
9 | let devServer: StencilDevServer;
10 |
11 | beforeAll(async function () {
12 | cmdArgs = [
13 | '--config', path.join(__dirname, './no-html5/stencil.config.js'),
14 | '--no-open',
15 | ];
16 | devServer = await createDevServer(cmdArgs);
17 | });
18 |
19 | afterAll(async function () {
20 | await devServer.close();
21 | });
22 |
23 | describe('Scenarios for html5mode false', function () {
24 | it('should return 200 for index', async function () {
25 | const response = await request(devServer.httpServer).get('/');
26 | expect(response.status).toBe(200);
27 | expect(response.header['content-type']).toEqual('text/html');
28 | expect(response.header['expires']).toEqual('0');
29 | expect(response.header['cache-control']).toEqual(
30 | 'no-cache, no-store, must-revalidate, max-age=0'
31 | );
32 | });
33 |
34 | it('should return 404 for files that do not exist', async function () {
35 | const response = await request(devServer.httpServer).get('/red.jpg');
36 | expect(response.status).toBe(404);
37 | });
38 |
39 | it('should return 404 for html files that do not exist', async function () {
40 | const response = await request(devServer.httpServer).get('/red.html');
41 | expect(response.status).toBe(404);
42 | });
43 |
44 | it('should return 404 for folders that do not exist', async function () {
45 | const response = await request(devServer.httpServer).get('/red');
46 | expect(response.status).toBe(404);
47 | });
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/src/__tests__/no-html5/alert.js:
--------------------------------------------------------------------------------
1 | console.log('alert');
2 |
--------------------------------------------------------------------------------
/src/__tests__/no-html5/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | there man wa test
5 |
6 |
--------------------------------------------------------------------------------
/src/__tests__/no-html5/stencil.config.js:
--------------------------------------------------------------------------------
1 | exports.devServer = {
2 | watchGlob: '**/*.js',
3 | html5mode: false,
4 | root: __dirname,
5 | };
6 |
--------------------------------------------------------------------------------
/src/__tests__/simple.ts:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import * as request from 'supertest';
4 | import * as path from 'path';
5 | import { run as createDevServer, StencilDevServer } from '..';
6 |
7 | describe('GET Collection', async function () {
8 | let cmdArgs: string[];
9 | let devServer: StencilDevServer;
10 |
11 | describe('Scenarios for html5mode true', function () {
12 |
13 | beforeAll(async function () {
14 | cmdArgs = [
15 | '--config', path.join(__dirname, './simple/stencil.config.js'),
16 | '--no-open',
17 | ];
18 | devServer = await createDevServer(cmdArgs);
19 | });
20 |
21 | afterAll(async function () {
22 | await devServer.close();
23 | });
24 |
25 | it('should return 200 for index', async function () {
26 | const response = await request(devServer.httpServer).get('/');
27 | expect(response.status).toBe(200);
28 | expect(response.header['content-type']).toEqual('text/html');
29 | expect(response.header['expires']).toEqual('0');
30 | expect(response.header['cache-control']).toEqual(
31 | 'no-cache, no-store, must-revalidate, max-age=0'
32 | );
33 | expect(response.text).toContain('/__stencil-dev-server__/js_includes/alert.js');
34 | expect(response.text).toContain('http://localhost:4444/red/blue.js');
35 | });
36 |
37 | it('should return 200 and index file for files that do not exist', async function () {
38 | const response = await request(devServer.httpServer).get('/red.jpg');
39 | expect(response.status).toBe(404);
40 | });
41 |
42 | it('should return 200 for js_incldues files that do exist', async function () {
43 | const response = await request(devServer.httpServer).get('/__stencil-dev-server__/js_includes/alert.js');
44 | expect(response.status).toBe(200);
45 | });
46 |
47 | it('should return 404 for js_incldues files that do not exist', async function () {
48 | const response = await request(devServer.httpServer).get('/__stencil-dev-server__/js_includes/bad-file.js');
49 | expect(response.status).toBe(404);
50 | });
51 |
52 | it('should return 200 and index file for html files that do not exist', async function () {
53 | const response = await request(devServer.httpServer).get('/red.html');
54 | expect(response.status).toBe(200);
55 | expect(response.header['content-type']).toEqual('text/html');
56 | });
57 |
58 | it('should return 200 and index file for folders that do not exist', async function () {
59 | const response = await request(devServer.httpServer).get('/red');
60 | expect(response.status).toBe(200);
61 | expect(response.header['content-type']).toEqual('text/html');
62 | });
63 | });
64 |
65 | describe('Scenarios for html5mode true and SSL set to true', function () {
66 |
67 | beforeAll(async function () {
68 | cmdArgs = [
69 | '--config', path.join(__dirname, './simple/stencil.config.ssl.js'),
70 | '--no-open',
71 | ];
72 | devServer = await createDevServer(cmdArgs);
73 | });
74 |
75 | afterAll(async function () {
76 | await devServer.close();
77 | });
78 |
79 | it('should return 200 for index', async function () {
80 | const response = await request(devServer.httpServer).get('/');
81 | expect(response.status).toBe(200);
82 | expect(response.header['content-type']).toEqual('text/html');
83 | expect(response.header['expires']).toEqual('0');
84 | expect(response.header['cache-control']).toEqual(
85 | 'no-cache, no-store, must-revalidate, max-age=0'
86 | );
87 | expect(response.text).toContain('/__stencil-dev-server__/js_includes/alert.js');
88 | expect(response.text).toContain('https://localhost:4444/red/blue.js');
89 | });
90 |
91 | it('should return 200 and index file for files that do not exist', async function () {
92 | const response = await request(devServer.httpServer).get('/red.jpg');
93 | expect(response.status).toBe(404);
94 | });
95 |
96 | it('should return 200 for js_incldues files that do exist', async function () {
97 | const response = await request(devServer.httpServer).get('/__stencil-dev-server__/js_includes/alert.js');
98 | expect(response.status).toBe(200);
99 | });
100 |
101 | it('should return 404 for js_incldues files that do not exist', async function () {
102 | const response = await request(devServer.httpServer).get('/__stencil-dev-server__/js_includes/bad-file.js');
103 | expect(response.status).toBe(404);
104 | });
105 |
106 | it('should return 200 and index file for html files that do not exist', async function () {
107 | const response = await request(devServer.httpServer).get('/red.html');
108 | expect(response.status).toBe(200);
109 | expect(response.header['content-type']).toEqual('text/html');
110 | });
111 |
112 | it('should return 200 and index file for folders that do not exist', async function () {
113 | const response = await request(devServer.httpServer).get('/red');
114 | expect(response.status).toBe(200);
115 | expect(response.header['content-type']).toEqual('text/html');
116 | });
117 | });
118 | });
119 |
--------------------------------------------------------------------------------
/src/__tests__/simple/alert.js:
--------------------------------------------------------------------------------
1 | console.log('alert');
2 |
--------------------------------------------------------------------------------
/src/__tests__/simple/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | there man wa test
5 |
6 |
--------------------------------------------------------------------------------
/src/__tests__/simple/stencil.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | exports.devServer = {
3 | additionalJsScripts: [path.join(__dirname, '/alert.js'), 'http://localhost:4444/red/blue.js'],
4 | watchGlob: '**/*.js',
5 | html5mode: true,
6 | root: __dirname
7 | };
8 |
--------------------------------------------------------------------------------
/src/__tests__/simple/stencil.config.ssl.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
3 | exports.devServer = {
4 | additionalJsScripts: [path.join(__dirname, '/alert.js'), 'https://localhost:4444/red/blue.js'],
5 | watchGlob: '**/*.js',
6 | html5mode: true,
7 | root: __dirname,
8 | ssl : true
9 | };
10 |
--------------------------------------------------------------------------------
/src/definitions.ts:
--------------------------------------------------------------------------------
1 | export interface InputOptions {
2 | [key: string]: InputType
3 | }
4 | export type InputType = {
5 | default: string | number | boolean;
6 | type: String | Number | Boolean;
7 | };
8 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import * as fs from 'fs';
3 | import * as url from 'url';
4 | import * as tinylr from 'tiny-lr';
5 | import * as ecstatic from 'ecstatic';
6 | import * as opn from 'opn';
7 | import * as http from 'http';
8 | import * as https from 'https';
9 | import { watch, FSWatcher } from 'chokidar';
10 | import { findClosestOpenPort, parseOptions, parseConfigFile,
11 | getRequestedPath, getFileFromPath, fsStatPr ,getSSL} from './utils';
12 | import { serveHtml, serveDirContents, sendError, sendFile } from './middlewares';
13 | import { newSilentPublisher } from '@ionic/discover';
14 |
15 |
16 |
17 | const RESERVED_STENCIL_PATH = '/__stencil-dev-server__';
18 |
19 | const optionInfo = {
20 | root: {
21 | default: 'www',
22 | type: String
23 | },
24 | verbose: {
25 | default: false,
26 | type: Boolean
27 | },
28 | html5mode: {
29 | default: true,
30 | type: Boolean
31 | },
32 | watchGlob: {
33 | default: '**/*',
34 | type: String
35 | },
36 | address: {
37 | default: '0.0.0.0',
38 | type: String
39 | },
40 | httpPort: {
41 | default: 3333,
42 | type: Number
43 | },
44 | liveReloadPort: {
45 | default: 35729,
46 | type: Number
47 | },
48 | additionalJsScripts: {
49 | default: '',
50 | type: String
51 | },
52 | config: {
53 | default: './stencil.config.js',
54 | type: String
55 | },
56 | ssl: {
57 | default: false,
58 | type: Boolean
59 | }
60 | }
61 |
62 | export interface StencilDevServer {
63 | httpServer: http.Server | https.Server,
64 | fileWatcher: FSWatcher,
65 | tinyLrServer: tinylr.server,
66 | close: () => Promise
67 | }
68 |
69 | export async function run(argv: string[]) {
70 | const cliDefaultedOptions = parseOptions(optionInfo, argv);
71 | cliDefaultedOptions.additionalJsScripts = cliDefaultedOptions.additionalJsScripts
72 | .split(',')
73 | .filter((name: string) => !!name);
74 | const isVerbose = cliDefaultedOptions.verbose;
75 | const configOptions = await parseConfigFile(process.cwd(), cliDefaultedOptions.config);
76 | const options = Object.keys(cliDefaultedOptions).reduce((options, optionName) => {
77 | const newValue = (configOptions[optionName] == null) ?
78 | cliDefaultedOptions[optionName] :
79 | configOptions[optionName];
80 | options[optionName] = newValue;
81 | return options;
82 | }, <{ [key: string]: any }>{});
83 |
84 | const [ foundHttpPort, foundLiveReloadPort ] = await Promise.all([
85 | findClosestOpenPort(options.address, options.httpPort),
86 | findClosestOpenPort(options.address, options.liveReloadPort),
87 | ]);
88 |
89 | const protocol:string = options.ssl ? 'https' : 'http';
90 | log(isVerbose, `Will serve requests using : ${protocol}`);
91 |
92 | const wwwRoot = path.resolve(options.root);
93 | const browserUrl = getAddressForBrowser(options.address);
94 | const [ tinyLrServer, lrScriptLocation, emitLiveReloadUpdate ] = await createLiveReload(foundLiveReloadPort, options.address, wwwRoot , options.ssl);
95 |
96 | const jsScriptLocations: string[] = options.additionalJsScripts
97 | .map((filePath: string) => filePath.trim())
98 | .concat(lrScriptLocation);
99 |
100 | const fileWatcher = createFileWatcher(wwwRoot, options.watchGlob, emitLiveReloadUpdate, isVerbose);
101 | log(isVerbose, `watching: ${wwwRoot} ${options.watchGlob}`);
102 |
103 | const requestHandler = createHttpRequestHandler(wwwRoot, jsScriptLocations, options.html5mode);
104 |
105 | const httpServer = options.ssl ? https.createServer( await getSSL() ,requestHandler).listen(foundHttpPort)
106 | : http.createServer(requestHandler).listen(foundHttpPort);
107 |
108 | log(true, `listening on ${protocol}://${browserUrl}:${foundHttpPort}`);
109 | log(isVerbose, `serving: ${wwwRoot}`);
110 |
111 | if (argv.indexOf('--no-open') === -1) {
112 | opn(`${protocol}://${browserUrl}:${foundHttpPort}`);
113 | }
114 |
115 | if (argv.indexOf('--broadcast') >= 0) {
116 | log(isVerbose, 'publishing broadcast');
117 | newSilentPublisher('devapp', 'stencil-dev', foundHttpPort);
118 | }
119 |
120 | async function close() {
121 | fileWatcher.close();
122 | tinyLrServer.close();
123 | await new Promise((resolve, reject) => {
124 | httpServer.close((err: Error) => {
125 | if (err) {
126 | reject(err);
127 | }
128 | resolve();
129 | });
130 | });
131 | }
132 |
133 | process.once('SIGINT', async () => {
134 | await close();
135 | process.exit(0);
136 | });
137 |
138 | return {
139 | httpServer,
140 | fileWatcher,
141 | tinyLrServer,
142 | close
143 | } as StencilDevServer;
144 | }
145 |
146 | function createHttpRequestHandler(wwwDir: string, jsScriptsList: string[], html5mode: boolean) {
147 | const jsScriptsMap = jsScriptsList.reduce((map, fileUrl: string): { [key: string ]: string } => {
148 | const urlParts = url.parse(fileUrl);
149 | if (urlParts.host) {
150 | map[fileUrl] = fileUrl;
151 | } else {
152 | const baseFileName = path.basename(fileUrl);
153 | map[path.join(RESERVED_STENCIL_PATH, 'js_includes', baseFileName)] = path.resolve(process.cwd(), fileUrl);
154 | }
155 | return map;
156 | }, <{ [key: string ]: string }>{});
157 |
158 | const staticFileMiddleware = ecstatic({ root: wwwDir, cache: 0 });
159 | const devServerFileMiddleware = ecstatic({ root: path.resolve(__dirname, '..', 'assets') });
160 | const sendHtml = serveHtml(wwwDir, Object.keys(jsScriptsMap));
161 | const sendDirectoryContents = serveDirContents(wwwDir);
162 |
163 | return async function(req: http.IncomingMessage, res: http.ServerResponse) {
164 | const reqPath = getRequestedPath(req.url || '');
165 | const filePath = getFileFromPath(wwwDir, req.url || '');
166 | let pathStat: fs.Stats;
167 |
168 | const serveIndexFile = async (directory: string) => {
169 | const indexFilePath = path.join(directory, 'index.html');
170 | let indexFileStat: fs.Stats | undefined;
171 | try {
172 | indexFileStat = await fsStatPr(indexFilePath);
173 | } catch (err) {}
174 |
175 | if (indexFileStat && indexFileStat.isFile()) {
176 | return sendHtml(indexFilePath, req, res);
177 | }
178 | }
179 |
180 | // If the file is a member of the scripts we autoload then serve it
181 | if (jsScriptsMap[(req.url || '')]) {
182 | return sendFile('application/javascript', jsScriptsMap[(req.url || '')], req, res);
183 | }
184 |
185 | // If the request is to a static file that is part of this package
186 | // then just send it on using the static file middleware
187 | if ((req.url || '').startsWith(RESERVED_STENCIL_PATH)) {
188 | return devServerFileMiddleware(req, res);
189 | }
190 |
191 | try {
192 | pathStat = await fsStatPr(filePath);
193 | } catch (err) {
194 |
195 | // File or path does not exist
196 | if (err.code === 'ENOENT' || err.code === 'ENOTDIR') {
197 | if (html5mode && (['.html', ''].indexOf(path.extname(filePath)) !== -1)) {
198 | await serveIndexFile(wwwDir);
199 | if (res.finished) {
200 | return;
201 | }
202 | }
203 |
204 | // The wwwDir index.html file does not exist.
205 | return sendError(404, res, { error: err });
206 | }
207 |
208 | // No access to the file.
209 | if (err.code === 'EACCES') {
210 | return sendError(403, res, { error: err });
211 | }
212 |
213 | // Not sure what happened.
214 | return sendError(500, res, { error: err });
215 | }
216 |
217 | // If this is the first request then try to serve an index.html file in the root dir
218 | if (reqPath === '/') {
219 | await serveIndexFile(wwwDir);
220 | if (res.finished) {
221 | return;
222 | }
223 | }
224 |
225 |
226 | // If the request is to a directory then serve the directory contents
227 | if (pathStat.isDirectory()) {
228 | await serveIndexFile(filePath);
229 | if (res.finished) {
230 | return;
231 | }
232 |
233 | // If the request is to a directory but does not end in slash then redirect to use a slash
234 | if (!reqPath.endsWith('/')) {
235 | res.writeHead(302, {
236 | 'location': reqPath + '/'
237 | });
238 | return res.end();
239 | }
240 |
241 | return await sendDirectoryContents(filePath, req, res);
242 | }
243 |
244 | // If the request is to a file and it is an html file then use sendHtml to parse and send on
245 | if (pathStat.isFile() && filePath.endsWith('.html')) {
246 | return await sendHtml(filePath, req, res);
247 | }
248 | if (pathStat.isFile()) {
249 | return staticFileMiddleware(req, res);
250 | }
251 |
252 | // Not sure what you are requesting but lets just send an error
253 | return sendError(415, res, { error: 'Resource requested cannot be served.' });
254 | }
255 | }
256 |
257 | let timeoutId: NodeJS.Timer;
258 |
259 | function createFileWatcher(wwwDir: string, watchGlob: string, changeCb: Function, isVerbose: boolean) {
260 | const watcher = watch(watchGlob, {
261 | cwd: wwwDir,
262 | ignored: /(^|[\/\\])\../ // Ignore dot files, ie .git
263 | });
264 |
265 | function fileChanged(filePath: string) {
266 | clearTimeout(timeoutId);
267 |
268 | timeoutId = setTimeout(() => {
269 | log(isVerbose, `[${new Date().toTimeString().slice(0, 8)}] ${filePath} changed`);
270 | changeCb([filePath]);
271 | }, 50);
272 | }
273 |
274 | watcher.on('change', fileChanged);
275 | watcher.on('error', (err: Error) => {
276 | log(true, err.toString());
277 | });
278 |
279 | return watcher;
280 | }
281 |
282 |
283 | async function createLiveReload(port: number, address: string, wwwDir: string , ssl: boolean): Promise<[ tinylr.server, string, (changedFile: string[]) => void]> {
284 |
285 | const options:any = ssl ? await getSSL() : {};
286 | const protocol:string = ssl ? 'https' : 'http';
287 |
288 | const liveReloadServer = tinylr(options);
289 | liveReloadServer.listen(port,address);
290 |
291 | return [
292 | liveReloadServer,
293 | `${protocol}://${getAddressForBrowser(address)}:${port}/livereload.js?snipver=1`,
294 | (changedFiles: string[]) => {
295 | liveReloadServer.changed({
296 | body: {
297 | files: changedFiles.map(changedFile => (
298 | '/' + path.relative(wwwDir, changedFile)
299 | ))
300 | }
301 | });
302 | }
303 | ];
304 | }
305 |
306 | function getAddressForBrowser(ipAddress: string) {
307 | return (ipAddress === '0.0.0.0') ? 'localhost' : ipAddress;
308 | }
309 |
310 | function log(test: boolean, ...args: any[]) {
311 | if (test) {
312 | console.log(...args);
313 | }
314 | }
315 |
--------------------------------------------------------------------------------
/src/middlewares.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import * as url from 'url';
3 | import * as fs from 'fs';
4 | import * as http from 'http';
5 | import { fsReadFilePr, fsReadDirPr, fsStatPr } from './utils';
6 |
7 | export function serveHtml(wwwDir: string, scriptLocations: string[]) {
8 | return async function(filePath: string, req: http.IncomingMessage, res: http.ServerResponse) {
9 | const indexHtml = await fsReadFilePr(filePath);
10 | const appendString = scriptLocations.map(sl => ``).join('\n');
11 | const htmlString: string = indexHtml.toString()
12 | .replace(
13 | `