Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module.exports = {
extends: ['eslint-config-salesforce-typescript', 'eslint-config-salesforce-license', 'plugin:sf-plugin/library'],
// ignore eslint files in NUT test repos
ignorePatterns: ['test/nuts/ebikes-lwc'],
ignorePatterns: ['test/nuts/ebikes-lwc', 'test/nuts/repros/reactinternalapp'],
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"dependencies": {
"@salesforce/core": "^8.28.1",
"@salesforce/kit": "^3.2.6",
"@salesforce/source-deploy-retrieve": "^12.32.3",
"@salesforce/source-deploy-retrieve": "^12.32.4",
"@salesforce/ts-types": "^2.0.12",
"fast-xml-parser": "^5.5.7",
"graceful-fs": "^4.2.11",
Expand Down
37 changes: 23 additions & 14 deletions src/shared/populateTypesAndNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,25 +69,34 @@ export const populateTypesAndNames =
resolveDeleted ? VirtualTreeContainer.fromFilePaths(filenames) : maybeGetTreeContainer(projectPath),
!!forceIgnore
);
const sourceComponents = filenames
.flatMap((filename) => {
try {
return resolver.getComponentsFromPath(filename);
} catch (e) {
logger.warn(`unable to resolve ${filename}`);
return undefined;
}
})
.filter(isDefined);

logger.debug(` matching SourceComponents have ${sourceComponents.length} items from local`);

const elementMap = new Map(
elements.flatMap((e) => (e.filenames ?? []).map((f) => [ensureRelative(projectPath)(f), e]))
);

// Deduplicate by fullName+type: all files in the same bundle component (e.g. uiBundles)
// resolve to the same SourceComponent, so without dedup getAllFiles/walkContent is called
// once per input file rather than once per unique component (O(N) walks instead of O(1)).
const uniqueSourceComponents = [
...new Map(
filenames
.flatMap((filename) => {
try {
return resolver.getComponentsFromPath(filename);
} catch (e) {
logger.warn(`unable to resolve ${filename}`);
return undefined;
}
})
.filter(isDefined)
.filter(sourceComponentHasFullNameAndType)
.map((sc) => [`${sc.fullName}:${sc.type.name}`, sc] as const)
).values(),
];

logger.debug(`populateTypesAndNames resolved ${uniqueSourceComponents.length} unique components`);

// iterates the local components and sets their filenames
sourceComponents.filter(sourceComponentHasFullNameAndType).map((matchingComponent) => {
uniqueSourceComponents.map((matchingComponent) => {
const filenamesFromMatchingComponent = getAllFiles(matchingComponent);
const ignored = filenamesFromMatchingComponent
.filter(excludeLwcLocalOnlyTest)
Expand Down
101 changes: 101 additions & 0 deletions test/nuts/local/populateTypesAndNames.nut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright 2026, Salesforce, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import path from 'node:path';
import fs from 'node:fs';
import os from 'node:os';
import { expect } from 'chai';
import { ForceIgnore, RegistryAccess } from '@salesforce/source-deploy-retrieve';
import { populateTypesAndNames } from '../../../src/shared/populateTypesAndNames';
import { ChangeResult } from '../../../src/shared/types';

// TestSession stubs process.cwd() to the project dir, which causes maybeGetTreeContainer
// to return undefined and the resolver to use the real cwd (workspace root) for FS ops.
// Use mkdtempSync so process.cwd() !== projectPath and the NodeFSTreeContainer(projectPath)
// is used, making relative-path resolution work correctly.

const registry = new RegistryAccess();

// Relative paths matching what isogit/localShadowRepo returns
const apexMeta = path.join('force-app', 'main', 'default', 'classes', 'OrderController.cls-meta.xml');
const lwcDir = path.join('force-app', 'main', 'default', 'lwc');

describe('populateTypesAndNames', () => {
let projectPath: string;

before(() => {
projectPath = fs.mkdtempSync(path.join(os.tmpdir(), 'populateTypesAndNames-'));
fs.cpSync(path.resolve(path.join('test', 'nuts', 'ebikes-lwc')), projectPath, { recursive: true });
});

after(() => {
fs.rmSync(projectPath, { recursive: true, force: true });
});

it('returns an empty array for empty input', () => {
expect(populateTypesAndNames({ projectPath, registry })([])).to.deep.equal([]);
});

it('resolves an Apex class to its type and name', () => {
const input: ChangeResult[] = [{ origin: 'local', filenames: [apexMeta] }];
const [result] = populateTypesAndNames({ projectPath, registry })(input);
expect(result.type).to.equal('ApexClass');
expect(result.name).to.equal('OrderController');
});

it('resolves multiple LWC bundle files to the same component type/name', () => {
const input: ChangeResult[] = [
{ origin: 'local', filenames: [path.join(lwcDir, 'accountMap', 'accountMap.js')] },
{ origin: 'local', filenames: [path.join(lwcDir, 'accountMap', 'accountMap.html')] },
];
const results = populateTypesAndNames({ projectPath, registry })(input);
expect(results).to.have.length(2);
results.forEach((r) => {
expect(r.type).to.equal('LightningComponentBundle');
expect(r.name).to.equal('accountMap');
});
});

it('marks a component as ignored when a content file matches .forceignore', () => {
// **/jsconfig.json is in the ebikes .forceignore. Writing one inside the bundle
// means forceIgnoreDenies returns true for this component.
const createCaseDir = path.join(projectPath, lwcDir, 'createCase');
fs.writeFileSync(path.join(createCaseDir, 'jsconfig.json'), '{}');

const forceIgnore = ForceIgnore.findAndCreate(projectPath);
const input: ChangeResult[] = [
{ origin: 'local', filenames: [path.join(lwcDir, 'createCase', 'createCase.js-meta.xml')] },
];
const [result] = populateTypesAndNames({ projectPath, registry, forceIgnore })(input);
expect(result.ignored).to.equal(true);
});

it('excludes unresolvable filenames when excludeUnresolvable is true', () => {
const input: ChangeResult[] = [
{ origin: 'local', filenames: ['force-app/main/default/classes/DoesNotExist.cls-meta.xml'] },
];
expect(populateTypesAndNames({ projectPath, registry, excludeUnresolvable: true })(input)).to.deep.equal([]);
});

it('preserves unresolvable elements when excludeUnresolvable is false', () => {
const input: ChangeResult[] = [
{ origin: 'local', filenames: ['force-app/main/default/classes/DoesNotExist.cls-meta.xml'] },
];
const [result] = populateTypesAndNames({ projectPath, registry })(input);
expect(result.origin).to.equal('local');
expect(result.type).to.equal(undefined);
expect(result.name).to.equal(undefined);
});
});
87 changes: 87 additions & 0 deletions test/nuts/local/reactInternalApp.nut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright 2026, Salesforce, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import path from 'node:path';
import fs from 'node:fs';
import { execSync } from 'node:child_process';
import { TestSession } from '@salesforce/cli-plugins-testkit';
import { expect } from 'chai';
import { RegistryAccess } from '@salesforce/source-deploy-retrieve';
import { ShadowRepo } from '../../../src/shared/local/localShadowRepo';
import { getGroupedFiles, getComponentSets } from '../../../src/shared/localComponentSetArray';

const findUiBundleDir = (projectDir: string): string => {
const uiBundlesRoot = path.join(projectDir, 'force-app', 'main', 'default', 'uiBundles');
const entries = fs.readdirSync(uiBundlesRoot, { withFileTypes: true });
const first = entries.find((e) => e.isDirectory() && !e.name.startsWith('.'));
if (!first) throw new Error(`No uiBundle directory found under ${uiBundlesRoot}`);
return path.join(uiBundlesRoot, first.name);
};

describe('reactinternalapp template: getComponentSets dedup check', () => {
let session: TestSession;
let projectPath: string;
const registry = new RegistryAccess();
const pkgDir = 'force-app';

before(async () => {
session = await TestSession.create({
project: {
sourceDir: path.join('test', 'nuts', 'repros', 'reactinternalapp'),
},
devhubAuthStrategy: 'NONE',
});
projectPath = session.project.dir;
execSync('npm install --registry https://registry.npmjs.org/', {
cwd: findUiBundleDir(projectPath),
stdio: 'inherit',
});
});

after(async () => {
await session?.clean();
});

it('single pkgDir: no duplicate filenames in groupings', async () => {
const repo = await ShadowRepo.getInstance({
orgId: 'fakeOrgId-reactapp-single',
projectPath,
packageDirs: [{ path: pkgDir, name: pkgDir, fullPath: path.join(projectPath, pkgDir) }],
registry,
});

const [nonDeletes, deletes] = await Promise.all([repo.getNonDeleteFilenames(), repo.getDeleteFilenames()]);

// All files are new (not committed), so deletes should be empty
expect(deletes).to.have.lengthOf(0);
expect(nonDeletes.length).to.be.greaterThan(0);

const groupings = getGroupedFiles(
{
packageDirs: [{ path: pkgDir, name: pkgDir, fullPath: path.join(projectPath, pkgDir) }],
nonDeletes,
deletes,
},
false
);

expect(groupings).to.have.lengthOf(1);
// No duplicates: grouping should have exactly the same count as the raw filenames
expect(groupings[0].nonDeletes.length).to.equal(nonDeletes.length);

// Calling getComponentSets triggers the instrumented lines
getComponentSets({ groupings, registry, projectPath });
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status
# More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm
#

package.xml

# LWC configuration files
**/jsconfig.json
**/.eslintrc.json

# LWC Jest
**/__tests__/**

node_modules/
.DS_Store
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: '../../../../../schema.graphql'
documents: 'src/**/*.{graphql,js,ts,jsx,tsx}'
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules
dist
build
.vite
coverage
*.min.js
*.min.css
*.map
package-lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "lf"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Change Log

All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

# [1.59.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.58.2...v1.59.0) (2026-02-27)

### Features

- auto bump base react app versions and fix issue with base ui-bundle json ([#175](https://github.com/salesforce-experience-platform-emu/webapps/issues/175)) ([048b5a8](https://github.com/salesforce-experience-platform-emu/webapps/commit/048b5a8449c899fc923aeebc3c76bc5bf1c5e0d4))
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# React Internal App

An internal React template for the Salesforce platform. Includes an Agentforce conversation client and global search; intended for internal (non-Experience Cloud) deployment. Built with React, Vite, TypeScript, and Tailwind/shadcn.

For project-level details (metadata, deploy), see the [project README](../../../../../../README.md).

## Prerequisites

```bash
npm install
```

## Run (development)

```bash
npm run dev
```

Starts the Vite dev server (default: http://localhost:5173).

## Build

```bash
npm run build
```

Writes the production bundle to `dist/` inside the UI Bundle folder.

## Test

```bash
npm test
```

Runs the unit test suite (Vitest).
Loading
Loading