iframe-coordinator-cli 5.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ > 1%
2
+ last 2 versions
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # wrapper-app
2
+
3
+ ## Project setup
4
+ ```
5
+ npm install
6
+ ```
7
+
8
+ ### Compiles and hot-reloads for development
9
+ ```
10
+ npm run serve
11
+ ```
12
+
13
+ ### Compiles and minifies for production
14
+ ```
15
+ npm run build
16
+ ```
17
+
18
+ ### Run your tests
19
+ ```
20
+ npm run test
21
+ ```
22
+
23
+ ### Lints and fixes files
24
+ ```
25
+ npm run lint
26
+ ```
27
+
28
+ ### Run your unit tests
29
+ ```
30
+ npm run test:unit
31
+ ```
32
+
33
+ ### Customize configuration
34
+ See [Configuration Reference](https://cli.vuejs.org/config/).
package/ifc-cli.js ADDED
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env node
2
+
3
+ const commander = require('commander');
4
+ const program = new commander.Command();
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const findRoot = require('find-root');
8
+ const express = require('express');
9
+ const cheerio = require('cheerio');
10
+ const https = require('https');
11
+ const devCertAuthority = require('dev-cert-authority');
12
+ const { createProxyMiddleware } = require('http-proxy-middleware');
13
+
14
+ const appPath = path.join(__dirname, './dist/');
15
+
16
+ main();
17
+
18
+ // MAIN
19
+ function main() {
20
+ const opts = parseProgramOptions();
21
+ const indexContent = generateIndex(appPath, opts.clientConfigFile);
22
+
23
+ app = express();
24
+ app.use(/^\/$/, serveIndex(indexContent));
25
+ app.use(
26
+ '/proxy',
27
+ createProxyMiddleware({
28
+ router: extractTargetHost,
29
+ pathRewrite: rewritePath,
30
+ onError: (err, req, res) => {
31
+ console.log('ERROR', err);
32
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
33
+ res.end(err.message);
34
+ },
35
+ target: `http://localhost:${opts.port}` //Required by middleware, but should be always overriden by the previous options
36
+ })
37
+ );
38
+ app.use(express.static(appPath));
39
+
40
+ if (opts.ssl) {
41
+ const options = getSslOpts(opts.sslCert, opts.sslKey);
42
+ https.createServer(options, app).listen(opts.port);
43
+ } else {
44
+ app.listen(opts.port);
45
+ }
46
+
47
+ const localhostUrl =
48
+ (opts.ssl ? 'https' : 'http') + '://localhost:' + opts.port + '/';
49
+ console.log(`Listening on port ${opts.port}...`);
50
+ console.log(`Visit host app at: ${localhostUrl}`);
51
+ }
52
+
53
+ // HELPER FUNCTIONS
54
+
55
+ function parseProgramOptions() {
56
+ const projRoot = findRoot(process.cwd());
57
+ const defaultJsConfig = path.join(projRoot, 'ifc-cli.config.js');
58
+
59
+ program
60
+ .option(
61
+ '-f, --config-file <file>',
62
+ 'iframe client configuration file',
63
+ defaultJsConfig
64
+ )
65
+ .option('-p, --port <port_num>', 'port number to host on', 3000)
66
+ .option('-s, --ssl', 'serve over https')
67
+ .option('--ssl-cert <cert_path>', 'certificate file to use for https')
68
+ .option('--ssl-key <key_path>', 'key file to use for https');
69
+ program.on('--help', showHelpText);
70
+
71
+ program.parse(process.argv);
72
+
73
+ return {
74
+ clientConfigFile: findConfigFile(program.configFile),
75
+ port: program.port,
76
+ ssl: program.ssl,
77
+ sslCert: program.sslCert,
78
+ sslKey: program.sslKey
79
+ };
80
+ }
81
+
82
+ function showHelpText() {
83
+ const configExample = path.join(__dirname, 'example-ifc.config.js');
84
+ console.log(`
85
+ This program will start a server for a basic iframe-coordinator host app. In
86
+ order to configure the frame-router element and any other custom logic needed
87
+ in the host app, a config file must be provided which should assign a
88
+ function to \`module.exports\` that will be passed the frame-router element
89
+ as an input once it has been mounted. The function should return a config
90
+ object with the following fields:
91
+
92
+ - publishTopics: A list of messaging topics the client publishes on
93
+
94
+ Keep in mind that the config file is not a true commonJS module, and
95
+ will be evaluated directly inside the browser in an immediately invoked
96
+ function expression.
97
+
98
+ Here is an example config file:
99
+ `);
100
+ console.log(fs.readFileSync(configExample).toString());
101
+ }
102
+
103
+ function serveIndex(indexContent) {
104
+ return function(req, res) {
105
+ res.send(indexContent);
106
+ };
107
+ }
108
+
109
+ function generateIndex(appPath, clientConfigFile) {
110
+ const baseIndex = fs
111
+ .readFileSync(path.join(appPath, 'index.html'))
112
+ .toString();
113
+ const $ = cheerio.load(baseIndex);
114
+ $('head').append(configScript(clientConfigFile));
115
+ return $.html();
116
+ }
117
+
118
+ function configScript(scriptFile) {
119
+ const scriptContents = fs.readFileSync(scriptFile).toString();
120
+ // This is a bit of a kludge but it should suffice for now.
121
+ return `
122
+ <script type="text/javascript">
123
+ (function () {
124
+ let module = {};
125
+ ${scriptContents}
126
+ window.routerSetup = module.exports;
127
+ })()
128
+ </script>
129
+ `;
130
+ }
131
+
132
+ // Find the configuration file
133
+ function findConfigFile(cliPath) {
134
+ let configPath = relativizePath(cliPath);
135
+ if (fs.existsSync(configPath)) {
136
+ return configPath;
137
+ } else {
138
+ console.log(`No client configuration file found @ ${cliPath}.`);
139
+ process.exit(1);
140
+ }
141
+ }
142
+
143
+ function getSslOpts(certPath, keyPath) {
144
+ if (!certPath || !keyPath) {
145
+ return devCertAuthority('localhost');
146
+ }
147
+ if (fs.existsSync(certPath) && fs.existsSync(keyPath)) {
148
+ return {
149
+ cert: fs.readFileSync(certPath),
150
+ key: fs.readFileSync(keyPath)
151
+ };
152
+ } else {
153
+ console.log(`Certificate files not found @ ${certPath}, and ${keyPath}`);
154
+ process.exit(1);
155
+ }
156
+ }
157
+
158
+ // Make sure a path isn't interpreted as a module when required.
159
+ function relativizePath(inPath) {
160
+ let outPath = path.relative(process.cwd(), inPath);
161
+ if (!outPath.startsWith('.')) {
162
+ outPath = './' + outPath;
163
+ }
164
+ return outPath;
165
+ }
166
+
167
+ function extractTargetHost(req) {
168
+ return extractProxyUrl(req.path).origin;
169
+ }
170
+
171
+ function rewritePath(path) {
172
+ return extractProxyUrl(path).pathname;
173
+ }
174
+
175
+ function extractProxyUrl(path) {
176
+ const proxyPath = path.replace(/^\/proxy\//, '');
177
+ let newUrl;
178
+ try {
179
+ newUrl = new URL(decodeURIComponent(proxyPath));
180
+ } catch (e) {
181
+ // It would be nice to failed more gracefully here and return a 500, but that doesn't
182
+ // seem to be possible with the way the proxy middleware works.
183
+ // See: https://github.com/chimurai/http-proxy-middleware/issues/411
184
+ console.error(`
185
+ **** INVALID URL IN PROXY PATH ****
186
+ ${e.message}
187
+ `);
188
+ throw e;
189
+ }
190
+ return newUrl;
191
+ }
package/index.html ADDED
@@ -0,0 +1,20 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
6
+ <meta name="viewport" content="width=device-width,initial-scale=1.0" />
7
+ <link rel="icon" href="/favicon.ico" />
8
+ <title>ifc-wrapper</title>
9
+ </head>
10
+ <body>
11
+ <noscript>
12
+ <strong
13
+ >We're sorry but ifc-wrapper doesn't work properly without JavaScript
14
+ enabled. Please enable it to continue.</strong
15
+ >
16
+ </noscript>
17
+ <div id="app"></div>
18
+ <script type="module" src="/src/main.ts"></script>
19
+ </body>
20
+ </html>
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "iframe-coordinator-cli",
3
+ "version": "5.0.0-beta.1",
4
+ "description": "CLI for local iframe-coordinator development",
5
+ "dependencies": {
6
+ "cheerio": "^1.0.0-rc.10",
7
+ "commander": "^2.20.3",
8
+ "custom-event-polyfill": "^1.0.7",
9
+ "dev-cert-authority": "^1.1.1",
10
+ "express": "^4.17.3",
11
+ "find-root": "^1.1.0",
12
+ "http-proxy-middleware": "^1.3.1",
13
+ "iframe-coordinator": "file:../iframe-coordinator",
14
+ "vue": "^2.7.10",
15
+ "vue-class-component": "^7.0.2",
16
+ "vue-notification": "^1.3.16",
17
+ "vue-property-decorator": "^8.1.0",
18
+ "vue-router": "^3.0.3",
19
+ "vuex": "^3.0.1"
20
+ },
21
+ "devDependencies": {
22
+ "@vitejs/plugin-vue2": "^2.0.0",
23
+ "typescript": "^4.8.3",
24
+ "vite": "^3.1.3"
25
+ },
26
+ "bin": {
27
+ "ifc-cli": "ifc-cli.js"
28
+ },
29
+ "scripts": {
30
+ "serve": "vite",
31
+ "build": "vite build"
32
+ },
33
+ "author": "",
34
+ "license": "MIT"
35
+ }
Binary file
package/src/App.vue ADDED
@@ -0,0 +1,39 @@
1
+ <template>
2
+ <div id="app">
3
+ <notifications group="toast" id="toastNotifications" position="top right"></notifications>
4
+ <notifications group="pubsub" id="pubsubEvents" position="top left"></notifications>
5
+ <notifications group="keydown" id="keydownEvents" position="bottom left"></notifications>
6
+ <router-view/>
7
+ </div>
8
+ </template>
9
+
10
+ <style>
11
+ html {
12
+ height: 100%;
13
+ }
14
+ body {
15
+ padding: 0;
16
+ margin: 0;
17
+ height: 100%;
18
+ }
19
+
20
+ #app {
21
+ font-family: 'Avenir', Helvetica, Arial, sans-serif;
22
+ -webkit-font-smoothing: antialiased;
23
+ -moz-osx-font-smoothing: grayscale;
24
+ text-align: center;
25
+ color: #2c3e50;
26
+ height: 100%;
27
+ }
28
+
29
+ .vue-notification.toast {
30
+ background-color: #2a60c8;
31
+ color: #fdfdfd;
32
+ border-color: #75a8ff;
33
+ }
34
+
35
+ .vue-notification.pubsub {
36
+ background-color: #444a52;
37
+ border-color: #ff4f1f;
38
+ }
39
+ </style>
Binary file
@@ -0,0 +1,36 @@
1
+ module.exports = function(frameRouter) {
2
+ frameRouter.setupFrames(
3
+ {
4
+ app1: {
5
+ url: 'http://localhost:8080/client-app-1/#/',
6
+ assignedRoute: '/app1'
7
+ },
8
+ app2: {
9
+ url: 'http://localhost:8080/client-app-2/#/',
10
+ assignedRoute: '/app2',
11
+ sandbox: 'allow-presentation', // optional
12
+ allow: 'microphone http://localhost:8080;' // optional
13
+ }
14
+ },
15
+ {
16
+ locale: 'en-US',
17
+ hostRootUrl: window.location.origin,
18
+ registeredKeys: [
19
+ { key: 'a', ctrlKey: true },
20
+ { key: 'b', altKey: true },
21
+ { key: 'a', ctrlKey: true, shiftKey: true }
22
+ ],
23
+ custom: getCustomClientData()
24
+ }
25
+ );
26
+
27
+ return {
28
+ // These are the topics that the host app should display payloads for when
29
+ // the client publishes on them.
30
+ publishTopics: ['publish.topic']
31
+ };
32
+ };
33
+
34
+ function getCustomClientData() {
35
+ return { test: 'This is only a test' };
36
+ }
package/src/main.ts ADDED
@@ -0,0 +1,41 @@
1
+ import { registerCustomElements } from 'iframe-coordinator/dist';
2
+
3
+ import Vue from 'vue';
4
+ import Notifications from 'vue-notification';
5
+ import App from './App.vue';
6
+ import router from './router';
7
+ import store from './store';
8
+
9
+ registerCustomElements();
10
+
11
+ window.addEventListener('error', evt => {
12
+ console.error('Uncaught Error:', evt.error);
13
+ });
14
+
15
+ Vue.config.productionTip = false;
16
+
17
+ Vue.use(Notifications);
18
+
19
+ new Vue({
20
+ router,
21
+ store,
22
+ render: h => h(App),
23
+ created() {
24
+ // IE11 routing workaround
25
+ if (
26
+ '-ms-scroll-limit' in document.documentElement.style &&
27
+ '-ms-ime-align' in document.documentElement.style
28
+ ) {
29
+ window.addEventListener(
30
+ 'hashchange',
31
+ event => {
32
+ const currentPath = window.location.hash.slice(1);
33
+ if (this.$route.path !== currentPath) {
34
+ this.$router.push(currentPath);
35
+ }
36
+ },
37
+ false
38
+ );
39
+ }
40
+ }
41
+ }).$mount('#app');
package/src/router.ts ADDED
@@ -0,0 +1,17 @@
1
+ import Vue from 'vue';
2
+ import Router from 'vue-router';
3
+ import IframeEmbed from './views/IframeEmbed.vue';
4
+
5
+ Vue.use(Router);
6
+
7
+ export default new Router({
8
+ routes: [
9
+ {
10
+ path: '*',
11
+ component: IframeEmbed,
12
+ props: router => ({
13
+ frameRoute: router.fullPath
14
+ })
15
+ }
16
+ ]
17
+ });
@@ -0,0 +1,13 @@
1
+ import Vue, { VNode } from 'vue';
2
+
3
+ declare global {
4
+ namespace JSX {
5
+ // tslint:disable no-empty-interface
6
+ interface Element extends VNode {}
7
+ // tslint:disable no-empty-interface
8
+ interface ElementClass extends Vue {}
9
+ interface IntrinsicElements {
10
+ [elem: string]: any;
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,4 @@
1
+ declare module '*.vue' {
2
+ import Vue from 'vue';
3
+ export default Vue;
4
+ }
package/src/store.ts ADDED
@@ -0,0 +1,16 @@
1
+ import Vue from 'vue';
2
+ import Vuex from 'vuex';
3
+
4
+ Vue.use(Vuex);
5
+
6
+ export default new Vuex.Store({
7
+ state: {
8
+
9
+ },
10
+ mutations: {
11
+
12
+ },
13
+ actions: {
14
+
15
+ },
16
+ });
@@ -0,0 +1,157 @@
1
+ <template>
2
+ <div id="routerLayout">
3
+ <div class="explainer">
4
+ Showing
5
+ <span class="app-route">{{ frameRoute }}</span>
6
+ as
7
+ <span class="frame-url">{{ frameUrl }}</span>
8
+ </div>
9
+ <div v-if="showMenu" id="appMenu">
10
+ <h2>No app is registered for {{ frameRoute }}</h2>
11
+
12
+ <div v-if="Object.keys(clientConfig).length > 0">
13
+ <p>To see your embedded apps, use one of the links below.</p>
14
+ <nav>
15
+ <ul>
16
+ <li v-for="(client, id) in clientConfig" v-bind:key="id">
17
+ <a v-bind:href="'#' + client.assignedRoute">{{id}} @ {{client.assignedRoute}}</a>
18
+ </li>
19
+ </ul>
20
+ </nav>
21
+ </div>
22
+ <div v-else>
23
+ <p>I couldn't find any registered client applications. Please check your ifc-cli configuration file.</p>
24
+ </div>
25
+ </div>
26
+ <frame-router
27
+ id="frameRouter"
28
+ v-bind:route="frameRoute"
29
+ v-on:notifyRequest="displayToast"
30
+ v-on:registeredKeyFired="handleKeyEvent"
31
+ v-on:navRequest="handleNav"
32
+ v-on:frameTransition="updateFrameUrl"
33
+ ></frame-router>
34
+ </div>
35
+ </template>
36
+
37
+ <script>
38
+ export default {
39
+ name: 'iframeEmbed',
40
+ props: ['frameRoute'],
41
+ data() {
42
+ return {
43
+ frameUrl: '',
44
+ showMenu: true,
45
+ clientConfig: {}
46
+ };
47
+ },
48
+ methods: {
49
+ displayToast(event) {
50
+ const customJson = JSON.stringify(event.detail.custom, null, 2);
51
+ const messageHtml = `<div class="message">${event.detail.message}</div>
52
+ <pre class="customData">${customJson}</pre>`;
53
+ this.$notify({
54
+ group: 'toast',
55
+ title: event.detail.title,
56
+ text: messageHtml,
57
+ duration: event.detail.custom.duration || -1,
58
+ type: 'toast'
59
+ });
60
+ },
61
+ handleNav(event) {
62
+ // TODO: detect and handle external URLs properly
63
+ const requestedUrl = new URL(event.detail.url);
64
+ window.location.hash = requestedUrl.hash;
65
+ },
66
+ handleKeyEvent(event) {
67
+ this.$notify({
68
+ group: 'keydown',
69
+ title: `registeredKeyFired event from ${event.detail.clientId}`,
70
+ text: `<pre>${JSON.stringify(event.detail, null, 2)}</pre>`,
71
+ duration: 3000,
72
+ type: 'registeredKeyFired'
73
+ });
74
+ },
75
+ notifyPubSub(event) {
76
+ const jsonStr = JSON.stringify(event, null, 2);
77
+ this.$notify({
78
+ group: 'pubsub',
79
+ title: `${event.clientId} on ${event.topic}`,
80
+ text: `<pre>${jsonStr}</pre>`,
81
+ duration: -1,
82
+ type: 'pubsub'
83
+ });
84
+ },
85
+ updateFrameUrl(event) {
86
+ this.frameUrl = event.detail;
87
+ this.showMenu = this.frameUrl === 'about:blank';
88
+ }
89
+ },
90
+ mounted() {
91
+ // Call the custom config set up on the CLI.
92
+ const oldSetupFrames = frameRouter.setupFrames;
93
+ frameRouter.setupFrames = (...args) => {
94
+ this.clientConfig = args[0];
95
+ oldSetupFrames.apply(frameRouter, args);
96
+ };
97
+
98
+ if (window.routerSetup && typeof window.routerSetup === 'function') {
99
+ const clientConfig = window.routerSetup(frameRouter);
100
+ if (clientConfig.publishTopics) {
101
+ clientConfig.publishTopics.forEach(topic => {
102
+ frameRouter.messaging.addListener(topic, publication => {
103
+ this.notifyPubSub(publication);
104
+ });
105
+ });
106
+ }
107
+ } else {
108
+ // tslint:disable-next-line
109
+ console.log(`====== ERROR ======
110
+ Could not find a function to set up the frame-router element with. Your
111
+ JS configuration file must assign a set-up function to \`module.exports\`.
112
+ Run:
113
+ ifc-cli --help
114
+ for more details.
115
+ `);
116
+ }
117
+ }
118
+ };
119
+ </script>
120
+
121
+ <style>
122
+ #routerLayout {
123
+ display: flex;
124
+ flex-direction: column;
125
+ height: 100%;
126
+ }
127
+ #frameRouter {
128
+ flex-grow: 1;
129
+ flex-shrink: 1;
130
+ flex-basis: 100vh;
131
+ }
132
+ #routerLayout .explainer {
133
+ padding: 20px;
134
+ background-color: #33383d;
135
+ color: #fdfdfd;
136
+ border-bottom: 2px solid #ff4f1f;
137
+ }
138
+ #routerLayout .app-route,
139
+ #routerLayout .frame-url {
140
+ color: #ff4f1f;
141
+ }
142
+
143
+ #appMenu {
144
+ max-width: 80ch;
145
+ margin: auto;
146
+ }
147
+ #appMenu nav {
148
+ text-align: left;
149
+ }
150
+ #appMenu ul {
151
+ margin: 0;
152
+ padding: 0;
153
+ }
154
+ #appMenu li {
155
+ list-style: none;
156
+ }
157
+ </style>
package/tsconfig.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "esnext",
4
+ "module": "esnext",
5
+ "strict": true,
6
+ "jsx": "preserve",
7
+ "importHelpers": true,
8
+ "moduleResolution": "node",
9
+ "experimentalDecorators": true,
10
+ "esModuleInterop": true,
11
+ "allowSyntheticDefaultImports": true,
12
+ "sourceMap": true,
13
+ "baseUrl": ".",
14
+ "paths": {
15
+ "@/*": [
16
+ "src/*"
17
+ ]
18
+ },
19
+ "lib": [
20
+ "esnext",
21
+ "dom",
22
+ "dom.iterable",
23
+ "scripthost"
24
+ ]
25
+ },
26
+ "include": [
27
+ "src/**/*.ts",
28
+ "src/**/*.tsx",
29
+ "src/**/*.vue",
30
+ "tests/**/*.ts",
31
+ "tests/**/*.tsx"
32
+ ],
33
+ "exclude": [
34
+ "node_modules"
35
+ ]
36
+ }
package/tslint.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "defaultSeverity": "warning",
3
+ "extends": ["tslint:recommended"],
4
+ "linterOptions": {
5
+ "exclude": ["node_modules/**"]
6
+ },
7
+ "rules": {
8
+ "quotemark": [true, "single"],
9
+ "indent": [true, "spaces", 2],
10
+ "interface-name": false,
11
+ "ordered-imports": false,
12
+ "object-literal-sort-keys": false,
13
+ "no-consecutive-blank-lines": false,
14
+ "trailing-comma": false,
15
+ "arrow-parens": false
16
+ }
17
+ }
package/vite.config.js ADDED
@@ -0,0 +1,5 @@
1
+ import vue from '@vitejs/plugin-vue2';
2
+
3
+ export default {
4
+ plugins: [vue()]
5
+ };