chore: tests for multi projects angular (#10675)

This commit is contained in:
Nikolay Vitkov 2023-08-03 13:02:25 +02:00 committed by GitHub
parent 2f6870651e
commit 7ecfe150a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 507 additions and 212 deletions

View File

@ -98,6 +98,12 @@ To run the creating of single test schematic:
npm run sandbox:test
```
To create a multi project workspace use the following command
```bash
npm run sandbox -- --init --multi
```
### Unit Testing
The schematics utilize `@angular-devkit/schematics/testing` for verifying correct file creation and `package.json` updates. To execute the test suit:

View File

@ -98,6 +98,12 @@ To run the creating of single test schematic:
npm run sandbox:test
```
To create a multi project workspace use the following command
```bash
npm run sandbox -- --init --multi
```
### Unit Testing
The schematics utilize `@angular-devkit/schematics/testing` for verifying correct file creation and `package.json` updates. To execute the test suit:

View File

@ -20,7 +20,8 @@ import {of} from 'rxjs';
import {concatMap, map, scan} from 'rxjs/operators';
import {
addBaseFiles,
addCommonFiles as addCommonFilesHelper,
addFilesSingle,
addFrameworkFiles,
getNgCommandName,
} from '../utils/files.js';
@ -34,15 +35,22 @@ import {
type NodePackage,
updateAngularJsonScripts,
} from '../utils/packages.js';
import {TestingFramework, type SchematicsOptions} from '../utils/types.js';
import {
TestingFramework,
type SchematicsOptions,
AngularProject,
} from '../utils/types.js';
// You don't have to export the function as default. You can also have more than one rule
// factory per file.
export function ngAdd(options: SchematicsOptions): Rule {
export function ngAdd(userArgs: Record<string, string>): Rule {
const options = parseUserAddArgs(userArgs);
return (tree: Tree, context: SchematicContext) => {
return chain([
addDependencies(options),
addPuppeteerFiles(options),
addPuppeteerConfig(options),
addCommonFiles(options),
addOtherFiles(options),
updateScripts(options),
updateAngularConfig(options),
@ -50,6 +58,32 @@ export function ngAdd(options: SchematicsOptions): Rule {
};
}
function parseUserAddArgs(userArgs: Record<string, string>): SchematicsOptions {
const options: Partial<SchematicsOptions> = {
...userArgs,
};
if ('p' in userArgs) {
options['port'] = Number(userArgs['p']);
}
if ('t' in userArgs) {
options['testingFramework'] = userArgs['t'] as TestingFramework;
}
if ('c' in userArgs) {
options['exportConfig'] =
typeof userArgs['c'] === 'string'
? userArgs['c'] === 'true'
: userArgs['c'];
}
if ('d' in userArgs) {
options['isDefaultTester'] =
typeof userArgs['d'] === 'string'
? userArgs['d'] === 'true'
: userArgs['d'];
}
return options as SchematicsOptions;
}
function addDependencies(options: SchematicsOptions): Rule {
return (tree: Tree, context: SchematicContext) => {
context.logger.debug('Adding dependencies to "package.json"');
@ -94,13 +128,28 @@ function updateScripts(options: SchematicsOptions): Rule {
};
}
function addPuppeteerFiles(options: SchematicsOptions): Rule {
function addPuppeteerConfig(options: SchematicsOptions): Rule {
return (tree: Tree, context: SchematicContext) => {
context.logger.debug('Adding Puppeteer config file.');
if (options.exportConfig) {
return addFilesSingle(tree, context, '', {root: ''} as AngularProject, {
options: options,
applyPath: './files/base',
relativeToWorkspacePath: `/`,
});
}
return tree;
};
}
function addCommonFiles(options: SchematicsOptions): Rule {
return (tree: Tree, context: SchematicContext) => {
context.logger.debug('Adding Puppeteer base files.');
const {projects} = getAngularConfig(tree);
return addBaseFiles(tree, context, {
projects,
return addCommonFilesHelper(tree, context, projects, {
options: {
...options,
ext:
@ -115,8 +164,7 @@ function addOtherFiles(options: SchematicsOptions): Rule {
context.logger.debug('Adding Puppeteer additional files.');
const {projects} = getAngularConfig(tree);
return addFrameworkFiles(tree, context, {
projects,
return addFrameworkFiles(tree, context, projects, {
options,
});
};

View File

@ -22,7 +22,7 @@ import {
Tree,
} from '@angular-devkit/schematics';
import {addBaseFiles} from '../utils/files.js';
import {addCommonFiles} from '../utils/files.js';
import {getAngularConfig} from '../utils/json.js';
import {
TestingFramework,
@ -33,12 +33,28 @@ import {
// You don't have to export the function as default. You can also have more than one rule
// factory per file.
export function test(options: SchematicsSpec): Rule {
export function test(userArgs: Record<string, string>): Rule {
const options = parseUserTestArgs(userArgs);
return (tree: Tree, context: SchematicContext) => {
return chain([addSpecFile(options)])(tree, context);
};
}
function parseUserTestArgs(userArgs: Record<string, string>): SchematicsSpec {
const options: Partial<SchematicsSpec> = {
...userArgs,
};
if ('p' in userArgs) {
options['project'] = userArgs['p'];
}
if ('n' in userArgs) {
options['name'] = userArgs['n'];
}
return options as SchematicsSpec;
}
function findTestingOption<Property extends keyof SchematicsOptions>(
[name, project]: [string, AngularProject | undefined],
property: Property
@ -76,7 +92,7 @@ function addSpecFile(options: SchematicsSpec): Rule {
});
if (!foundProject) {
throw new SchematicsException(
`Project not found! Please use -p to specify in which project to run.`
`Project not found! Please run "ng generate @puppeteer/ng-schematics:test <Test> <Project>"`
);
}
@ -88,8 +104,11 @@ function addSpecFile(options: SchematicsSpec): Rule {
context.logger.debug('Creating Spec file.');
return addBaseFiles(tree, context, {
projects: {[foundProject[0]]: foundProject[1]},
return addCommonFiles(
tree,
context,
{[foundProject[0]]: foundProject[1]} as Record<string, AngularProject>,
{
options: {
name: options.name,
testingFramework,
@ -98,6 +117,7 @@ function addSpecFile(options: SchematicsSpec): Rule {
ext: testingFramework === TestingFramework.Node ? 'test' : 'e2e',
port,
},
});
}
);
};
}

View File

@ -15,6 +15,10 @@
},
"project": {
"type": "string",
"$default": {
"$source": "argv",
"index": 1
},
"alias": "p"
}
},

View File

@ -23,7 +23,6 @@ import {
apply,
applyTemplates,
chain,
filter,
mergeWith,
move,
url,
@ -32,7 +31,6 @@ import {
import {AngularProject, SchematicsOptions, TestingFramework} from './types.js';
export interface FilesOptions {
projects: Record<string, any>;
options: {
testingFramework: TestingFramework;
port: number;
@ -43,26 +41,34 @@ export interface FilesOptions {
applyPath: string;
relativeToWorkspacePath: string;
movePath?: string;
filterPredicate?: (path: string) => boolean;
}
const PUPPETEER_CONFIG_TEMPLATE = '.puppeteerrc.cjs.template';
export function addFiles(
export function addFilesToProjects(
tree: Tree,
context: SchematicContext,
{
projects,
options,
applyPath,
movePath,
relativeToWorkspacePath,
filterPredicate,
}: FilesOptions
projects: Record<string, AngularProject>,
options: FilesOptions
): any {
return chain(
Object.keys(projects).map(name => {
const project = projects[name] as AngularProject;
return addFilesSingle(
tree,
context,
name,
projects[name] as AngularProject,
options
);
})
)(tree, context);
}
export function addFilesSingle(
_tree: Tree,
_context: SchematicContext,
name: string,
project: AngularProject,
{options, applyPath, movePath, relativeToWorkspacePath}: FilesOptions
): any {
const projectPath = resolve(getSystemPath(normalize(project.root)));
const workspacePath = resolve(getSystemPath(normalize('')));
@ -76,12 +82,6 @@ export function addFiles(
return mergeWith(
apply(url(applyPath), [
filter(
filterPredicate ??
(() => {
return true;
})
),
move(movePath ? `${project.root}${movePath}` : project.root),
applyTemplates({
...options,
@ -94,8 +94,6 @@ export function addFiles(
}),
])
);
})
)(tree, context);
}
function getProjectBaseUrl(project: any, port: number): string {
@ -118,29 +116,25 @@ function getTsConfigPath(project: AngularProject): string {
return `../tsconfig.app.json`;
}
export function addBaseFiles(
export function addCommonFiles(
tree: Tree,
context: SchematicContext,
projects: Record<string, AngularProject>,
filesOptions: Omit<FilesOptions, 'applyPath' | 'relativeToWorkspacePath'>
): any {
const options: FilesOptions = {
...filesOptions,
applyPath: './files/base',
applyPath: './files/common',
relativeToWorkspacePath: `/`,
filterPredicate: path => {
return path.includes(PUPPETEER_CONFIG_TEMPLATE) &&
!filesOptions.options.exportConfig
? false
: true;
},
};
return addFiles(tree, context, options);
return addFilesToProjects(tree, context, projects, options);
}
export function addFrameworkFiles(
tree: Tree,
context: SchematicContext,
projects: Record<string, AngularProject>,
filesOptions: Omit<FilesOptions, 'applyPath' | 'relativeToWorkspacePath'>
): any {
const testingFramework = filesOptions.options.testingFramework;
@ -150,7 +144,7 @@ export function addFrameworkFiles(
relativeToWorkspacePath: `/`,
};
return addFiles(tree, context, options);
return addFilesToProjects(tree, context, projects, options);
}
export function getScriptFromOptions(

View File

@ -16,7 +16,7 @@
import {SchematicsException, Tree} from '@angular-devkit/schematics';
import {AngularJson} from './types.js';
import type {AngularJson} from './types.js';
export function getJsonFileAsObject(
tree: Tree,

View File

@ -3,6 +3,7 @@ import expect from 'expect';
import {
buildTestingTree,
getAngularJsonScripts,
getMultiProjectFile,
getPackageJson,
setupHttpHooks,
} from './utils.js';
@ -10,6 +11,7 @@ import {
describe('@puppeteer/ng-schematics: ng-add', () => {
setupHttpHooks();
describe('Single Project', () => {
it('should create base files and update to "package.json"', async () => {
const tree = await buildTestingTree('ng-add');
const {devDependencies, scripts} = getPackageJson(tree);
@ -27,9 +29,8 @@ describe('@puppeteer/ng-schematics: ng-add', () => {
},
});
});
it('should update create proper "ng" command for non default tester', async () => {
const tree = await buildTestingTree('ng-add', {
const tree = await buildTestingTree('ng-add', 'single', {
isDefaultTester: false,
});
const {scripts} = getPackageJson(tree);
@ -38,25 +39,22 @@ describe('@puppeteer/ng-schematics: ng-add', () => {
expect(scripts['puppeteer']).toBe('ng run sandbox:puppeteer');
expect(builder).toBe('@puppeteer/ng-schematics:puppeteer');
});
it('should create Puppeteer config', async () => {
const {files} = await buildTestingTree('ng-add', {
const {files} = await buildTestingTree('ng-add', 'single', {
exportConfig: true,
});
expect(files).toContain('/.puppeteerrc.cjs');
});
it('should not create Puppeteer config', async () => {
const {files} = await buildTestingTree('ng-add', {
const {files} = await buildTestingTree('ng-add', 'single', {
exportConfig: false,
});
expect(files).not.toContain('/.puppeteerrc.cjs');
});
it('should create Jasmine files and update "package.json"', async () => {
const tree = await buildTestingTree('ng-add', {
const tree = await buildTestingTree('ng-add', 'single', {
testingFramework: 'jasmine',
});
const {devDependencies} = getPackageJson(tree);
@ -72,9 +70,8 @@ describe('@puppeteer/ng-schematics: ng-add', () => {
[`./node_modules/.bin/jasmine`, '--config=./e2e/support/jasmine.json'],
]);
});
it('should create Jest files and update "package.json"', async () => {
const tree = await buildTestingTree('ng-add', {
const tree = await buildTestingTree('ng-add', 'single', {
testingFramework: 'jest',
});
const {devDependencies} = getPackageJson(tree);
@ -88,9 +85,8 @@ describe('@puppeteer/ng-schematics: ng-add', () => {
[`./node_modules/.bin/jest`, '-c', 'e2e/jest.config.js'],
]);
});
it('should create Mocha files and update "package.json"', async () => {
const tree = await buildTestingTree('ng-add', {
const tree = await buildTestingTree('ng-add', 'single', {
testingFramework: 'mocha',
});
const {devDependencies} = getPackageJson(tree);
@ -107,9 +103,8 @@ describe('@puppeteer/ng-schematics: ng-add', () => {
[`./node_modules/.bin/mocha`, '--config=./e2e/.mocharc.js'],
]);
});
it('should create Node files', async () => {
const tree = await buildTestingTree('ng-add', {
const tree = await buildTestingTree('ng-add', 'single', {
testingFramework: 'node',
});
const {options} = getAngularJsonScripts(tree);
@ -122,7 +117,6 @@ describe('@puppeteer/ng-schematics: ng-add', () => {
['node', '--test', '--test-reporter', 'spec', 'e2e/build/'],
]);
});
it('should not create port option', async () => {
const tree = await buildTestingTree('ng-add');
@ -131,7 +125,7 @@ describe('@puppeteer/ng-schematics: ng-add', () => {
});
it('should create port option when specified', async () => {
const port = 8080;
const tree = await buildTestingTree('ng-add', {
const tree = await buildTestingTree('ng-add', 'single', {
port,
});
@ -139,3 +133,137 @@ describe('@puppeteer/ng-schematics: ng-add', () => {
expect(options['port']).toBe(port);
});
});
describe('Multi projects', () => {
it('should create base files and update to "package.json"', async () => {
const tree = await buildTestingTree('ng-add', 'multi');
const {devDependencies, scripts} = getPackageJson(tree);
const {builder, configurations} = getAngularJsonScripts(tree);
expect(tree.files).toContain(getMultiProjectFile('e2e/tsconfig.json'));
expect(tree.files).toContain(getMultiProjectFile('e2e/tests/app.e2e.ts'));
expect(tree.files).toContain(getMultiProjectFile('e2e/tests/utils.ts'));
expect(devDependencies).toContain('puppeteer');
expect(scripts['e2e']).toBe('ng e2e');
expect(builder).toBe('@puppeteer/ng-schematics:puppeteer');
expect(configurations).toEqual({
production: {
devServerTarget: 'sandbox:serve:production',
},
});
});
it('should update create proper "ng" command for non default tester', async () => {
const tree = await buildTestingTree('ng-add', 'multi', {
isDefaultTester: false,
});
const {scripts} = getPackageJson(tree);
const {builder} = getAngularJsonScripts(tree, false);
expect(scripts['puppeteer']).toBe('ng run sandbox:puppeteer');
expect(builder).toBe('@puppeteer/ng-schematics:puppeteer');
});
it('should create Puppeteer config', async () => {
const {files} = await buildTestingTree('ng-add', 'multi', {
exportConfig: true,
});
expect(files).toContain('/.puppeteerrc.cjs');
});
it('should not create Puppeteer config', async () => {
const {files} = await buildTestingTree('ng-add', 'multi', {
exportConfig: false,
});
expect(files).not.toContain(getMultiProjectFile('.puppeteerrc.cjs'));
expect(files).not.toContain('/.puppeteerrc.cjs');
});
it('should create Jasmine files and update "package.json"', async () => {
const tree = await buildTestingTree('ng-add', 'multi', {
testingFramework: 'jasmine',
});
const {devDependencies} = getPackageJson(tree);
const {options} = getAngularJsonScripts(tree);
expect(tree.files).toContain(
getMultiProjectFile('e2e/support/jasmine.json')
);
expect(tree.files).toContain(getMultiProjectFile('e2e/helpers/babel.js'));
expect(devDependencies).toContain('jasmine');
expect(devDependencies).toContain('@babel/core');
expect(devDependencies).toContain('@babel/register');
expect(devDependencies).toContain('@babel/preset-typescript');
expect(options['commands']).toEqual([
[
`../../node_modules/.bin/jasmine`,
'--config=./e2e/support/jasmine.json',
],
]);
});
it('should create Jest files and update "package.json"', async () => {
const tree = await buildTestingTree('ng-add', 'multi', {
testingFramework: 'jest',
});
const {devDependencies} = getPackageJson(tree);
const {options} = getAngularJsonScripts(tree);
expect(tree.files).toContain(getMultiProjectFile('e2e/jest.config.js'));
expect(devDependencies).toContain('jest');
expect(devDependencies).toContain('@types/jest');
expect(devDependencies).toContain('ts-jest');
expect(options['commands']).toEqual([
[`../../node_modules/.bin/jest`, '-c', 'e2e/jest.config.js'],
]);
});
it('should create Mocha files and update "package.json"', async () => {
const tree = await buildTestingTree('ng-add', 'multi', {
testingFramework: 'mocha',
});
const {devDependencies} = getPackageJson(tree);
const {options} = getAngularJsonScripts(tree);
expect(tree.files).toContain(getMultiProjectFile('e2e/.mocharc.js'));
expect(tree.files).toContain(getMultiProjectFile('e2e/babel.js'));
expect(devDependencies).toContain('mocha');
expect(devDependencies).toContain('@types/mocha');
expect(devDependencies).toContain('@babel/core');
expect(devDependencies).toContain('@babel/register');
expect(devDependencies).toContain('@babel/preset-typescript');
expect(options['commands']).toEqual([
[`../../node_modules/.bin/mocha`, '--config=./e2e/.mocharc.js'],
]);
});
it('should create Node files', async () => {
const tree = await buildTestingTree('ng-add', 'multi', {
testingFramework: 'node',
});
const {options} = getAngularJsonScripts(tree);
expect(tree.files).toContain(getMultiProjectFile('e2e/.gitignore'));
expect(tree.files).not.toContain(
getMultiProjectFile('e2e/tests/app.e2e.ts')
);
expect(tree.files).toContain(
getMultiProjectFile('e2e/tests/app.test.ts')
);
expect(options['commands']).toEqual([
[`../../node_modules/.bin/tsc`, '-p', 'e2e/tsconfig.json'],
['node', '--test', '--test-reporter', 'spec', 'e2e/build/'],
]);
});
it('should not create port option', async () => {
const tree = await buildTestingTree('ng-add');
const {options} = getAngularJsonScripts(tree);
expect(options['port']).toBeUndefined();
});
it('should create port option when specified', async () => {
const port = 8080;
const tree = await buildTestingTree('ng-add', 'multi', {
port,
});
const {options} = getAngularJsonScripts(tree);
expect(options['port']).toBe(port);
});
});
});

View File

@ -1,12 +1,17 @@
import expect from 'expect';
import {buildTestingTree, setupHttpHooks} from './utils.js';
import {
buildTestingTree,
getMultiProjectFile,
setupHttpHooks,
} from './utils.js';
describe('@puppeteer/ng-schematics: test', () => {
setupHttpHooks();
describe('Single Project', () => {
it('should create default file', async () => {
const tree = await buildTestingTree('test', {
const tree = await buildTestingTree('test', 'single', {
name: 'myTest',
});
expect(tree.files).toContain('/e2e/tests/my-test.e2e.ts');
@ -14,7 +19,7 @@ describe('@puppeteer/ng-schematics: test', () => {
});
it('should create Node file', async () => {
const tree = await buildTestingTree('test', {
const tree = await buildTestingTree('test', 'single', {
name: 'myTest',
testingFramework: 'node',
});
@ -22,3 +27,31 @@ describe('@puppeteer/ng-schematics: test', () => {
expect(tree.files).toContain('/e2e/tests/my-test.test.ts');
});
});
describe('Multi projects', () => {
it('should create default file', async () => {
const tree = await buildTestingTree('test', 'multi', {
name: 'myTest',
});
expect(tree.files).toContain(
getMultiProjectFile('e2e/tests/my-test.e2e.ts')
);
expect(tree.files).not.toContain(
getMultiProjectFile('e2e/tests/my-test.test.ts')
);
});
it('should create Node file', async () => {
const tree = await buildTestingTree('test', 'multi', {
name: 'myTest',
testingFramework: 'node',
});
expect(tree.files).not.toContain(
getMultiProjectFile('e2e/tests/my-test.e2e.ts')
);
expect(tree.files).toContain(
getMultiProjectFile('e2e/tests/my-test.test.ts')
);
});
});
});

View File

@ -8,7 +8,17 @@ import {
} from '@angular-devkit/schematics/testing';
import sinon from 'sinon';
const APPLICATION_OPTIONS = {
const WORKSPACE_OPTIONS = {
name: 'workspace',
newProjectRoot: 'projects',
version: '14.0.0',
};
const MULTI_APPLICATION_OPTIONS = {
name: 'sandbox',
};
const SINGLE_APPLICATION_OPTIONS = {
name: 'sandbox',
directory: '.',
createApplication: true,
@ -41,9 +51,9 @@ export function getAngularJsonScripts(
} {
const angularJson = tree.readJson('angular.json') as any;
const e2eScript = isDefault ? 'e2e' : 'puppeteer';
return angularJson['projects']?.[APPLICATION_OPTIONS.name]?.['architect'][
e2eScript
];
return angularJson['projects']?.[SINGLE_APPLICATION_OPTIONS.name]?.[
'architect'
][e2eScript];
}
export function getPackageJson(tree: UnitTestTree): {
@ -59,8 +69,13 @@ export function getPackageJson(tree: UnitTestTree): {
};
}
export function getMultiProjectFile(file: string): string {
return `/${WORKSPACE_OPTIONS.newProjectRoot}/${MULTI_APPLICATION_OPTIONS.name}/${file}`;
}
export async function buildTestingTree(
command: 'ng-add' | 'test',
type: 'single' | 'multi' = 'single',
userOptions?: Record<string, any>
): Promise<UnitTestTree> {
const runner = new SchematicTestRunner(
@ -76,11 +91,27 @@ export async function buildTestingTree(
let workingTree: UnitTestTree;
// Build workspace
if (type === 'single') {
workingTree = await runner.runExternalSchematic(
'@schematics/angular',
'ng-new',
APPLICATION_OPTIONS
SINGLE_APPLICATION_OPTIONS
);
} else {
// Build workspace
workingTree = await runner.runExternalSchematic(
'@schematics/angular',
'workspace',
WORKSPACE_OPTIONS
);
// Build dummy application
workingTree = await runner.runExternalSchematic(
'@schematics/angular',
'application',
MULTI_APPLICATION_OPTIONS,
workingTree
);
}
if (command !== 'ng-add') {
// We want to create update the proper files with `ng-add`

View File

@ -20,11 +20,29 @@ const {join} = require('path');
const {cwd} = require('process');
const isInit = process.argv.indexOf('--init') !== -1;
const isMulti = process.argv.indexOf('--multi') !== -1;
const isBuild = process.argv.indexOf('--build') !== -1;
const isTest = process.argv.indexOf('--test') !== -1;
const commands = {
build: ['npm run build'],
createSandbox: ['npx ng new sandbox --defaults'],
createMultiWorkspace: [
'ng new sandbox --create-application=false --directory=multi',
],
createMultiProjects: [
{
command: 'ng generate application core --style=css --routing=false',
options: {
cwd: join(cwd(), '/multi/'),
},
},
{
command: 'ng generate application admin --style=css --routing=false',
options: {
cwd: join(cwd(), '/multi/'),
},
},
],
runSchematics: [
{
command: 'npm run schematics',
@ -51,7 +69,7 @@ const scripts = {
// Runs the Puppeteer Ng-Schematics against the sandbox
schematics:
'npm run delete:file && npm run build:schematics && schematics ../:ng-add --dry-run=false',
'schematics:spec':
'schematics:test':
'npm run build:schematics && schematics ../:test --dry-run=false',
};
/**
@ -79,7 +97,7 @@ async function executeCommand(commands) {
});
createProcess.on('error', message => {
console.error(message);
console.error(`Running ${toExecute} exited with error:`, message);
reject(message);
});
@ -96,9 +114,16 @@ async function executeCommand(commands) {
async function main() {
if (isInit) {
if (isMulti) {
await executeCommand(commands.createMultiWorkspace);
await executeCommand(commands.createMultiProjects);
} else {
await executeCommand(commands.createSandbox);
}
const packageJsonFile = join(cwd(), '/sandbox/package.json');
const directory = isMulti ? 'multi' : 'sandbox';
const packageJsonFile = join(cwd(), `/${directory}/package.json`);
const packageJson = JSON.parse(await readFile(packageJsonFile));
packageJson['scripts'] = {
...packageJson['scripts'],

View File

@ -10,7 +10,7 @@
},
"packages/testserver": {},
"packages/ng-schematics": {
"release-as": "0.3.0"
"release-as": "0.4.0"
},
"packages/browsers": {}
},