diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..e258e18 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,74 @@ +# Javascript Node CircleCI 2.0 configuration file +# +# Check https://circleci.com/docs/2.0/language-javascript/ for more details +# +version: 2 +jobs: + build: + docker: + # specify the version you desire here + - image: circleci/node:10.16 + + working_directory: ~/hypernova-vue + + steps: + - checkout + + # Download and cache dependencies + - restore_cache: + keys: + - v1-dependencies-{{ checksum "package.json" }} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- + + - run: yarn install + + - save_cache: + paths: + - node_modules + key: v1-dependencies-{{ checksum "package.json" }} + + # run linter + - run: yarn lint + + # run tests + - run: yarn test + + publish: + docker: + # specify the version you desire here + - image: circleci/node:10.16 + + working_directory: ~/hypernova-vue + + steps: + - checkout + + # Download and cache dependencies + - restore_cache: + keys: + - v1-dependencies-{{ checksum "package.json" }} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- + + - run: yarn install + + - save_cache: + paths: + - node_modules + key: v1-dependencies-{{ checksum "package.json" }} + + # run linter + - run: yarn semantic-release -d + +workflows: + version: 2 + main: + jobs: + - build + - publish: + requires: + - build + filters: + branches: + only: master \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..a9f4ed5 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +lib +node_modules \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index b8e390a..be789b4 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,5 +1,11 @@ { + "env": { + "browser": true, + "node": true, + "jest": true + }, "extends": [ + "plugin:@typescript-eslint/recommended", "airbnb-base" ], "rules": { diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..6d7b8ae --- /dev/null +++ b/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + roots: ['/src'], + transform: { + '^.+\\.tsx?$': 'ts-jest', + }, + testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], +}; diff --git a/package.json b/package.json index 47e9c30..4fa9dea 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,15 @@ { "name": "hypernova-vue", - "version": "2.1.0", + "version": "3.0.0-alpha.0", "description": "Vue bindings for Hypernova", "main": "lib/index.js", + "types": "lib/index.d.ts", "author": "Felipe Guizar Diaz ", "scripts": { - "prepublish": "npm run build", - "lint": "eslint src", - "build": "babel src -d lib" + "lint": "eslint src/**/*.ts", + "build": "tsc", + "test": "jest", + "semantic-release": "semantic-release" }, "keywords": [ "vuew", @@ -18,16 +20,21 @@ "license": "MIT", "repository": { "type": "git", - "url": "git@github.com:marconi1992/hypernova-vue.git" + "url": "https://github.com/ara-framework/hypernova-vue.git" }, "devDependencies": { - "@babel/cli": "^7.5.5", - "@babel/core": "^7.5.5", - "@babel/runtime": "^7.5.5", - "babel-preset-airbnb": "^4.0.1", - "eslint": "^5.14.1", - "eslint-config-airbnb-base": "^13.1.0", - "eslint-plugin-import": "^2.16.0" + "@babel/runtime": "^7.6.0", + "@types/jest": "^24.0.18", + "@types/node": "^12.7.4", + "@typescript-eslint/eslint-plugin": "^2.1.0", + "@typescript-eslint/parser": "^2.1.0", + "eslint": "^6.3.0", + "eslint-config-airbnb-base": "^14.0.0", + "eslint-plugin-import": "^2.18.2", + "jest": "^24.9.0", + "ts-jest": "^24.0.2", + "typescript": "^3.6.0", + "semantic-release": "^15.13.24" }, "dependencies": { "hypernova": "^2.5.0", diff --git a/server.js b/server.js new file mode 100644 index 0000000..429f220 --- /dev/null +++ b/server.js @@ -0,0 +1 @@ +module.exports = require('./lib/server.js'); diff --git a/src/__test__/index.spec.ts b/src/__test__/index.spec.ts new file mode 100644 index 0000000..e6b52d3 --- /dev/null +++ b/src/__test__/index.spec.ts @@ -0,0 +1,120 @@ +import Vue, { VNode } from 'vue'; +import { + loadById, + mountComponent, + renderInPlaceholder, + renderVue, +} from '..'; + +describe('loadById', () => { + beforeEach(() => { + document.body.innerHTML = ''; + }); + + test('should load payload by id', () => { + document.body.innerHTML = ` +
+ + `; + + const payload = loadById('Example', 'd0a0b082-dad0-4bf2-ae4f-08eff16575b4'); + + const { node, data } = payload; + + expect(node.getAttribute('data-hypernova-key')).toEqual('Example'); + expect(node.getAttribute('data-hypernova-id')).toEqual('d0a0b082-dad0-4bf2-ae4f-08eff16575b4'); + expect(data).toEqual({ + title: 'Ara Framework', + }); + }); + + test('should not load payload by id', () => { + const payload = loadById('Example', 'd0a0b082-dad0-4bf2-ae4f-08eff16575b4'); + + expect(payload).toBeNull(); + }); +}); + +describe('mountComponent', () => { + beforeEach(() => { + document.body.innerHTML = ''; + }); + + test('should mount component correctly', () => { + document.body.innerHTML = '
'; + + const app = Vue.extend({ + props: ['title'], + render(h): VNode { + return h('h1', {}, this.title); + }, + }); + + const node = document.getElementById('app'); + + mountComponent(app, node, { title: 'Ara Framework' }); + + expect(node.innerHTML).toEqual('

Ara Framework

'); + }); +}); + +describe('renderInPlaceholder', () => { + beforeEach(() => { + document.body.innerHTML = ''; + }); + + test('should render component in placeholder correctly', () => { + document.body.innerHTML = ` +
+ + `; + + const app = Vue.extend({ + props: ['title'], + render(h): VNode { + return h('h1', {}, this.title); + }, + }); + + renderInPlaceholder('Example', app, 'd0a0b082-dad0-4bf2-ae4f-08eff16575b4'); + + const expectedHTML = ` +

Ara Framework

+ + `; + expect(document.body.innerHTML).toEqual(expectedHTML); + }); +}); + +describe('renderVue', () => { + beforeEach(() => { + document.body.innerHTML = ''; + }); + + test('should render all the components in the body', () => { + document.body.innerHTML = ` +
+ +
+ + `; + + const app = Vue.extend({ + props: ['title'], + render(h): VNode { + return h('h1', {}, this.title); + }, + }); + + renderVue('Example', app); + + const expectedHTML = ` +

Ara Framework

+ +

Ara Framework 2

+ + `; + + expect(document.body.innerHTML).toEqual(expectedHTML); + }); +}); diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 5cac0dd..0000000 --- a/src/index.js +++ /dev/null @@ -1,112 +0,0 @@ -import vue from 'vue'; -import { createRenderer } from 'vue-server-renderer'; -import hypernova, { serialize, load } from 'hypernova'; -import { findNode, getData } from 'nova-helpers'; - -const { document } = global; - -const mountComponent = (Component, node, data) => { - const vm = new Component({ - propsData: data, - }); - - if (!node.firstChild) { - node.appendChild(document.createElement('div')); - } - - vm.$mount(node.children[0]); -}; - -export const Vue = vue; - -export const renderInPlaceholder = (name, Component, id) => { - const node = findNode(name, id); - const data = getData(name, id); - - if (node && data) { - mountComponent(Component, node, data); - } -}; - -export const renderVue = (name, Component) => hypernova({ - server() { - return async (propsData) => { - const vm = new Component({ - propsData, - }); - - const renderer = createRenderer(); - - const contents = await renderer.renderToString(vm); - - return serialize(name, contents, propsData); - }; - }, - - client() { - const payloads = load(name); - if (payloads) { - payloads.forEach((payload) => { - const { node, data: propsData } = payload; - - const vm = new Component({ - propsData, - }); - - vm.$mount(node.children[0]); - }); - } - - return Component; - }, -}); - - -export const renderVuex = (name, ComponentDefinition, createStore) => hypernova({ - server() { - return async (propsData) => { - const store = createStore(); - - const Component = Vue.extend({ - ...ComponentDefinition, - store, - }); - - const vm = new Component({ - propsData, - }); - - const renderer = createRenderer(); - - const contents = await renderer.renderToString(vm); - - return serialize(name, contents, { propsData, state: vm.$store.state }); - }; - }, - - client() { - const payloads = load(name); - if (payloads) { - payloads.forEach((payload) => { - const { node, data } = payload; - const { propsData, state } = data; - const store = createStore(); - - const Component = Vue.extend({ - ...ComponentDefinition, - store, - }); - - const vm = new Component({ - propsData, - }); - - vm.$store.replaceState(state); - - vm.$mount(node.children[0]); - }); - } - - return ComponentDefinition; - }, -}); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..d4950bd --- /dev/null +++ b/src/index.ts @@ -0,0 +1,117 @@ +import Vue, { VueConstructor } from 'vue'; +import hypernova, { load } from 'hypernova'; +import { findNode, getData } from 'nova-helpers'; +import { CombinedVueInstance } from 'vue/types/vue'; + +type HypernovaPayload = { + node: HTMLElement; + data: any; +} + +type VueInstance = CombinedVueInstance + +type VueWithStoreInstance = + CombinedVueInstance & { $store: any }; + +export { default as Vue } from 'vue'; + +export { load } from 'hypernova'; + +export const mountComponent = ( + Component: VueConstructor, + node: HTMLElement, + data: any, +): VueInstance => { + const vm = new Component({ + propsData: data, + }); + + if (!node.firstChild) { + node.appendChild(document.createElement('div')); + } + + vm.$mount(node.children[0]); + + return vm; +}; + +export const renderInPlaceholder = ( + name: string, + Component: VueConstructor, + id: string, +): VueInstance => { + const node: HTMLElement = findNode(name, id); + const data: any = getData(name, id); + + if (node && data) { + return mountComponent(Component, node, data); + } + + return null; +}; + +export const loadById = (name: string, id: string): HypernovaPayload => { + const node = findNode(name, id); + const data = getData(name, id); + + if (node && data) { + return { + node, + data, + }; + } + + return null; +}; + +export const renderVue = (name: string, Component: VueConstructor): void => hypernova({ + server() { + throw new Error('Use hypernova-vue/server instead'); + }, + + client() { + const payloads = load(name); + if (payloads) { + payloads.forEach((payload: HypernovaPayload) => { + const { node, data: propsData } = payload; + + mountComponent(Component, node, propsData); + }); + } + + return Component; + }, +}); + +export const renderVuex = ( + name: string, + ComponentDefinition: any, + createStore: Function, +): void => hypernova({ + server() { + throw new Error('Use hypernova-vue/server instead'); + }, + + client() { + const payloads = load(name); + if (payloads) { + payloads.forEach((payload: HypernovaPayload) => { + const { node, data } = payload; + const { propsData, state } = data; + + const store = createStore(); + + const Component: VueConstructor = Vue.extend({ + ...ComponentDefinition, + store, + }); + + const vm = mountComponent(Component, node, propsData) as VueWithStoreInstance; + + vm.$store.replaceState(state); + }); + } + + return ComponentDefinition; + }, +}); diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..2e93ce0 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,62 @@ +import Vue, { VueConstructor } from 'vue'; +import { createRenderer } from 'vue-server-renderer'; +import hypernova, { serialize } from 'hypernova'; +import { CombinedVueInstance } from 'vue/types/vue'; + + +type VueWithStoreInstance = + CombinedVueInstance & { $store: any }; + +export { default as Vue } from 'vue'; + +export const renderVue = (name: string, Component: VueConstructor): void => hypernova({ + server() { + return async (propsData: object): Promise => { + const vm = new Component({ + propsData, + }); + + const renderer = createRenderer(); + + const contents = await renderer.renderToString(vm); + + return serialize(name, contents, propsData); + }; + }, + + client() { + throw new Error('Use hypernova-vue instead'); + }, +}); + + +export const renderVuex = ( + name: string, + ComponentDefinition: any, + createStore: Function, +): void => hypernova({ + server() { + return async (propsData: object): Promise => { + const store = createStore(); + + const Component = Vue.extend({ + ...ComponentDefinition, + store, + }); + + const vm = (new Component({ + propsData, + })) as VueWithStoreInstance; + + const renderer = createRenderer(); + + const contents = await renderer.renderToString(vm); + + return serialize(name, contents, { propsData, state: vm.$store.state }); + }; + }, + + client() { + throw new Error('Use hypernova-vue instead'); + }, +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..43cddb9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "esnext", + "declaration": true, + "outDir": "./lib", + "esModuleInterop": true + }, + "include": [ + "src/**/*" + ], + "exclude": ["node_modules", "**/*.spec.ts"] +} \ No newline at end of file