Skip to content

Commit c906c31

Browse files
committed
fix: support explicit locale exports
1 parent cf9c015 commit c906c31

5 files changed

Lines changed: 237 additions & 19 deletions

File tree

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/**
2+
* Copyright 2023-present DreamNum Co., Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
18+
import { tmpdir } from 'node:os';
19+
import path from 'node:path';
20+
import { afterEach, describe, expect, it } from 'vitest';
21+
import { cleanupPackageJson } from './cleanup-pkg';
22+
23+
function writeJson(filePath: string, value: unknown) {
24+
writeFileSync(filePath, `${JSON.stringify(value, null, 4)}\n`);
25+
}
26+
27+
function readPackageJson(packageDir: string) {
28+
return JSON.parse(readFileSync(path.join(packageDir, 'package.json'), 'utf8')) as {
29+
exports?: Record<string, unknown>;
30+
publishConfig?: { exports?: Record<string, unknown> };
31+
};
32+
}
33+
34+
function createPackageDir(packageJson: Record<string, unknown>) {
35+
const packageDir = mkdtempSync(path.join(tmpdir(), 'univer-cleanup-pkg-'));
36+
37+
mkdirSync(path.join(packageDir, 'src'), { recursive: true });
38+
writeFileSync(path.join(packageDir, 'src/index.ts'), 'export const value = 1;\n');
39+
writeJson(path.join(packageDir, 'package.json'), packageJson);
40+
41+
return packageDir;
42+
}
43+
44+
describe('cleanupPackageJson publish manifest', () => {
45+
const packageDirs: string[] = [];
46+
47+
afterEach(() => {
48+
while (packageDirs.length > 0) {
49+
const packageDir = packageDirs.pop();
50+
if (packageDir) {
51+
rmSync(packageDir, { force: true, recursive: true });
52+
}
53+
}
54+
});
55+
56+
it('keeps wildcard publish exports by default', () => {
57+
const packageJson = {
58+
name: '@univerjs-pro/default-wildcards',
59+
version: '0.0.0',
60+
dependencies: {},
61+
devDependencies: {},
62+
};
63+
const packageDir = createPackageDir(packageJson);
64+
packageDirs.push(packageDir);
65+
66+
cleanupPackageJson(packageDir, packageJson);
67+
68+
const nextPackageJson = readPackageJson(packageDir);
69+
70+
expect(nextPackageJson.publishConfig?.exports).toHaveProperty('./*');
71+
expect(nextPackageJson.publishConfig?.exports).toHaveProperty('./lib/*');
72+
});
73+
74+
it('omits wildcard publish exports when package opts out', () => {
75+
const packageJson = {
76+
name: '@univerjs-pro/explicit-entries',
77+
version: '0.0.0',
78+
dependencies: {},
79+
devDependencies: {},
80+
univerCli: {
81+
publishWildcardExports: false,
82+
},
83+
};
84+
const packageDir = createPackageDir(packageJson);
85+
packageDirs.push(packageDir);
86+
87+
mkdirSync(path.join(packageDir, 'src/facade'), { recursive: true });
88+
writeFileSync(path.join(packageDir, 'src/facade/index.ts'), 'export const facade = 1;\n');
89+
90+
cleanupPackageJson(packageDir, packageJson);
91+
92+
const nextPackageJson = readPackageJson(packageDir);
93+
94+
expect(nextPackageJson.publishConfig?.exports).toHaveProperty('.');
95+
expect(nextPackageJson.publishConfig?.exports).toHaveProperty('./facade');
96+
expect(nextPackageJson.publishConfig?.exports).toHaveProperty('./lib/facade');
97+
expect(Object.hasOwn(nextPackageJson.publishConfig?.exports ?? {}, './*')).toBe(false);
98+
expect(Object.hasOwn(nextPackageJson.publishConfig?.exports ?? {}, './lib/*')).toBe(false);
99+
});
100+
101+
it('keeps locale exports explicit when package opts out of wildcard publish exports', () => {
102+
const packageJson = {
103+
name: '@univerjs-pro/explicit-locale-entries',
104+
version: '0.0.0',
105+
dependencies: {},
106+
devDependencies: {},
107+
exports: {
108+
'.': './src/index.ts',
109+
'./locale/en-US': './src/locale/en-US.ts',
110+
'./locale/zh-CN': './src/locale/zh-CN.ts',
111+
},
112+
univerCli: {
113+
publishWildcardExports: false,
114+
},
115+
};
116+
const packageDir = createPackageDir(packageJson);
117+
packageDirs.push(packageDir);
118+
119+
mkdirSync(path.join(packageDir, 'src/locale'), { recursive: true });
120+
writeFileSync(path.join(packageDir, 'src/locale/en-US.ts'), 'export default {};\n');
121+
writeFileSync(path.join(packageDir, 'src/locale/zh-CN.ts'), 'export default {};\n');
122+
123+
cleanupPackageJson(packageDir, packageJson);
124+
125+
const nextPackageJson = readPackageJson(packageDir);
126+
127+
expect(nextPackageJson.exports).toHaveProperty('./locale/en-US');
128+
expect(nextPackageJson.exports).toHaveProperty('./locale/zh-CN');
129+
expect(nextPackageJson.publishConfig?.exports).toHaveProperty('./locale/en-US');
130+
expect(nextPackageJson.publishConfig?.exports).toHaveProperty('./locale/zh-CN');
131+
expect(Object.hasOwn(nextPackageJson.exports ?? {}, './locale/*')).toBe(false);
132+
expect(Object.hasOwn(nextPackageJson.publishConfig?.exports ?? {}, './locale/*')).toBe(false);
133+
});
134+
135+
it('removes stale locale wildcard source exports when opt-out packages no longer have locale files', () => {
136+
const packageJson = {
137+
name: '@univerjs-pro/stale-locale-wildcard',
138+
version: '0.0.0',
139+
dependencies: {},
140+
devDependencies: {},
141+
exports: {
142+
'.': './src/index.ts',
143+
'./locale/*': './src/locale/*.ts',
144+
},
145+
univerCli: {
146+
publishWildcardExports: false,
147+
},
148+
};
149+
const packageDir = createPackageDir(packageJson);
150+
packageDirs.push(packageDir);
151+
152+
cleanupPackageJson(packageDir, packageJson);
153+
154+
const nextPackageJson = readPackageJson(packageDir);
155+
156+
expect(nextPackageJson.exports).toHaveProperty('.');
157+
expect(Object.hasOwn(nextPackageJson.exports ?? {}, './locale/*')).toBe(false);
158+
expect(Object.hasOwn(nextPackageJson.publishConfig?.exports ?? {}, './locale/*')).toBe(false);
159+
});
160+
});

common/shared/tsdown/utils/cleanup-pkg.ts

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ type CleanupPackageJson = IPackageJson & {
2020
main: string;
2121
module: string;
2222
};
23+
univerCli?: {
24+
publishWildcardExports?: boolean;
25+
};
2326
};
2427
interface IDerivedDependencyGroups {
2528
dependencies: StringMap;
@@ -357,8 +360,16 @@ function deriveDependencyGroups(packageDir: string, packageJson: IPackageJson):
357360
}
358361

359362
function applyPublishManifest(pkg: CleanupPackageJson, packageDir: string) {
360-
const hasLocales = fs.existsSync(path.resolve(packageDir, 'src/locale'));
363+
const localeDir = path.resolve(packageDir, 'src/locale');
364+
const localeNames = fs.existsSync(localeDir)
365+
? fs.readdirSync(localeDir)
366+
.filter((fileName) => path.extname(fileName) === '.ts')
367+
.map((fileName) => path.basename(fileName, '.ts'))
368+
.sort((left, right) => left.localeCompare(right))
369+
: [];
370+
const hasLocales = localeNames.length > 0;
361371
const hasFacade = fs.existsSync(path.resolve(packageDir, 'src/facade/index.ts'));
372+
const publishWildcardExports = pkg.univerCli?.publishWildcardExports !== false;
362373

363374
pkg.publishConfig = {
364375
access: 'public',
@@ -370,23 +381,41 @@ function applyPublishManifest(pkg: CleanupPackageJson, packageDir: string) {
370381
require: './lib/cjs/index.js',
371382
types: './lib/types/index.d.ts',
372383
},
373-
'./*': {
374-
import: './lib/es/*',
375-
require: './lib/cjs/*',
376-
types: './lib/types/index.d.ts',
377-
},
378384
},
379385
};
380386

387+
if (publishWildcardExports) {
388+
pkg.publishConfig.exports['./*'] = {
389+
import: './lib/es/*',
390+
require: './lib/cjs/*',
391+
types: './lib/types/index.d.ts',
392+
};
393+
}
394+
381395
pkg.exports ||= {};
382396

397+
if (!publishWildcardExports) {
398+
delete pkg.exports['./locale/*'];
399+
}
400+
383401
if (hasLocales) {
384-
pkg.exports['./locale/*'] = './src/locale/*.ts';
385-
pkg.publishConfig.exports['./locale/*'] = {
386-
import: './lib/es/locale/*.js',
387-
require: './lib/cjs/locale/*.js',
388-
types: './lib/types/locale/*.d.ts',
389-
};
402+
if (publishWildcardExports) {
403+
pkg.exports['./locale/*'] = './src/locale/*.ts';
404+
pkg.publishConfig.exports['./locale/*'] = {
405+
import: './lib/es/locale/*.js',
406+
require: './lib/cjs/locale/*.js',
407+
types: './lib/types/locale/*.d.ts',
408+
};
409+
} else {
410+
for (const localeName of localeNames) {
411+
pkg.exports[`./locale/${localeName}`] = `./src/locale/${localeName}.ts`;
412+
pkg.publishConfig.exports[`./locale/${localeName}`] = {
413+
import: `./lib/es/locale/${localeName}.js`,
414+
require: `./lib/cjs/locale/${localeName}.js`,
415+
types: `./lib/types/locale/${localeName}.d.ts`,
416+
};
417+
}
418+
}
390419
}
391420

392421
if (hasFacade) {
@@ -399,7 +428,9 @@ function applyPublishManifest(pkg: CleanupPackageJson, packageDir: string) {
399428
pkg.publishConfig.exports['./lib/facade'] = pkg.publishConfig.exports['./facade'];
400429
}
401430

402-
pkg.publishConfig.exports['./lib/*'] = './lib/*';
431+
if (publishWildcardExports) {
432+
pkg.publishConfig.exports['./lib/*'] = './lib/*';
433+
}
403434
}
404435

405436
function assignPeerDependencies(pkg: CleanupPackageJson, peerDeps: StringMap) {

packages/design/src/components/button/Button.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import type { VariantProps } from 'class-variance-authority';
18-
import type { ButtonHTMLAttributes } from 'react';
18+
import type { ButtonHTMLAttributes, ComponentPropsWithoutRef } from 'react';
1919
import { Slot } from '@radix-ui/react-slot';
2020
import { cva } from 'class-variance-authority';
2121
import { forwardRef } from 'react';
@@ -94,10 +94,24 @@ export interface IButtonProps
9494

9595
export const Button = forwardRef<HTMLButtonElement, IButtonProps>(
9696
({ className, variant, size, asChild = false, ...props }, ref) => {
97-
const Comp = asChild ? Slot : 'button';
97+
const buttonClassName = clsx(buttonVariants({ variant, size, className }));
98+
99+
if (asChild) {
100+
const slotProps = props as ComponentPropsWithoutRef<typeof Slot>;
101+
102+
return (
103+
<Slot
104+
className={buttonClassName}
105+
ref={ref}
106+
data-u-comp="button"
107+
{...slotProps}
108+
/>
109+
);
110+
}
111+
98112
return (
99-
<Comp
100-
className={clsx(buttonVariants({ variant, size, className }))}
113+
<button
114+
className={buttonClassName}
101115
ref={ref}
102116
data-u-comp="button"
103117
{...props}

packages/design/src/components/switch/Switch.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,18 @@
1414
* limitations under the License.
1515
*/
1616

17+
import type { InputHTMLAttributes } from 'react';
1718
import { useEffect, useState } from 'react';
1819
import { clsx } from '../../helper/clsx';
1920

2021
export interface ISwitchProps {
2122
defaultChecked?: boolean;
2223
onChange?: (checked: boolean) => void;
24+
inputProps?: Pick<InputHTMLAttributes<HTMLInputElement>, 'aria-label' | 'aria-labelledby'>;
2325
}
2426

2527
const Switch = (props: ISwitchProps) => {
26-
const { defaultChecked = false, onChange } = props;
28+
const { defaultChecked = false, inputProps, onChange } = props;
2729
const [checked, setChecked] = useState(defaultChecked);
2830

2931
const handleChange = () => {
@@ -39,7 +41,9 @@ const Switch = (props: ISwitchProps) => {
3941
<div className="univer-h-4">
4042
<label className="univer-relative univer-inline-block univer-h-4 univer-w-7">
4143
<input
44+
{...inputProps}
4245
className="univer-size-0 univer-opacity-0"
46+
role="switch"
4347
type="checkbox"
4448
checked={checked}
4549
onChange={handleChange}

packages/design/src/components/switch/__tests__/index.spec.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { cleanup, render } from '@testing-library/react';
17+
import { cleanup, render, screen } from '@testing-library/react';
1818
import { afterEach, describe, expect, it } from 'vitest';
1919
import { Switch } from '../Switch';
2020

@@ -45,4 +45,13 @@ describe('Switch', () => {
4545

4646
expect(checkboxElement.checked).toBeFalsy();
4747
});
48+
49+
it('exposes switch semantics and input accessibility labels', () => {
50+
render(<Switch defaultChecked inputProps={{ 'aria-label': 'Grid' }} />);
51+
52+
const switchElement = screen.getByRole('switch', { name: 'Grid' });
53+
54+
expect(switchElement).toBeInstanceOf(HTMLInputElement);
55+
expect((switchElement as HTMLInputElement).checked).toBe(true);
56+
});
4857
});

0 commit comments

Comments
 (0)