mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-01 17:50:50 +08:00
fix(server): blank screen on mobile (#8460)
Co-authored-by: forehalo <forehalo@gmail.com>
This commit is contained in:
@@ -135,83 +135,6 @@ jobs:
|
||||
path: ./packages/frontend/apps/mobile/dist
|
||||
if-no-files-found: error
|
||||
|
||||
build-web-selfhost:
|
||||
name: Build @affine/web selfhost
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ github.event.inputs.flavor }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Version
|
||||
id: version
|
||||
uses: ./.github/actions/setup-version
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Build Core
|
||||
run: yarn nx build @affine/web --skip-nx-cache
|
||||
env:
|
||||
BUILD_TYPE: ${{ github.event.inputs.flavor }}
|
||||
PUBLIC_PATH: '/'
|
||||
SELF_HOSTED: true
|
||||
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
|
||||
- name: Download selfhost fonts
|
||||
run: node ./scripts/download-blocksuite-fonts.mjs
|
||||
- name: Upload web artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: selfhost-web
|
||||
path: ./packages/frontend/apps/web/dist
|
||||
if-no-files-found: error
|
||||
|
||||
build-mobile-selfhost:
|
||||
name: Build @affine/mobile selfhost
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ github.event.inputs.flavor }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Version
|
||||
id: version
|
||||
uses: ./.github/actions/setup-version
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Build Mobile
|
||||
run: yarn nx build @affine/mobile --skip-nx-cache
|
||||
env:
|
||||
BUILD_TYPE: ${{ github.event.inputs.flavor }}
|
||||
PUBLIC_PATH: '/'
|
||||
SELF_HOSTED: true
|
||||
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
|
||||
- name: Upload mobile artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: selfhost-mobile
|
||||
path: ./packages/frontend/apps/mobile/dist
|
||||
if-no-files-found: error
|
||||
|
||||
build-admin-selfhost:
|
||||
name: Build @affine/admin selfhost
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ github.event.inputs.flavor }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Version
|
||||
id: version
|
||||
uses: ./.github/actions/setup-version
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Build admin
|
||||
run: yarn nx build @affine/admin --skip-nx-cache
|
||||
env:
|
||||
BUILD_TYPE: ${{ github.event.inputs.flavor }}
|
||||
PUBLIC_PATH: '/admin/'
|
||||
SELF_HOSTED: true
|
||||
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
|
||||
- name: Upload admin artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: selfhost-admin
|
||||
path: ./packages/frontend/admin/dist
|
||||
if-no-files-found: error
|
||||
|
||||
build-server-native:
|
||||
name: Build Server native - ${{ matrix.targets.name }}
|
||||
runs-on: ubuntu-latest
|
||||
@@ -256,9 +179,6 @@ jobs:
|
||||
- build-web
|
||||
- build-mobile
|
||||
- build-admin
|
||||
- build-web-selfhost
|
||||
- build-mobile-selfhost
|
||||
- build-admin-selfhost
|
||||
- build-server-native
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -334,24 +254,6 @@ jobs:
|
||||
name: admin
|
||||
path: ./packages/frontend/admin/dist
|
||||
|
||||
- name: Download selfhost web artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: selfhost-web
|
||||
path: ./packages/frontend/apps/web/dist/selfhost
|
||||
|
||||
- name: Download selfhost mobile artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: selfhost-mobile
|
||||
path: ./packages/frontend/apps/mobile/dist/selfhost
|
||||
|
||||
- name: Download selfhost admin artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: selfhost-admin
|
||||
path: ./packages/frontend/admin/dist/selfhost
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: |
|
||||
yarn config set --json supportedArchitectures.cpu '["x64", "arm64", "arm"]'
|
||||
|
||||
@@ -24,6 +24,10 @@ export async function createApp() {
|
||||
logger: AFFiNE.affine.stable ? ['log'] : ['verbose'],
|
||||
});
|
||||
|
||||
if (AFFiNE.server.path) {
|
||||
app.setGlobalPrefix(AFFiNE.server.path);
|
||||
}
|
||||
|
||||
app.use(serverTimingAndCache);
|
||||
|
||||
app.use(
|
||||
|
||||
@@ -18,7 +18,6 @@ interface RenderOptions {
|
||||
}
|
||||
|
||||
interface HtmlAssets {
|
||||
html: string;
|
||||
css: string[];
|
||||
js: string[];
|
||||
publicPath: string;
|
||||
@@ -27,7 +26,6 @@ interface HtmlAssets {
|
||||
}
|
||||
|
||||
const defaultAssets: HtmlAssets = {
|
||||
html: '',
|
||||
css: [],
|
||||
js: [],
|
||||
publicPath: '/',
|
||||
@@ -152,9 +150,15 @@ export class DocRendererController {
|
||||
return null;
|
||||
}
|
||||
|
||||
// @TODO(@forehalo): pre-compile html template to accelerate serializing
|
||||
_render(opts: RenderOptions | null, assets: HtmlAssets): string {
|
||||
if (!opts && assets.html) {
|
||||
return assets.html;
|
||||
// TODO(@forehalo): how can we enable the type reference to @affine/env
|
||||
const env: Record<string, any> = {
|
||||
publicPath: assets.publicPath,
|
||||
};
|
||||
|
||||
if (this.config.isSelfhosted) {
|
||||
env.isSelfHosted = true;
|
||||
}
|
||||
|
||||
const title = opts?.title
|
||||
@@ -182,7 +186,7 @@ export class DocRendererController {
|
||||
|
||||
<title>${title}</title>
|
||||
<meta name="theme-color" content="#fafafa" />
|
||||
<link rel="preconnect" href="${assets.publicPath}">
|
||||
${assets.publicPath.startsWith('/') ? '' : `<link rel="preconnect" href="${assets.publicPath}">`}
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" sizes="192x192" href="/favicon-192.png" />
|
||||
@@ -199,6 +203,10 @@ export class DocRendererController {
|
||||
<meta property="og:title" content="${title}" />
|
||||
<meta property="og:description" content="${summary}" />
|
||||
<meta property="og:image" content="${image}" />
|
||||
<meta name="renderer" content="ssr" />
|
||||
${Object.entries(env)
|
||||
.map(([key, val]) => `<meta name="env:${key}" content="${val}" />`)
|
||||
.join('\n')}
|
||||
${assets.css.map(url => `<link rel="stylesheet" href="${url}" />`).join('\n')}
|
||||
</head>
|
||||
<body>
|
||||
@@ -214,11 +222,20 @@ export class DocRendererController {
|
||||
*/
|
||||
private readHtmlAssets(path: string): HtmlAssets {
|
||||
const manifestPath = join(path, 'assets-manifest.json');
|
||||
const htmlPath = join(path, 'index.html');
|
||||
|
||||
try {
|
||||
const assets = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
||||
assets.html = readFileSync(htmlPath, 'utf-8');
|
||||
const assets: HtmlAssets = JSON.parse(
|
||||
readFileSync(manifestPath, 'utf-8')
|
||||
);
|
||||
|
||||
const publicPath = this.config.isSelfhosted
|
||||
? this.config.server.host + '/'
|
||||
: assets.publicPath;
|
||||
|
||||
assets.publicPath = publicPath;
|
||||
assets.js = assets.js.map(path => publicPath + path);
|
||||
assets.css = assets.css.map(path => publicPath + path);
|
||||
|
||||
return assets;
|
||||
} catch (e) {
|
||||
if (this.config.node.prod) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { HttpAdapterHost } from '@nestjs/core';
|
||||
import type { Application, Request, Response } from 'express';
|
||||
import { static as serveStatic } from 'express';
|
||||
import isMobile from 'is-mobile';
|
||||
|
||||
import { Config } from '../../fundamentals';
|
||||
import { AuthModule } from '../auth';
|
||||
@@ -58,50 +59,106 @@ export class SelfhostModule implements OnModuleInit {
|
||||
) {}
|
||||
|
||||
onModuleInit() {
|
||||
// selfhost static file location
|
||||
// web => 'static/selfhost'
|
||||
// admin => 'static/admin/selfhost'
|
||||
// mobile => 'static/mobile/selfhost'
|
||||
const staticPath = join(this.config.projectRoot, 'static');
|
||||
// in command line mode
|
||||
if (!this.adapterHost.httpAdapter) {
|
||||
return;
|
||||
}
|
||||
|
||||
const app = this.adapterHost.httpAdapter.getInstance<Application>();
|
||||
// for example, '/affine' in host [//host.com/affine]
|
||||
const basePath = this.config.server.path;
|
||||
const staticPath = join(this.config.projectRoot, 'static');
|
||||
|
||||
// web => {
|
||||
// affine: 'static/index.html',
|
||||
// selfhost: 'static/selfhost.html'
|
||||
// }
|
||||
// admin => {
|
||||
// affine: 'static/admin/index.html',
|
||||
// selfhost: 'static/admin/selfhost.html'
|
||||
// }
|
||||
// mobile => {
|
||||
// affine: 'static/mobile/index.html',
|
||||
// selfhost: 'static/mobile/selfhost.html'
|
||||
// }
|
||||
// NOTE(@forehalo):
|
||||
// the order following routes should be respected,
|
||||
// otherwise the app won't work properly.
|
||||
|
||||
// START REGION: /admin
|
||||
// do not allow '/index.html' url, redirect to '/'
|
||||
app.get(basePath + '/admin/index.html', (_req, res) => {
|
||||
res.redirect(basePath + '/admin');
|
||||
return res.redirect(basePath + '/admin');
|
||||
});
|
||||
|
||||
// serve all static files
|
||||
app.use(
|
||||
basePath + '/admin',
|
||||
serveStatic(join(staticPath, 'admin', 'selfhost'), {
|
||||
basePath,
|
||||
serveStatic(join(staticPath, 'admin'), {
|
||||
redirect: false,
|
||||
index: false,
|
||||
fallthrough: true,
|
||||
})
|
||||
);
|
||||
|
||||
// fallback all unknown routes
|
||||
app.get(
|
||||
[basePath + '/admin', basePath + '/admin/*'],
|
||||
this.check.use,
|
||||
(_req, res) => {
|
||||
res.sendFile(join(staticPath, 'admin', 'selfhost', 'index.html'));
|
||||
res.sendFile(
|
||||
join(
|
||||
staticPath,
|
||||
'admin',
|
||||
this.config.isSelfhosted ? 'selfhost.html' : 'index.html'
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
// END REGION
|
||||
|
||||
app.get(basePath + '/index.html', (_req, res) => {
|
||||
res.redirect(basePath);
|
||||
});
|
||||
// START REGION: /mobile
|
||||
// serve all static files
|
||||
app.use(
|
||||
basePath,
|
||||
serveStatic(join(staticPath, 'selfhost'), {
|
||||
serveStatic(join(staticPath, 'mobile'), {
|
||||
redirect: false,
|
||||
index: false,
|
||||
fallthrough: true,
|
||||
})
|
||||
);
|
||||
app.get('*', this.check.use, (_req, res) => {
|
||||
res.sendFile(join(staticPath, 'selfhost', 'index.html'));
|
||||
// END REGION
|
||||
|
||||
// START REGION: /
|
||||
// do not allow '/index.html' url, redirect to '/'
|
||||
app.get(basePath + '/index.html', (_req, res) => {
|
||||
return res.redirect(basePath);
|
||||
});
|
||||
|
||||
// serve all static files
|
||||
app.use(
|
||||
basePath,
|
||||
serveStatic(staticPath, {
|
||||
redirect: false,
|
||||
index: false,
|
||||
fallthrough: true,
|
||||
})
|
||||
);
|
||||
|
||||
// fallback all unknown routes
|
||||
app.get([basePath, basePath + '/*'], this.check.use, (req, res) => {
|
||||
const mobile = isMobile({
|
||||
ua: req.headers['user-agent'] ?? undefined,
|
||||
});
|
||||
|
||||
return res.sendFile(
|
||||
join(
|
||||
staticPath,
|
||||
mobile ? 'mobile' : '',
|
||||
this.config.isSelfhosted ? 'selfhost.html' : 'index.html'
|
||||
)
|
||||
);
|
||||
});
|
||||
// END REGION
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { defineStartupConfig, ModuleConfig } from '../../fundamentals/config';
|
||||
export interface ServerStartupConfigurations {
|
||||
/**
|
||||
* Base url of AFFiNE server, used for generating external urls.
|
||||
* default to be `[AFFiNE.protocol]://[AFFiNE.host][:AFFiNE.port]?[AFFiNE.path]` if not specified
|
||||
* default to be `[AFFiNE.protocol]://[AFFiNE.host][:AFFiNE.port]/[AFFiNE.path]` if not specified
|
||||
*/
|
||||
externalUrl: string;
|
||||
/**
|
||||
|
||||
@@ -17,12 +17,17 @@ const test = ava as TestFn<{
|
||||
db: PrismaClient;
|
||||
}>;
|
||||
|
||||
const mobileUAString =
|
||||
'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Mobile Safari/537.36';
|
||||
|
||||
function initTestStaticFiles(staticPath: string) {
|
||||
const files = {
|
||||
'selfhost/index.html': `<!DOCTYPE html><html><body>AFFiNE</body><script src="main.js"/></html>`,
|
||||
'selfhost/main.js': `const name = 'affine'`,
|
||||
'admin/selfhost/index.html': `<!DOCTYPE html><html><body>AFFiNE Admin</body><script src="/admin/main.js"/></html>`,
|
||||
'admin/selfhost/main.js': `const name = 'affine-admin'`,
|
||||
'selfhost.html': `<!DOCTYPE html><html><body>AFFiNE</body><script src="main.a.js"/></html>`,
|
||||
'main.a.js': `const name = 'affine'`,
|
||||
'admin/selfhost.html': `<!DOCTYPE html><html><body>AFFiNE Admin</body><script src="/admin/main.b.js"/></html>`,
|
||||
'admin/main.b.js': `const name = 'affine-admin'`,
|
||||
'mobile/selfhost.html': `<!DOCTYPE html><html><body>AFFiNE mobile</body><script src="/mobile/main.c.js"/></html>`,
|
||||
'mobile/main.c.js': `const name = 'affine-mobile'`,
|
||||
};
|
||||
|
||||
for (const [filename, content] of Object.entries(files)) {
|
||||
@@ -35,6 +40,7 @@ function initTestStaticFiles(staticPath: string) {
|
||||
test.before('init selfhost server', async t => {
|
||||
// @ts-expect-error override
|
||||
AFFiNE.isSelfhosted = true;
|
||||
AFFiNE.flavor.renderer = true;
|
||||
const { app } = await createTestingApp({
|
||||
imports: [buildAppModule()],
|
||||
});
|
||||
@@ -54,7 +60,7 @@ test.beforeEach(async t => {
|
||||
server._initialized = false;
|
||||
});
|
||||
|
||||
test.afterEach.always(async t => {
|
||||
test.after.always(async t => {
|
||||
await t.context.app.close();
|
||||
});
|
||||
|
||||
@@ -70,19 +76,28 @@ test('do not allow visit index.html directly', async t => {
|
||||
.expect(302);
|
||||
|
||||
t.is(res.header.location, '/admin');
|
||||
|
||||
res = await request(t.context.app.getHttpServer())
|
||||
.get('/mobile/index.html')
|
||||
.expect(302);
|
||||
});
|
||||
|
||||
test('should always return static asset files', async t => {
|
||||
let res = await request(t.context.app.getHttpServer())
|
||||
.get('/main.js')
|
||||
.get('/main.a.js')
|
||||
.expect(200);
|
||||
t.is(res.text, "const name = 'affine'");
|
||||
|
||||
res = await request(t.context.app.getHttpServer())
|
||||
.get('/admin/main.js')
|
||||
.get('/main.b.js')
|
||||
.expect(200);
|
||||
t.is(res.text, "const name = 'affine-admin'");
|
||||
|
||||
res = await request(t.context.app.getHttpServer())
|
||||
.get('/main.c.js')
|
||||
.expect(200);
|
||||
t.is(res.text, "const name = 'affine-mobile'");
|
||||
|
||||
await t.context.db.user.create({
|
||||
data: {
|
||||
name: 'test',
|
||||
@@ -91,14 +106,19 @@ test('should always return static asset files', async t => {
|
||||
});
|
||||
|
||||
res = await request(t.context.app.getHttpServer())
|
||||
.get('/main.js')
|
||||
.get('/main.a.js')
|
||||
.expect(200);
|
||||
t.is(res.text, "const name = 'affine'");
|
||||
|
||||
res = await request(t.context.app.getHttpServer())
|
||||
.get('/admin/main.js')
|
||||
.get('/main.b.js')
|
||||
.expect(200);
|
||||
t.is(res.text, "const name = 'affine-admin'");
|
||||
|
||||
res = await request(t.context.app.getHttpServer())
|
||||
.get('/main.c.js')
|
||||
.expect(200);
|
||||
t.is(res.text, "const name = 'affine-mobile'");
|
||||
});
|
||||
|
||||
test('should be able to call apis', async t => {
|
||||
@@ -167,3 +187,19 @@ test('should redirect to admin if initialized', async t => {
|
||||
|
||||
t.is(res.header.location, '/admin');
|
||||
});
|
||||
|
||||
test('should return mobile assets if visited by mobile', async t => {
|
||||
await t.context.db.user.create({
|
||||
data: {
|
||||
name: 'test',
|
||||
email: 'test@affine.pro',
|
||||
},
|
||||
});
|
||||
|
||||
const res = await request(t.context.app.getHttpServer())
|
||||
.get('/')
|
||||
.set('user-agent', mobileUAString)
|
||||
.expect(200);
|
||||
|
||||
t.true(res.text.includes('AFFiNE mobile'));
|
||||
});
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import type { TestFn } from 'ava';
|
||||
import ava from 'ava';
|
||||
import request from 'supertest';
|
||||
|
||||
import { DocRendererModule } from '../../src/core/doc-renderer';
|
||||
import { createTestingApp } from '../utils';
|
||||
|
||||
const test = ava as TestFn<{
|
||||
app: INestApplication;
|
||||
db: PrismaClient;
|
||||
}>;
|
||||
|
||||
const mobileUAString =
|
||||
'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Mobile Safari/537.36';
|
||||
|
||||
function initTestStaticFiles(staticPath: string) {
|
||||
const files = {
|
||||
'main.a.js': `const name = 'affine'`,
|
||||
'assets-manifest.json': JSON.stringify({
|
||||
js: ['main.a.js'],
|
||||
css: [],
|
||||
publicPath: 'https://app.affine.pro/',
|
||||
gitHash: '',
|
||||
description: '',
|
||||
}),
|
||||
'admin/main.b.js': `const name = 'affine-admin'`,
|
||||
'mobile/main.c.js': `const name = 'affine-mobile'`,
|
||||
'mobile/assets-manifest.json': JSON.stringify({
|
||||
js: ['main.c.js'],
|
||||
css: [],
|
||||
publicPath: 'https://app.affine.pro/',
|
||||
gitHash: '',
|
||||
description: '',
|
||||
}),
|
||||
};
|
||||
|
||||
for (const [filename, content] of Object.entries(files)) {
|
||||
const filePath = path.join(staticPath, filename);
|
||||
mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
writeFileSync(filePath, content);
|
||||
}
|
||||
}
|
||||
|
||||
test.before('init selfhost server', async t => {
|
||||
const staticPath = path.join(
|
||||
fileURLToPath(import.meta.url),
|
||||
'../../../static'
|
||||
);
|
||||
initTestStaticFiles(staticPath);
|
||||
|
||||
const { app } = await createTestingApp({
|
||||
imports: [DocRendererModule],
|
||||
});
|
||||
|
||||
t.context.app = app;
|
||||
t.context.db = t.context.app.get(PrismaClient);
|
||||
});
|
||||
|
||||
test.after.always(async t => {
|
||||
await t.context.app.close();
|
||||
});
|
||||
|
||||
test('should render correct html', async t => {
|
||||
const res = await request(t.context.app.getHttpServer())
|
||||
.get('/workspace/xxxx/xxx')
|
||||
.expect(200);
|
||||
|
||||
t.true(
|
||||
res.text.includes(
|
||||
`<script src="https://app.affine.pro/main.a.js"></script>`
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test('should render correct mobile html', async t => {
|
||||
const res = await request(t.context.app.getHttpServer())
|
||||
.get('/workspace/xxxx/xxx')
|
||||
.set('user-agent', mobileUAString)
|
||||
.expect(200);
|
||||
|
||||
t.true(
|
||||
res.text.includes(
|
||||
`<script src="https://app.affine.pro/main.c.js"></script>`
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test.todo('should render correct page preview');
|
||||
Vendored
+50
-19
@@ -31,12 +31,12 @@ export type BUILD_CONFIG_TYPE = {
|
||||
// see: tools/workers
|
||||
imageProxyUrl: string;
|
||||
linkPreviewUrl: string;
|
||||
|
||||
// TODO(@forehalo): remove
|
||||
isSelfHosted: boolean;
|
||||
};
|
||||
|
||||
export type Environment = {
|
||||
// Variant
|
||||
isSelfHosted: boolean;
|
||||
|
||||
// Device
|
||||
isLinux: boolean;
|
||||
isMacOs: boolean;
|
||||
@@ -47,8 +47,10 @@ export type Environment = {
|
||||
isMobile: boolean;
|
||||
isChrome: boolean;
|
||||
isPwa: boolean;
|
||||
|
||||
chromeVersion?: number;
|
||||
|
||||
// runtime configs
|
||||
publicPath: string;
|
||||
};
|
||||
|
||||
export function setupGlobal() {
|
||||
@@ -56,24 +58,25 @@ export function setupGlobal() {
|
||||
return;
|
||||
}
|
||||
|
||||
let environment: Environment;
|
||||
let environment: Environment = {
|
||||
isLinux: false,
|
||||
isMacOs: false,
|
||||
isSafari: false,
|
||||
isWindows: false,
|
||||
isFireFox: false,
|
||||
isChrome: false,
|
||||
isIOS: false,
|
||||
isPwa: false,
|
||||
isMobile: false,
|
||||
isSelfHosted: false,
|
||||
publicPath: '/',
|
||||
};
|
||||
|
||||
if (!globalThis.navigator) {
|
||||
environment = {
|
||||
isLinux: false,
|
||||
isMacOs: false,
|
||||
isSafari: false,
|
||||
isWindows: false,
|
||||
isFireFox: false,
|
||||
isChrome: false,
|
||||
isIOS: false,
|
||||
isPwa: false,
|
||||
isMobile: false,
|
||||
};
|
||||
} else {
|
||||
if (globalThis.navigator) {
|
||||
const uaHelper = new UaHelper(globalThis.navigator);
|
||||
|
||||
environment = {
|
||||
...environment,
|
||||
isMobile: uaHelper.isMobile,
|
||||
isLinux: uaHelper.isLinux,
|
||||
isMacOs: uaHelper.isMacOs,
|
||||
@@ -96,7 +99,35 @@ export function setupGlobal() {
|
||||
}
|
||||
}
|
||||
|
||||
globalThis.environment = environment;
|
||||
applyEnvironmentOverrides(environment);
|
||||
|
||||
globalThis.environment = environment;
|
||||
globalThis.$AFFINE_SETUP = true;
|
||||
}
|
||||
|
||||
function applyEnvironmentOverrides(environment: Environment) {
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const metaTags = document.querySelectorAll('meta');
|
||||
|
||||
metaTags.forEach(meta => {
|
||||
if (!meta.name.startsWith('env:')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = meta.name.substring(4);
|
||||
|
||||
// all environments should have default value
|
||||
// so ignore non-defined overrides
|
||||
if (name in environment) {
|
||||
// @ts-expect-error safe
|
||||
environment[name] =
|
||||
// @ts-expect-error safe
|
||||
typeof environment[name] === 'string'
|
||||
? meta.content
|
||||
: JSON.parse(meta.content);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import './global.css';
|
||||
import './setup';
|
||||
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { setupBrowser } from '@affine/core/bootstrap';
|
||||
|
||||
await setupBrowser();
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -4,11 +4,12 @@ import { setupEnvironment } from './app';
|
||||
import { polyfillBrowser, polyfillElectron } from './polyfill';
|
||||
|
||||
export function setupElectron() {
|
||||
polyfillElectron();
|
||||
setupEnvironment();
|
||||
polyfillElectron();
|
||||
}
|
||||
|
||||
export async function setupBrowser() {
|
||||
await polyfillBrowser();
|
||||
setupEnvironment();
|
||||
__webpack_public_path__ = environment.publicPath;
|
||||
await polyfillBrowser();
|
||||
}
|
||||
|
||||
+4
-7
@@ -5,12 +5,9 @@ import {
|
||||
|
||||
export function getFontConfigExtension() {
|
||||
return FontConfigExtension(
|
||||
BUILD_CONFIG.isSelfHosted
|
||||
? AffineCanvasTextFonts.map(font => ({
|
||||
...font,
|
||||
// self-hosted fonts are served from /assets
|
||||
url: '/assets/' + new URL(font.url).pathname.split('/').pop(),
|
||||
}))
|
||||
: AffineCanvasTextFonts
|
||||
AffineCanvasTextFonts.map(font => ({
|
||||
...font,
|
||||
url: environment.publicPath + 'fonts/' + font.url.split('/').pop(),
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ function createMixpanel() {
|
||||
appVersion: BUILD_CONFIG.appVersion,
|
||||
environment: BUILD_CONFIG.appBuildType,
|
||||
editorVersion: BUILD_CONFIG.editorVersion,
|
||||
isSelfHosted: BUILD_CONFIG.isSelfHosted,
|
||||
isDesktop: BUILD_CONFIG.isElectron,
|
||||
isSelfHosted: environment.isSelfHosted,
|
||||
});
|
||||
},
|
||||
reset() {
|
||||
|
||||
@@ -10,10 +10,9 @@ const fontPath = join(
|
||||
'..',
|
||||
'packages',
|
||||
'frontend',
|
||||
'apps',
|
||||
'web',
|
||||
'dist',
|
||||
'assets'
|
||||
'core',
|
||||
'public',
|
||||
'fonts'
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
|
||||
@@ -71,19 +71,23 @@ export const getPublicPath = (buildFlags: BuildFlags) => {
|
||||
if (typeof process.env.PUBLIC_PATH === 'string') {
|
||||
return process.env.PUBLIC_PATH;
|
||||
}
|
||||
const publicPath = '/';
|
||||
if (process.env.COVERAGE || buildFlags.distribution === 'desktop') {
|
||||
return publicPath;
|
||||
|
||||
if (
|
||||
buildFlags.mode === 'development' ||
|
||||
process.env.COVERAGE ||
|
||||
buildFlags.distribution === 'desktop'
|
||||
) {
|
||||
return '/';
|
||||
}
|
||||
|
||||
if (BUILD_TYPE === 'canary') {
|
||||
return `https://dev.affineassets.com/`;
|
||||
} else if (BUILD_TYPE === 'beta') {
|
||||
return `https://beta.affineassets.com/`;
|
||||
} else if (BUILD_TYPE === 'stable') {
|
||||
return `https://prod.affineassets.com/`;
|
||||
switch (BUILD_TYPE) {
|
||||
case 'stable':
|
||||
return 'https://prod.affineassets.com/';
|
||||
case 'beta':
|
||||
return 'https://beta.affineassets.com/';
|
||||
default:
|
||||
return 'https://dev.affineassets.com/';
|
||||
}
|
||||
return publicPath;
|
||||
};
|
||||
|
||||
export const createConfiguration: (
|
||||
@@ -126,7 +130,8 @@ export const createConfiguration: (
|
||||
path: join(cwd, 'dist'),
|
||||
clean: buildFlags.mode === 'production',
|
||||
globalObject: 'globalThis',
|
||||
publicPath: getPublicPath(buildFlags),
|
||||
// NOTE(@forehalo): always keep it '/'
|
||||
publicPath: '/',
|
||||
workerPublicPath: '/',
|
||||
},
|
||||
target: ['web', 'es2022'],
|
||||
|
||||
@@ -26,7 +26,7 @@ export class WebpackS3Plugin implements WebpackPluginInstance {
|
||||
compiler.hooks.assetEmitted.tapPromise(
|
||||
'WebpackS3Plugin',
|
||||
async (asset, { outputPath }) => {
|
||||
if (asset === 'index.html') {
|
||||
if (asset.endsWith('.html')) {
|
||||
return;
|
||||
}
|
||||
const assetPath = join(outputPath, asset);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<title>AFFiNE</title>
|
||||
<meta name="theme-color" content="#fafafa" />
|
||||
<link rel="preconnect" href="<%= PUBLIC_PATH %>" />
|
||||
<%= PRECONNECT %>
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" sizes="192x192" href="/favicon-192.png" />
|
||||
|
||||
@@ -5,10 +5,16 @@ import type { BuildFlags } from '@affine/cli/config';
|
||||
import { Repository } from '@napi-rs/simple-git';
|
||||
import HTMLPlugin from 'html-webpack-plugin';
|
||||
import { once } from 'lodash-es';
|
||||
import type { Compiler } from 'webpack';
|
||||
import webpack from 'webpack';
|
||||
import { merge } from 'webpack-merge';
|
||||
|
||||
import { createConfiguration, rootPath, workspaceRoot } from './config.js';
|
||||
import {
|
||||
createConfiguration,
|
||||
getPublicPath,
|
||||
rootPath,
|
||||
workspaceRoot,
|
||||
} from './config.js';
|
||||
import { getBuildConfig } from './runtime-config.js';
|
||||
|
||||
const DESCRIPTION = `There can be more than Notion and Miro. AFFiNE is a next-gen knowledge base that brings planning, sorting and creating all together.`;
|
||||
@@ -41,46 +47,106 @@ export function createWebpackConfig(cwd: string, flags: BuildFlags) {
|
||||
}
|
||||
: flags.entry;
|
||||
|
||||
const createHTMLPlugin = (entryName = 'app') => {
|
||||
return new HTMLPlugin({
|
||||
const publicPath = getPublicPath(flags);
|
||||
const cdnOrigin = publicPath.startsWith('/')
|
||||
? undefined
|
||||
: new URL(publicPath).origin;
|
||||
|
||||
const templateParams = {
|
||||
GIT_SHORT_SHA: gitShortHash(),
|
||||
DESCRIPTION,
|
||||
PRECONNECT: cdnOrigin
|
||||
? `<link rel="preconnect" href="${cdnOrigin}" />`
|
||||
: '',
|
||||
VIEWPORT_FIT: flags.distribution === 'mobile' ? 'cover' : 'auto',
|
||||
};
|
||||
|
||||
const createHTMLPlugins = (entryName: string) => {
|
||||
const htmlPluginOptions = {
|
||||
template: join(rootPath, 'webpack', 'template.html'),
|
||||
inject: 'body',
|
||||
filename: 'index.html',
|
||||
minify: false,
|
||||
templateParameters: templateParams,
|
||||
chunks: [entryName],
|
||||
filename: `${entryName === 'app' ? 'index' : entryName}.html`, // main entry should take name index.html
|
||||
templateParameters: (compilation, assets) => {
|
||||
if (entryName === 'app') {
|
||||
// emit assets manifest for ssr
|
||||
compilation.emitAsset(
|
||||
`assets-manifest.json`,
|
||||
new webpack.sources.RawSource(
|
||||
JSON.stringify(
|
||||
{
|
||||
...assets,
|
||||
gitHash: gitShortHash(),
|
||||
description: DESCRIPTION,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
),
|
||||
{
|
||||
immutable: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
return {
|
||||
GIT_SHORT_SHA: gitShortHash(),
|
||||
DESCRIPTION,
|
||||
PUBLIC_PATH: config.output?.publicPath,
|
||||
VIEWPORT_FIT: flags.distribution === 'mobile' ? 'cover' : 'auto',
|
||||
};
|
||||
},
|
||||
});
|
||||
} satisfies HTMLPlugin.Options;
|
||||
|
||||
if (entryName === 'app') {
|
||||
return [
|
||||
{
|
||||
apply(compiler: Compiler) {
|
||||
compiler.hooks.compilation.tap(
|
||||
'assets-manifest-plugin',
|
||||
compilation => {
|
||||
HTMLPlugin.getHooks(compilation).beforeAssetTagGeneration.tap(
|
||||
'assets-manifest-plugin',
|
||||
arg => {
|
||||
if (!compilation.getAsset('assets-manifest.json')) {
|
||||
compilation.emitAsset(
|
||||
`assets-manifest.json`,
|
||||
new webpack.sources.RawSource(
|
||||
JSON.stringify(
|
||||
{
|
||||
...arg.assets,
|
||||
js: arg.assets.js.map(file =>
|
||||
file.substring(arg.assets.publicPath.length)
|
||||
),
|
||||
css: arg.assets.css.map(file =>
|
||||
file.substring(arg.assets.publicPath.length)
|
||||
),
|
||||
gitHash: templateParams.GIT_SHORT_SHA,
|
||||
description: templateParams.DESCRIPTION,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
),
|
||||
{
|
||||
immutable: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return arg;
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
new HTMLPlugin({
|
||||
...htmlPluginOptions,
|
||||
publicPath,
|
||||
meta: {
|
||||
'env:publicPath': publicPath,
|
||||
},
|
||||
}),
|
||||
// selfhost html
|
||||
new HTMLPlugin({
|
||||
...htmlPluginOptions,
|
||||
meta: {
|
||||
'env:isSelfHosted': 'true',
|
||||
'env:publicPath': '/',
|
||||
},
|
||||
filename: 'selfhost.html',
|
||||
templateParameters: {
|
||||
...htmlPluginOptions.templateParameters,
|
||||
PRECONNECT: '',
|
||||
},
|
||||
}),
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
new HTMLPlugin({
|
||||
...htmlPluginOptions,
|
||||
filename: `${entryName}.html`,
|
||||
}),
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
return merge(config, {
|
||||
entry: entry,
|
||||
plugins: Object.keys(entry).map(createHTMLPlugin),
|
||||
entry,
|
||||
plugins: Object.keys(entry).map(createHTMLPlugins).flat(),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user