diff --git a/jenkins/Jenkinsfile b/jenkins/Jenkinsfile new file mode 100644 index 0000000..cf6ea71 --- /dev/null +++ b/jenkins/Jenkinsfile @@ -0,0 +1,30 @@ +pipeline { + agent { + docker { + image 'node:6-alpine' + args '-p 3000:3000' + } + } + environment { + CI = 'true' + } + stages { + stage('Build') { + steps { + sh 'npm install' + } + } + stage('Test') { + steps { + sh './jenkins/scripts/test.sh' + } + } + stage('Deliver') { + steps { + sh './jenkins/scripts/deliver.sh' + input message: 'Finished using the web site? (Click "Proceed" to continue)' + sh './jenkins/scripts/kill.sh' + } + } + } +} diff --git a/jenkins/scripts/deliver.sh b/jenkins/scripts/deliver.sh new file mode 100755 index 0000000..21ddafd --- /dev/null +++ b/jenkins/scripts/deliver.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env sh + +echo 'The following "npm" command builds your Node.js/React application for' +echo 'production in the local "build" directory (i.e. within the' +echo '"/var/jenkins_home/workspace/simple-node-js-react-app" directory),' +echo 'correctly bundles React in production mode and optimizes the build for' +echo 'the best performance.' +set -x +npm run build +set +x + +echo 'The following "npm" command runs your Node.js/React application in' +echo 'development mode and makes the application available for web browsing.' +echo 'The "npm start" command has a trailing ampersand so that the command runs' +echo 'as a background process (i.e. asynchronously). Otherwise, this command' +echo 'can pause running builds of CI/CD applications indefinitely. "npm start"' +echo 'is followed by another command that retrieves the process ID (PID) value' +echo 'of the previously run process (i.e. "npm start") and writes this value to' +echo 'the file ".pidfile".' +set -x +npm start & +sleep 1 +echo $! > .pidfile +set +x + +echo 'Now...' +echo 'Visit http://localhost:3000 to see your Node.js/React application in action.' +echo '(This is why you specified the "args ''-p 3000:3000''" parameter when you' +echo 'created your initial Pipeline as a Jenkinsfile.)' diff --git a/jenkins/scripts/kill.sh b/jenkins/scripts/kill.sh new file mode 100755 index 0000000..94ae46b --- /dev/null +++ b/jenkins/scripts/kill.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env sh + +echo 'The following command terminates the "npm start" process using its PID' +echo '(written to ".pidfile"), all of which were conducted when "deliver.sh"' +echo 'was executed.' +set -x +kill $(cat .pidfile) diff --git a/jenkins/scripts/test.sh b/jenkins/scripts/test.sh new file mode 100755 index 0000000..360d00b --- /dev/null +++ b/jenkins/scripts/test.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env sh + +echo 'The following "npm" command (if executed) installs the "cross-env"' +echo 'dependency into the local "node_modules" directory, which will ultimately' +echo 'be stored in the Jenkins home directory. As described in' +echo 'https://docs.npmjs.com/cli/install, the "--save-dev" flag causes the' +echo '"cross-env" dependency to be installed as "devDependencies". For the' +echo 'purposes of this tutorial, this flag is not important. However, when' +echo 'installing this dependency, it would typically be done so using this' +echo 'flag. For a comprehensive explanation about "devDependencies", see' +echo 'https://stackoverflow.com/questions/18875674/whats-the-difference-between-dependencies-devdependencies-and-peerdependencies.' +set -x +# npm install --save-dev cross-env +set +x + +echo 'The following "npm" command tests that your simple Node.js/React' +echo 'application renders satisfactorily. This command actually invokes the test' +echo 'runner Jest (https://facebook.github.io/jest/).' +set -x +npm test diff --git a/package.json b/package.json new file mode 100644 index 0000000..53720bd --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "my-app", + "version": "0.1.0", + "private": true, + "dependencies": { + "react": "^16.0.0", + "react-dom": "^16.0.0", + "react-scripts": "1.0.14" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..a11777c Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..7bee027 --- /dev/null +++ b/public/index.html @@ -0,0 +1,40 @@ + + + + + + + + + + + React App + + + +
+ + + diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..be607e4 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "192x192", + "type": "image/png" + } + ], + "start_url": "./index.html", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..c5c6e8a --- /dev/null +++ b/src/App.css @@ -0,0 +1,28 @@ +.App { + text-align: center; +} + +.App-logo { + animation: App-logo-spin infinite 20s linear; + height: 80px; +} + +.App-header { + background-color: #222; + height: 150px; + padding: 20px; + color: white; +} + +.App-title { + font-size: 1.5em; +} + +.App-intro { + font-size: large; +} + +@keyframes App-logo-spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} diff --git a/src/App.js b/src/App.js new file mode 100644 index 0000000..203067e --- /dev/null +++ b/src/App.js @@ -0,0 +1,21 @@ +import React, { Component } from 'react'; +import logo from './logo.svg'; +import './App.css'; + +class App extends Component { + render() { + return ( +
+
+ logo +

Welcome to React

+
+

+ To get started, edit src/App.js and save to reload. +

+
+ ); + } +} + +export default App; diff --git a/src/App.test.js b/src/App.test.js new file mode 100644 index 0000000..b84af98 --- /dev/null +++ b/src/App.test.js @@ -0,0 +1,8 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; + +it('renders without crashing', () => { + const div = document.createElement('div'); + ReactDOM.render(, div); +}); diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..b4cc725 --- /dev/null +++ b/src/index.css @@ -0,0 +1,5 @@ +body { + margin: 0; + padding: 0; + font-family: sans-serif; +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..fae3e35 --- /dev/null +++ b/src/index.js @@ -0,0 +1,8 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import './index.css'; +import App from './App'; +import registerServiceWorker from './registerServiceWorker'; + +ReactDOM.render(, document.getElementById('root')); +registerServiceWorker(); diff --git a/src/logo.svg b/src/logo.svg new file mode 100644 index 0000000..6b60c10 --- /dev/null +++ b/src/logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/registerServiceWorker.js b/src/registerServiceWorker.js new file mode 100644 index 0000000..4a3ccf0 --- /dev/null +++ b/src/registerServiceWorker.js @@ -0,0 +1,108 @@ +// In production, we register a service worker to serve assets from local cache. + +// This lets the app load faster on subsequent visits in production, and gives +// it offline capabilities. However, it also means that developers (and users) +// will only see deployed updates on the "N+1" visit to a page, since previously +// cached resources are updated in the background. + +// To learn more about the benefits of this model, read https://goo.gl/KwvDNy. +// This link also includes instructions on opting out of this behavior. + +const isLocalhost = Boolean( + window.location.hostname === 'localhost' || + // [::1] is the IPv6 localhost address. + window.location.hostname === '[::1]' || + // 127.0.0.1/8 is considered localhost for IPv4. + window.location.hostname.match( + /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ + ) +); + +export default function register() { + if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { + // The URL constructor is available in all browsers that support SW. + const publicUrl = new URL(process.env.PUBLIC_URL, window.location); + if (publicUrl.origin !== window.location.origin) { + // Our service worker won't work if PUBLIC_URL is on a different origin + // from what our page is served on. This might happen if a CDN is used to + // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 + return; + } + + window.addEventListener('load', () => { + const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; + + if (!isLocalhost) { + // Is not local host. Just register service worker + registerValidSW(swUrl); + } else { + // This is running on localhost. Lets check if a service worker still exists or not. + checkValidServiceWorker(swUrl); + } + }); + } +} + +function registerValidSW(swUrl) { + navigator.serviceWorker + .register(swUrl) + .then(registration => { + registration.onupdatefound = () => { + const installingWorker = registration.installing; + installingWorker.onstatechange = () => { + if (installingWorker.state === 'installed') { + if (navigator.serviceWorker.controller) { + // At this point, the old content will have been purged and + // the fresh content will have been added to the cache. + // It's the perfect time to display a "New content is + // available; please refresh." message in your web app. + console.log('New content is available; please refresh.'); + } else { + // At this point, everything has been precached. + // It's the perfect time to display a + // "Content is cached for offline use." message. + console.log('Content is cached for offline use.'); + } + } + }; + }; + }) + .catch(error => { + console.error('Error during service worker registration:', error); + }); +} + +function checkValidServiceWorker(swUrl) { + // Check if the service worker can be found. If it can't reload the page. + fetch(swUrl) + .then(response => { + // Ensure service worker exists, and that we really are getting a JS file. + if ( + response.status === 404 || + response.headers.get('content-type').indexOf('javascript') === -1 + ) { + // No service worker found. Probably a different app. Reload the page. + navigator.serviceWorker.ready.then(registration => { + registration.unregister().then(() => { + window.location.reload(); + }); + }); + } else { + // Service worker found. Proceed as normal. + registerValidSW(swUrl); + } + }) + .catch(() => { + console.log( + 'No internet connection found. App is running in offline mode.' + ); + }); +} + +export function unregister() { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.ready.then(registration => { + registration.unregister(); + }); + } +}