Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
11 changes: 11 additions & 0 deletions packages/extension-starter/.force_overrides
Copy link
Contributor

@adamraya adamraya May 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to update the script npm run list-overrides to list the unused file override defined in the extensions .force-overrides?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out that this script doesn't use the force-overrides directly. So we are going to create a new ticket for this once we get a better idea on the scope of the requirements.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// DISCLAIMER
//
// BY USING THIS FILE, YOU AGREE THAT THE FUNCTIONALITY OF YOUR INSTALLED EXTENSION(S) IS NOT GUARANTEED.
// ADDITIONALLY UPGRADABILITY OF EXTENSIONS CAN ALSO NO LONGER BE GUARANTEED AND IS NOT SUPPORTED BY SALESFORCE.
// USE ONLY AS A TEMPORARY SOLUTION TO URGENTLY PATCH/UPDATE AN EXTENSION.
//
// USAGE:
// PLACE THE RELATIVE __POSIX__ PATH TO THE EXTENSION FILE YOU WANT TO OVERRIDE STARTING WITH THE EXTENSION PACKAGE NAME.
// MULTIPLE OVERRIDES CAN BE ADDED TO THIS FILE, ONE PER LINE.
// EXAMPLE:
// ./node_modules/@salesforce/extension-chakra-store-locator/src/components/content.tsx
15 changes: 11 additions & 4 deletions packages/pwa-kit-dev/src/configs/webpack/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ const baseConfig = (target) => {
throw Error(`The value "${target}" is not a supported webpack target`)
}

const extensions = getConfiguredExtensions(getConfig())
class Builder {
constructor() {
this.config = {
Expand Down Expand Up @@ -239,7 +240,7 @@ const baseConfig = (target) => {
},
plugins: [
new ApplicationExtensionConfigPlugin({
extensions: getConfiguredExtensions(getConfig())
extensions
}),
new webpack.DefinePlugin({
DEBUG,
Expand Down Expand Up @@ -284,17 +285,23 @@ const baseConfig = (target) => {
},
ruleForApplicationExtensibility({
loaderOptions: {
configured: getConfiguredExtensions(getConfig()),
configured: extensions,
target: 'web'
}
}),
ruleForApplicationExtensibility({
loaderOptions: {
configured: getConfiguredExtensions(getConfig()),
configured: extensions,
target: 'node'
}
}),
ruleForOverrideResolver({target, projectDir, isMonoRepo})
ruleForOverrideResolver({
extensions,
resolveExtensions: SUPPORTED_FILE_EXTENSIONS,
isMonoRepo,
projectDir,
target
})
].filter(Boolean)
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/pwa-kit-extension-sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
## v4.0.0-extensibility-preview.4 (Feb 12, 2025)
- Support `.force_overries` in extension projects [#2380](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2380)
- Replace `extendRoutes` with `getRoutes` and `getRoutesAsync` to have simpler API and allow for async SCAPI calls (for example, Shopper SEO's getUrlMapping) [#2308](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2308)
- Change signature of `beforeRouteMatch` to allow for more parameters (now also passing in the locals object) [#2308](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2308)
- Added caching to `getRoutesAsync` and implemented serialization for application extension async routes [#2300](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2300)
Expand Down
17 changes: 17 additions & 0 deletions packages/pwa-kit-extension-sdk/src/configs/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright (c) 2024, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

// Constants // TODO: Move to a shared location
export const LOCAL_EXTENSIONS_DIR = 'application-extensions'
export const EXTENSION_PACKAGE_PREFIX = 'extension-'
export const EXTENSION_PACKAGE_NAMESPACE = '@salesforce'
export const IMPORT_REGEX = /import\s+(?:(?:[\w*\s{},]*)\s+from\s+)?['"](\..*?)['"]/g
export const OVERRIDABLE_FILE_NAME = '.force_overrides'
export const MONO_REPO_WORKSPACE_FOLDER = 'packages'
export const NODE_MODULES_FOLDER = 'node_modules'
export const REQUIRES_REGEX = /require\(['"](\..*?)['"]\)/g
export const SRC_FOLDER = 'src'
85 changes: 83 additions & 2 deletions packages/pwa-kit-extension-sdk/src/configs/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@
import * as fse from 'fs-extra'
import * as path from 'path'
import {buildBabelExtensibilityArgs} from './babel/utils'
import {ApplicationExtensionEntryTuple} from '../types'
import {ApplicationExtensionEntry, ApplicationExtensionEntryTuple} from '../types'
import {LOCAL_EXTENSIONS_DIR, OVERRIDABLE_FILE_NAME, NODE_MODULES_FOLDER} from './constants'
import {getOverridesFromFile, getForceOverridesFilePaths} from './utils'
import e from 'express'

jest.mock('fs-extra', () => ({
...jest.requireActual('fs-extra'),
realpathSync: jest.fn()
realpathSync: jest.fn(),
readFileSync: jest.fn()
}))

const EXTENSIONS: ApplicationExtensionEntryTuple[] = [
Expand Down Expand Up @@ -85,3 +89,80 @@ describe('buildBabelExtensibilityArgs', () => {
expect(result2).toBe(expectedArgs)
})
})

describe('getOverridesFromFile', () => {
const readFileSyncMock = jest.spyOn(fse, 'readFileSync') as jest.Mock

test('returns parsed override entries, skipping comments and blank lines', () => {
readFileSyncMock.mockReturnValue(`
// This is a comment
override1
override2 // inline comment

// Another comment
override3
`)

const result = getOverridesFromFile('mock/path/.force_overrides')

expect(result).toEqual(['override1', 'override2', 'override3'])
})

test('returns an empty array if file not found (ENOENT)', () => {
const error = new Error('File not found') as any
error.code = 'ENOENT'
readFileSyncMock.mockImplementation(() => {
throw error
})

const result = getOverridesFromFile('nonexistent/path')
expect(result).toEqual([])
})

test('logs warning for other read errors and returns empty array', () => {
const error = new Error('Permission denied') as any
error.code = 'EACCES'
const warnSpy = jest.spyOn(console, 'warn').mockImplementation()

readFileSyncMock.mockImplementation(() => {
throw error
})

const result = getOverridesFromFile('bad/path')
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('Error reading override file at'),
error
)
expect(result).toEqual([])

warnSpy.mockRestore()
})
})

describe('getForceOverridesFilePaths', () => {
const projectDir = '/project'
const extensions: ApplicationExtensionEntry[] = [
['ext-a', {enabled: true}],
['ext-b', {enabled: true}],
['ext-c', {enabled: true}]
]

test('returns all potential override paths', () => {
const result = getForceOverridesFilePaths(projectDir, extensions)

expect(result).toEqual([
path.join(projectDir, OVERRIDABLE_FILE_NAME),
path.join(projectDir, LOCAL_EXTENSIONS_DIR, 'ext-a', OVERRIDABLE_FILE_NAME),
path.join(projectDir, NODE_MODULES_FOLDER, 'ext-a', OVERRIDABLE_FILE_NAME),
path.join(projectDir, LOCAL_EXTENSIONS_DIR, 'ext-b', OVERRIDABLE_FILE_NAME),
path.join(projectDir, NODE_MODULES_FOLDER, 'ext-b', OVERRIDABLE_FILE_NAME),
path.join(projectDir, LOCAL_EXTENSIONS_DIR, 'ext-c', OVERRIDABLE_FILE_NAME),
path.join(projectDir, NODE_MODULES_FOLDER, 'ext-c', OVERRIDABLE_FILE_NAME)
])
})

test('handles an empty extensions array', () => {
const result = getForceOverridesFilePaths(projectDir, [])
expect(result).toEqual([path.join(projectDir, OVERRIDABLE_FILE_NAME)])
})
})
61 changes: 61 additions & 0 deletions packages/pwa-kit-extension-sdk/src/configs/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@

// Third-Party
import dedent from 'dedent'
import fse from 'fs-extra'
import Handlebars from 'handlebars'
import path from 'path'

// Local
import {kebabToUpperCamelCase} from '../shared/utils'

// Types
import {ApplicationExtensionsLoaderOptions} from './webpack/types'
import type {ApplicationExtensionEntry} from '../types'

// Constants
import {LOCAL_EXTENSIONS_DIR, OVERRIDABLE_FILE_NAME, NODE_MODULES_FOLDER} from './constants'

// Register Handlebars helpers
Handlebars.registerHelper('getInstanceName', (aString: string) => {
Expand Down Expand Up @@ -84,3 +90,58 @@ export const renderTemplate = (data: ApplicationExtensionsLoaderOptions) => {
// Apply data to the compiled template
return template(data).trim()
}

/**
* @private
* Reads and parses a .force_overrides file into a list of clean override entries.
* - Skips empty lines
* - Skips full-line comments (`// comment`)
* - Supports inline comments (`override // comment`)
*/
export const getOverridesFromFile = (filePath: string): string[] => {
try {
const content = fse.readFileSync(filePath, 'utf8')
return content
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line && !line.startsWith('//'))
.map((line) => line.split('//')[0].trim()) // remove inline comments
.filter(Boolean)
Comment on lines +104 to +109
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .filter(Boolean) at the end looks like is redundant

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is a line with only a comment on it, this filter ensures that the empty line isn't included.

} catch (e: any) {
if (e.code !== 'ENOENT') {
console.warn(`Error reading override file at ${filePath}:`, e)
}
return []
}
}

/**
* PRIVATE: Returns a list of potential file paths where `.force_overrides` files might exist.
*
* These paths include:
* - A top-level `.force_overrides` file in the root of the project.
* - One per extension, prioritized with the local package version first.
*
* Note:
* - Each extension is assumed to be either a string or a tuple, where the first item is the extension name.
* - This function does not check if the files actually exist; it simply builds candidate paths.
*
* @param projectDir - The root directory of the project.
* @param extensions - A list of extension names or tuples where the first element is the extension name.
* @returns An array of string file paths to check for overrides.
*/
export const getForceOverridesFilePaths = (
projectDir: string,
extensions: ApplicationExtensionEntry[]
): string[] => {
return [
path.join(projectDir, OVERRIDABLE_FILE_NAME),
...extensions.flatMap((ext) => {
const name = typeof ext === 'string' ? ext : ext[0]
return [
path.join(projectDir, LOCAL_EXTENSIONS_DIR, name, OVERRIDABLE_FILE_NAME),
path.join(projectDir, NODE_MODULES_FOLDER, name, OVERRIDABLE_FILE_NAME)
]
})
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,7 @@ describe('Overrides Resolver Loader', () => {
),
options: {
baseDir: '/',
resolveExtensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
resolveOptions: {
// Override the `fs` methods used by `resolve` to point to the virtual file system
existsSync: (filePath: string) => {
Expand Down
Loading
Loading