Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions common/shared/tsdown/utils/cleanup-pkg.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/**
* 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
*
* https://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 { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import { cleanupPackageJson } from './cleanup-pkg';

function writeJson(filePath: string, value: unknown) {
writeFileSync(filePath, `${JSON.stringify(value, null, 4)}\n`);
}

function readPackageJson(packageDir: string) {
return JSON.parse(readFileSync(path.join(packageDir, 'package.json'), 'utf8')) as {
exports?: Record<string, unknown>;
publishConfig?: { exports?: Record<string, unknown> };
};
}

function createPackageDir(packageJson: Record<string, unknown>) {
const packageDir = mkdtempSync(path.join(tmpdir(), 'univer-cleanup-pkg-'));

mkdirSync(path.join(packageDir, 'src'), { recursive: true });
writeFileSync(path.join(packageDir, 'src/index.ts'), 'export const value = 1;\n');
writeJson(path.join(packageDir, 'package.json'), packageJson);

return packageDir;
}

describe('cleanupPackageJson publish manifest', () => {
const packageDirs: string[] = [];

afterEach(() => {
while (packageDirs.length > 0) {
const packageDir = packageDirs.pop();
if (packageDir) {
rmSync(packageDir, { force: true, recursive: true });
}
}
});

it('keeps wildcard publish exports by default', () => {
const packageJson = {
name: '@univerjs-pro/default-wildcards',
version: '0.0.0',
dependencies: {},
devDependencies: {},
};
const packageDir = createPackageDir(packageJson);
packageDirs.push(packageDir);

cleanupPackageJson(packageDir, packageJson);

const nextPackageJson = readPackageJson(packageDir);

expect(nextPackageJson.publishConfig?.exports).toHaveProperty('./*');
expect(nextPackageJson.publishConfig?.exports).toHaveProperty('./lib/*');
});

it('omits wildcard publish exports when package opts out', () => {
const packageJson = {
name: '@univerjs-pro/explicit-entries',
version: '0.0.0',
dependencies: {},
devDependencies: {},
univerCli: {
publishWildcardExports: false,
},
};
const packageDir = createPackageDir(packageJson);
packageDirs.push(packageDir);

mkdirSync(path.join(packageDir, 'src/facade'), { recursive: true });
writeFileSync(path.join(packageDir, 'src/facade/index.ts'), 'export const facade = 1;\n');

cleanupPackageJson(packageDir, packageJson);

const nextPackageJson = readPackageJson(packageDir);

expect(nextPackageJson.publishConfig?.exports).toHaveProperty('.');
expect(nextPackageJson.publishConfig?.exports).toHaveProperty('./facade');
expect(nextPackageJson.publishConfig?.exports).toHaveProperty('./lib/facade');
expect(Object.hasOwn(nextPackageJson.publishConfig?.exports ?? {}, './*')).toBe(false);
expect(Object.hasOwn(nextPackageJson.publishConfig?.exports ?? {}, './lib/*')).toBe(false);
});

it('keeps locale exports explicit when package opts out of wildcard publish exports', () => {
const packageJson = {
name: '@univerjs-pro/explicit-locale-entries',
version: '0.0.0',
dependencies: {},
devDependencies: {},
exports: {
'.': './src/index.ts',
'./locale/en-US': './src/locale/en-US.ts',
'./locale/zh-CN': './src/locale/zh-CN.ts',
},
univerCli: {
publishWildcardExports: false,
},
};
const packageDir = createPackageDir(packageJson);
packageDirs.push(packageDir);

mkdirSync(path.join(packageDir, 'src/locale'), { recursive: true });
writeFileSync(path.join(packageDir, 'src/locale/en-US.ts'), 'export default {};\n');
writeFileSync(path.join(packageDir, 'src/locale/zh-CN.ts'), 'export default {};\n');

cleanupPackageJson(packageDir, packageJson);

const nextPackageJson = readPackageJson(packageDir);

expect(nextPackageJson.exports).toHaveProperty('./locale/en-US');
expect(nextPackageJson.exports).toHaveProperty('./locale/zh-CN');
expect(nextPackageJson.publishConfig?.exports).toHaveProperty('./locale/en-US');
expect(nextPackageJson.publishConfig?.exports).toHaveProperty('./locale/zh-CN');
expect(Object.hasOwn(nextPackageJson.exports ?? {}, './locale/*')).toBe(false);
expect(Object.hasOwn(nextPackageJson.publishConfig?.exports ?? {}, './locale/*')).toBe(false);
});

it('removes stale locale wildcard source exports when opt-out packages no longer have locale files', () => {
const packageJson = {
name: '@univerjs-pro/stale-locale-wildcard',
version: '0.0.0',
dependencies: {},
devDependencies: {},
exports: {
'.': './src/index.ts',
'./locale/*': './src/locale/*.ts',
},
univerCli: {
publishWildcardExports: false,
},
};
const packageDir = createPackageDir(packageJson);
packageDirs.push(packageDir);

cleanupPackageJson(packageDir, packageJson);

const nextPackageJson = readPackageJson(packageDir);

expect(nextPackageJson.exports).toHaveProperty('.');
expect(Object.hasOwn(nextPackageJson.exports ?? {}, './locale/*')).toBe(false);
expect(Object.hasOwn(nextPackageJson.publishConfig?.exports ?? {}, './locale/*')).toBe(false);
});
});
57 changes: 44 additions & 13 deletions common/shared/tsdown/utils/cleanup-pkg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ type CleanupPackageJson = IPackageJson & {
main: string;
module: string;
};
univerCli?: {
publishWildcardExports?: boolean;
};
};
interface IDerivedDependencyGroups {
dependencies: StringMap;
Expand Down Expand Up @@ -357,8 +360,16 @@ function deriveDependencyGroups(packageDir: string, packageJson: IPackageJson):
}

function applyPublishManifest(pkg: CleanupPackageJson, packageDir: string) {
const hasLocales = fs.existsSync(path.resolve(packageDir, 'src/locale'));
const localeDir = path.resolve(packageDir, 'src/locale');
const localeNames = fs.existsSync(localeDir)
? fs.readdirSync(localeDir)
.filter((fileName) => path.extname(fileName) === '.ts')
.map((fileName) => path.basename(fileName, '.ts'))
.sort((left, right) => left.localeCompare(right))
: [];
const hasLocales = localeNames.length > 0;
const hasFacade = fs.existsSync(path.resolve(packageDir, 'src/facade/index.ts'));
const publishWildcardExports = pkg.univerCli?.publishWildcardExports !== false;

pkg.publishConfig = {
access: 'public',
Expand All @@ -370,23 +381,41 @@ function applyPublishManifest(pkg: CleanupPackageJson, packageDir: string) {
require: './lib/cjs/index.js',
types: './lib/types/index.d.ts',
},
'./*': {
import: './lib/es/*',
require: './lib/cjs/*',
types: './lib/types/index.d.ts',
},
},
};

if (publishWildcardExports) {
pkg.publishConfig.exports['./*'] = {
import: './lib/es/*',
require: './lib/cjs/*',
types: './lib/types/index.d.ts',
};
}

pkg.exports ||= {};

if (!publishWildcardExports) {
delete pkg.exports['./locale/*'];
}

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 (publishWildcardExports) {
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',
};
} else {
for (const localeName of localeNames) {
pkg.exports[`./locale/${localeName}`] = `./src/locale/${localeName}.ts`;
pkg.publishConfig.exports[`./locale/${localeName}`] = {
import: `./lib/es/locale/${localeName}.js`,
require: `./lib/cjs/locale/${localeName}.js`,
types: `./lib/types/locale/${localeName}.d.ts`,
};
}
}
}

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

pkg.publishConfig.exports['./lib/*'] = './lib/*';
if (publishWildcardExports) {
pkg.publishConfig.exports['./lib/*'] = './lib/*';
}
}

function assignPeerDependencies(pkg: CleanupPackageJson, peerDeps: StringMap) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,59 @@

import type { Univer } from '../../univer';
import { UnitAction, UnitObject, UnitRole } from '@univerjs/protocol';
import { BehaviorSubject } from 'rxjs';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { UniverInstanceType } from '../../common/unit';
import { UnitModel, UniverInstanceType } from '../../common/unit';
import { IAuthzIoService } from '../authz-io/type';
import { IUniverInstanceService } from '../instance/instance.service';
import { IMentionIOService } from '../mention-io/type';
import { IResourceLoaderService } from '../resource-loader/type';
import { UserManagerService } from '../user-manager/user-manager.service';
import { createTestBed } from './create-test-bed';

interface ITestBoardData {
id: string;
name?: string;
resources?: Array<{ name: string; data: string }>;
}

class MockBoardUnit extends UnitModel<ITestBoardData, UniverInstanceType.UNIVER_BOARD> {
override readonly type = UniverInstanceType.UNIVER_BOARD;
override name$ = new BehaviorSubject('');
private readonly _snapshot: ITestBoardData;

constructor(snapshot: Partial<ITestBoardData> = {}) {
super();
this._snapshot = {
id: 'board-resource',
name: '',
...snapshot,
};
this.name$.next(this._snapshot.name ?? '');
}

override getUnitId(): string {
return this._snapshot.id;
}

override setName(name: string): void {
this._snapshot.name = name;
this.name$.next(name);
}

override getSnapshot(): ITestBoardData {
return this._snapshot;
}

override getRev(): number {
return 1;
}

override incrementRev(): void { }

override setRev(): void { }
}

describe('Authz/resource integration', () => {
let univer: Univer;
let unitId: string;
Expand Down Expand Up @@ -133,6 +177,55 @@ describe('Authz/resource integration', () => {
expect(reloaded[0].actions).toEqual([{ action: UnitAction.Edit, allowed: false }]);
});

it('should persist and reload permission resources for board units', async () => {
const injector = univer.__getInjector();
const authzIoService = injector.get(IAuthzIoService);
const resourceLoaderService = injector.get(IResourceLoaderService);
const univerInstanceService = injector.get(IUniverInstanceService);

univerInstanceService.registerCtorForType(UniverInstanceType.UNIVER_BOARD, MockBoardUnit);
const board = univer.createUnit<ITestBoardData, MockBoardUnit>(UniverInstanceType.UNIVER_BOARD, {
id: 'board-resource',
});
const objectID = await authzIoService.create({
objectType: UnitObject.Workbook,
worksheetObject: {
unitID: board.getUnitId(),
name: 'Board permission',
collaborators: [],
strategies: [{ action: UnitAction.Edit, role: UnitRole.Owner }],
scope: undefined,
},
});

const snapshot = resourceLoaderService.saveUnit<ITestBoardData>(board.getUnitId());
const authzResource = snapshot?.resources?.find((resource) => resource.name === 'SHEET_AuthzIoMockService_PLUGIN');

expect(authzResource?.data).toContain(objectID);

expect(univerInstanceService.disposeUnit(board.getUnitId())).toBe(true);

const unloaded = await authzIoService.list({
unitID: board.getUnitId(),
objectIDs: [objectID],
actions: [UnitAction.Edit],
});

expect(unloaded[0].name).toBe('');
expect(unloaded[0].actions).toEqual([{ action: UnitAction.Edit, allowed: true }]);

univer.createUnit<ITestBoardData, MockBoardUnit>(UniverInstanceType.UNIVER_BOARD, snapshot!);

const reloaded = await authzIoService.list({
unitID: board.getUnitId(),
objectIDs: [objectID],
actions: [UnitAction.Edit],
});

expect(reloaded[0].name).toBe('Board permission');
expect(reloaded[0].actions).toEqual([{ action: UnitAction.Edit, allowed: true }]);
});

it('should expose current user data consistently through mention and user services', async () => {
const injector = univer.__getInjector();
const userManagerService = injector.get(UserManagerService);
Expand Down
Loading