mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-11-22 09:13:18 +03:00
fix(server): blank screen on mobile (#8460)
Co-authored-by: forehalo <forehalo@gmail.com>
This commit is contained in:
parent
82916e8264
commit
f393f89a3f
98
.github/workflows/build-images.yml
vendored
98
.github/workflows/build-images.yml
vendored
@ -135,83 +135,6 @@ jobs:
|
|||||||
path: ./packages/frontend/apps/mobile/dist
|
path: ./packages/frontend/apps/mobile/dist
|
||||||
if-no-files-found: error
|
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:
|
build-server-native:
|
||||||
name: Build Server native - ${{ matrix.targets.name }}
|
name: Build Server native - ${{ matrix.targets.name }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -256,9 +179,6 @@ jobs:
|
|||||||
- build-web
|
- build-web
|
||||||
- build-mobile
|
- build-mobile
|
||||||
- build-admin
|
- build-admin
|
||||||
- build-web-selfhost
|
|
||||||
- build-mobile-selfhost
|
|
||||||
- build-admin-selfhost
|
|
||||||
- build-server-native
|
- build-server-native
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@ -334,24 +254,6 @@ jobs:
|
|||||||
name: admin
|
name: admin
|
||||||
path: ./packages/frontend/admin/dist
|
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
|
- name: Install Node.js dependencies
|
||||||
run: |
|
run: |
|
||||||
yarn config set --json supportedArchitectures.cpu '["x64", "arm64", "arm"]'
|
yarn config set --json supportedArchitectures.cpu '["x64", "arm64", "arm"]'
|
||||||
|
@ -24,6 +24,10 @@ export async function createApp() {
|
|||||||
logger: AFFiNE.affine.stable ? ['log'] : ['verbose'],
|
logger: AFFiNE.affine.stable ? ['log'] : ['verbose'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (AFFiNE.server.path) {
|
||||||
|
app.setGlobalPrefix(AFFiNE.server.path);
|
||||||
|
}
|
||||||
|
|
||||||
app.use(serverTimingAndCache);
|
app.use(serverTimingAndCache);
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
|
@ -18,7 +18,6 @@ interface RenderOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface HtmlAssets {
|
interface HtmlAssets {
|
||||||
html: string;
|
|
||||||
css: string[];
|
css: string[];
|
||||||
js: string[];
|
js: string[];
|
||||||
publicPath: string;
|
publicPath: string;
|
||||||
@ -27,7 +26,6 @@ interface HtmlAssets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultAssets: HtmlAssets = {
|
const defaultAssets: HtmlAssets = {
|
||||||
html: '',
|
|
||||||
css: [],
|
css: [],
|
||||||
js: [],
|
js: [],
|
||||||
publicPath: '/',
|
publicPath: '/',
|
||||||
@ -152,9 +150,15 @@ export class DocRendererController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @TODO(@forehalo): pre-compile html template to accelerate serializing
|
||||||
_render(opts: RenderOptions | null, assets: HtmlAssets): string {
|
_render(opts: RenderOptions | null, assets: HtmlAssets): string {
|
||||||
if (!opts && assets.html) {
|
// TODO(@forehalo): how can we enable the type reference to @affine/env
|
||||||
return assets.html;
|
const env: Record<string, any> = {
|
||||||
|
publicPath: assets.publicPath,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.config.isSelfhosted) {
|
||||||
|
env.isSelfHosted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = opts?.title
|
const title = opts?.title
|
||||||
@ -182,7 +186,7 @@ export class DocRendererController {
|
|||||||
|
|
||||||
<title>${title}</title>
|
<title>${title}</title>
|
||||||
<meta name="theme-color" content="#fafafa" />
|
<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="manifest" href="/manifest.json" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
<link rel="icon" sizes="192x192" href="/favicon-192.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:title" content="${title}" />
|
||||||
<meta property="og:description" content="${summary}" />
|
<meta property="og:description" content="${summary}" />
|
||||||
<meta property="og:image" content="${image}" />
|
<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')}
|
${assets.css.map(url => `<link rel="stylesheet" href="${url}" />`).join('\n')}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -214,11 +222,20 @@ export class DocRendererController {
|
|||||||
*/
|
*/
|
||||||
private readHtmlAssets(path: string): HtmlAssets {
|
private readHtmlAssets(path: string): HtmlAssets {
|
||||||
const manifestPath = join(path, 'assets-manifest.json');
|
const manifestPath = join(path, 'assets-manifest.json');
|
||||||
const htmlPath = join(path, 'index.html');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const assets = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
const assets: HtmlAssets = JSON.parse(
|
||||||
assets.html = readFileSync(htmlPath, 'utf-8');
|
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;
|
return assets;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (this.config.node.prod) {
|
if (this.config.node.prod) {
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
import { HttpAdapterHost } from '@nestjs/core';
|
import { HttpAdapterHost } from '@nestjs/core';
|
||||||
import type { Application, Request, Response } from 'express';
|
import type { Application, Request, Response } from 'express';
|
||||||
import { static as serveStatic } from 'express';
|
import { static as serveStatic } from 'express';
|
||||||
|
import isMobile from 'is-mobile';
|
||||||
|
|
||||||
import { Config } from '../../fundamentals';
|
import { Config } from '../../fundamentals';
|
||||||
import { AuthModule } from '../auth';
|
import { AuthModule } from '../auth';
|
||||||
@ -58,50 +59,106 @@ export class SelfhostModule implements OnModuleInit {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
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
|
// in command line mode
|
||||||
if (!this.adapterHost.httpAdapter) {
|
if (!this.adapterHost.httpAdapter) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = this.adapterHost.httpAdapter.getInstance<Application>();
|
const app = this.adapterHost.httpAdapter.getInstance<Application>();
|
||||||
|
// for example, '/affine' in host [//host.com/affine]
|
||||||
const basePath = this.config.server.path;
|
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) => {
|
app.get(basePath + '/admin/index.html', (_req, res) => {
|
||||||
res.redirect(basePath + '/admin');
|
return res.redirect(basePath + '/admin');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// serve all static files
|
||||||
app.use(
|
app.use(
|
||||||
basePath + '/admin',
|
basePath,
|
||||||
serveStatic(join(staticPath, 'admin', 'selfhost'), {
|
serveStatic(join(staticPath, 'admin'), {
|
||||||
redirect: false,
|
redirect: false,
|
||||||
index: false,
|
index: false,
|
||||||
|
fallthrough: true,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// fallback all unknown routes
|
||||||
app.get(
|
app.get(
|
||||||
[basePath + '/admin', basePath + '/admin/*'],
|
[basePath + '/admin', basePath + '/admin/*'],
|
||||||
this.check.use,
|
this.check.use,
|
||||||
(_req, res) => {
|
(_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) => {
|
// START REGION: /mobile
|
||||||
res.redirect(basePath);
|
// serve all static files
|
||||||
});
|
|
||||||
app.use(
|
app.use(
|
||||||
basePath,
|
basePath,
|
||||||
serveStatic(join(staticPath, 'selfhost'), {
|
serveStatic(join(staticPath, 'mobile'), {
|
||||||
redirect: false,
|
redirect: false,
|
||||||
index: false,
|
index: false,
|
||||||
|
fallthrough: true,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
app.get('*', this.check.use, (_req, res) => {
|
// END REGION
|
||||||
res.sendFile(join(staticPath, 'selfhost', 'index.html'));
|
|
||||||
|
// 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 {
|
export interface ServerStartupConfigurations {
|
||||||
/**
|
/**
|
||||||
* Base url of AFFiNE server, used for generating external urls.
|
* 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;
|
externalUrl: string;
|
||||||
/**
|
/**
|
||||||
|
@ -17,12 +17,17 @@ const test = ava as TestFn<{
|
|||||||
db: PrismaClient;
|
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) {
|
function initTestStaticFiles(staticPath: string) {
|
||||||
const files = {
|
const files = {
|
||||||
'selfhost/index.html': `<!DOCTYPE html><html><body>AFFiNE</body><script src="main.js"/></html>`,
|
'selfhost.html': `<!DOCTYPE html><html><body>AFFiNE</body><script src="main.a.js"/></html>`,
|
||||||
'selfhost/main.js': `const name = 'affine'`,
|
'main.a.js': `const name = 'affine'`,
|
||||||
'admin/selfhost/index.html': `<!DOCTYPE html><html><body>AFFiNE Admin</body><script src="/admin/main.js"/></html>`,
|
'admin/selfhost.html': `<!DOCTYPE html><html><body>AFFiNE Admin</body><script src="/admin/main.b.js"/></html>`,
|
||||||
'admin/selfhost/main.js': `const name = 'affine-admin'`,
|
'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)) {
|
for (const [filename, content] of Object.entries(files)) {
|
||||||
@ -35,6 +40,7 @@ function initTestStaticFiles(staticPath: string) {
|
|||||||
test.before('init selfhost server', async t => {
|
test.before('init selfhost server', async t => {
|
||||||
// @ts-expect-error override
|
// @ts-expect-error override
|
||||||
AFFiNE.isSelfhosted = true;
|
AFFiNE.isSelfhosted = true;
|
||||||
|
AFFiNE.flavor.renderer = true;
|
||||||
const { app } = await createTestingApp({
|
const { app } = await createTestingApp({
|
||||||
imports: [buildAppModule()],
|
imports: [buildAppModule()],
|
||||||
});
|
});
|
||||||
@ -54,7 +60,7 @@ test.beforeEach(async t => {
|
|||||||
server._initialized = false;
|
server._initialized = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
test.afterEach.always(async t => {
|
test.after.always(async t => {
|
||||||
await t.context.app.close();
|
await t.context.app.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -70,19 +76,28 @@ test('do not allow visit index.html directly', async t => {
|
|||||||
.expect(302);
|
.expect(302);
|
||||||
|
|
||||||
t.is(res.header.location, '/admin');
|
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 => {
|
test('should always return static asset files', async t => {
|
||||||
let res = await request(t.context.app.getHttpServer())
|
let res = await request(t.context.app.getHttpServer())
|
||||||
.get('/main.js')
|
.get('/main.a.js')
|
||||||
.expect(200);
|
.expect(200);
|
||||||
t.is(res.text, "const name = 'affine'");
|
t.is(res.text, "const name = 'affine'");
|
||||||
|
|
||||||
res = await request(t.context.app.getHttpServer())
|
res = await request(t.context.app.getHttpServer())
|
||||||
.get('/admin/main.js')
|
.get('/main.b.js')
|
||||||
.expect(200);
|
.expect(200);
|
||||||
t.is(res.text, "const name = 'affine-admin'");
|
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({
|
await t.context.db.user.create({
|
||||||
data: {
|
data: {
|
||||||
name: 'test',
|
name: 'test',
|
||||||
@ -91,14 +106,19 @@ test('should always return static asset files', async t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
res = await request(t.context.app.getHttpServer())
|
res = await request(t.context.app.getHttpServer())
|
||||||
.get('/main.js')
|
.get('/main.a.js')
|
||||||
.expect(200);
|
.expect(200);
|
||||||
t.is(res.text, "const name = 'affine'");
|
t.is(res.text, "const name = 'affine'");
|
||||||
|
|
||||||
res = await request(t.context.app.getHttpServer())
|
res = await request(t.context.app.getHttpServer())
|
||||||
.get('/admin/main.js')
|
.get('/main.b.js')
|
||||||
.expect(200);
|
.expect(200);
|
||||||
t.is(res.text, "const name = 'affine-admin'");
|
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 => {
|
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');
|
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'));
|
||||||
|
});
|
||||||
|
94
packages/backend/server/tests/doc/renderer.spec.ts
Normal file
94
packages/backend/server/tests/doc/renderer.spec.ts
Normal file
@ -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');
|
69
packages/common/env/src/global.ts
vendored
69
packages/common/env/src/global.ts
vendored
@ -31,12 +31,12 @@ export type BUILD_CONFIG_TYPE = {
|
|||||||
// see: tools/workers
|
// see: tools/workers
|
||||||
imageProxyUrl: string;
|
imageProxyUrl: string;
|
||||||
linkPreviewUrl: string;
|
linkPreviewUrl: string;
|
||||||
|
|
||||||
// TODO(@forehalo): remove
|
|
||||||
isSelfHosted: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Environment = {
|
export type Environment = {
|
||||||
|
// Variant
|
||||||
|
isSelfHosted: boolean;
|
||||||
|
|
||||||
// Device
|
// Device
|
||||||
isLinux: boolean;
|
isLinux: boolean;
|
||||||
isMacOs: boolean;
|
isMacOs: boolean;
|
||||||
@ -47,8 +47,10 @@ export type Environment = {
|
|||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
isChrome: boolean;
|
isChrome: boolean;
|
||||||
isPwa: boolean;
|
isPwa: boolean;
|
||||||
|
|
||||||
chromeVersion?: number;
|
chromeVersion?: number;
|
||||||
|
|
||||||
|
// runtime configs
|
||||||
|
publicPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function setupGlobal() {
|
export function setupGlobal() {
|
||||||
@ -56,24 +58,25 @@ export function setupGlobal() {
|
|||||||
return;
|
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) {
|
if (globalThis.navigator) {
|
||||||
environment = {
|
|
||||||
isLinux: false,
|
|
||||||
isMacOs: false,
|
|
||||||
isSafari: false,
|
|
||||||
isWindows: false,
|
|
||||||
isFireFox: false,
|
|
||||||
isChrome: false,
|
|
||||||
isIOS: false,
|
|
||||||
isPwa: false,
|
|
||||||
isMobile: false,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const uaHelper = new UaHelper(globalThis.navigator);
|
const uaHelper = new UaHelper(globalThis.navigator);
|
||||||
|
|
||||||
environment = {
|
environment = {
|
||||||
|
...environment,
|
||||||
isMobile: uaHelper.isMobile,
|
isMobile: uaHelper.isMobile,
|
||||||
isLinux: uaHelper.isLinux,
|
isLinux: uaHelper.isLinux,
|
||||||
isMacOs: uaHelper.isMacOs,
|
isMacOs: uaHelper.isMacOs,
|
||||||
@ -96,7 +99,35 @@ export function setupGlobal() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
globalThis.environment = environment;
|
applyEnvironmentOverrides(environment);
|
||||||
|
|
||||||
|
globalThis.environment = environment;
|
||||||
globalThis.$AFFINE_SETUP = true;
|
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 './global.css';
|
||||||
|
import './setup';
|
||||||
|
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
|
3
packages/frontend/admin/src/setup.ts
Normal file
3
packages/frontend/admin/src/setup.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { setupBrowser } from '@affine/core/bootstrap';
|
||||||
|
|
||||||
|
await setupBrowser();
|
BIN
packages/frontend/core/public/fonts/BebasNeue-Light.woff
Normal file
BIN
packages/frontend/core/public/fonts/BebasNeue-Light.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/BebasNeue-Regular.woff
Normal file
BIN
packages/frontend/core/public/fonts/BebasNeue-Regular.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/Inter-Italic.woff
Normal file
BIN
packages/frontend/core/public/fonts/Inter-Italic.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/Inter-Light-BETA.woff
Normal file
BIN
packages/frontend/core/public/fonts/Inter-Light-BETA.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/Inter-LightItalic-BETA.woff
Normal file
BIN
packages/frontend/core/public/fonts/Inter-LightItalic-BETA.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/Inter-Regular.woff
Normal file
BIN
packages/frontend/core/public/fonts/Inter-Regular.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/Inter-SemiBold.woff
Normal file
BIN
packages/frontend/core/public/fonts/Inter-SemiBold.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/Inter-SemiBoldItalic.woff
Normal file
BIN
packages/frontend/core/public/fonts/Inter-SemiBoldItalic.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/Kalam-Bold.woff
Normal file
BIN
packages/frontend/core/public/fonts/Kalam-Bold.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/Kalam-Light.woff
Normal file
BIN
packages/frontend/core/public/fonts/Kalam-Light.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/Kalam-Regular.woff
Normal file
BIN
packages/frontend/core/public/fonts/Kalam-Regular.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/Lora-Bold.woff
Normal file
BIN
packages/frontend/core/public/fonts/Lora-Bold.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/Lora-BoldItalic.woff
Normal file
BIN
packages/frontend/core/public/fonts/Lora-BoldItalic.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/Lora-Italic.woff
Normal file
BIN
packages/frontend/core/public/fonts/Lora-Italic.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/Lora-Regular.woff
Normal file
BIN
packages/frontend/core/public/fonts/Lora-Regular.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/OrelegaOne-Regular.woff
Normal file
BIN
packages/frontend/core/public/fonts/OrelegaOne-Regular.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/Poppins-Italic.woff
Normal file
BIN
packages/frontend/core/public/fonts/Poppins-Italic.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/Poppins-Light.woff
Normal file
BIN
packages/frontend/core/public/fonts/Poppins-Light.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/Poppins-LightItalic.woff
Normal file
BIN
packages/frontend/core/public/fonts/Poppins-LightItalic.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/Poppins-Medium.woff
Normal file
BIN
packages/frontend/core/public/fonts/Poppins-Medium.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/Poppins-Regular.woff
Normal file
BIN
packages/frontend/core/public/fonts/Poppins-Regular.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/Poppins-SemiBold.woff
Normal file
BIN
packages/frontend/core/public/fonts/Poppins-SemiBold.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/Poppins-SemiBoldItalic.woff
Normal file
BIN
packages/frontend/core/public/fonts/Poppins-SemiBoldItalic.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/Satoshi-Bold.woff
Normal file
BIN
packages/frontend/core/public/fonts/Satoshi-Bold.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/Satoshi-BoldItalic.woff
Normal file
BIN
packages/frontend/core/public/fonts/Satoshi-BoldItalic.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/Satoshi-Italic.woff
Normal file
BIN
packages/frontend/core/public/fonts/Satoshi-Italic.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/Satoshi-Light.woff
Normal file
BIN
packages/frontend/core/public/fonts/Satoshi-Light.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/Satoshi-LightItalic.woff
Normal file
BIN
packages/frontend/core/public/fonts/Satoshi-LightItalic.woff
Normal file
Binary file not shown.
BIN
packages/frontend/core/public/fonts/Satoshi-Regular.woff
Normal file
BIN
packages/frontend/core/public/fonts/Satoshi-Regular.woff
Normal file
Binary file not shown.
@ -4,11 +4,12 @@ import { setupEnvironment } from './app';
|
|||||||
import { polyfillBrowser, polyfillElectron } from './polyfill';
|
import { polyfillBrowser, polyfillElectron } from './polyfill';
|
||||||
|
|
||||||
export function setupElectron() {
|
export function setupElectron() {
|
||||||
polyfillElectron();
|
|
||||||
setupEnvironment();
|
setupEnvironment();
|
||||||
|
polyfillElectron();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupBrowser() {
|
export async function setupBrowser() {
|
||||||
await polyfillBrowser();
|
|
||||||
setupEnvironment();
|
setupEnvironment();
|
||||||
|
__webpack_public_path__ = environment.publicPath;
|
||||||
|
await polyfillBrowser();
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,9 @@ import {
|
|||||||
|
|
||||||
export function getFontConfigExtension() {
|
export function getFontConfigExtension() {
|
||||||
return FontConfigExtension(
|
return FontConfigExtension(
|
||||||
BUILD_CONFIG.isSelfHosted
|
AffineCanvasTextFonts.map(font => ({
|
||||||
? AffineCanvasTextFonts.map(font => ({
|
...font,
|
||||||
...font,
|
url: environment.publicPath + 'fonts/' + font.url.split('/').pop(),
|
||||||
// self-hosted fonts are served from /assets
|
}))
|
||||||
url: '/assets/' + new URL(font.url).pathname.split('/').pop(),
|
|
||||||
}))
|
|
||||||
: AffineCanvasTextFonts
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -35,8 +35,8 @@ function createMixpanel() {
|
|||||||
appVersion: BUILD_CONFIG.appVersion,
|
appVersion: BUILD_CONFIG.appVersion,
|
||||||
environment: BUILD_CONFIG.appBuildType,
|
environment: BUILD_CONFIG.appBuildType,
|
||||||
editorVersion: BUILD_CONFIG.editorVersion,
|
editorVersion: BUILD_CONFIG.editorVersion,
|
||||||
isSelfHosted: BUILD_CONFIG.isSelfHosted,
|
|
||||||
isDesktop: BUILD_CONFIG.isElectron,
|
isDesktop: BUILD_CONFIG.isElectron,
|
||||||
|
isSelfHosted: environment.isSelfHosted,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
reset() {
|
reset() {
|
||||||
|
@ -10,10 +10,9 @@ const fontPath = join(
|
|||||||
'..',
|
'..',
|
||||||
'packages',
|
'packages',
|
||||||
'frontend',
|
'frontend',
|
||||||
'apps',
|
'core',
|
||||||
'web',
|
'public',
|
||||||
'dist',
|
'fonts'
|
||||||
'assets'
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
|
@ -71,19 +71,23 @@ export const getPublicPath = (buildFlags: BuildFlags) => {
|
|||||||
if (typeof process.env.PUBLIC_PATH === 'string') {
|
if (typeof process.env.PUBLIC_PATH === 'string') {
|
||||||
return process.env.PUBLIC_PATH;
|
return process.env.PUBLIC_PATH;
|
||||||
}
|
}
|
||||||
const publicPath = '/';
|
|
||||||
if (process.env.COVERAGE || buildFlags.distribution === 'desktop') {
|
if (
|
||||||
return publicPath;
|
buildFlags.mode === 'development' ||
|
||||||
|
process.env.COVERAGE ||
|
||||||
|
buildFlags.distribution === 'desktop'
|
||||||
|
) {
|
||||||
|
return '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BUILD_TYPE === 'canary') {
|
switch (BUILD_TYPE) {
|
||||||
return `https://dev.affineassets.com/`;
|
case 'stable':
|
||||||
} else if (BUILD_TYPE === 'beta') {
|
return 'https://prod.affineassets.com/';
|
||||||
return `https://beta.affineassets.com/`;
|
case 'beta':
|
||||||
} else if (BUILD_TYPE === 'stable') {
|
return 'https://beta.affineassets.com/';
|
||||||
return `https://prod.affineassets.com/`;
|
default:
|
||||||
|
return 'https://dev.affineassets.com/';
|
||||||
}
|
}
|
||||||
return publicPath;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createConfiguration: (
|
export const createConfiguration: (
|
||||||
@ -126,7 +130,8 @@ export const createConfiguration: (
|
|||||||
path: join(cwd, 'dist'),
|
path: join(cwd, 'dist'),
|
||||||
clean: buildFlags.mode === 'production',
|
clean: buildFlags.mode === 'production',
|
||||||
globalObject: 'globalThis',
|
globalObject: 'globalThis',
|
||||||
publicPath: getPublicPath(buildFlags),
|
// NOTE(@forehalo): always keep it '/'
|
||||||
|
publicPath: '/',
|
||||||
workerPublicPath: '/',
|
workerPublicPath: '/',
|
||||||
},
|
},
|
||||||
target: ['web', 'es2022'],
|
target: ['web', 'es2022'],
|
||||||
|
@ -26,7 +26,7 @@ export class WebpackS3Plugin implements WebpackPluginInstance {
|
|||||||
compiler.hooks.assetEmitted.tapPromise(
|
compiler.hooks.assetEmitted.tapPromise(
|
||||||
'WebpackS3Plugin',
|
'WebpackS3Plugin',
|
||||||
async (asset, { outputPath }) => {
|
async (asset, { outputPath }) => {
|
||||||
if (asset === 'index.html') {
|
if (asset.endsWith('.html')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const assetPath = join(outputPath, asset);
|
const assetPath = join(outputPath, asset);
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
<title>AFFiNE</title>
|
<title>AFFiNE</title>
|
||||||
<meta name="theme-color" content="#fafafa" />
|
<meta name="theme-color" content="#fafafa" />
|
||||||
<link rel="preconnect" href="<%= PUBLIC_PATH %>" />
|
<%= PRECONNECT %>
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
<link rel="icon" sizes="192x192" href="/favicon-192.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 { Repository } from '@napi-rs/simple-git';
|
||||||
import HTMLPlugin from 'html-webpack-plugin';
|
import HTMLPlugin from 'html-webpack-plugin';
|
||||||
import { once } from 'lodash-es';
|
import { once } from 'lodash-es';
|
||||||
|
import type { Compiler } from 'webpack';
|
||||||
import webpack from 'webpack';
|
import webpack from 'webpack';
|
||||||
import { merge } from 'webpack-merge';
|
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';
|
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.`;
|
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;
|
: flags.entry;
|
||||||
|
|
||||||
const createHTMLPlugin = (entryName = 'app') => {
|
const publicPath = getPublicPath(flags);
|
||||||
return new HTMLPlugin({
|
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'),
|
template: join(rootPath, 'webpack', 'template.html'),
|
||||||
inject: 'body',
|
inject: 'body',
|
||||||
|
filename: 'index.html',
|
||||||
minify: false,
|
minify: false,
|
||||||
|
templateParameters: templateParams,
|
||||||
chunks: [entryName],
|
chunks: [entryName],
|
||||||
filename: `${entryName === 'app' ? 'index' : entryName}.html`, // main entry should take name index.html
|
} satisfies HTMLPlugin.Options;
|
||||||
templateParameters: (compilation, assets) => {
|
|
||||||
if (entryName === 'app') {
|
if (entryName === 'app') {
|
||||||
// emit assets manifest for ssr
|
return [
|
||||||
compilation.emitAsset(
|
{
|
||||||
`assets-manifest.json`,
|
apply(compiler: Compiler) {
|
||||||
new webpack.sources.RawSource(
|
compiler.hooks.compilation.tap(
|
||||||
JSON.stringify(
|
'assets-manifest-plugin',
|
||||||
{
|
compilation => {
|
||||||
...assets,
|
HTMLPlugin.getHooks(compilation).beforeAssetTagGeneration.tap(
|
||||||
gitHash: gitShortHash(),
|
'assets-manifest-plugin',
|
||||||
description: DESCRIPTION,
|
arg => {
|
||||||
},
|
if (!compilation.getAsset('assets-manifest.json')) {
|
||||||
null,
|
compilation.emitAsset(
|
||||||
2
|
`assets-manifest.json`,
|
||||||
)
|
new webpack.sources.RawSource(
|
||||||
),
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
immutable: true,
|
...arg.assets,
|
||||||
}
|
js: arg.assets.js.map(file =>
|
||||||
);
|
file.substring(arg.assets.publicPath.length)
|
||||||
}
|
),
|
||||||
return {
|
css: arg.assets.css.map(file =>
|
||||||
GIT_SHORT_SHA: gitShortHash(),
|
file.substring(arg.assets.publicPath.length)
|
||||||
DESCRIPTION,
|
),
|
||||||
PUBLIC_PATH: config.output?.publicPath,
|
gitHash: templateParams.GIT_SHORT_SHA,
|
||||||
VIEWPORT_FIT: flags.distribution === 'mobile' ? 'cover' : 'auto',
|
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, {
|
return merge(config, {
|
||||||
entry: entry,
|
entry,
|
||||||
plugins: Object.keys(entry).map(createHTMLPlugin),
|
plugins: Object.keys(entry).map(createHTMLPlugins).flat(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user