diff --git a/core/projectify/src/index.ts b/core/projectify/src/index.ts index 2bae22568bc5..1bfe98d433a6 100644 --- a/core/projectify/src/index.ts +++ b/core/projectify/src/index.ts @@ -14,6 +14,9 @@ import {Stream} from 'stream'; // See the License for the specific language governing permissions and // limitations under the License. +const PROJECT_ID_TOKEN = '{{projectId}}'; +const PROJECT_ID_TOKEN_REGEX = /{{projectId}}/g; + /** * Populate the `{{projectId}}` placeholder. * @@ -25,33 +28,43 @@ import {Stream} from 'stream'; */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function replaceProjectIdToken(value: any, projectId: string): any { - if (Array.isArray(value)) { - value = (value as string[]).map(v => replaceProjectIdToken(v, projectId)); + if (typeof value === 'string') { + if (value.includes(PROJECT_ID_TOKEN)) { + if (!projectId || projectId === PROJECT_ID_TOKEN) { + throw new MissingProjectIdError(); + } + return value.replace(PROJECT_ID_TOKEN_REGEX, projectId); + } + return value; + } + + if (value === null || typeof value !== 'object') { + return value; } - if ( - value !== null && - typeof value === 'object' && - !(value instanceof Buffer) && - !(value instanceof Stream) && - typeof value.hasOwnProperty === 'function' - ) { - for (const opt in value) { - // eslint-disable-next-line no-prototype-builtins - if (value.hasOwnProperty(opt)) { - value[opt] = replaceProjectIdToken(value[opt], projectId); + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + const original = value[i]; + const processed = replaceProjectIdToken(original, projectId); + if (processed !== original) { + value[i] = processed; } } + return value; } - if ( - typeof value === 'string' && - (value as string).indexOf('{{projectId}}') > -1 - ) { - if (!projectId || projectId === '{{projectId}}') { - throw new MissingProjectIdError(); + if (value instanceof Buffer || value instanceof Stream) { + return value; + } + + for (const key in value) { + if (Object.prototype.hasOwnProperty.call(value, key)) { + const original = value[key]; + const processed = replaceProjectIdToken(original, projectId); + if (processed !== original) { + value[key] = processed; + } } - value = (value as string).replace(/{{projectId}}/g, projectId); } return value; diff --git a/core/projectify/test/index.ts b/core/projectify/test/index.ts index b14e2214e39b..8ff9393cc93d 100644 --- a/core/projectify/test/index.ts +++ b/core/projectify/test/index.ts @@ -47,6 +47,7 @@ describe('projectId placeholder', () => { ], }, ], + simpleArray: ['A {{projectId}} Z'], }, PROJECT_ID, ), @@ -74,6 +75,7 @@ describe('projectId placeholder', () => { ], }, ], + simpleArray: ['A ' + PROJECT_ID + ' Z'], }, ); }); @@ -116,6 +118,36 @@ describe('projectId placeholder', () => { ); }); + it('should return values without placeholder as-is', () => { + assert.strictEqual( + replaceProjectIdToken('no-placeholder', PROJECT_ID), + 'no-placeholder', + ); + assert.strictEqual(replaceProjectIdToken(123, PROJECT_ID), 123); + assert.strictEqual(replaceProjectIdToken(true, PROJECT_ID), true); + assert.strictEqual(replaceProjectIdToken(null, PROJECT_ID), null); + assert.strictEqual(replaceProjectIdToken(undefined, PROJECT_ID), undefined); + + const array = [1, 2, 3]; + assert.strictEqual(replaceProjectIdToken(array, PROJECT_ID), array); + + const object = {a: 1, b: 2}; + assert.strictEqual(replaceProjectIdToken(object, PROJECT_ID), object); + }); + + it('should handle frozen arrays and objects without placeholders correctly without throwing', () => { + const frozenArray = Object.freeze(['no-placeholder', 123, true]); + const replacedArray = replaceProjectIdToken(frozenArray, PROJECT_ID); + assert.strictEqual(frozenArray, replacedArray); + + const frozenObject = Object.freeze({ + prop: 'no-placeholder', + other: 123, + }); + const replacedObject = replaceProjectIdToken(frozenObject, PROJECT_ID); + assert.strictEqual(frozenObject, replacedObject); + }); + it('should not inject projectId into stream', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const transform = new stream.Transform() as any;