From d1ec57667eec5aed9d18ba9fb2ed547970298eaf Mon Sep 17 00:00:00 2001 From: "ankitatripathi.mp@gmail.com" Date: Fri, 15 May 2026 12:25:56 +0530 Subject: [PATCH 1/3] fix: preserve keyword markers in themes for default export configuration --- src/keywordPreservation.ts | 19 +++++-- src/tools/auth0/handlers/themes.ts | 2 +- test/tools/auth0/handlers/themes.tests.js | 64 ++++++++++++++++++++--- 3 files changed, 75 insertions(+), 10 deletions(-) diff --git a/src/keywordPreservation.ts b/src/keywordPreservation.ts index 3e2463480..78dd03876 100644 --- a/src/keywordPreservation.ts +++ b/src/keywordPreservation.ts @@ -66,10 +66,10 @@ export const getPreservableFieldsFromAssets = ( const specificAddress = resourceIdentifiers.reduce( (aggregateAddress, resourceIdentifier) => { resourceSpecificIdentifiers[address]; - if (resourceIdentifier === undefined) return ''; // See if this specific resource type has an identifier + if (resourceIdentifier === undefined) return aggregateAddress; // See if this specific resource type has an identifier const identifierFieldValue = arrayItem[resourceIdentifier]; - if (identifierFieldValue === undefined) return ''; // See if this specific array item possess the resource-specific identifier + if (identifierFieldValue === undefined) return aggregateAddress; // See if this specific array item possess the resource-specific identifier if (aggregateAddress === '') { return `${resourceIdentifier}=${identifierFieldValue}`; @@ -81,7 +81,20 @@ export const getPreservableFieldsFromAssets = ( ); if (specificAddress.length === 0) { - return []; + // If identifiers are registered for this resource type but none were found on the + // local array item (e.g. themeId stripped by default export), fall back to positional + // index so keyword markers are still preserved. Safe for singletons like themes. + // If no identifiers are registered at all (unrecognized nested arrays), skip. + if (resourceIdentifiers.length === 0) { + return []; + } + const arrayIndex = (asset as any[]).indexOf(arrayItem); + return getPreservableFieldsFromAssets( + arrayItem, + keywordMappings, + resourceSpecificIdentifiers, + `${address}${shouldRenderDot ? '.' : ''}${arrayIndex}` + ); } return getPreservableFieldsFromAssets( diff --git a/src/tools/auth0/handlers/themes.ts b/src/tools/auth0/handlers/themes.ts index abdcc3674..6c6533fde 100644 --- a/src/tools/auth0/handlers/themes.ts +++ b/src/tools/auth0/handlers/themes.ts @@ -429,7 +429,7 @@ export default class ThemesHandler extends DefaultHandler { ...options, type: 'themes', id: 'themeId', - identifiers: ['themeId'], + identifiers: ['themeId', 'displayName'], }); } diff --git a/test/tools/auth0/handlers/themes.tests.js b/test/tools/auth0/handlers/themes.tests.js index 215121125..4b11f2a02 100644 --- a/test/tools/auth0/handlers/themes.tests.js +++ b/test/tools/auth0/handlers/themes.tests.js @@ -352,16 +352,68 @@ describe('#themes keyword preservation', () => { expect(result.themes[0].widget.logo_url).to.equal('##CDN_URL##/logo.png'); }); - it('should NOT preserve keyword placeholders when themeId is absent from handler identifiers', () => { - // Regression: prior to the fix, ThemesHandler used identifiers ['id', 'name']. - // getPreservableFieldsFromAssets looks for the identifier field on each array item to build - // a dot-notation address; if the field is missing (themes have themeId, not id/name), - // it silently returns no addresses and the raw remote URLs are returned unchanged. + it('should preserve keyword placeholders in theme fields when themeId is absent from local file (default export without AUTH0_EXPORT_IDENTIFIERS)', () => { + // Real-world scenario: exported with AUTH0_EXPORT_IDENTIFIERS: false (the default), + // so themeId is stripped from the local file. Keyword preservation must still work + // using displayName as a fallback identifier. + const localThemeWithoutThemeId = { + displayName: 'Default theme', + fonts: { font_url: '##CDN_URL##/fonts/custom.woff2' }, + widget: { logo_url: '##CDN_URL##/logo.png' }, + }; + + const remoteThemeWithThemeId = { + themeId, + displayName: 'Default theme', + fonts: { font_url: `${CDN_URL}/fonts/custom.woff2` }, + widget: { logo_url: `${CDN_URL}/logo.png` }, + }; + + const result = preserveKeywords({ + localAssets: { themes: [localThemeWithoutThemeId] }, + remoteAssets: { themes: [remoteThemeWithThemeId] }, + keywordMappings: { CDN_URL }, + auth0Handlers: [{ id: 'themeId', identifiers: ['themeId', 'displayName'], type: 'themes' }], + }); + + expect(result.themes[0].fonts.font_url).to.equal('##CDN_URL##/fonts/custom.woff2'); + expect(result.themes[0].widget.logo_url).to.equal('##CDN_URL##/logo.png'); + }); + + it('should preserve keyword placeholders using index fallback when neither themeId nor displayName is in local file', () => { + // The exact customer scenario: AUTH0_EXPORT_IDENTIFIERS defaults to false AND no displayName + // is set on the theme. The local file has no identifier fields at all — only keyword markers. + // The index-based fallback should still preserve the keywords. + const localThemeNoIdentifiers = { + fonts: { font_url: '##CDN_URL##/fonts/custom.woff2' }, + widget: { logo_url: '##CDN_URL##/logo.png' }, + }; + + const remoteThemeWithThemeId = { + themeId, + fonts: { font_url: `${CDN_URL}/fonts/custom.woff2` }, + widget: { logo_url: `${CDN_URL}/logo.png` }, + }; + + const result = preserveKeywords({ + localAssets: { themes: [localThemeNoIdentifiers] }, + remoteAssets: { themes: [remoteThemeWithThemeId] }, + keywordMappings: { CDN_URL }, + auth0Handlers: [{ id: 'themeId', identifiers: ['themeId', 'displayName'], type: 'themes' }], + }); + + expect(result.themes[0].fonts.font_url).to.equal('##CDN_URL##/fonts/custom.woff2'); + expect(result.themes[0].widget.logo_url).to.equal('##CDN_URL##/logo.png'); + }); + + it('should NOT preserve keyword placeholders for unregistered array types (no identifiers configured)', () => { + // When identifiers is empty, the handler has not registered this array type for preservation. + // No index fallback should apply — raw remote values are returned unchanged. const result = preserveKeywords({ localAssets: { themes: [localThemeWithKeywords] }, remoteAssets: { themes: [remoteThemeWithResolvedUrls] }, keywordMappings: { CDN_URL }, - auth0Handlers: [{ id: 'themeId', identifiers: ['id', 'name'], type: 'themes' }], + auth0Handlers: [{ id: 'themeId', identifiers: [], type: 'themes' }], }); expect(result.themes[0].fonts.font_url).to.equal(`${CDN_URL}/fonts/custom.woff2`); From cd8a01bb3fb85574b7de4381f6eea59572ab6261 Mon Sep 17 00:00:00 2001 From: "ankitatripathi.mp@gmail.com" Date: Tue, 19 May 2026 11:51:46 +0530 Subject: [PATCH 2/3] fix: drop displayName from theme identifiers, rely on positional fallback --- src/tools/auth0/handlers/themes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/auth0/handlers/themes.ts b/src/tools/auth0/handlers/themes.ts index 6c6533fde..abdcc3674 100644 --- a/src/tools/auth0/handlers/themes.ts +++ b/src/tools/auth0/handlers/themes.ts @@ -429,7 +429,7 @@ export default class ThemesHandler extends DefaultHandler { ...options, type: 'themes', id: 'themeId', - identifiers: ['themeId', 'displayName'], + identifiers: ['themeId'], }); } From 11e7b17fb4c3d8501635b300f331e8140ebf3633 Mon Sep 17 00:00:00 2001 From: "ankitatripathi.mp@gmail.com" Date: Tue, 19 May 2026 16:06:29 +0530 Subject: [PATCH 3/3] fix: shorten positional fallback comment in getPreservableFieldsFromAssets --- src/keywordPreservation.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/keywordPreservation.ts b/src/keywordPreservation.ts index 78dd03876..f7ed1154f 100644 --- a/src/keywordPreservation.ts +++ b/src/keywordPreservation.ts @@ -81,10 +81,7 @@ export const getPreservableFieldsFromAssets = ( ); if (specificAddress.length === 0) { - // If identifiers are registered for this resource type but none were found on the - // local array item (e.g. themeId stripped by default export), fall back to positional - // index so keyword markers are still preserved. Safe for singletons like themes. - // If no identifiers are registered at all (unrecognized nested arrays), skip. + // No identifiers registered: skip. Identifiers registered but absent from item: fall back to positional index. if (resourceIdentifiers.length === 0) { return []; }