snapshot of https://github.com/dream-num/univer.git (dev, 2026-09-16; blobs > 131072B omitted)
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-16 05:09:45 +05:00
commit bcf6ef63ad
7118 changed files with 1236854 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
import type { UserConfig } from 'tsdown';
import type { IEntryConfig } from '../types.ts';
import { defineConfig } from 'tsdown';
import { createCssNoopInputOptions } from '../plugins/css-noop.ts';
import { createOutputAliasPlugin } from '../plugins/output-alias.ts';
import { createOutputObfuscatorPlugin } from '../plugins/output-obfuscator.ts';
export type TModuleFormat = 'cjs' | 'esm';
export interface ICreateModuleConfigOptions {
baseConfig: Partial<UserConfig>;
enableObfuscation: boolean;
entries: IEntryConfig[];
externalPackages: string[];
facadeExternalPackages: string[];
format: TModuleFormat;
obfuscatorIgnorePatterns?: RegExp[];
outDir: string;
packageDir: string;
plugins: any[];
}
/**
* Creates the common ESM/CJS bundle config for a single package entry.
*/
export function createModuleConfig(options: ICreateModuleConfigOptions): UserConfig {
const { baseConfig, enableObfuscation, entries, externalPackages, facadeExternalPackages, format, obfuscatorIgnorePatterns, outDir, packageDir, plugins } = options;
const hasFacadeEntry = entries.some((entry) => entry.type === 'facade');
const hasIndexEntry = entries.some((entry) => entry.type === 'index');
const neverBundle = hasFacadeEntry ? facadeExternalPackages : externalPackages;
const copyToRoot = format === 'esm';
const keepRootIndexCss = hasIndexEntry && format === 'esm';
return defineConfig({
...baseConfig,
deps: {
neverBundle,
},
dts: false,
entry: Object.fromEntries(entries.map((entry) => [entry.key, entry.path])),
format,
inputOptions: keepRootIndexCss ? baseConfig.inputOptions : createCssNoopInputOptions(baseConfig.inputOptions),
outputOptions: {
codeSplitting: true,
minify: enableObfuscation,
},
outDir,
plugins: [
...plugins,
...(enableObfuscation ? [createOutputObfuscatorPlugin(obfuscatorIgnorePatterns)] : []),
createOutputAliasPlugin({
copyToRoot,
keepRootIndexCss,
packageDir,
}),
],
});
}

View File

@@ -0,0 +1,132 @@
import type { UserConfig } from 'tsdown';
import type { IEntryConfig } from '../types.ts';
import { defineConfig } from 'tsdown';
import { peerDepsMap } from '../data/peer-deps.ts';
import { createCssNoopInputOptions } from '../plugins/css-noop.ts';
import { createOutputAliasPlugin } from '../plugins/output-alias.ts';
import { createOutputObfuscatorPlugin } from '../plugins/output-obfuscator.ts';
export interface ICreateUmdConfigOptions {
baseConfig: Partial<UserConfig>;
enableObfuscation: boolean;
entry: IEntryConfig;
obfuscatorIgnorePatterns?: RegExp[];
outDir: string;
packageDir: string;
packageName: string;
plugins: any[];
}
const UMD_GLOBALS: Record<string, string> = Object.fromEntries(
Object.entries(peerDepsMap).map(([source, value]) => [source, value.global])
);
const CSS_UMD_GLOBAL = 'UniverCssNoop';
function convertLibNameFromPackageName(name: string) {
return name
.replace(/^@(univerjs(?:-pro)?)\//, (_, matchedPrefix) => {
return matchedPrefix === 'univerjs-pro' ? 'univer-pro-' : 'univer-';
})
.replace('/facade', '-facade')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
}
/**
* Maps externals to their browser globals in UMD builds.
*/
function resolveUmdGlobal(source: string) {
if (source.endsWith('.css')) {
return null;
}
if (source in UMD_GLOBALS) {
return UMD_GLOBALS[source];
}
if (source.startsWith('@univerjs')) {
if (source === '@univerjs/icons') {
return null;
}
const localeMatch = source.match(/^(@univerjs(?:-pro)?\/[^/]+)\/(?:locale|locales)\/([^/]+)$/);
if (localeMatch) {
return `${convertLibNameFromPackageName(localeMatch[1])}${convertLibNameFromPackageName(localeMatch[2])}`;
}
return convertLibNameFromPackageName(source);
}
return null;
}
function resolveOutputGlobal(source: string) {
const global = resolveUmdGlobal(source);
if (global !== null) {
return global;
}
if (source.endsWith('.css')) {
return CSS_UMD_GLOBAL;
}
return convertLibNameFromPackageName(source);
}
/**
* Produces a stable UMD global name for primary, facade and locale entries.
*/
function getGlobalName(packageName: string, entryKey: string) {
const name = convertLibNameFromPackageName(packageName);
if (entryKey === 'facade') {
return `${name}Facade`;
}
if (entryKey.startsWith('locale/') || entryKey.startsWith('locales/')) {
const localeKey = entryKey.split('/')[1];
return `${name}${convertLibNameFromPackageName(localeKey)}`;
}
return name;
}
/**
* Creates the browser-oriented UMD bundle config for a single package entry.
*/
export function createUmdConfig(options: ICreateUmdConfigOptions): UserConfig {
const { baseConfig, enableObfuscation, entry, obfuscatorIgnorePatterns, outDir, packageDir, packageName, plugins } = options;
return defineConfig({
...baseConfig,
deps: {
alwaysBundle: [/.*/],
neverBundle: (source: string) => Boolean(resolveUmdGlobal(source)),
onlyBundle: false,
},
dts: false,
entry: { [entry.key]: entry.path },
format: 'umd',
globalName: getGlobalName(packageName, entry.key),
inputOptions: createCssNoopInputOptions(baseConfig.inputOptions),
outDir,
outputOptions: {
entryFileNames: '[name].js',
globals: (source: string) => resolveOutputGlobal(source),
minify: true,
},
platform: 'browser',
plugins: [
...plugins,
...(enableObfuscation ? [createOutputObfuscatorPlugin(obfuscatorIgnorePatterns)] : []),
createOutputAliasPlugin({
copyToRoot: false,
keepRootIndexCss: false,
packageDir,
}),
],
});
}

View File

@@ -0,0 +1,16 @@
export const BUILD_OUTPUT_ROOT = 'dist';
export const BUILD_OUTPUT_DIRECTORIES = {
cjs: 'lib/cjs',
esm: 'lib/es',
umd: 'lib/umd',
} as const;
export const CLEANUP_DIRECTORIES = ['dist', 'lib', 'coverage'] as const;
export const DEFAULT_BROWSER_TARGET = 'chrome88';
export const DEFAULT_ENTRY_FILE = 'src/index.ts';
export const FACADE_ENTRY_FILE = 'src/facade/index.ts';
export const LOCALE_DIRECTORY = 'src/locale';
export const SOURCE_DIRECTORY = 'src';
export const TYPE_BUILD_CONFIG_FILE = 'tsconfig.tsdown.build.json';
export const TYPE_BUILD_EXCLUDES = ['src/**/__tests__/**', 'src/**/*.spec.ts', 'src/**/*.test.ts'];

View File

@@ -0,0 +1,47 @@
export const peerDepsMap = {
react: {
global: 'React',
name: 'react',
version: '^16.9.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc',
},
'react/jsx-runtime': {
global: 'React',
name: 'react',
version: 'react',
},
'react-dom': {
global: 'ReactDOM',
name: 'react-dom',
version: '^16.9.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc',
},
'react-dom/client': {
global: 'ReactDOM',
name: 'react-dom',
version: 'react-dom',
},
rxjs: {
global: 'rxjs',
name: 'rxjs',
version: '>=7.0.0',
},
'rxjs/operators': {
global: 'rxjs.operators',
name: 'rxjs',
version: 'rxjs',
},
'@wendellhu/redi': {
global: '@wendellhu/redi',
name: '@wendellhu/redi',
version: '1.1.3',
},
'@wendellhu/redi/react-bindings': {
global: '@wendellhu/redi/react-bindings',
name: '@wendellhu/redi',
version: '@wendellhu/redi',
},
vue: {
global: 'Vue',
name: 'vue',
version: '>=3.0.0',
},
} as const;

View File

@@ -0,0 +1,142 @@
import type { TModuleFormat } from './configs/module.ts';
import type { IBuildContext, IBuildOptions } from './types.ts';
import { existsSync, rmSync } from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import { pathToFileURL } from 'node:url';
import { mergeConfig, build as tsdownBuild } from 'tsdown';
import { createModuleConfig } from './configs/module.ts';
import { createUmdConfig } from './configs/umd.ts';
import { BUILD_OUTPUT_DIRECTORIES, BUILD_OUTPUT_ROOT, CLEANUP_DIRECTORIES } from './constants.ts';
import { createBaseConfig, createInputOptions, createInputPlugins } from './utils/base-config.ts';
import { cleanupPackageJson } from './utils/cleanup-pkg.ts';
import { getEntries } from './utils/entries.ts';
import { removeCssArtifacts } from './utils/files.ts';
import { createExternalPackages, readPackageJson } from './utils/package.ts';
import { emitPublishPackageJson } from './utils/publish-manifest.ts';
export { DEFAULT_BROWSER_TARGET } from './constants.ts';
/**
* Builds the shared context consumed by all output format factories.
*/
function createBuildContext(packageDir: string, options: IBuildOptions): IBuildContext {
const packageJson = readPackageJson(packageDir);
const externalPackages = createExternalPackages(packageJson, options.ignorePackages);
return {
entries: getEntries(packageDir),
externalPackages,
facadeExternalPackages: [...externalPackages, packageJson.name, `${packageJson.name}/*`],
inputOptions: createInputOptions(options),
packageDir,
packageJson,
plugins: createInputPlugins(packageDir),
};
}
/**
* Expands the package context into all required tsdown configs.
*/
function createModuleEntryGroups(entries: IBuildContext['entries']) {
const primaryEntries = entries.filter((entry) => entry.type === 'index' || entry.type === 'locale');
const isolatedEntries = entries.filter((entry) => entry.type !== 'index' && entry.type !== 'locale');
return [
...(primaryEntries.length > 0 ? [primaryEntries] : []),
...isolatedEntries.map((entry) => [entry]),
];
}
export function createConfigs(context: IBuildContext, options: IBuildOptions) {
const baseConfig = createBaseConfig(context);
const moduleFormats: TModuleFormat[] = ['esm', 'cjs'];
const enableObfuscation = context.packageJson.name.startsWith('@univerjs-pro/');
const moduleConfigs = createModuleEntryGroups(context.entries).flatMap((entries) => {
return moduleFormats.map((format) => createModuleConfig({
baseConfig,
enableObfuscation,
entries,
externalPackages: context.externalPackages,
facadeExternalPackages: context.facadeExternalPackages,
format,
obfuscatorIgnorePatterns: options.obfuscatorIgnorePatterns,
outDir: BUILD_OUTPUT_DIRECTORIES[format],
packageDir: context.packageDir,
plugins: context.plugins,
}));
});
if (options.skipUMD) {
return moduleConfigs;
}
const umdConfigs = context.entries.map((entry) => createUmdConfig({
baseConfig,
enableObfuscation,
entry,
obfuscatorIgnorePatterns: options.obfuscatorIgnorePatterns,
outDir: BUILD_OUTPUT_DIRECTORIES.umd,
packageDir: context.packageDir,
packageName: context.packageJson.name,
plugins: context.plugins,
}));
return [...moduleConfigs, ...umdConfigs];
}
export function remove() {
const cwd = process.cwd();
for (const dir of CLEANUP_DIRECTORIES) {
const targetDir = path.resolve(cwd, dir);
if (existsSync(targetDir)) {
rmSync(targetDir, { force: true, recursive: true });
}
}
}
export async function build(options: IBuildOptions = {}) {
const packageDir = process.cwd();
if (options.cleanup) {
remove();
}
removeCssArtifacts(path.join(packageDir, BUILD_OUTPUT_ROOT));
/**
* If we specifically have a tsdown config path provided, we will use that and ignore any default configs. However, if no specific config is provided, we will look for default configs in the package root (tsdown.config.mjs, tsdown.config.ts, tsdown.config.js) and merge any found obfuscatorIgnorePatterns into the build context. This allows users to specify obfuscation ignore patterns without needing to provide a full custom config.
*/
if (!options.tsdownConfigPath) {
const configExtensions = ['.mjs', '.ts', '.js'];
for (const ext of configExtensions) {
const defaultConfigPath = path.resolve(packageDir, `tsdown.config${ext}`);
if (existsSync(defaultConfigPath)) {
const { default: userConfigModule } = await import(pathToFileURL(defaultConfigPath).href);
if (userConfigModule.obfuscatorIgnorePatterns) {
options.obfuscatorIgnorePatterns = userConfigModule.obfuscatorIgnorePatterns;
}
break;
}
}
}
const context = createBuildContext(packageDir, options);
let configs = createConfigs(context, options);
if (options.tsdownConfigPath) {
const configPath = path.resolve(packageDir, options.tsdownConfigPath);
let userConfig = (await import(pathToFileURL(configPath).href)).default;
if (typeof userConfig === 'function') {
userConfig = await userConfig();
}
configs = configs.map((config) => mergeConfig(config, userConfig));
}
await Promise.all(configs.map((config) => tsdownBuild(config)));
cleanupPackageJson(packageDir, context.packageJson);
emitPublishPackageJson(packageDir);
}

View File

@@ -0,0 +1,133 @@
import { describe, expect, it } from 'vitest';
import { cleanupClassNameTemplateWhitespace } from './class-name-whitespace-cleanup';
describe('cleanupClassNameTemplateWhitespace', () => {
it('should normalize whitespace for clsx template arguments', () => {
const sourceCode = `
const value = clsx(\`
univer-flex univer-size-6 univer-cursor-pointer univer-items-center univer-justify-center
univer-justify-self-center univer-rounded
hover:univer-bg-gray-100
dark:hover:!univer-bg-gray-700
\`, {
active: true,
});
`;
expect(cleanupClassNameTemplateWhitespace(sourceCode, '/tmp/example.tsx')).toContain(
'clsx("univer-flex univer-size-6 univer-cursor-pointer univer-items-center univer-justify-center univer-justify-self-center univer-rounded hover:univer-bg-gray-100 dark:hover:!univer-bg-gray-700", {'
);
});
it('should normalize whitespace for conditional clsx template arguments', () => {
const sourceCode = `
const value = clsx(
"univer-relative univer-transition-all univer-duration-150",
isDraggingItem && "univer-opacity-0",
dragOverId === itemId && !isDraggingItem && \`
univer-bg-primary-50/60
dark:!univer-bg-primary-900/20
univer-rounded univer-border univer-border-primary-200
dark:!univer-border-primary-700
\`
);
`;
expect(cleanupClassNameTemplateWhitespace(sourceCode, '/tmp/example.tsx')).toContain(
'dragOverId === itemId && !isDraggingItem && "univer-bg-primary-50/60 dark:!univer-bg-primary-900/20 univer-rounded univer-border univer-border-primary-200 dark:!univer-border-primary-700"'
);
});
it('should normalize whitespace for className template literals', () => {
const sourceCode = `
const value = (
<div
className={\`
univer-box-border univer-grid univer-grid-cols-5 univer-gap-2 univer-text-gray-600
dark:!univer-text-gray-200
\`}
/>
);
`;
expect(cleanupClassNameTemplateWhitespace(sourceCode, '/tmp/example.tsx')).toContain(
'className={"univer-box-border univer-grid univer-grid-cols-5 univer-gap-2 univer-text-gray-600 dark:!univer-text-gray-200"}'
);
});
it('should normalize whitespace for className string literals', () => {
const sourceCode = `
const value = (
<button
className="
univer-flex univer-cursor-pointer univer-items-center univer-justify-center
univer-border-none
hover:univer-opacity-70
"
/>
);
`;
expect(cleanupClassNameTemplateWhitespace(sourceCode, '/tmp/example.tsx')).toContain(
'className="univer-flex univer-cursor-pointer univer-items-center univer-justify-center univer-border-none hover:univer-opacity-70"'
);
});
it('should normalize whitespace for compiled className properties', () => {
const sourceCode = `
jsx("button", {
className: "\\n univer-flex univer-cursor-pointer univer-items-center univer-justify-center\\n univer-border-none\\n hover:univer-opacity-70\\n ",
type: "button",
});
`;
expect(cleanupClassNameTemplateWhitespace(sourceCode, '/tmp/example.js')).toContain(
'className: "univer-flex univer-cursor-pointer univer-items-center univer-justify-center univer-border-none hover:univer-opacity-70"'
);
});
it('should normalize whitespace inside cva definitions', () => {
const sourceCode = `
const buttonVariants = cva(
\`
univer-box-border univer-inline-flex univer-cursor-pointer
disabled:univer-pointer-events-none
\`,
{
variants: {
variant: {
primary: \`
univer-border-primary-600 univer-bg-primary-600 univer-text-gray-0
hover:univer-bg-primary-500
\`,
},
},
compoundVariants: [{
className: \`
univer-gap-1 univer-rounded
dark:!univer-bg-gray-700
\`,
}],
}
);
`;
const cleanedCode = cleanupClassNameTemplateWhitespace(sourceCode, '/tmp/example.tsx');
expect(cleanedCode).toContain(
'cva(\n "univer-box-border univer-inline-flex univer-cursor-pointer disabled:univer-pointer-events-none",'
);
expect(cleanedCode).toContain(
'primary: "univer-border-primary-600 univer-bg-primary-600 univer-text-gray-0 hover:univer-bg-primary-500"'
);
expect(cleanedCode).toContain(
'className: "univer-gap-1 univer-rounded dark:!univer-bg-gray-700"'
);
});
it('should not touch unrelated template literals', () => {
const sourceCode = 'const message = ` hello\\n world `;';
expect(cleanupClassNameTemplateWhitespace(sourceCode, '/tmp/example.ts')).toBe(sourceCode);
});
});

View File

@@ -0,0 +1,175 @@
import path from 'node:path';
import * as ts from 'typescript';
interface ITextEdit {
end: number;
start: number;
text: string;
}
const SUPPORTED_SCRIPT_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx']);
function stripQueryAndHash(filePath: string) {
return filePath.replace(/[?#].*$/, '');
}
function shouldProcessFile(filePath: string) {
const normalizedPath = stripQueryAndHash(filePath);
if (normalizedPath.includes('/node_modules/')) {
return false;
}
return SUPPORTED_SCRIPT_EXTENSIONS.has(path.extname(normalizedPath));
}
function getScriptKind(filePath: string) {
switch (path.extname(stripQueryAndHash(filePath))) {
case '.jsx':
return ts.ScriptKind.JSX;
case '.tsx':
return ts.ScriptKind.TSX;
case '.js':
return ts.ScriptKind.JS;
default:
return ts.ScriptKind.TS;
}
}
function normalizeClassNameWhitespace(value: string) {
return value.replace(/\s+/g, ' ').trim();
}
function mayContainCleanupTarget(sourceCode: string) {
return /(?:className|clsx\s*\(|cva\s*\()/.test(sourceCode)
&& /(?:\s{2,}|\\[nr])/.test(sourceCode);
}
function isClsxIdentifier(node: ts.Expression) {
return ts.isIdentifier(node) && node.text === 'clsx';
}
function isCvaIdentifier(node: ts.Expression) {
return ts.isIdentifier(node) && node.text === 'cva';
}
function isStaticStringLiteral(node: ts.Node): node is ts.NoSubstitutionTemplateLiteral | ts.StringLiteral {
return ts.isNoSubstitutionTemplateLiteral(node) || ts.isStringLiteral(node);
}
function isClassNameIdentifier(name: ts.PropertyName | ts.JsxAttributeName) {
return ts.isIdentifier(name) && name.text === 'className';
}
function isClsxArgument(node: ts.Expression) {
let current: ts.Node = node;
while (current.parent) {
if (ts.isCallExpression(current.parent)) {
return current.parent.arguments.includes(current as ts.Expression)
&& isClsxIdentifier(current.parent.expression);
}
current = current.parent;
}
return false;
}
function isClassNameJsxAttributeValue(node: ts.Expression) {
if (ts.isJsxAttribute(node.parent)) {
return node.parent.initializer === node && isClassNameIdentifier(node.parent.name);
}
return ts.isJsxExpression(node.parent)
&& node.parent.expression === node
&& ts.isJsxAttribute(node.parent.parent)
&& isClassNameIdentifier(node.parent.parent.name);
}
function isClassNamePropertyValue(node: ts.Expression) {
return ts.isPropertyAssignment(node.parent)
&& node.parent.initializer === node
&& (
isClassNameIdentifier(node.parent.name)
|| (ts.isStringLiteral(node.parent.name) && node.parent.name.text === 'className')
);
}
function isInsideCvaCall(node: ts.Node) {
let current: ts.Node | undefined = node.parent;
while (current) {
if (ts.isCallExpression(current) && isCvaIdentifier(current.expression)) {
return true;
}
current = current.parent;
}
return false;
}
function isClassNameWhitespaceTarget(node: ts.NoSubstitutionTemplateLiteral | ts.StringLiteral) {
return isClsxArgument(node)
|| isClassNameJsxAttributeValue(node)
|| isClassNamePropertyValue(node)
|| isInsideCvaCall(node);
}
function applyTextEdits(sourceCode: string, edits: ITextEdit[]) {
return edits
.sort((left, right) => right.start - left.start)
.reduce((code, edit) => `${code.slice(0, edit.start)}${edit.text}${code.slice(edit.end)}`, sourceCode);
}
export function cleanupClassNameTemplateWhitespace(sourceCode: string, filePath: string) {
if (!shouldProcessFile(filePath) || !mayContainCleanupTarget(sourceCode)) {
return sourceCode;
}
const sourceFile = ts.createSourceFile(filePath, sourceCode, ts.ScriptTarget.Latest, true, getScriptKind(filePath));
const edits: ITextEdit[] = [];
function visit(node: ts.Node) {
if (isStaticStringLiteral(node) && isClassNameWhitespaceTarget(node)) {
const normalized = normalizeClassNameWhitespace(node.text);
if (normalized !== node.text) {
edits.push({
end: node.end,
start: node.getStart(sourceFile),
text: JSON.stringify(normalized),
});
}
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
if (edits.length === 0) {
return sourceCode;
}
return applyTextEdits(sourceCode, edits);
}
export function createClassNameWhitespaceCleanupPlugin() {
return {
name: 'class-name-whitespace-cleanup',
transform(sourceCode: string, id: string) {
const cleanedCode = cleanupClassNameTemplateWhitespace(sourceCode, id);
if (cleanedCode === sourceCode) {
return null;
}
return {
code: cleanedCode,
map: null,
};
},
};
}

View File

@@ -0,0 +1,95 @@
import type { UserConfig } from 'tsdown';
const CSS_NOOP_PLUGIN_NAME = 'univer-css-noop';
const CSS_IMPORT_RE = /\.(?:css|less|sass|scss|styl|stylus)(?:$|\?)/;
const CSS_NOOP_ID_PREFIX = '\0univer-css-noop:';
const CSS_NOOP_ID_RE = /^\0univer-css-noop:/;
type TInputOptions = NonNullable<UserConfig['inputOptions']>;
type TInputOptionsFunction = Extract<TInputOptions, (...args: any[]) => any>;
type TInputOptionsObject = Exclude<TInputOptions, (...args: any[]) => any>;
function toPluginArray(plugins: unknown): any[] {
return Array.isArray(plugins)
? plugins
: plugins
? [plugins]
: [];
}
function withoutCssNoopPlugin(plugins: any[]) {
return plugins.filter((plugin) => !plugin || plugin.name !== CSS_NOOP_PLUGIN_NAME);
}
export function createCssNoopPlugin() {
let nextId = 0;
return {
name: CSS_NOOP_PLUGIN_NAME,
resolveId: {
filter: { id: CSS_IMPORT_RE },
handler() {
return `${CSS_NOOP_ID_PREFIX}${nextId++}`;
},
},
load: {
filter: { id: CSS_NOOP_ID_RE },
handler() {
return {
code: 'export default {};',
moduleSideEffects: false,
moduleType: 'js',
};
},
},
};
}
function prependCssNoopPlugin(plugins: any[]) {
return [
createCssNoopPlugin(),
...withoutCssNoopPlugin(plugins),
];
}
export function createCssNoopInputOptions(inputOptions?: TInputOptions): TInputOptionsFunction {
return (async (defaultOptions: { plugins?: unknown }, ...args: any[]) => {
const defaultPlugins = toPluginArray(defaultOptions.plugins);
if (!inputOptions) {
return {
...defaultOptions,
plugins: prependCssNoopPlugin(defaultPlugins),
};
}
if (typeof inputOptions === 'function') {
const defaultOptionsWithCssNoop = {
...defaultOptions,
plugins: prependCssNoopPlugin(defaultPlugins),
};
const resolvedOptions = await (inputOptions as any)(defaultOptionsWithCssNoop, ...args);
if (!resolvedOptions) {
return defaultOptionsWithCssNoop;
}
return {
...defaultOptionsWithCssNoop,
...resolvedOptions,
plugins: prependCssNoopPlugin(toPluginArray(resolvedOptions.plugins ?? defaultOptionsWithCssNoop.plugins)),
};
}
const inputOptionsObject = inputOptions as TInputOptionsObject & { plugins?: unknown };
return {
...defaultOptions,
...inputOptionsObject,
plugins: prependCssNoopPlugin([
...toPluginArray(inputOptionsObject.plugins),
...defaultPlugins,
]),
};
}) as TInputOptionsFunction;
}

View File

@@ -0,0 +1,58 @@
import { cpSync, existsSync, mkdirSync, unlinkSync } from 'node:fs';
import path from 'node:path';
interface IBundleOutput {
fileName?: string;
}
export interface ICreateOutputAliasPluginOptions {
copyToRoot: boolean;
keepRootIndexCss: boolean;
packageDir: string;
}
export function createOutputAliasPlugin(options: ICreateOutputAliasPluginOptions) {
const { copyToRoot, keepRootIndexCss, packageDir } = options;
return {
name: 'create-output-alias',
writeBundle(outputOptions: { dir?: string }, bundle: Record<string, IBundleOutput>) {
if (!outputOptions.dir) {
return;
}
const outDir = path.resolve(packageDir, outputOptions.dir);
const rootDir = path.resolve(outDir, '..');
for (const output of Object.values(bundle)) {
if (!output.fileName || output.fileName.endsWith('.map')) {
continue;
}
const sourcePath = path.join(outDir, output.fileName);
if (output.fileName.endsWith('.css')) {
if (keepRootIndexCss) {
const rootCssPath = path.join(rootDir, 'index.css');
mkdirSync(path.dirname(rootCssPath), { recursive: true });
cpSync(sourcePath, rootCssPath);
}
if (existsSync(sourcePath)) {
unlinkSync(sourcePath);
}
continue;
}
if (!copyToRoot) {
continue;
}
const targetPath = path.join(rootDir, output.fileName);
mkdirSync(path.dirname(targetPath), { recursive: true });
cpSync(sourcePath, targetPath);
}
},
};
}

View File

@@ -0,0 +1,42 @@
import type { ObfuscatorOptions } from 'javascript-obfuscator';
import { readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import JavaScriptObfuscator from 'javascript-obfuscator';
interface IBundleOutput {
fileName?: string;
}
export function createOutputObfuscatorPlugin(ignorePatterns?: RegExp[]) {
const obfuscationOptions: ObfuscatorOptions = {
ignoreImports: true,
};
return {
name: 'create-output-obfuscator',
writeBundle(outputOptions: { dir?: string }, bundle: Record<string, IBundleOutput>) {
if (!outputOptions.dir) {
return;
}
const outDir = outputOptions.dir;
for (const output of Object.values(bundle)) {
const fileName = output.fileName;
if (!fileName || !fileName.endsWith('.js') || fileName.endsWith('.map')) {
continue;
}
if (ignorePatterns?.some((pattern) => pattern.test(fileName))) {
continue;
}
const targetPath = path.resolve(outDir, fileName);
const sourceCode = readFileSync(targetPath, 'utf8');
const obfuscatedCode = JavaScriptObfuscator.obfuscate(sourceCode, obfuscationOptions).getObfuscatedCode();
writeFileSync(targetPath, obfuscatedCode, 'utf8');
}
},
};
}

View File

@@ -0,0 +1,36 @@
import type { UserConfig } from 'tsdown';
export interface IBuildOptions {
skipUMD?: boolean;
cleanup?: boolean;
ignorePackages?: string[];
nodeFirst?: boolean;
tsdownConfigPath?: string;
obfuscatorIgnorePatterns?: RegExp[];
}
export type TEntryType = 'facade' | 'index' | 'locale' | 'worker';
export interface IEntryConfig {
key: string;
path: string;
type: TEntryType;
}
export interface IPackageJson {
[key: string]: unknown;
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
name: string;
peerDependencies?: Record<string, string>;
}
export interface IBuildContext {
entries: IEntryConfig[];
externalPackages: string[];
facadeExternalPackages: string[];
inputOptions?: NonNullable<UserConfig['inputOptions']>;
packageDir: string;
packageJson: IPackageJson;
plugins: any[];
}

View File

@@ -0,0 +1,55 @@
import type { UserConfig } from 'tsdown';
import type { IBuildContext, IBuildOptions } from '../types.ts';
import path from 'node:path';
import vue from 'unplugin-vue/rolldown';
import { DEFAULT_BROWSER_TARGET } from '../constants.ts';
import { createClassNameWhitespaceCleanupPlugin } from '../plugins/class-name-whitespace-cleanup.ts';
import { hasSourceFiles } from './files.ts';
/**
* Resolves extra bundler plugins based on the package source layout.
*/
export function createInputPlugins(packageDir: string): any[] {
return [
createClassNameWhitespaceCleanupPlugin(),
...(hasSourceFiles(packageDir, '.vue') ? [vue()] : []),
];
}
/**
* Builds resolver conditions for packages that prefer Node-first exports.
*/
export function createInputOptions(options: IBuildOptions): IBuildContext['inputOptions'] {
if (!options.nodeFirst) {
return undefined;
}
return {
resolve: {
conditionNames: ['node', 'default'],
},
};
}
/**
* Creates the shared tsdown base config used by all output formats.
*/
export function createBaseConfig(context: IBuildContext): Partial<UserConfig> {
return {
clean: false,
css: {
transformer: 'postcss' as const,
},
cwd: context.packageDir,
define: {
'process.env.BUILD_TIMESTAMP': JSON.stringify(Math.floor(Date.now() / 1000)),
'process.env.NODE_ENV': JSON.stringify('production'),
'process.env.WS_NO_BUFFER_UTIL': JSON.stringify(true),
},
inputOptions: context.inputOptions,
outExtensions: () => ({ js: '.js' }),
report: true,
target: DEFAULT_BROWSER_TARGET,
tsconfig: path.join(context.packageDir, 'tsconfig.json'),
} satisfies Partial<UserConfig>;
}

View File

@@ -0,0 +1,442 @@
/* eslint-disable max-lines-per-function */
import type { IPackageJson } from '../types.ts';
import fs from 'node:fs';
import path from 'node:path';
import sortKeys from 'sort-keys';
import * as ts from 'typescript';
import { peerDepsMap } from '../data/peer-deps.ts';
type StringMap = Record<string, string>;
type PeerDepValue = (typeof peerDepsMap)[keyof typeof peerDepsMap] & { optional?: boolean };
type CleanupPackageJson = IPackageJson & {
dependencies?: StringMap;
devDependencies?: StringMap;
exports?: Record<string, unknown>;
optionalDependencies?: StringMap;
peerDependencies?: StringMap;
publishConfig?: {
access: string;
exports: Record<string, unknown>;
main: string;
module: string;
};
};
interface IDerivedDependencyGroups {
dependencies: StringMap;
devDependencies: StringMap;
optionalDependencies: StringMap;
peerDependencies: StringMap;
}
const SOURCE_EXTS = new Set(['.ts', '.tsx']);
const TEST_FILE_RE = /\.(spec|test)\.[cm]?tsx?$/;
const IMMUTABLE_MANAGED_DEPENDENCIES = new Set(['@univerjs/icons', '@univerjs/icons-svg']);
function filterPackageName(packageName: string): string {
if (packageName.startsWith('@univerjs/')) {
return packageName.split('/').slice(0, 2).join('/');
} else if (packageName.startsWith('@univerjs-pro/')) {
return packageName.split('/').slice(0, 2).join('/');
} else {
return packageName;
}
}
function isManagedUniverPackage(packageName: string) {
return packageName.startsWith('@univerjs/') || packageName.startsWith('@univerjs-pro/');
}
function shouldPreserveExistingDependency(packageName: string) {
return !isManagedUniverPackage(packageName) || IMMUTABLE_MANAGED_DEPENDENCIES.has(packageName);
}
function mergePreservingNonUniver(existing: StringMap | undefined, next: StringMap) {
const preserved = Object.fromEntries(
Object.entries(existing ?? {}).filter(([name]) => shouldPreserveExistingDependency(name))
) as StringMap;
return sortKeys({ ...preserved, ...next });
}
function assignDependencyGroup(
pkg: CleanupPackageJson,
key: 'dependencies' | 'devDependencies' | 'optionalDependencies',
next: StringMap
) {
const merged = mergePreservingNonUniver(pkg[key], next);
if (Object.keys(merged).length === 0) {
delete pkg[key];
return;
}
pkg[key] = merged;
}
function isTestSourceFile(filePath: string) {
const normalized = filePath.split(path.sep).join('/');
if (normalized.includes('/__tests__/')) {
return true;
}
return TEST_FILE_RE.test(normalized);
}
function parseModuleSpecifiersFromCode(code: string, filePath: string) {
const runtime = new Set<string>();
const typeOnly = new Set<string>();
const kind = path.extname(filePath) === '.tsx' ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
const sourceFile = ts.createSourceFile(filePath, code, ts.ScriptTarget.Latest, true, kind);
function add(spec: string, isType: boolean) {
if (!spec || spec.startsWith('\0') || spec.startsWith('.') || spec.startsWith('/')) {
return;
}
(isType ? typeOnly : runtime).add(spec);
}
function addFromImportLike(moduleSpecifier: ts.Expression | undefined, isType: boolean) {
if (moduleSpecifier && ts.isStringLiteralLike(moduleSpecifier)) {
add(moduleSpecifier.text, isType);
}
}
for (const stmt of sourceFile.statements) {
if (ts.isImportDeclaration(stmt)) {
const spec = stmt.moduleSpecifier;
const clause = stmt.importClause;
if (!clause) {
addFromImportLike(spec, false);
continue;
}
if (clause.isTypeOnly) {
addFromImportLike(spec, true);
continue;
}
let hasValue = Boolean(clause.name);
if (clause.namedBindings) {
if (ts.isNamespaceImport(clause.namedBindings)) {
hasValue = true;
} else if (ts.isNamedImports(clause.namedBindings)) {
const allTypeOnly = clause.namedBindings.elements.length > 0
&& clause.namedBindings.elements.every((el) => el.isTypeOnly);
const anyValue = clause.namedBindings.elements.some((el) => !el.isTypeOnly);
if (anyValue) {
hasValue = true;
} else if (allTypeOnly && !hasValue) {
addFromImportLike(spec, true);
continue;
}
}
}
addFromImportLike(spec, !hasValue);
continue;
}
if (ts.isExportDeclaration(stmt) && stmt.moduleSpecifier) {
const spec = stmt.moduleSpecifier;
if (stmt.isTypeOnly) {
addFromImportLike(spec, true);
continue;
}
if (stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {
const allTypeOnly = stmt.exportClause.elements.length > 0
&& stmt.exportClause.elements.every((el) => el.isTypeOnly);
addFromImportLike(spec, allTypeOnly);
continue;
}
addFromImportLike(spec, false);
continue;
}
}
function visit(node: ts.Node) {
if (ts.isImportTypeNode(node)) {
const arg = node.argument;
if (ts.isLiteralTypeNode(arg) && ts.isStringLiteralLike(arg.literal)) {
add(arg.literal.text, true);
}
}
if (ts.isCallExpression(node) && node.arguments.length === 1 && ts.isStringLiteralLike(node.arguments[0])) {
const callee = node.expression;
if (callee.kind === ts.SyntaxKind.ImportKeyword) {
add(node.arguments[0].text, false);
} else if (ts.isIdentifier(callee) && callee.text === 'require') {
add(node.arguments[0].text, false);
}
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
return { runtime, typeOnly };
}
function walkSourceFiles(dir: string, files: string[] = []) {
if (!fs.existsSync(dir)) {
return files;
}
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === 'lib' || entry.name === 'coverage') {
continue;
}
walkSourceFiles(fullPath, files);
continue;
}
if (SOURCE_EXTS.has(path.extname(entry.name))) {
files.push(fullPath);
}
}
return files;
}
function collectBareSpecifiersFromSource(packageDir: string) {
const runtimeValueSpecifiers = new Set<string>();
const runtimeTypeSpecifiers = new Set<string>();
const testSpecifiers = new Set<string>();
const srcDir = path.resolve(packageDir, 'src');
for (const file of walkSourceFiles(srcDir)) {
const code = fs.readFileSync(file, 'utf8');
const isTest = isTestSourceFile(file);
const { runtime, typeOnly } = parseModuleSpecifiersFromCode(code, file);
for (const spec of runtime) {
if (isTest) {
testSpecifiers.add(spec);
} else {
runtimeValueSpecifiers.add(spec);
}
}
for (const spec of typeOnly) {
if (isTest) {
testSpecifiers.add(spec);
} else {
runtimeTypeSpecifiers.add(spec);
}
}
}
return {
runtimeValue: [...runtimeValueSpecifiers],
runtimeType: [...runtimeTypeSpecifiers],
test: [...testSpecifiers],
};
}
function resolvePeerDepVersion(value: PeerDepValue): { name: string; version: string; optional: boolean } | null {
let version = value.version;
let name = value.name;
const optional = Boolean(value.optional);
const visited = new Set<string>();
while (version in peerDepsMap) {
if (visited.has(version)) {
return null;
}
visited.add(version);
const next = peerDepsMap[version as keyof typeof peerDepsMap] as PeerDepValue;
name = next.name;
version = next.version;
}
return { name, optional, version };
}
function resolveUniverDependencyVersion(name: string) {
if (IMMUTABLE_MANAGED_DEPENDENCIES.has(name)) {
return undefined;
}
return 'workspace:*';
}
function deriveDependencyGroups(packageDir: string, packageJson: IPackageJson): IDerivedDependencyGroups {
const declaredDependencies = packageJson.dependencies ?? {};
const declaredOptionalDependencies = (packageJson as CleanupPackageJson).optionalDependencies ?? {};
const peerDeps: StringMap = {};
const deps: StringMap = {};
const optionalDeps: StringMap = {};
const devDeps: StringMap = {};
const { runtimeValue, runtimeType, test } = collectBareSpecifiersFromSource(packageDir);
const runtimeUniver = new Set<string>();
const runtimeTypeOnlyUniver = new Set<string>();
const testOnlyUniver = new Set<string>();
for (const source of runtimeValue) {
if (Object.prototype.hasOwnProperty.call(peerDepsMap, source)) {
const value = peerDepsMap[source as keyof typeof peerDepsMap] as PeerDepValue;
const resolved = resolvePeerDepVersion(value);
if (!resolved) {
continue;
}
// Local package manifests win over the shared peer-dependency registry.
if (resolved.name in declaredDependencies || resolved.name in declaredOptionalDependencies) {
continue;
}
if (resolved.optional) {
optionalDeps[resolved.name] = resolved.version;
} else {
peerDeps[resolved.name] = resolved.version;
}
continue;
}
if (source.startsWith('@univerjs')) {
runtimeUniver.add(filterPackageName(source));
}
}
for (const source of runtimeType) {
if (source.startsWith('@univerjs')) {
runtimeTypeOnlyUniver.add(filterPackageName(source));
}
}
for (const source of test) {
if (source.startsWith('@univerjs')) {
testOnlyUniver.add(filterPackageName(source));
}
}
for (const name of runtimeUniver) {
if (name === packageJson.name) {
continue;
}
const dependencyVersion = resolveUniverDependencyVersion(name);
if (dependencyVersion) {
deps[name] = dependencyVersion;
}
}
for (const name of new Set([...testOnlyUniver, ...runtimeTypeOnlyUniver])) {
if (runtimeUniver.has(name) || name === packageJson.name) {
continue;
}
const dependencyVersion = resolveUniverDependencyVersion(name);
if (dependencyVersion) {
devDeps[name] = dependencyVersion;
}
}
const declaredDevDependencies: Record<string, string> = (packageJson as CleanupPackageJson).devDependencies ?? {};
if ('react' in declaredDevDependencies && !('react' in peerDeps)) {
const reactPeerDep = peerDepsMap.react;
if (reactPeerDep) {
peerDeps.react = reactPeerDep.version;
}
}
return {
dependencies: deps,
devDependencies: devDeps,
optionalDependencies: optionalDeps,
peerDependencies: peerDeps,
};
}
function applyPublishManifest(pkg: CleanupPackageJson, packageDir: string) {
const hasLocales = fs.existsSync(path.resolve(packageDir, 'src/locale'));
const hasFacade = fs.existsSync(path.resolve(packageDir, 'src/facade/index.ts'));
pkg.publishConfig = {
access: 'public',
main: './lib/es/index.js',
module: './lib/es/index.js',
exports: {
'.': {
import: './lib/es/index.js',
require: './lib/cjs/index.js',
types: './lib/types/index.d.ts',
},
'./*': {
import: './lib/es/*',
require: './lib/cjs/*',
types: './lib/types/index.d.ts',
},
},
};
pkg.exports ||= {};
if (hasLocales) {
pkg.exports['./locale/*'] = './src/locale/*.ts';
pkg.publishConfig.exports['./locale/*'] = {
import: './lib/es/locale/*.js',
require: './lib/cjs/locale/*.js',
types: './lib/types/locale/*.d.ts',
};
}
if (hasFacade) {
pkg.exports['./facade'] = './src/facade/index.ts';
pkg.publishConfig.exports['./facade'] = {
import: './lib/es/facade.js',
require: './lib/cjs/facade.js',
types: './lib/types/facade/index.d.ts',
};
pkg.publishConfig.exports['./lib/facade'] = pkg.publishConfig.exports['./facade'];
}
pkg.publishConfig.exports['./lib/*'] = './lib/*';
}
function assignPeerDependencies(pkg: CleanupPackageJson, peerDeps: StringMap) {
const managedPeerDepNames = [...new Set(Object.values(peerDepsMap).map((value) => value.name))];
const existing = { ...pkg.peerDependencies };
for (const key of managedPeerDepNames) {
delete existing[key];
}
const merged = Object.keys(peerDeps).length > 0 ? sortKeys({ ...existing, ...peerDeps }) : sortKeys(existing);
if (Object.keys(merged).length === 0) {
delete pkg.peerDependencies;
return;
}
pkg.peerDependencies = merged;
}
function readJsonFile<T>(filePath: string): T {
return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T;
}
function writeJsonFile(filePath: string, value: unknown) {
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 4)}\n`);
}
export function cleanupPackageJson(packageDir: string, packageJson: IPackageJson) {
const pkgPath = path.resolve(packageDir, 'package.json');
const pkg = readJsonFile<CleanupPackageJson>(pkgPath);
const dependencyGroups = deriveDependencyGroups(packageDir, packageJson);
applyPublishManifest(pkg, packageDir);
assignDependencyGroup(pkg, 'optionalDependencies', dependencyGroups.optionalDependencies);
assignPeerDependencies(pkg, dependencyGroups.peerDependencies);
assignDependencyGroup(pkg, 'dependencies', dependencyGroups.dependencies);
// This rewrite only owns @univerjs-managed entries; unrelated dev deps stay intact.
assignDependencyGroup(pkg, 'devDependencies', dependencyGroups.devDependencies);
writeJsonFile(pkgPath, pkg);
}

View File

@@ -0,0 +1,45 @@
import type { IEntryConfig } from '../types.ts';
import { existsSync, readdirSync, statSync } from 'node:fs';
import path from 'node:path';
import { DEFAULT_ENTRY_FILE, FACADE_ENTRY_FILE, LOCALE_DIRECTORY } from '../constants.ts';
/**
* Collects all canonical build entries for a package.
*/
export function getEntries(packageDir: string): IEntryConfig[] {
const entries: IEntryConfig[] = [{
key: 'index',
path: path.join(packageDir, DEFAULT_ENTRY_FILE),
type: 'index',
}];
const facadeEntry = path.join(packageDir, FACADE_ENTRY_FILE);
if (existsSync(facadeEntry)) {
entries.push({
key: 'facade',
path: facadeEntry,
type: 'facade',
});
}
const localeDir = path.join(packageDir, LOCALE_DIRECTORY);
if (!existsSync(localeDir)) {
return entries;
}
for (const fileName of readdirSync(localeDir).sort((left, right) => left.localeCompare(right))) {
const fullPath = path.join(localeDir, fileName);
if (statSync(fullPath).isDirectory() || !fileName.endsWith('.ts') || !fileName.includes('-')) {
continue;
}
entries.push({
key: `locale/${fileName.replace(/\.ts$/, '')}`,
path: fullPath,
type: 'locale',
});
}
return entries;
}

View File

@@ -0,0 +1,60 @@
import { existsSync, readdirSync, unlinkSync } from 'node:fs';
import path from 'node:path';
import { SOURCE_DIRECTORY } from '../constants.ts';
/**
* Walks the package source tree and returns whether a given file suffix exists.
*/
export function hasSourceFiles(packageDir: string, extension: string) {
const sourceDir = path.join(packageDir, SOURCE_DIRECTORY);
if (!existsSync(sourceDir)) {
return false;
}
const stack = [sourceDir];
while (stack.length > 0) {
const currentDir = stack.pop()!;
const entries = readdirSync(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
stack.push(fullPath);
continue;
}
if (entry.name.endsWith(extension)) {
return true;
}
}
}
return false;
}
/**
* Removes emitted css artifacts before a new build so alias copies stay deterministic.
*/
export function removeCssArtifacts(dir: string) {
if (!existsSync(dir)) {
return;
}
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
removeCssArtifacts(fullPath);
continue;
}
if (entry.name.endsWith('.css')) {
unlinkSync(fullPath);
}
}
}

View File

@@ -0,0 +1,42 @@
import type { IPackageJson } from '../types.ts';
import { readFileSync } from 'node:fs';
import path from 'node:path';
function getProductionDependencyNames(packageJson: IPackageJson) {
const names = new Set([
...Object.keys(packageJson.dependencies ?? {}),
...Object.keys(packageJson.peerDependencies ?? {}),
]);
const devDeps = packageJson.devDependencies ?? {};
if ('react' in devDeps) {
names.add('react');
}
return [...names].sort((left, right) => left.localeCompare(right));
}
function normalizeIgnoredPackages(ignorePackages: string[] = []) {
return [...new Set(ignorePackages)].sort((left, right) => left.localeCompare(right));
}
/**
* Reads the package manifest used to derive dependency externalization rules.
*/
export function readPackageJson(packageDir: string): IPackageJson {
return JSON.parse(readFileSync(path.join(packageDir, 'package.json'), 'utf-8')) as IPackageJson;
}
/**
* Produces the external package allowlist including subpath imports.
*/
export function createExternalPackages(packageJson: IPackageJson, ignorePackages: string[] = []) {
const ignoredPackages = normalizeIgnoredPackages(ignorePackages);
return [...new Set([
...getProductionDependencyNames(packageJson),
...ignoredPackages,
])]
.sort((left, right) => left.localeCompare(right))
.flatMap((packageName) => [packageName, `${packageName}/*`]);
}

View File

@@ -0,0 +1,65 @@
import type { IPackageJson } from '../types.ts';
import { mkdirSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { readPackageJson } from './package.ts';
const ROOT_MANIFEST_FIELDS = [
'author',
'bugs',
'dependencies',
'description',
'engines',
'funding',
'homepage',
'keywords',
'license',
'name',
'optionalDependencies',
'peerDependencies',
'peerDependenciesMeta',
'repository',
'sideEffects',
'type',
'version',
] as const;
function pickRootManifest(pkg: IPackageJson) {
const manifest: Record<string, unknown> = {};
for (const field of ROOT_MANIFEST_FIELDS) {
if (pkg[field] !== undefined) {
manifest[field] = pkg[field];
}
}
return manifest;
}
/**
* Creates the publish-time package manifest inside the dist directory.
*/
export function emitPublishPackageJson(packageDir: string) {
const pkg = readPackageJson(packageDir);
const publishConfig = pkg.publishConfig as Record<string, unknown> | undefined;
const publishDirectory = typeof publishConfig?.directory === 'string' ? publishConfig.directory : null;
if (!publishConfig || !publishDirectory) {
return;
}
const distDir = path.join(packageDir, publishDirectory);
const manifest = {
...pickRootManifest(pkg),
} as Record<string, unknown>;
for (const [key, value] of Object.entries(publishConfig)) {
if (key === 'access' || key === 'directory') {
continue;
}
manifest[key] = value;
}
mkdirSync(distDir, { recursive: true });
writeFileSync(path.join(distDir, 'package.json'), `${JSON.stringify(manifest, null, 4)}\n`);
}