Skip to content

Commit

Permalink
Release 5.12.0
Browse files Browse the repository at this point in the history
### BREAKING CHANGES:
- Removed uWSGI as main server. Now only uviccorn settings is required.
- Web server now execute migration before start.
- Setting ``addrport`` migrated to ``[web]`` section.
- Removed ``htmlmin`` from default middlewares.
- Removed ``jsmin`` dependency by default.
- Remove ``JSMinTemplateResponse``.
- Service Worker was moved to SPA generation.

### Changelog:
- Feature(backend): Complete migration to uvicorn and asyncio.
- Feature(backend): New ``web_worker`` command to start webserver (without celery worker).
- Feature(frontend): Provided support to build Service Worker by vite.

See merge request vst/vst-utils!686
  • Loading branch information
flwd3m committed Dec 20, 2024
2 parents 5804444 + fb0e658 commit 901f6ce
Show file tree
Hide file tree
Showing 54 changed files with 3,196 additions and 970 deletions.
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
include README.rst LICENSE pyproject.toml
include vstutils/settings.ini vstutils/web.ini
include vstutils/settings.ini
include requirements.txt
include requirements-git.txt
include requirements-test.txt
Expand Down
147 changes: 80 additions & 67 deletions doc/locale/ru/LC_MESSAGES/quickstart-front.po

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions doc/quickstart-front.rst
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ from root dir of your project to build static files.
Output files will be built into `{AppName}/static/spa` directory. During vstutils installation through `pip`
frontend code are being build automatically, so you may need to add `spa` directory to `.gitignore`.

You can specify the `path` to the service worker and its `scope`. By default, these values are `'/service-worker.js'` and `'/'` respectively. The `defaultNotificationOptions` allows setting properties such as icon, title, or any other notification preferences for consistency across the app.

Example of simple frontend entrypoint:

.. sourcecode:: typescript
Expand All @@ -92,6 +94,13 @@ Example of simple frontend entrypoint:
api: {
url: new URL('/api/', window.location.origin).toString(),
},
sw: {
path: '/new-sw.js',
scope: '/some-route',
},
defaultNotificationOptions: {
icon: '/static/icons/logo.svg',
},
});


Expand Down
93 changes: 93 additions & 0 deletions frontend_src/service-worker/notification-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
declare const clients: Clients;
declare const self: ServiceWorkerGlobalScope;
interface PushSubscriptionChangeEvent extends ExtendableEvent {
readonly oldSubscription: PushSubscription | null;
readonly newSubscription: PushSubscription | null;
}

export const handlePush = (defaultNotificationOptions?: NotificationOptions) => {
return (e: PushEvent) => {
try {
if (!e.data) {
console.warn('Push event has no data');
return;
}

const { type, data } = e.data.json();
if (type === 'notification') {
self.registration.showNotification(data.title, {
...(defaultNotificationOptions ?? {}),
...data.options,
});
}
} catch (err) {
console.warn('Error handling push event:', err);
}
};
};

export const handleNotificationClick = () => {
return (e: NotificationEvent) => {
e.notification.close();
if (!e.notification.data || !e.notification.data.url) {
return;
}
e.waitUntil(
clients.matchAll({ type: 'window' }).then((clientsArr) => {
const client = clientsArr[0];
if (client) {
return client.navigate(e.notification.data.url).then(() => client.focus());
}

return clients.openWindow(e.notification.data.url).then((client) => {
if (client) {
return client.focus();
}
});
}),
);
};
};

export const handlePushSubscriptionChange = (apiUrl?: string | URL) => {
return (e: Event) => {
const event = e as PushSubscriptionChangeEvent;
if (!event.oldSubscription || !event.oldSubscription.endpoint) {
console.warn('Push subscription change event has no old subscription');
return;
}
event.waitUntil(
self.registration.pushManager
.subscribe(event.oldSubscription.options)
.then((newSubscription) => {
const data = {
old_endpoint: event.oldSubscription!.endpoint,
subscription_data: newSubscription.toJSON(),
};
const url = new URL('webpush/pushsubscriptionchange/', apiUrl ? apiUrl : '/api/');
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
})
.catch((err) => {
console.warn(err);
}),
);
};
};

export const handleNotificationsMessage = (event: ExtendableMessageEvent) => {
if (event.data === 'authPageOpened') {
event.waitUntil(
self.registration.pushManager.getSubscription().then((subscription) => {
if (subscription) {
return subscription.unsubscribe();
}
}),
);
}
};
97 changes: 97 additions & 0 deletions frontend_src/service-worker/service-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import {
handlePush,
handleNotificationClick,
handlePushSubscriptionChange,
handleNotificationsMessage,
} from './notification-handler';

declare const self: ServiceWorkerGlobalScope & {
apiUrl: string | undefined;
defaultNotificationOptions: NotificationOptions;
};

const OFFLINE_PAGE = '/offline.html';
const FAVICON = '/favicon.ico';

self.addEventListener('push', (e: PushEvent) => handlePush(self.defaultNotificationOptions)(e));
self.addEventListener('notificationclick', (e: NotificationEvent) => handleNotificationClick()(e));
self.addEventListener('pushsubscriptionchange', (e: Event) => handlePushSubscriptionChange(self.apiUrl)(e));
self.addEventListener('message', handleNotificationsMessage);

self.addEventListener('install', () => {
const url = new URL(self.serviceWorker.scriptURL);

const encodedOptions = url.searchParams.get('options');
if (encodedOptions) {
try {
const { apiUrl, defaultOptions } = JSON.parse(decodeURIComponent(encodedOptions));
self.defaultNotificationOptions = defaultOptions || {};
self.apiUrl = apiUrl || undefined;
} catch (error) {
console.error('Error decoding options:', error);
}
} else {
console.warn('No "options" parameter found in scriptURL.');
}
});

function updateCache(): Promise<void> {
return caches.open('offline').then((cache) => {
return cache.addAll([OFFLINE_PAGE, FAVICON]);
});
}

self.addEventListener('activate', (event: ExtendableEvent) => {
event.waitUntil(updateCache().then(() => self.clients.claim()));
});

self.addEventListener('message', (event) => {
if (event.data === 'OFFLINE_CACHE_UPDATE') {
updateCache();
}
if (event.data === 'triggerPushSubscriptionChange') {
self.registration.pushManager.getSubscription().then((subscription) => {
const pushChangeEvent = new Event('pushsubscriptionchange');
Object.assign(pushChangeEvent, {
oldSubscription: subscription,
});
self.dispatchEvent(pushChangeEvent);
});
}
});

self.addEventListener('fetch', (event: FetchEvent) => {
const request = event.request;
try {
if (
request.method === 'GET' &&
(!request.headers ||
!request.headers.get('accept') ||
request.headers.get('accept')?.includes('text/html'))
) {
event.respondWith(
fetch(request).catch(() =>
caches.match(OFFLINE_PAGE, { ignoreVary: true }).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
return new Response('Offline page not found', { status: 503 });
}),
),
);
} else if (request.method === 'GET' && request.url.endsWith(FAVICON)) {
event.respondWith(
fetch(request).catch(() =>
caches.match(FAVICON, { ignoreVary: true }).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
return new Response('Favicon not found', { status: 404 });
}),
),
);
}
} catch (e) {
console.log('SW error on:', request, e);
}
});
20 changes: 19 additions & 1 deletion frontend_src/spa/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { fileURLToPath } from 'node:url';
import { type PluginOption, defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue2';
import tsconfigPaths from 'vite-tsconfig-paths';
import { VitePWA } from 'vite-plugin-pwa';

const currentDir = dirname(fileURLToPath(import.meta.url));

Expand All @@ -14,7 +15,24 @@ export default defineConfig(({ mode }) => {
return {
root: currentDir,
base: '/spa/',
plugins: [tsconfigPaths({ root: frontendSrc }) as PluginOption, vue()],
plugins: [
tsconfigPaths({ root: frontendSrc }) as PluginOption,
vue(),
VitePWA({
srcDir: '../service-worker',
filename: 'service-worker.ts',
strategies: 'injectManifest',
manifest: false,
injectRegister: false,
injectManifest: {
injectionPoint: undefined,
},
devOptions: {
enabled: true,
type: 'module',
},
}),
],
build: {
sourcemap: isDev,
minify: !isDev,
Expand Down
18 changes: 4 additions & 14 deletions frontend_src/tsconfig.app.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
{
"include": [
"./**/*",
],
"exclude": [
"vite.config.ts",
"build-package.ts",
],
"include": ["./**/*"],
"exclude": ["vite.config.ts", "build-package.ts", "service-worker/**/*"],
"compilerOptions": {
"outDir": "./dist",
"allowJs": true,
Expand All @@ -21,17 +16,12 @@
"skipLibCheck": true,
"resolveJsonModule": true,
"useDefineForClassFields": false,
"types": [
"vite/client",
"vitest/globals",
"jquery",
"bootstrap",
],
"types": ["vite/client", "vitest/globals", "jquery", "bootstrap"],
"paths": {
"#vstutils/*": ["./vstutils/*"],
"#libs/*": ["./libs/*"],
"#unittests/*": ["./unittests/*"],
"#unittests": ["./unittests"],
"#unittests": ["./unittests"]
}
},
"vueCompilerOptions": {
Expand Down
7 changes: 4 additions & 3 deletions frontend_src/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" },
{ "path": "./tsconfig.sw.json" }
]
}
}
11 changes: 11 additions & 0 deletions frontend_src/tsconfig.sw.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": "dist",
"lib": ["WebWorker", "ES2015"],
"target": "esnext",
"strict": true
},
"include": ["service-worker/**/*"]
}
1 change: 1 addition & 0 deletions frontend_src/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export default defineConfig({
entry: {
index: join(frontendSrc, 'index.ts'),
'auth-app': join(frontendSrc, 'auth-app.ts'),
sw: join(frontendSrc, 'service-worker/service-worker.ts'),
},
formats: ['es'],
cssFileName: 'style',
Expand Down
Loading

0 comments on commit 901f6ce

Please sign in to comment.