mirror of
https://github.com/dream-num/univer.git
synced 2026-10-02 04:24:34 +08:00
chore(examples): add interactive entry selection for dev builds (#7012)
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { BuildOptions, Plugin, SameShape } from 'esbuild';
|
||||
import { execSync } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { ignoreGlobalCssPlugin, removeClassnameNewlinesPlugin } from '@univerjs-infra/shared/esbuild';
|
||||
import aliasPlugin from 'esbuild-plugin-alias';
|
||||
import copyPlugin from 'esbuild-plugin-copy';
|
||||
import vue3 from 'esbuild-plugin-vue3';
|
||||
import stylePlugin from 'esbuild-style-plugin';
|
||||
import * as React from 'react';
|
||||
import tailwindcss from 'tailwindcss';
|
||||
import { syncDemoArtifacts } from '../sync-demos';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const nodeModules = path.resolve(process.cwd(), './node_modules');
|
||||
const isReact16 = React.version.startsWith('16');
|
||||
|
||||
export interface IBuildArgs {
|
||||
watch: boolean;
|
||||
e2e: boolean;
|
||||
all: boolean;
|
||||
}
|
||||
|
||||
export function createEnvDefinitions(args: IBuildArgs): Record<string, string> {
|
||||
const define: Record<string, string> = {
|
||||
'process.env.NODE_ENV': args.watch ? '"development"' : '"production"',
|
||||
'process.env.IS_E2E': args.e2e ? 'true' : 'false',
|
||||
};
|
||||
|
||||
if (!args.watch) {
|
||||
const gitCommitHash = args.e2e ? 'E2E' : execSync('git rev-parse --short HEAD').toString().trim();
|
||||
const gitRefName = args.e2e ? 'E2E' : execSync('git symbolic-ref -q --short HEAD || git describe --tags --exact-match').toString().trim();
|
||||
|
||||
define['process.env.GIT_COMMIT_HASH'] = `"${gitCommitHash}"`;
|
||||
define['process.env.GIT_REF_NAME'] = `"${gitRefName}"`;
|
||||
define['process.env.BUILD_TIME'] = `"${new Date().toISOString()}"`;
|
||||
}
|
||||
|
||||
return define;
|
||||
}
|
||||
|
||||
export async function createBuildConfig(args: IBuildArgs): Promise<SameShape<BuildOptions, BuildOptions>> {
|
||||
const isAll = !!args.all;
|
||||
let entryPoints: string[];
|
||||
|
||||
if (args.watch && !isAll) {
|
||||
const { selectEntries } = await import('../select-entries');
|
||||
const result = await selectEntries();
|
||||
entryPoints = result.entryPoints;
|
||||
} else {
|
||||
const { entryPoints: eps } = syncDemoArtifacts();
|
||||
entryPoints = eps;
|
||||
}
|
||||
|
||||
const config: SameShape<BuildOptions, BuildOptions> = {
|
||||
bundle: true,
|
||||
format: 'esm',
|
||||
splitting: true,
|
||||
color: true,
|
||||
loader: { '.svg': 'file', '.ttf': 'file' },
|
||||
sourcemap: args.watch,
|
||||
minify: false,
|
||||
target: 'chrome70',
|
||||
plugins: [
|
||||
ignoreGlobalCssPlugin(),
|
||||
removeClassnameNewlinesPlugin(),
|
||||
copyPlugin({
|
||||
assets: {
|
||||
from: ['./public/**/*'],
|
||||
to: ['./'],
|
||||
},
|
||||
}),
|
||||
stylePlugin({
|
||||
postcss: {
|
||||
plugins: [tailwindcss as unknown as { postcssPlugin: string }],
|
||||
},
|
||||
renderOptions: {
|
||||
lessOptions: {
|
||||
paths: [nodeModules],
|
||||
},
|
||||
},
|
||||
}),
|
||||
vue3() as unknown as Plugin,
|
||||
],
|
||||
entryPoints,
|
||||
outdir: './local',
|
||||
define: createEnvDefinitions(args),
|
||||
};
|
||||
|
||||
if (isReact16) {
|
||||
config.plugins?.push(
|
||||
aliasPlugin({
|
||||
'react-dom/client': path.resolve(__dirname, '../../src/client.ts'),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import esbuild from 'esbuild';
|
||||
import cleanPlugin from 'esbuild-plugin-clean';
|
||||
|
||||
export function monacoBuildTask() {
|
||||
const monacoEditorEntryPoints = [
|
||||
'vs/language/json/json.worker.js',
|
||||
'vs/language/css/css.worker.js',
|
||||
'vs/language/html/html.worker.js',
|
||||
'vs/language/typescript/ts.worker.js',
|
||||
'vs/editor/editor.worker.js',
|
||||
];
|
||||
|
||||
return esbuild.build({
|
||||
entryPoints: monacoEditorEntryPoints.map((entry) => `./node_modules/monaco-editor/esm/${entry}`),
|
||||
bundle: true,
|
||||
color: true,
|
||||
target: 'chrome70',
|
||||
format: 'iife',
|
||||
outbase: './node_modules/monaco-editor/esm/',
|
||||
outdir: './local',
|
||||
plugins: [
|
||||
cleanPlugin({
|
||||
patterns: ['./local'],
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export function nodeBuildTask() {
|
||||
return esbuild.build({
|
||||
bundle: true,
|
||||
color: true,
|
||||
minify: false,
|
||||
target: 'chrome70',
|
||||
entryPoints: [
|
||||
'./src/node/cases/basic.ts',
|
||||
'./src/node/sdk/worker.ts',
|
||||
],
|
||||
platform: 'node',
|
||||
outdir: './dist',
|
||||
define: {
|
||||
'process.env.NODE_ENV': '"production"',
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import process from 'node:process';
|
||||
|
||||
const supportsColor = !!process.stdout.isTTY && process.env.NO_COLOR == null;
|
||||
|
||||
const ansi = {
|
||||
reset: '\x1B[0m',
|
||||
bold: '\x1B[1m',
|
||||
dim: '\x1B[2m',
|
||||
red: '\x1B[31m',
|
||||
green: '\x1B[32m',
|
||||
yellow: '\x1B[33m',
|
||||
blue: '\x1B[34m',
|
||||
cyan: '\x1B[36m',
|
||||
gray: '\x1B[90m',
|
||||
underline: '\x1B[4m',
|
||||
} as const;
|
||||
|
||||
type TLogLevel = 'error' | 'info' | 'ready' | 'recover' | 'watch';
|
||||
|
||||
const levelStyleMap: Record<TLogLevel, string[]> = {
|
||||
error: [ansi.bold, ansi.red],
|
||||
info: [ansi.bold, ansi.blue],
|
||||
ready: [ansi.bold, ansi.green],
|
||||
recover: [ansi.bold, ansi.cyan],
|
||||
watch: [ansi.bold, ansi.yellow],
|
||||
};
|
||||
|
||||
function colorize(text: string, styles: string[]) {
|
||||
if (!supportsColor || styles.length === 0) {
|
||||
return text;
|
||||
}
|
||||
return `${styles.join('')}${text}${ansi.reset}`;
|
||||
}
|
||||
|
||||
function formatLabel(level: TLogLevel) {
|
||||
return colorize(`[${level}]`, levelStyleMap[level]);
|
||||
}
|
||||
|
||||
export function formatDuration(durationMs: number) {
|
||||
if (durationMs < 1000) {
|
||||
return `${durationMs}ms`;
|
||||
}
|
||||
return `${(durationMs / 1000).toFixed(durationMs < 10_000 ? 1 : 0)}s`;
|
||||
}
|
||||
|
||||
export function createLogger() {
|
||||
function write(level: TLogLevel, message: string, method: 'error' | 'log' = 'log') {
|
||||
console[method](`${formatLabel(level)} ${message}`);
|
||||
}
|
||||
|
||||
return {
|
||||
error(message: string) {
|
||||
write('error', message, 'error');
|
||||
},
|
||||
info(message: string) {
|
||||
write('info', message);
|
||||
},
|
||||
ready(message: string) {
|
||||
write('ready', message);
|
||||
},
|
||||
recover(message: string) {
|
||||
write('recover', message);
|
||||
},
|
||||
watch(message: string) {
|
||||
write('watch', message);
|
||||
},
|
||||
muted(message: string) {
|
||||
console.log(colorize(message, [ansi.dim, ansi.gray]));
|
||||
},
|
||||
url(url: string) {
|
||||
return colorize(url, [ansi.bold, ansi.underline, ansi.cyan]);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DemoDefinition } from './sync-demos';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import * as p from '@clack/prompts';
|
||||
import fs from 'fs-extra';
|
||||
import { discoverDemos, syncDemoArtifacts } from './sync-demos';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const EXAMPLES_ROOT = path.resolve(__dirname, '..');
|
||||
const SELECTION_FILE = path.resolve(EXAMPLES_ROOT, 'entry-selection.json');
|
||||
|
||||
export interface ISelectionFile {
|
||||
selected: string[];
|
||||
known: string[];
|
||||
}
|
||||
|
||||
export interface ISelectionResult {
|
||||
entryPoints: string[];
|
||||
selectedDirs: string[];
|
||||
}
|
||||
|
||||
export async function selectEntries(): Promise<ISelectionResult> {
|
||||
const demos = discoverDemos();
|
||||
|
||||
if (!process.stdout.isTTY) {
|
||||
const { entryPoints } = syncDemoArtifacts();
|
||||
return {
|
||||
entryPoints,
|
||||
selectedDirs: demos.map((d) => d.dir),
|
||||
};
|
||||
}
|
||||
|
||||
const saved = await readSelection();
|
||||
const merged = mergeSelection(demos, saved);
|
||||
|
||||
const selectedDirs = await promptSelection(demos, merged);
|
||||
await writeSelection(selectedDirs, demos);
|
||||
|
||||
const { entryPoints } = syncDemoArtifacts(selectedDirs);
|
||||
|
||||
return { entryPoints, selectedDirs };
|
||||
}
|
||||
|
||||
export async function readSelection(filePath = SELECTION_FILE): Promise<ISelectionFile | null> {
|
||||
try {
|
||||
if (!await fs.pathExists(filePath)) {
|
||||
return null;
|
||||
}
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
const parsed = JSON.parse(content);
|
||||
// Backward compat: old format was just an array of strings
|
||||
if (Array.isArray(parsed)) {
|
||||
return { selected: parsed.filter((item): item is string => typeof item === 'string'), known: [] };
|
||||
}
|
||||
if (parsed && typeof parsed === 'object' && Array.isArray(parsed.selected) && Array.isArray(parsed.known)) {
|
||||
return {
|
||||
selected: parsed.selected.filter((item: unknown): item is string => typeof item === 'string'),
|
||||
known: parsed.known.filter((item: unknown): item is string => typeof item === 'string'),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function mergeSelection(demos: DemoDefinition[], saved: ISelectionFile | null): string[] {
|
||||
const currentDirs = new Set(demos.map((d) => d.dir));
|
||||
if (!saved) {
|
||||
return [...currentDirs].sort();
|
||||
}
|
||||
const selectedValid = saved.selected.filter((dir) => currentDirs.has(dir));
|
||||
const knownSet = new Set(saved.known);
|
||||
const merged = new Set(selectedValid);
|
||||
for (const dir of currentDirs) {
|
||||
if (!knownSet.has(dir)) {
|
||||
merged.add(dir); // new demo, default select
|
||||
}
|
||||
}
|
||||
return [...merged].sort();
|
||||
}
|
||||
|
||||
async function promptSelection(demos: DemoDefinition[], initial: string[]): Promise<string[]> {
|
||||
const options = demos.map((demo) => ({
|
||||
value: demo.dir,
|
||||
label: demo.title,
|
||||
}));
|
||||
|
||||
const initialSet = new Set(initial);
|
||||
|
||||
const result = await p.multiselect({
|
||||
message: 'Select demo entries to start (a: toggle all, space: select, enter: confirm, esc: cancel)',
|
||||
options,
|
||||
initialValues: options.filter((opt) => initialSet.has(opt.value)).map((opt) => opt.value),
|
||||
});
|
||||
|
||||
if (p.isCancel(result)) {
|
||||
p.cancel('Operation cancelled');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function writeSelection(selectedDirs: string[], demos: DemoDefinition[]): Promise<void> {
|
||||
const file: ISelectionFile = {
|
||||
selected: selectedDirs,
|
||||
known: demos.map((d) => d.dir),
|
||||
};
|
||||
await fs.writeFile(SELECTION_FILE, `${JSON.stringify(file, null, 4)}\n`);
|
||||
}
|
||||
@@ -141,6 +141,10 @@ export function discoverDemos() {
|
||||
.filter((demo): demo is DemoDefinition => demo !== null);
|
||||
}
|
||||
|
||||
function filterDemos(demos: DemoDefinition[], selectedDirs?: string[]): DemoDefinition[] {
|
||||
return selectedDirs ? demos.filter((demo) => selectedDirs.includes(demo.dir)) : demos;
|
||||
}
|
||||
|
||||
function syncDemoHtml(demos: DemoDefinition[]) {
|
||||
fs.ensureDirSync(PUBLIC_DIR);
|
||||
|
||||
@@ -189,12 +193,13 @@ function syncDemosModule(demos: DemoDefinition[]) {
|
||||
fs.writeFileSync(GENERATED_DEMOS_FILE, content);
|
||||
}
|
||||
|
||||
export function getDemoEntryPoints() {
|
||||
return ['./src/main.tsx', ...discoverDemos().flatMap((demo) => demo.entryPoints)];
|
||||
export function getDemoEntryPoints(selectedDirs?: string[]) {
|
||||
const demos = filterDemos(discoverDemos(), selectedDirs);
|
||||
return ['./src/main.tsx', ...demos.flatMap((demo) => demo.entryPoints)];
|
||||
}
|
||||
|
||||
export function syncDemoArtifacts() {
|
||||
const demos = discoverDemos();
|
||||
export function syncDemoArtifacts(selectedDirs?: string[]) {
|
||||
const demos = filterDemos(discoverDemos(), selectedDirs);
|
||||
syncDemoHtml(demos);
|
||||
syncDemosModule(demos);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user