create-jwn-js 1.0.2 → 1.0.3

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.
Files changed (41) hide show
  1. package/index.js +11 -0
  2. package/package.json +1 -1
  3. package/template-vite-vue3-ts/.gitignore +5 -0
  4. package/template-vite-vue3-ts/connect-template.json +36 -0
  5. package/template-vite-vue3-ts/cron.js +90 -0
  6. package/template-vite-vue3-ts/docs/index.md +27 -0
  7. package/template-vite-vue3-ts/index.html +18 -0
  8. package/template-vite-vue3-ts/jest.config.ts +38 -0
  9. package/template-vite-vue3-ts/package.json +57 -0
  10. package/template-vite-vue3-ts/postcss.config.js +28 -0
  11. package/template-vite-vue3-ts/src/backend/server/helpers/createConfigServer.ts +16 -0
  12. package/template-vite-vue3-ts/src/backend/server/helpers/createMariadb.ts +16 -0
  13. package/template-vite-vue3-ts/src/backend/server/helpers/createMemcached.ts +15 -0
  14. package/template-vite-vue3-ts/src/backend/server/helpers/readManifest.ts +16 -0
  15. package/template-vite-vue3-ts/src/backend/server/index.ts +51 -0
  16. package/template-vite-vue3-ts/src/backend/server/site-schema.ts +21 -0
  17. package/template-vite-vue3-ts/src/backend/server/web-routes/index.ts +9 -0
  18. package/template-vite-vue3-ts/src/backend/views/Index/IndexController.ts +16 -0
  19. package/template-vite-vue3-ts/src/frontend/App.vue +30 -0
  20. package/template-vite-vue3-ts/src/frontend/entry-client.ts +2 -0
  21. package/template-vite-vue3-ts/src/frontend/entry-server.ts +4 -0
  22. package/template-vite-vue3-ts/src/frontend/helpers/customerPrefetch.ts +46 -0
  23. package/template-vite-vue3-ts/src/frontend/main.ts +68 -0
  24. package/template-vite-vue3-ts/src/frontend/routes/index.ts +42 -0
  25. package/template-vite-vue3-ts/src/frontend/shims-plugins.d.ts +18 -0
  26. package/template-vite-vue3-ts/src/frontend/shims-vue.d.ts +5 -0
  27. package/template-vite-vue3-ts/src/frontend/shims-vuex.d.ts +10 -0
  28. package/template-vite-vue3-ts/src/frontend/store/fetch.ts +174 -0
  29. package/template-vite-vue3-ts/src/frontend/store/index.ts +131 -0
  30. package/template-vite-vue3-ts/src/frontend/store/types.d.ts +83 -0
  31. package/template-vite-vue3-ts/src/frontend/views/Error/Error.vue +62 -0
  32. package/template-vite-vue3-ts/src/frontend/views/Index/Index.vue +27 -0
  33. package/template-vite-vue3-ts/src/frontend/views/Index/__tests__/Index.spec.ts +31 -0
  34. package/template-vite-vue3-ts/src/frontend/vite-env.d.ts +1 -0
  35. package/template-vite-vue3-ts/src/helpers/langLink.ts +17 -0
  36. package/template-vite-vue3-ts/src/helpers/parseLang.ts +10 -0
  37. package/template-vite-vue3-ts/src/scripts/jest.global.setup.ts +23 -0
  38. package/template-vite-vue3-ts/src/scripts/jest.global.teardown.ts +10 -0
  39. package/template-vite-vue3-ts/src/scripts/jest.setup.ts +16 -0
  40. package/template-vite-vue3-ts/tsconfig.json +47 -0
  41. package/template-vite-vue3-ts/vite.config.ts +87 -0
@@ -0,0 +1,42 @@
1
+ import {createRouter, createMemoryHistory, createWebHistory} from "vue-router";
2
+ import Error from "../views/Error/Error.vue";
3
+ import Index from "../views/Index/Index.vue";
4
+
5
+ export default (opt: Record<string, any>) => createRouter({
6
+ history: opt.isClient ? createWebHistory() : createMemoryHistory(),
7
+ routes: [
8
+ {
9
+ path: '/:lang(ua|ru|en)?',
10
+ component: Index,
11
+ name: "Index"
12
+ },
13
+ // Error
14
+ {
15
+ path: '/:code(400|401|402|403|404)',
16
+ name: 'Error',
17
+ component: Error,
18
+ props: true
19
+ },
20
+ {
21
+ path: '/:path(.*)',
22
+ redirect: '/404'
23
+ },
24
+ ],
25
+ async scrollBehavior(to, from, savedPosition) {
26
+
27
+ // Scroll to element
28
+ if(to.query?.el) {
29
+ const el: string = to.query.el as string;
30
+
31
+ return {
32
+ el
33
+ };
34
+ }
35
+
36
+ // meta scrollToTop
37
+ if(to.meta.scrollToTop) {
38
+
39
+ return {left: 0, top: 0};
40
+ }
41
+ }
42
+ });
@@ -0,0 +1,18 @@
1
+ import { ComponentCustomProperties, ComponentCustomOptions } from "vue";
2
+ import {AmittEmitter} from "amitt";
3
+ import type {NavigationGuardFetchWithThis} from "@vuemod/prefetch";
4
+
5
+ declare module '@vue/runtime-core' {
6
+ interface ComponentCustomProperties {
7
+ $getLang: () => string,
8
+ $l:(params: Record<string, any>) => string,
9
+ $smoothScroll:(params: Record<string, any>) => string,
10
+ $emitter: AmittEmitter,
11
+ $refs: Record<string, any>,
12
+ $confirm: Record<string, any>
13
+ }
14
+
15
+ interface ComponentCustomOptions {
16
+ prefetch?: NavigationGuardFetchWithThis<undefined>
17
+ }
18
+ }
@@ -0,0 +1,5 @@
1
+ declare module '*.vue' {
2
+ import { DefineComponent } from 'vue';
3
+ const component: DefineComponent<{}, {}, any>
4
+ export default component
5
+ }
@@ -0,0 +1,10 @@
1
+ import { ComponentCustomProperties } from 'vue'
2
+ import { Store } from 'vuex'
3
+
4
+ declare module '@vue/runtime-core' {
5
+
6
+ // provide typings for `this.$store`
7
+ interface ComponentCustomProperties {
8
+ $store: Store<any>
9
+ }
10
+ }
@@ -0,0 +1,174 @@
1
+ import {isBrowser} from "easy-ash";
2
+ import fetch from "cross-fetch";
3
+ import {ApiError} from "@jwn-js/common/ApiError";
4
+ import type {ActionContext} from "vuex";
5
+
6
+ import type {FetchOptions, FetchPayload} from "./types";
7
+ import {StateBase} from "./index";
8
+
9
+ /**
10
+ * Fetch action
11
+ * @param state store state
12
+ * @param commit
13
+ * @param getters store getters
14
+ * @param dispatch
15
+ * @param params payload params
16
+ * @param options request options
17
+ */
18
+ export const fetchAction = async(
19
+ {state, commit, getters, dispatch}: ActionContext<StateBase, StateBase>,
20
+ [
21
+ params = {},
22
+ options
23
+ ]: FetchPayload
24
+ ): Promise<any> => {
25
+ const headers: Record<string, string> = {};
26
+ try {
27
+ const isFormData = typeof FormData !== "undefined" && params instanceof FormData;
28
+ const ssrHeaders = (state.context.headers || {});
29
+
30
+ // Add auto lang param
31
+ if(isFormData) {
32
+ if(!params.has("lang")) {
33
+ params.append("lang", state.lang);
34
+ }
35
+ } else {
36
+ if(!(params as Record<string, any>).lang) {
37
+ (params as Record<string, any>).lang = state.lang;
38
+ }
39
+ }
40
+
41
+ if(isBrowser()) {
42
+ [
43
+ "host",
44
+ "connection",
45
+ "user-agent",
46
+ "referer",
47
+ "accept-encoding",
48
+ "cookie",
49
+ "sec-ch-ua",
50
+ "sec-ch-ua-mobile",
51
+ "sec-fetch-site",
52
+ "sec-fetch-mode",
53
+ "sec-fetch-dest",
54
+ "sec-fetch-user",
55
+ "sec-ch-ua-platform"
56
+ ].map(key => delete ssrHeaders[key]);
57
+ } else {
58
+ // if server request - add ip to x-forwarded-for
59
+ ssrHeaders["x-forwarded-for"] = state.context.ip;
60
+ }
61
+
62
+ const defaultOptions: FetchOptions = {
63
+ url: `${getters.getBaseUrl}/web`,
64
+ method: "get",
65
+ preloader: true,
66
+ error: true,
67
+ compress: true,
68
+ headers: {
69
+ // Proxy ssr headers to request
70
+ ...ssrHeaders,
71
+ ...(isFormData ? {} : {"content-type": "application/json"})
72
+ }
73
+ }
74
+ options = Object.assign(
75
+ {},
76
+ defaultOptions,
77
+ options || {}
78
+ ) as FetchOptions;
79
+
80
+ if(options?.preloader) {
81
+ commit('SET_LOADING', true);
82
+ }
83
+
84
+ // Merge headers
85
+ if(options.mergeHeaders) {
86
+ options.headers = Object.assign(options.headers || {}, options.mergeHeaders);
87
+ delete options.mergeHeaders;
88
+ }
89
+
90
+ const url = new URL(options.url as string);
91
+
92
+ if(!["get", "head"].includes(options?.method?.toLowerCase() as string)) {
93
+
94
+ if(isFormData) {
95
+ options.body = params as FormData;
96
+ } else {
97
+ options.body = JSON.stringify(params);
98
+ }
99
+ } else {
100
+ url.search = new URLSearchParams(params as Record<string, any>).toString();
101
+ }
102
+
103
+ const response = await fetch(url.toString(), options);
104
+ response.headers.forEach((value: string, key: string) => headers[key] = value);
105
+ const json = await response.json();
106
+
107
+ // status code error
108
+ if(!response.ok) {
109
+ throw new ApiError({
110
+ code: json.code || 0,
111
+ statusCode: response.status,
112
+ message: "Api request error",
113
+ headers,
114
+ data: json
115
+ });
116
+ }
117
+
118
+ return {body: json, response};
119
+ } catch(e: any) {
120
+
121
+ // Network error
122
+ if(e instanceof TypeError) {
123
+ if(isBrowser()) {
124
+ commit('SET_NOTIFICATION', {
125
+ type: "danger",
126
+ text: "Check your internet connection. No contact)"
127
+ });
128
+ }
129
+ }
130
+
131
+ // Api Error instance
132
+ if(e instanceof ApiError) {
133
+ const statusCode = e.getStatusCode();
134
+
135
+ // Error notification
136
+ if(statusCode === 400 && options?.error) {
137
+ commit('SET_NOTIFICATION', {type: "danger", text: e.getData()["error"]});
138
+ }
139
+
140
+ if(statusCode === 404 && options?.error) {
141
+ if(isBrowser()) {
142
+ commit('SET_NOTIFICATION', {
143
+ type: "danger",
144
+ text: "Something went wrong. We are working hard to fix this)"
145
+ });
146
+ } else {
147
+ await dispatch('redirect', {
148
+ url: "/404",
149
+ code: 307
150
+ });
151
+ }
152
+ }
153
+
154
+ // Redirect (if response code 410)
155
+ if(options?.method?.toLowerCase() === "get") {
156
+ if(statusCode === 410) {
157
+ const location = e.getHeaders()?.location;
158
+
159
+ if(location) {
160
+ await dispatch("redirect", {url: location, code: 307});
161
+ }
162
+ }
163
+ }
164
+ }
165
+
166
+ throw e;
167
+ } finally {
168
+
169
+ if(options?.preloader) {
170
+ commit('SET_LOADING', false);
171
+ }
172
+
173
+ }
174
+ }
@@ -0,0 +1,131 @@
1
+ import { createStore } from "vuex";
2
+ import {langLink} from "@/helpers/langLink";
3
+ import {fetchAction} from "./fetch";
4
+
5
+ import type {Context} from "vite-ssr-vue";
6
+ import type {HeadObject} from "@vueuse/head";
7
+ import type {Options, Breadcrumbs, Notification, FetchPayload} from "./types";
8
+
9
+ /**
10
+ * Lang values
11
+ */
12
+ type Lang = "ru"|"ua";
13
+
14
+ /**
15
+ * Base store structure
16
+ */
17
+ export interface StateBase {
18
+
19
+ // Ssr context
20
+ context: Context,
21
+
22
+ // Head Object
23
+ page: HeadObject,
24
+
25
+ // Breadcrumbs
26
+ way: Array<Breadcrumbs>,
27
+
28
+ // Before route - set to new value
29
+ toLang: Lang,
30
+
31
+ // After route set to new value
32
+ lang: Lang,
33
+
34
+ // is data loading
35
+ loading: boolean
36
+ }
37
+
38
+ /**
39
+ * Create store instance
40
+ * @param opt
41
+ */
42
+ export default (opt: Options) => createStore({
43
+ state: () => {
44
+
45
+ return {
46
+ context: {} as Context,
47
+ page: {},
48
+ way: [],
49
+ toLang: "ru",
50
+ lang: "ru",
51
+ loading: false,
52
+ } as StateBase
53
+ },
54
+ mutations: {
55
+ MERGE_CONTEXT(state, context) {
56
+ Object.assign(state.context, context);
57
+ },
58
+ MERGE_CONTEXT_HEADERS(state, headers: Record<string, any>) {
59
+ state.context.responseHeaders = Object.assign(state.context.responseHeaders || {}, headers);
60
+ },
61
+ SET_PAGE(state, page: HeadObject) {
62
+ state.page = page || {};
63
+ },
64
+ SET_WAY(state, way: Array<Breadcrumbs>) {
65
+ state.way = way || [];
66
+ },
67
+ SET_LANG(state, lang: Lang) {
68
+ state.lang = lang;
69
+ },
70
+ SET_TO_LANG(state, lang: Lang) {
71
+ state.toLang = lang;
72
+ },
73
+ SET_LOADING(state, loading: boolean) {
74
+ state.loading = loading
75
+ }
76
+ },
77
+ getters: {
78
+ getToLang(state): Lang {
79
+
80
+ return state.toLang;
81
+ },
82
+ getLang(state): Lang {
83
+
84
+ return state.lang;
85
+ },
86
+ urlFromLang: state => (url = '/', lang = state.toLang) => langLink(url, lang),
87
+
88
+ /**
89
+ * Get absolute url for requests
90
+ * @param state
91
+ */
92
+ getBaseUrl(state): string {
93
+
94
+ return `${state.context.protocol}://${state.context.hostname}`;
95
+ }
96
+ },
97
+ actions: {
98
+
99
+ /**
100
+ * Base fetch data action
101
+ * @param state
102
+ * @param payload
103
+ * @returns
104
+ */
105
+ async fetch(
106
+ state, payload: FetchPayload
107
+ ): Promise<{body: Record<string, any>, response: Response}> {
108
+
109
+ return fetchAction(state, payload);
110
+ },
111
+
112
+ /**
113
+ * Redirect
114
+ * @param context
115
+ * @param url auto transform to multilang
116
+ * @param query query params
117
+ * @param code status code for server, when on the client is discarded
118
+ */
119
+ async redirect(
120
+ {state, getters},
121
+ {url, code, query} : {url: string, code: 301|307, query?:Record<string, any>}
122
+ ):Promise<void> {
123
+
124
+ if(query) {
125
+ url = url + new URLSearchParams(query as Record<string, any>).toString();
126
+ }
127
+
128
+ await opt.redirect(langLink(url, state.toLang), code || 307);
129
+ }
130
+ }
131
+ });
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Fetch payload
3
+ */
4
+ export declare type FetchPayload = [
5
+ params:Record<string, any> | FormData,
6
+ options?:FetchOptions
7
+ ];
8
+
9
+ /**
10
+ * Addition fetch options
11
+ */
12
+ export interface FetchOptions {
13
+
14
+ /**
15
+ * relative url string
16
+ */
17
+ url?: string,
18
+
19
+ /**
20
+ * request method
21
+ */
22
+ method?: string,
23
+
24
+ /**
25
+ * is need preloader
26
+ */
27
+ preloader?: boolean,
28
+
29
+ /**
30
+ * is need error notification
31
+ */
32
+ error?: boolean,
33
+
34
+ /**
35
+ * if need compress
36
+ */
37
+ compress?: boolean,
38
+
39
+ /**
40
+ * request headers
41
+ */
42
+ headers?: Record<string, string>,
43
+
44
+ /**
45
+ * Request body|FormData
46
+ */
47
+ body?:string|FormData,
48
+
49
+ /**
50
+ * mege headers to default
51
+ */
52
+ mergeHeaders?:Record<string, any>,
53
+
54
+ /**
55
+ * any addition params
56
+ */
57
+ [key:string]:any
58
+ }
59
+
60
+ /**
61
+ * Notification object
62
+ */
63
+ export interface Notification {
64
+ type: string|null,
65
+ text: string|null,
66
+ [key: string]: any
67
+ }
68
+
69
+ /**
70
+ * Create store options
71
+ */
72
+ export interface Options {
73
+ redirect: (url: string, statusCode: 301|307) => Promise<void>,
74
+ [key:string]:any
75
+ }
76
+
77
+ /**
78
+ * Breadcrumbs object
79
+ */
80
+ export interface Breadcrumbs {
81
+ test: string,
82
+ to: string|null|undefined
83
+ }
@@ -0,0 +1,62 @@
1
+ <template>
2
+ <div class="error-page">
3
+ <div class="code">{{code}}</div>
4
+ <div class="content">Oops. Something went wrong</div>
5
+ <RouterLink
6
+ class="go-home"
7
+ to="/"
8
+ underline
9
+ >Go home</RouterLink>
10
+ </div>
11
+ </template>
12
+
13
+ <script>
14
+ import {defineComponent} from "vue";
15
+ import {mapMutations} from "vuex";
16
+
17
+ export default defineComponent({
18
+ name: "Error",
19
+ async prefetch({store, isClient}, to, from) {
20
+ store.commit('SET_PAGE', {
21
+ title: "Error page. Something went wrong",
22
+ htmlAttrs: {
23
+ lang: "en"
24
+ }
25
+ });
26
+ store.commit("MERGE_CONTEXT", {statusCode: 404});
27
+ },
28
+ props: {
29
+ code: {
30
+ type: String,
31
+ default: () => "404"
32
+ }
33
+ },
34
+ created() {
35
+ this.mergeContext({statusCode: this.code})
36
+ },
37
+ methods: {
38
+ ...mapMutations({
39
+ mergeContext: 'MERGE_CONTEXT'
40
+ })
41
+ }
42
+ })
43
+ </script>
44
+
45
+ <style lang="postcss">
46
+ .error-page {
47
+ display: flex;
48
+ flex-basis: 100%;
49
+ align-items: center;
50
+ flex-flow: column wrap;
51
+ .code {
52
+ font-weight: 400;
53
+ font-size: 11rem;
54
+ }
55
+ .content {
56
+ font-size: 1.2rem;
57
+ }
58
+ .go-home {
59
+ font-weight: bold;
60
+ }
61
+ }
62
+ </style>
@@ -0,0 +1,27 @@
1
+ <template>
2
+ <h1 class="index">
3
+ {{page.h1}}
4
+ </h1>
5
+ </template>
6
+
7
+ <script lang="ts">
8
+ import {defineComponent} from "vue";
9
+ import {mapState} from "vuex";
10
+ import {customerPrefetch} from "@/frontend/helpers/customerPrefetch";
11
+
12
+ export default defineComponent({
13
+ name: "Index",
14
+ components: {
15
+ },
16
+ prefetch: customerPrefetch({controller: "Index"}),
17
+ computed: {
18
+ ...mapState(["page", "context"]),
19
+ },
20
+ })
21
+ </script>
22
+
23
+ <style lang="postcss">
24
+ .index {
25
+ font-size: 18px;
26
+ }
27
+ </style>
@@ -0,0 +1,31 @@
1
+ import { mount } from "@vue/test-utils";
2
+ import { createRouter, createWebHistory } from "vue-router";
3
+ import Index from "../Index.vue"
4
+ import {createStore} from "vuex";
5
+
6
+ // https://next.vue-test-utils.vuejs.org/guide/
7
+ describe("Index", () => {
8
+ const router = createRouter({
9
+ history: createWebHistory(),
10
+ routes: [
11
+ {path: "/", component: {}}
12
+ ]
13
+ });
14
+ const store = createStore({
15
+ state: {
16
+ page: {
17
+ h1: "Test Page"
18
+ }
19
+ }
20
+ })
21
+
22
+ it("should display header text", async() => {
23
+ const wrapper = mount(Index, {
24
+ global: {
25
+ plugins: [router, store]
26
+ }
27
+ });
28
+
29
+ expect(wrapper.html()).toContain("Test Page")
30
+ })
31
+ });
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,17 @@
1
+ /**
2
+ * For link by lang
3
+ * @param url
4
+ * @param lang
5
+ * @param defaultLang
6
+ * @returns lang-link
7
+ */
8
+ export const langLink = (
9
+ url: string,
10
+ lang: string = "ru",
11
+ defaultLang: string = "ru"
12
+ ): string => {
13
+ url = (url || "").replace(/\/(ru|ua|en)(\/|$)/, '/');
14
+ const result = (lang === defaultLang ? '' : ('/' + lang)) + url;
15
+
16
+ return result === '/' ? result : result.replace(/\/$/, '');
17
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Parse current lang from url
3
+ * @param url
4
+ * @param defaultLang
5
+ */
6
+ export const parseLang = (url: string, defaultLang = "ru") => {
7
+ const result = url.match(/^\/(ru|ua|en)\/?/i);
8
+
9
+ return result && result.length > 0 ? result[1] : defaultLang;
10
+ }
@@ -0,0 +1,23 @@
1
+ import {createServer, ViteDevServer} from "vite";
2
+ import fetch from "cross-fetch";
3
+ import {apiUrl} from "./jest.setup";
4
+
5
+ declare global {
6
+ var server: ViteDevServer;
7
+ }
8
+
9
+ export default async(config: Record<string, any>) => {
10
+ global.server = await createServer();
11
+ await global.server.listen();
12
+ await new Promise(resolve => {
13
+ const interval = setInterval(async() => {
14
+ try {
15
+ await fetch(apiUrl);
16
+ clearInterval(interval);
17
+ resolve(true);
18
+ } catch(e) {}
19
+
20
+ }, 50);
21
+ });
22
+
23
+ }
@@ -0,0 +1,10 @@
1
+ import {ViteDevServer} from "vite";
2
+
3
+ declare global {
4
+ var server: ViteDevServer;
5
+ }
6
+
7
+ export default async() => {
8
+ await new Promise(resolve => setTimeout(resolve, 100));
9
+ await global.server.close();
10
+ }
@@ -0,0 +1,16 @@
1
+ import crossFetch from "cross-fetch";
2
+ import connect from "../../connect.json"
3
+
4
+ const apiUrl = `http://${connect.server.development.host}:${connect.server.development.vite.port}`;
5
+ const defaultHeaders = {
6
+ headers: {
7
+ "user-agent": "fetch-tests",
8
+ "content-type": "application/json"
9
+ }
10
+ }
11
+ const fetch = async(
12
+ input: RequestInfo,
13
+ init: RequestInit = {}
14
+ ) => crossFetch(input, Object.assign({}, defaultHeaders, init));
15
+
16
+ export {fetch, apiUrl};