mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-24 22:09:08 +08:00
feat: cleanup webpack deps (#14530)
#### PR Dependency Tree * **PR #14530** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Breaking Changes** * Webpack bundler support removed from the build system * Bundler selection parameter removed from build and development commands * **Refactor** * Build configuration consolidated to a single bundler approach * Webpack-specific build paths and workflows removed; development server simplified * **Chores** * Removed webpack-related dev dependencies and tooling * Updated package build scripts for a unified bundle command * **Dependencies** * Upgraded Sentry packages across frontend packages (react/electron/esbuild plugin) <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
function testPackageName(regexp: RegExp): (module: any) => boolean {
|
||||
return (module: any) =>
|
||||
module.nameForCondition && regexp.test(module.nameForCondition());
|
||||
}
|
||||
|
||||
// https://hackernoon.com/the-100-correct-way-to-split-your-chunks-with-webpack-f8a9df5b7758
|
||||
export const productionCacheGroups = {
|
||||
i18n: {
|
||||
test: /frontend[\\/]i18n[\\/]/,
|
||||
name: (module: any) => {
|
||||
const name = module.resource.match(/[\\/]([^\\/]+)\.json$/)?.[1];
|
||||
if (name && name !== 'en') {
|
||||
return `i18n-langs.${name}`;
|
||||
}
|
||||
|
||||
return 'i18n';
|
||||
},
|
||||
priority: 200,
|
||||
enforce: true,
|
||||
},
|
||||
asyncVendor: {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
name(module: any) {
|
||||
const modulePath =
|
||||
module?.nameForCondition?.() || module?.context || module?.resource;
|
||||
|
||||
if (!modulePath || typeof modulePath !== 'string') {
|
||||
return 'app-async';
|
||||
}
|
||||
|
||||
// monorepo linked in node_modules, so it's not a npm package
|
||||
if (!modulePath.includes('node_modules')) {
|
||||
return `app-async`;
|
||||
}
|
||||
const name = modulePath.match(
|
||||
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
|
||||
)?.[1];
|
||||
return `npm-async-${name}`;
|
||||
},
|
||||
priority: Number.MAX_SAFE_INTEGER,
|
||||
chunks: 'async' as const,
|
||||
},
|
||||
blocksuite: {
|
||||
name: `npm-blocksuite`,
|
||||
test: testPackageName(/[\\/]node_modules[\\/](@blocksuite)[\\/]/),
|
||||
priority: 200,
|
||||
enforce: true,
|
||||
},
|
||||
react: {
|
||||
name: `npm-react`,
|
||||
test: testPackageName(
|
||||
/[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/
|
||||
),
|
||||
priority: 200,
|
||||
enforce: true,
|
||||
},
|
||||
jotai: {
|
||||
name: `npm-jotai`,
|
||||
test: testPackageName(/[\\/]node_modules[\\/](jotai)[\\/]/),
|
||||
priority: 200,
|
||||
enforce: true,
|
||||
},
|
||||
rxjs: {
|
||||
name: `npm-rxjs`,
|
||||
test: testPackageName(/[\\/]node_modules[\\/]rxjs[\\/]/),
|
||||
priority: 200,
|
||||
enforce: true,
|
||||
},
|
||||
lodash: {
|
||||
name: `npm-lodash`,
|
||||
test: testPackageName(/[\\/]node_modules[\\/]lodash[\\/]/),
|
||||
priority: 200,
|
||||
enforce: true,
|
||||
},
|
||||
emotion: {
|
||||
name: `npm-emotion`,
|
||||
test: testPackageName(/[\\/]node_modules[\\/](@emotion)[\\/]/),
|
||||
priority: 200,
|
||||
enforce: true,
|
||||
},
|
||||
vendor: {
|
||||
name: 'vendor',
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
priority: 190,
|
||||
enforce: true,
|
||||
},
|
||||
styles: {
|
||||
name: 'styles',
|
||||
test: (module: any) =>
|
||||
module.nameForCondition &&
|
||||
module.nameForCondition()?.endsWith('.css') &&
|
||||
!module.type.startsWith('javascript'),
|
||||
chunks: 'all' as const,
|
||||
minSize: 1,
|
||||
minChunks: 1,
|
||||
reuseExistingChunk: true,
|
||||
priority: 1000,
|
||||
enforce: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
(function () {
|
||||
let errorEl = null;
|
||||
function showGlobalErrorPage() {
|
||||
if (errorEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
errorEl = document.createElement('div');
|
||||
errorEl.innerHTML = [
|
||||
'<style>',
|
||||
'.gue {display:flex;flex-direction:column;align-items:center;justify-content:center;width:380px;}',
|
||||
'.gue img{width:380px;}',
|
||||
'.gue div{padding:16px 40px 0 40px;text-align:center;}',
|
||||
'.gue .p1{color:#141414;line-height:24px;font-weight:500;}',
|
||||
'.gue .p2{color:#7A7A7A;line-height:22px;}',
|
||||
'</style>',
|
||||
'<div class="gue">',
|
||||
'<img src="https://cdn.affine.pro/error.png" />',
|
||||
'<div>',
|
||||
'<p class="p1">Unsupported Environment</p>',
|
||||
'<p class="p2">',
|
||||
'It looks like AFFiNE cannot run in this environment.',
|
||||
"Please ensure you are using a supported browser or update your device's operating system to the latest version.",
|
||||
'If the issue persists, visit our <a href="https://github.com/toeverything/AFFiNE/issues">support page</a> for further assistance.',
|
||||
'</p>',
|
||||
'</div>',
|
||||
'</div>',
|
||||
].join('');
|
||||
errorEl.setAttribute(
|
||||
'style',
|
||||
'position:absolute;top:0;left:0;height:100vh;width:100vw;display:flex;flex-direction:column;align-items:center;justify-content:center;background:white;z-index:999;'
|
||||
);
|
||||
document.body.append(errorEl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param event {PromiseRejectionEvent|ErrorEvent}
|
||||
*/
|
||||
function handler(event) {
|
||||
let error;
|
||||
|
||||
if ('error' in event) {
|
||||
error =
|
||||
event.error ||
|
||||
(event.message === 'Script error.'
|
||||
? new SyntaxError(event.message)
|
||||
: new Error(event.message));
|
||||
} else {
|
||||
error = event.reason;
|
||||
}
|
||||
|
||||
console.error('unhandled unrecoverable error', error);
|
||||
|
||||
const shouldCache =
|
||||
// syntax error
|
||||
error && error instanceof SyntaxError;
|
||||
|
||||
if (!shouldCache) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.stopImmediatePropagation();
|
||||
showGlobalErrorPage();
|
||||
}
|
||||
|
||||
function registerGlobalErrorHandler() {
|
||||
if (typeof document !== 'undefined') {
|
||||
globalThis.addEventListener('unhandledrejection', handler);
|
||||
globalThis.addEventListener('error', handler);
|
||||
|
||||
return function () {
|
||||
globalThis.removeEventListener('unhandledrejection', handler);
|
||||
globalThis.removeEventListener('error', handler);
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function unregisterRegisterGlobalErrorHandler(fn) {
|
||||
if (typeof fn === 'function') {
|
||||
const app = document.getElementById('app');
|
||||
if (app) {
|
||||
let ob = new MutationObserver(function () {
|
||||
fn();
|
||||
ob.disconnect();
|
||||
ob = null;
|
||||
});
|
||||
|
||||
ob.observe(app, { childList: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ensureBasicEnvironment() {
|
||||
const globals = [
|
||||
'Promise',
|
||||
'Map',
|
||||
'fetch',
|
||||
'customElements',
|
||||
'MutationObserver',
|
||||
];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
||||
for (let i = 0; i < globals.length; i++) {
|
||||
if (!(globals[i] in globalThis)) {
|
||||
showGlobalErrorPage();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ensureBasicEnvironment();
|
||||
const goodtogo = registerGlobalErrorHandler();
|
||||
unregisterRegisterGlobalErrorHandler(goodtogo);
|
||||
})();
|
||||
@@ -0,0 +1,292 @@
|
||||
import { execSync } from 'node:child_process';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
import { Path, ProjectRoot } from '@affine-tools/utils/path';
|
||||
import { Repository } from '@napi-rs/simple-git';
|
||||
import HTMLPlugin from 'html-webpack-plugin';
|
||||
import { once } from 'lodash-es';
|
||||
|
||||
type PluginLike = {
|
||||
apply: (compiler: CompilerLike) => void;
|
||||
};
|
||||
|
||||
type CompilerLike = {
|
||||
webpack?: {
|
||||
sources?: {
|
||||
RawSource?: new (source: string) => unknown;
|
||||
};
|
||||
};
|
||||
hooks: {
|
||||
compilation: {
|
||||
tap: (name: string, callback: (compilation: any) => void) => void;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
function createRawSource(compiler: CompilerLike, source: string) {
|
||||
const RawSource = compiler.webpack?.sources?.RawSource;
|
||||
if (!RawSource) {
|
||||
throw new Error(
|
||||
'compiler.webpack.sources.RawSource is required for html plugin assets emission'
|
||||
);
|
||||
}
|
||||
|
||||
return new RawSource(source);
|
||||
}
|
||||
|
||||
export const getPublicPath = (BUILD_CONFIG: BUILD_CONFIG_TYPE) => {
|
||||
const { BUILD_TYPE } = process.env;
|
||||
if (typeof process.env.PUBLIC_PATH === 'string') {
|
||||
return process.env.PUBLIC_PATH;
|
||||
}
|
||||
|
||||
if (
|
||||
BUILD_CONFIG.debug ||
|
||||
BUILD_CONFIG.distribution === 'desktop' ||
|
||||
BUILD_CONFIG.distribution === 'ios' ||
|
||||
BUILD_CONFIG.distribution === 'android'
|
||||
) {
|
||||
return '/';
|
||||
}
|
||||
|
||||
switch (BUILD_TYPE) {
|
||||
case 'stable':
|
||||
return 'https://prod.affineassets.com/';
|
||||
case 'beta':
|
||||
return 'https://beta.affineassets.com/';
|
||||
default:
|
||||
return 'https://dev.affineassets.com/';
|
||||
}
|
||||
};
|
||||
|
||||
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 gitShortHash = once(() => {
|
||||
const { GITHUB_SHA } = process.env;
|
||||
if (GITHUB_SHA) {
|
||||
return GITHUB_SHA.substring(0, 9);
|
||||
}
|
||||
const repo = new Repository(ProjectRoot.value);
|
||||
const shortSha = repo.head().target()?.substring(0, 9);
|
||||
if (shortSha) {
|
||||
return shortSha;
|
||||
}
|
||||
const sha = execSync(`git rev-parse --short HEAD`, {
|
||||
encoding: 'utf-8',
|
||||
}).trim();
|
||||
return sha;
|
||||
});
|
||||
|
||||
const currentDir = Path.dir(import.meta.url);
|
||||
|
||||
export interface CreateHTMLPluginConfig {
|
||||
filename?: string;
|
||||
additionalEntryForSelfhost?: boolean;
|
||||
selfhostPublicPath?: string;
|
||||
injectGlobalErrorHandler?: boolean;
|
||||
emitAssetsManifest?: boolean;
|
||||
}
|
||||
|
||||
function getHTMLPluginOptions(BUILD_CONFIG: BUILD_CONFIG_TYPE) {
|
||||
const publicPath = getPublicPath(BUILD_CONFIG);
|
||||
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: BUILD_CONFIG.isMobileEdition ? 'cover' : 'auto',
|
||||
};
|
||||
|
||||
return {
|
||||
template: currentDir.join('template.html').toString(),
|
||||
inject: 'body',
|
||||
minify: false,
|
||||
templateParameters: templateParams,
|
||||
chunks: ['app'],
|
||||
scriptLoading: 'blocking',
|
||||
} satisfies HTMLPlugin.Options;
|
||||
}
|
||||
|
||||
const AssetsManifestPlugin = {
|
||||
apply(compiler: CompilerLike) {
|
||||
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`,
|
||||
createRawSource(
|
||||
compiler,
|
||||
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: gitShortHash(),
|
||||
description: DESCRIPTION,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
),
|
||||
{
|
||||
immutable: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return arg;
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const GlobalErrorHandlerPlugin = {
|
||||
apply(compiler: CompilerLike) {
|
||||
const globalErrorHandler = [
|
||||
'js/global-error-handler.js',
|
||||
readFileSync(currentDir.join('./error-handler.js').toString(), 'utf-8'),
|
||||
];
|
||||
|
||||
compiler.hooks.compilation.tap(
|
||||
'global-error-handler-plugin',
|
||||
compilation => {
|
||||
HTMLPlugin.getHooks(compilation).beforeAssetTagGeneration.tap(
|
||||
'global-error-handler-plugin',
|
||||
arg => {
|
||||
if (!compilation.getAsset(globalErrorHandler[0])) {
|
||||
compilation.emitAsset(
|
||||
globalErrorHandler[0],
|
||||
createRawSource(compiler, globalErrorHandler[1])
|
||||
);
|
||||
arg.assets.js.unshift(
|
||||
arg.assets.publicPath + globalErrorHandler[0]
|
||||
);
|
||||
}
|
||||
|
||||
return arg;
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const CorsPlugin = {
|
||||
apply(compiler: CompilerLike) {
|
||||
compiler.hooks.compilation.tap('html-js-cors-plugin', compilation => {
|
||||
HTMLPlugin.getHooks(compilation).alterAssetTags.tap(
|
||||
'html-js-cors-plugin',
|
||||
options => {
|
||||
if (options.publicPath !== '/') {
|
||||
options.assetTags.scripts.forEach(script => {
|
||||
script.attributes.crossorigin = true;
|
||||
});
|
||||
options.assetTags.styles.forEach(style => {
|
||||
style.attributes.crossorigin = true;
|
||||
});
|
||||
}
|
||||
return options;
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export function createHTMLPlugins(
|
||||
BUILD_CONFIG: BUILD_CONFIG_TYPE,
|
||||
config: CreateHTMLPluginConfig
|
||||
): (HTMLPlugin | PluginLike)[] {
|
||||
const publicPath = getPublicPath(BUILD_CONFIG);
|
||||
const htmlPluginOptions = getHTMLPluginOptions(BUILD_CONFIG);
|
||||
const selfhostPublicPath = config.selfhostPublicPath ?? '/';
|
||||
|
||||
const plugins: (HTMLPlugin | PluginLike)[] = [];
|
||||
plugins.push(
|
||||
new HTMLPlugin({
|
||||
...htmlPluginOptions,
|
||||
chunks: ['index'],
|
||||
filename: config.filename,
|
||||
publicPath,
|
||||
meta: {
|
||||
'env:publicPath': publicPath,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
if (BUILD_CONFIG.isElectron) {
|
||||
plugins.push(
|
||||
new HTMLPlugin({
|
||||
...htmlPluginOptions,
|
||||
chunks: ['shell'],
|
||||
filename: 'shell.html',
|
||||
publicPath,
|
||||
meta: {
|
||||
'env:publicPath': publicPath,
|
||||
},
|
||||
}),
|
||||
new HTMLPlugin({
|
||||
...htmlPluginOptions,
|
||||
filename: 'popup.html',
|
||||
chunks: ['popup'],
|
||||
publicPath,
|
||||
meta: {
|
||||
'env:publicPath': publicPath,
|
||||
},
|
||||
}),
|
||||
new HTMLPlugin({
|
||||
...htmlPluginOptions,
|
||||
filename: 'background-worker.html',
|
||||
chunks: ['backgroundWorker'],
|
||||
publicPath,
|
||||
meta: {
|
||||
'env:publicPath': publicPath,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (!BUILD_CONFIG.isElectron) {
|
||||
plugins.push(CorsPlugin);
|
||||
}
|
||||
|
||||
if (config.emitAssetsManifest) {
|
||||
plugins.push(AssetsManifestPlugin);
|
||||
}
|
||||
|
||||
if (config.injectGlobalErrorHandler) {
|
||||
plugins.push(GlobalErrorHandlerPlugin);
|
||||
}
|
||||
|
||||
if (config.additionalEntryForSelfhost) {
|
||||
plugins.push(
|
||||
new HTMLPlugin({
|
||||
...htmlPluginOptions,
|
||||
chunks: ['index'],
|
||||
publicPath: selfhostPublicPath,
|
||||
meta: {
|
||||
'env:isSelfHosted': 'true',
|
||||
'env:publicPath': selfhostPublicPath,
|
||||
},
|
||||
filename: 'selfhost.html',
|
||||
templateParameters: {
|
||||
...htmlPluginOptions.templateParameters,
|
||||
PRECONNECT: '',
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { parse } from 'node:path';
|
||||
|
||||
export const raw = true;
|
||||
/**
|
||||
* @type {import('webpack').LoaderDefinitionFunction}
|
||||
*/
|
||||
export default function loader(content) {
|
||||
const name = parse(this.resourcePath).base;
|
||||
this.emitFile(name, content);
|
||||
|
||||
return `
|
||||
import { createRequire } from 'node:module'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const binding = require('./${name}')
|
||||
export default binding
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { readdir, readFile } from 'node:fs/promises';
|
||||
import { join, relative, sep } from 'node:path';
|
||||
|
||||
import { createS3CompatClient } from '@affine/s3-compat';
|
||||
import { lookup } from 'mime-types';
|
||||
|
||||
export const R2_BUCKET =
|
||||
process.env.R2_BUCKET ??
|
||||
(process.env.BUILD_TYPE === 'canary' ? 'assets-dev' : 'assets-prod');
|
||||
|
||||
const S3_UPLOAD_PACKAGE_NAMES = new Set([
|
||||
'@affine/web',
|
||||
'@affine/mobile',
|
||||
'@affine/admin',
|
||||
]);
|
||||
const MAX_UPLOAD_RETRIES = 3;
|
||||
const UPLOAD_RETRY_BASE_DELAY_MS = 500;
|
||||
|
||||
function createR2Client() {
|
||||
const { R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY } = process.env;
|
||||
if (!R2_ACCOUNT_ID || !R2_ACCESS_KEY_ID || !R2_SECRET_ACCESS_KEY) {
|
||||
throw new Error('Missing R2 credentials for uploading release assets');
|
||||
}
|
||||
|
||||
return createS3CompatClient(
|
||||
{
|
||||
region: 'auto',
|
||||
bucket: R2_BUCKET,
|
||||
forcePathStyle: true,
|
||||
endpoint: `https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
||||
},
|
||||
{
|
||||
accessKeyId: R2_ACCESS_KEY_ID,
|
||||
secretAccessKey: R2_SECRET_ACCESS_KEY,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function collectFiles(dir: string): Promise<string[]> {
|
||||
const dirs = [dir];
|
||||
const files: string[] = [];
|
||||
|
||||
while (dirs.length > 0) {
|
||||
const current = dirs.pop()!;
|
||||
const entries = await readdir(current, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(current, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
dirs.push(fullPath);
|
||||
} else if (entry.isFile()) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function toAssetKey(outputPath: string, filePath: string): string {
|
||||
return relative(outputPath, filePath).split(sep).join('/');
|
||||
}
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function putObjectWithRetry(
|
||||
s3: ReturnType<typeof createR2Client>,
|
||||
asset: string,
|
||||
assetSource: Buffer,
|
||||
contentType: string | false | undefined
|
||||
) {
|
||||
let retries = 0;
|
||||
while (true) {
|
||||
try {
|
||||
await s3.putObject(asset, assetSource, {
|
||||
contentType: contentType || undefined,
|
||||
contentLength: assetSource.byteLength,
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
if (retries >= MAX_UPLOAD_RETRIES) {
|
||||
throw error;
|
||||
}
|
||||
retries += 1;
|
||||
const delay = UPLOAD_RETRY_BASE_DELAY_MS * 2 ** (retries - 1);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
console.warn(
|
||||
`[s3-upload] Retry ${retries}/${MAX_UPLOAD_RETRIES} for ${asset}: ${errorMessage}`
|
||||
);
|
||||
await sleep(delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function runInParallel<T>(
|
||||
values: T[],
|
||||
worker: (value: T) => Promise<void>,
|
||||
concurrency = 16
|
||||
) {
|
||||
if (values.length === 0) {
|
||||
return;
|
||||
}
|
||||
let nextIndex = 0;
|
||||
const workers = Array.from(
|
||||
{ length: Math.min(concurrency, values.length) },
|
||||
async () => {
|
||||
while (true) {
|
||||
const index = nextIndex++;
|
||||
if (index >= values.length) {
|
||||
return;
|
||||
}
|
||||
await worker(values[index]!);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await Promise.all(workers);
|
||||
}
|
||||
|
||||
export function shouldUploadReleaseAssets(pkgName: string): boolean {
|
||||
return S3_UPLOAD_PACKAGE_NAMES.has(pkgName);
|
||||
}
|
||||
|
||||
export async function uploadDistAssetsToS3(outputPath: string) {
|
||||
const allFiles = await collectFiles(outputPath);
|
||||
const uploadFiles = allFiles.filter(file => !file.endsWith('.html'));
|
||||
|
||||
if (uploadFiles.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const s3 = createR2Client();
|
||||
await runInParallel(uploadFiles, async filePath => {
|
||||
const asset = toAssetKey(outputPath, filePath);
|
||||
const assetSource = await readFile(filePath);
|
||||
const contentType = lookup(asset);
|
||||
await putObjectWithRetry(s3, asset, assetSource, contentType);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=<%= VIEWPORT_FIT %>"
|
||||
/>
|
||||
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent"
|
||||
/>
|
||||
|
||||
<title>AFFiNE</title>
|
||||
<meta name="theme-color" content="#fafafa" />
|
||||
<%= 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" />
|
||||
<link rel="shortcut icon" href="/favicon.ico?v=2" />
|
||||
<meta name="emotion-insertion-point" content="" />
|
||||
<meta property="description" content="<%= DESCRIPTION %>" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:url" content="https://app.affine.pro/" />
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content="AFFiNE: There can be more than Notion and Miro."
|
||||
/>
|
||||
<meta name="twitter:description" content="<%= DESCRIPTION %>" />
|
||||
<meta name="twitter:site" content="@AffineOfficial" />
|
||||
<meta name="twitter:image" content="https://affine.pro/og.jpeg" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content="AFFiNE: There can be more than Notion and Miro."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:description" content="<%= DESCRIPTION %>" />
|
||||
<meta property="og:url" content="https://app.affine.pro/" />
|
||||
<meta property="og:image" content="https://affine.pro/og.jpeg" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app" data-version="<%= GIT_SHORT_SHA %>"></div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user