From 37d92311b3448d3bffab022b5b85865b8c16ac8f Mon Sep 17 00:00:00 2001 From: John Hauck Date: Thu, 25 Jun 2026 16:19:52 -0600 Subject: [PATCH 1/2] truncate and update after --- packages/common/src/featureServiceHelpers.ts | 111 ++++++++- .../common/test/featureServiceHelpers.test.ts | 220 +++++++++++++++++- packages/deployer/src/deploySolutionItems.ts | 2 +- 3 files changed, 320 insertions(+), 13 deletions(-) diff --git a/packages/common/src/featureServiceHelpers.ts b/packages/common/src/featureServiceHelpers.ts index 8eeffbffa..53dd7b006 100644 --- a/packages/common/src/featureServiceHelpers.ts +++ b/packages/common/src/featureServiceHelpers.ts @@ -534,9 +534,12 @@ export function getLayerSettings(layerInfos: any, url: string, itemId: string, e * The feature service name will have a generated GUID appended. * * @param templates A collection of AGO item templates. + * @param templateDictionary Optional hash of values used to resolve `{{...}}` tokens in the name + * before applying the 50-char base-name limit. When omitted, names containing unresolved tokens + * are passed through untruncated (legacy behavior). * @returns An updated collection of AGO templates with unique feature service names. */ -export function setNamesAndTitles(templates: IItemTemplate[]): IItemTemplate[] { +export function setNamesAndTitles(templates: IItemTemplate[], templateDictionary?: any): IItemTemplate[] { const guid: string = generateGUID(); const names: string[] = []; return templates.map((t) => { @@ -553,12 +556,21 @@ export function setNamesAndTitles(templates: IItemTemplate[]): IItemTemplate[] { // If the name already contains a GUID remove it baseName = baseName.replace(/_[0-9A-F]{32}/gi, ""); - // The name length limit is 98 - // Limit the baseName to 50 characters before the _ - // If the baseName includes '{{params' it is likely being used in a template replacement, so do not truncate. - const name: string = baseName.includes("{{params") - ? baseName + "_" + guid - : baseName.substring(0, 50) + "_" + guid; + // Resolve any {{...}} tokens (e.g. {{params.buildSolution.items..title}}) so the + // base-name length limit reflects the value AGOL will actually see at create time. + const resolvedBaseName: string = templateDictionary + ? replaceInTemplate(baseName, templateDictionary) + : baseName; + + // Limit the base name to 50 chars so the full service name (base + "_" + 32-char guid) + // stays within AGOL's service-name length limit. This 50-char cap is long-standing + // behavior. If unresolved {{...}} tokens remain we can't know the resolved length, so + // leave the name untruncated. + const maxBaseNameLength = 50; + const stillTemplatized = resolvedBaseName.includes("{{"); + const name: string = stillTemplatized + ? resolvedBaseName + "_" + guid + : resolvedBaseName.substring(0, maxBaseNameLength) + "_" + guid; // If the name + GUID already exists then append "_occurrenceCount" t.item.name = names.indexOf(name) === -1 ? name : `${name}_${names.filter((n) => n === name).length}`; @@ -881,6 +893,59 @@ export function addFeatureServiceLayersAndTables( }); } +/** + * Shorten any layer or table name longer than `maxNameLength` to a unique value of at most + * `maxNameLength` characters, in preparation for an addToDefinition call, and report the original + * names so they can be restored afterward. + * + * Mutates the `name` of each over-long layer/table in `layerChunks` in place. + * + * @param layerChunks The addToDefinition chunks whose `layers` and `tables` will be added + * @param maxNameLength Names longer than this are truncated to a unique value of this length + * @returns The original names to restore, as `{ layers: [{ id, name }], tables: [{ id, name }] }` + * @private + */ +export function _truncateNamesForAddToDefinition( + layerChunks: any[], + maxNameLength: number, +): { layers: Array<{ id: any; name: string }>; tables: Array<{ id: any; name: string }> } { + const restore: { layers: Array<{ id: any; name: string }>; tables: Array<{ id: any; name: string }> } = { + layers: [], + tables: [], + }; + const allItems: Array<{ item: any; isLayer: boolean }> = []; + layerChunks.forEach((chunk) => { + chunk.layers.forEach((layer: any) => allItems.push({ item: layer, isLayer: true })); + chunk.tables.forEach((table: any) => allItems.push({ item: table, isLayer: false })); + }); + + // Reserve names that are already short enough so the truncated names won't collide with them. + const usedNames = new Set(); + allItems.forEach(({ item }) => { + if (item.name.length <= maxNameLength) { + usedNames.add(item.name.toLowerCase()); + } + }); + + allItems.forEach(({ item, isLayer }) => { + if (item.name.length > maxNameLength) { + const originalName: string = item.name; + let candidate = originalName.substring(0, maxNameLength); + let counter = 1; + // Keep the truncated name unique by trimming room for a numeric suffix when needed. + while (usedNames.has(candidate.toLowerCase())) { + const suffix = `_${counter++}`; + candidate = originalName.substring(0, maxNameLength - suffix.length) + suffix; + } + usedNames.add(candidate.toLowerCase()); + item.name = candidate; + (isLayer ? restore.layers : restore.tables).push({ id: item.id, name: originalName }); + } + }); + + return restore; +} + /** * Updates a feature service with a list of layers and/or tables. * @@ -1013,13 +1078,43 @@ export function addFeatureServiceDefinition( // than the defined chunk size const useAsync: boolean = listToAdd.length > chunkSize; + // Shorten any over-long layer/table names to short, unique values before adding them. + // Names will be restored afterward with one updateDefinition call per layer/table. + const maxBackingTableNameLength = 30; + const restoreNames = _truncateNamesForAddToDefinition(layerChunks, maxBackingTableNameLength); + + // Base admin URL used for the per-layer updateDefinition restore calls below. + const adminServiceUrl = checkUrlPathTermination(serviceUrl.replace("/rest/services", "/rest/admin/services")); + layerChunks .reduce( (prev, curr) => prev.then(() => addToServiceDefinition(serviceUrl, curr, false, useAsync)), Promise.resolve(null), ) .then( - () => resolve(null), + () => { + // Restore the original (pre-truncation) display names. + // I was seeing the updateDefinition call succeed but the names not being restored when I was doing this + // with a single call at the service endpoint. So now we are doing this sequentially. + const restoreItems = [...restoreNames.layers, ...restoreNames.tables]; + restoreItems + .reduce( + (prev, entry) => + prev.then(() => { + const restoreUpdate: IUpdate = { + url: adminServiceUrl + entry.id + "/updateDefinition", + params: { updateDefinition: { name: entry.name } }, + args: { authentication } as IPostProcessArgs, + }; + return getRequest(restoreUpdate, false, false, templateDictionary.isPortal); + }), + Promise.resolve(null), + ) + .then( + () => resolve(null), + (e: any) => reject(fail(e)), + ); + }, (e: any) => reject(fail(e)), ); } diff --git a/packages/common/test/featureServiceHelpers.test.ts b/packages/common/test/featureServiceHelpers.test.ts index 4067e57ef..703011aca 100644 --- a/packages/common/test/featureServiceHelpers.test.ts +++ b/packages/common/test/featureServiceHelpers.test.ts @@ -35,6 +35,7 @@ import { getLayersAndTables, getExistingLayersAndTables, addFeatureServiceDefinition, + _truncateNamesForAddToDefinition, addFeatureServiceLayersAndTables, updateLayerFieldReferences, postProcessFields, @@ -1938,7 +1939,7 @@ describe("Module `featureServiceHelpers`: utility functions for feature-service it("should limit the base name to 50 chars", () => { const t: IItemTemplate = templates.getItemTemplateSkeleton(); t.item.type = "Feature Service"; - t.item.name = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab"; + t.item.name = "a".repeat(60); t.item.title = "TheName"; const _templates: IItemTemplate[] = [t]; @@ -1946,7 +1947,7 @@ describe("Module `featureServiceHelpers`: utility functions for feature-service const expectedTemplate: IItemTemplate = templates.getItemTemplateSkeleton(); expectedTemplate.item.type = "Feature Service"; - expectedTemplate.item.name = `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_212dbc19b03943008fdfaf8d6adca00e`; + expectedTemplate.item.name = "a".repeat(50) + "_212dbc19b03943008fdfaf8d6adca00e"; expectedTemplate.item.title = "TheName"; const expected: IItemTemplate[] = [expectedTemplate]; @@ -1957,7 +1958,7 @@ describe("Module `featureServiceHelpers`: utility functions for feature-service it("should limit the base name to 50 chars and handle existing guid in the name", () => { const t: IItemTemplate = templates.getItemTemplateSkeleton(); t.item.type = "Feature Service"; - t.item.name = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab_aa766cba0dd44ec080420acc10990282"; + t.item.name = "a".repeat(60) + "_aa766cba0dd44ec080420acc10990282"; t.item.title = "TheName"; const _templates: IItemTemplate[] = [t]; @@ -1965,7 +1966,7 @@ describe("Module `featureServiceHelpers`: utility functions for feature-service const expectedTemplate: IItemTemplate = templates.getItemTemplateSkeleton(); expectedTemplate.item.type = "Feature Service"; - expectedTemplate.item.name = `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_212dbc19b03943008fdfaf8d6adca00e`; + expectedTemplate.item.name = "a".repeat(50) + "_212dbc19b03943008fdfaf8d6adca00e"; expectedTemplate.item.title = "TheName"; const expected: IItemTemplate[] = [expectedTemplate]; @@ -1991,6 +1992,42 @@ describe("Module `featureServiceHelpers`: utility functions for feature-service const actual: IItemTemplate[] = setNamesAndTitles(_templates); expect(actual).toEqual(expected); }); + + it("should resolve '{{params...}}' via the templateDictionary and truncate the resolved value to 50 chars", () => { + const t: IItemTemplate = templates.getItemTemplateSkeleton(); + t.item.type = "Feature Service"; + t.item.name = "{{params.buildSolution.items.46c2f61bc5bc4f3aad6b391360e8e53d.title}}"; + t.item.title = "TheName"; + const _templates: IItemTemplate[] = [t]; + + spyOn(generalHelpers, "generateGUID").and.returnValue("212dbc19b03943008fdfaf8d6adca00e"); + + // 80-char resolved title — well past the 50-char base limit + const longTitle = "TrailConditions" + "a".repeat(65); + expect(longTitle.length).toBe(80); + const templateDictionary: any = { + params: { + buildSolution: { + items: { + "46c2f61bc5bc4f3aad6b391360e8e53d": { + title: longTitle, + }, + }, + }, + }, + }; + + const expectedTemplate: IItemTemplate = templates.getItemTemplateSkeleton(); + expectedTemplate.item.type = "Feature Service"; + expectedTemplate.item.name = longTitle.substring(0, 50) + "_212dbc19b03943008fdfaf8d6adca00e"; + expectedTemplate.item.title = "TheName"; + const expected: IItemTemplate[] = [expectedTemplate]; + + const actual: IItemTemplate[] = setNamesAndTitles(_templates, templateDictionary); + expect(actual).toEqual(expected); + // Confirm the resolved value is truncated to the 50-char base (50 + "_" + 32-char guid). + expect(actual[0].item.name.length).toBe(50 + 1 + 32); + }); }); describe("updateSettingsFieldInfos", () => { @@ -3794,6 +3831,181 @@ describe("Module `featureServiceHelpers`: utility functions for feature-service await addFeatureServiceDefinition(expectedUrl, [], {}, MOCK_USER_SESSION, "", {}, {}, itemTemplate); }); + + it("should truncate over-long names for the add, then restore each via a per-layer updateDefinition call", async () => { + const expectedUrl: string = + "https://services123.arcgis.com/org1234567890/arcgis/rest/services/ROWPermits_publiccomment/FeatureServer"; + const adminAddUrl: string = + "https://services123.arcgis.com/org1234567890/arcgis/rest/admin/services/ROWPermits_publiccomment/FeatureServer/addToDefinition"; + + itemTemplate = templates.getItemTemplate("Feature Service", [], expectedUrl); + + // Two layers and one table whose names are all longer than 30 chars. + const longName0 = "Ski Hills " + "x".repeat(40); + const longName1 = "Tasks " + "y".repeat(40); + const longName2 = "Inspections " + "z".repeat(40); + const layer0 = mockItems.getAGOLLayerOrTable(0, longName0, "Feature Layer", []); + const layer1 = mockItems.getAGOLLayerOrTable(1, longName1, "Feature Layer", []); + const table2 = mockItems.getAGOLLayerOrTable(2, longName2, "Table", []); + const listToAdd = [ + { item: layer0, type: "layer" }, + { item: layer1, type: "layer" }, + { item: table2, type: "table" }, + ]; + + fetchMock.post(adminAddUrl, '{"success": true}').post(/updateDefinition/, '{"success": true}'); + + await addFeatureServiceDefinition(expectedUrl, listToAdd, {}, MOCK_USER_SESSION, "", {}, {}, itemTemplate); + + // All layers and tables are added in a single batched call, with names truncated to <= 30. + const addCalls: any[] = fetchMock.calls(/addToDefinition/); + expect(addCalls.length).withContext("a single batched addToDefinition call").toEqual(1); + const addBody: string = addCalls[0][1].body as string; + const addToDef = JSON.parse(decodeURIComponent(/addToDefinition=([^&]*)/.exec(addBody)![1])); + expect(addToDef.layers.length).toEqual(2); + expect(addToDef.tables.length).toEqual(1); + addToDef.layers.concat(addToDef.tables).forEach((item: any) => { + expect(item.name.length).withContext("truncated to <= 30 chars").toBeLessThanOrEqual(30); + }); + + // One layer-level updateDefinition call per truncated item restores the original name. + const updateCalls: any[] = fetchMock.calls(/updateDefinition/); + expect(updateCalls.length).withContext("one updateDefinition call per truncated item").toEqual(3); + const restoredById: any = {}; + updateCalls.forEach((call) => { + const url: string = call[0] as string; + const id = /FeatureServer\/(\d+)\/updateDefinition/.exec(url)![1]; + const body: string = call[1].body as string; + const def = JSON.parse(decodeURIComponent(/updateDefinition=([^&]*)/.exec(body)![1])); + restoredById[id] = def.name; + }); + expect(restoredById["0"]).toEqual(longName0); + expect(restoredById["1"]).toEqual(longName1); + expect(restoredById["2"]).toEqual(longName2); + }); + + it("should add in a single call with no updateDefinition when no name is longer than 30", async () => { + const expectedUrl: string = + "https://services123.arcgis.com/org1234567890/arcgis/rest/services/ROWPermits_publiccomment/FeatureServer"; + const adminAddUrl: string = + "https://services123.arcgis.com/org1234567890/arcgis/rest/admin/services/ROWPermits_publiccomment/FeatureServer/addToDefinition"; + + itemTemplate = templates.getItemTemplate("Feature Service", [], expectedUrl); + + // All names are <= 30 chars, so nothing is truncated and no updateDefinition call is made. + const layer0 = mockItems.getAGOLLayerOrTable(0, "Ski Hills", "Feature Layer", []); + const layer1 = mockItems.getAGOLLayerOrTable(1, "Tasks", "Feature Layer", []); + const table2 = mockItems.getAGOLLayerOrTable(2, "Inspections", "Table", []); + const listToAdd = [ + { item: layer0, type: "layer" }, + { item: layer1, type: "layer" }, + { item: table2, type: "table" }, + ]; + + fetchMock.post(adminAddUrl, '{"success": true}'); + + await addFeatureServiceDefinition(expectedUrl, listToAdd, {}, MOCK_USER_SESSION, "", {}, {}, itemTemplate); + + const addCalls: any[] = fetchMock.calls(/addToDefinition/); + expect(addCalls.length).withContext("a single batched addToDefinition call").toEqual(1); + const updateCalls: any[] = fetchMock.calls(/updateDefinition/); + expect(updateCalls.length).withContext("no updateDefinition call").toEqual(0); + + const body: string = addCalls[0][1].body as string; + const addToDef = JSON.parse(decodeURIComponent(/addToDefinition=([^&]*)/.exec(body)![1])); + expect(addToDef.layers.length).toEqual(2); + expect(addToDef.tables.length).toEqual(1); + }); + + it("should reject when a restore updateDefinition call fails", async () => { + const expectedUrl: string = + "https://services123.arcgis.com/org1234567890/arcgis/rest/services/ROWPermits_publiccomment/FeatureServer"; + const adminAddUrl: string = + "https://services123.arcgis.com/org1234567890/arcgis/rest/admin/services/ROWPermits_publiccomment/FeatureServer/addToDefinition"; + + itemTemplate = templates.getItemTemplate("Feature Service", [], expectedUrl); + + const layer0 = mockItems.getAGOLLayerOrTable(0, "Ski Hills " + "x".repeat(40), "Feature Layer", []); + const listToAdd = [{ item: layer0, type: "layer" }]; + + fetchMock.post(adminAddUrl, '{"success": true}').post(/updateDefinition/, mockItems.get400Failure()); + + await expectAsync( + addFeatureServiceDefinition(expectedUrl, listToAdd, {}, MOCK_USER_SESSION, "", {}, {}, itemTemplate), + ).toBeRejected(); + }); + }); + + describe("_truncateNamesForAddToDefinition", () => { + it("truncates over-long names to <= maxNameLength and reports originals to restore", () => { + const longLayer = "Ski Hills " + "x".repeat(40); + const longTable = "Inspections " + "z".repeat(40); + const chunks = [ + { + layers: [ + { id: 0, name: longLayer }, + { id: 1, name: "Short Layer" }, + ], + tables: [{ id: 2, name: longTable }], + }, + ]; + + const restore = _truncateNamesForAddToDefinition(chunks, 30); + + // Over-long names truncated to 30; short name left alone. + expect(chunks[0].layers[0].name).toEqual(longLayer.substring(0, 30)); + expect(chunks[0].layers[0].name.length).toEqual(30); + expect(chunks[0].layers[1].name).toEqual("Short Layer"); + expect(chunks[0].tables[0].name).toEqual(longTable.substring(0, 30)); + + // Originals captured for restore, split by layer vs table. + expect(restore.layers).toEqual([{ id: 0, name: longLayer }]); + expect(restore.tables).toEqual([{ id: 2, name: longTable }]); + }); + + it("keeps truncated names unique when they share a prefix", () => { + // Two names with the same first 30 characters must not collide after truncation. + const sharedPrefix = "Trailheads ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + const nameA = sharedPrefix + " first"; + const nameB = sharedPrefix + " second"; + const chunks = [ + { + layers: [ + { id: 0, name: nameA }, + { id: 1, name: nameB }, + ], + tables: [], + }, + ]; + + const restore = _truncateNamesForAddToDefinition(chunks, 30); + + const truncatedA = chunks[0].layers[0].name; + const truncatedB = chunks[0].layers[1].name; + expect(truncatedA.length).toBeLessThanOrEqual(30); + expect(truncatedB.length).toBeLessThanOrEqual(30); + expect(truncatedA).not.toEqual(truncatedB); + // Originals are preserved regardless of the uniquifying suffix. + expect(restore.layers).toEqual([ + { id: 0, name: nameA }, + { id: 1, name: nameB }, + ]); + }); + + it("makes no changes when all names are within the limit", () => { + const chunks = [ + { + layers: [{ id: 0, name: "Ski Hills" }], + tables: [{ id: 2, name: "Inspections" }], + }, + ]; + + const restore = _truncateNamesForAddToDefinition(chunks, 30); + + expect(chunks[0].layers[0].name).toEqual("Ski Hills"); + expect(chunks[0].tables[0].name).toEqual("Inspections"); + expect(restore).toEqual({ layers: [], tables: [] }); + }); }); describe("updateLayerFieldReferences", () => { diff --git a/packages/deployer/src/deploySolutionItems.ts b/packages/deployer/src/deploySolutionItems.ts index c993034f6..6d6317f0f 100644 --- a/packages/deployer/src/deploySolutionItems.ts +++ b/packages/deployer/src/deploySolutionItems.ts @@ -194,7 +194,7 @@ export function deploySolutionItems( useExistingItemsDef.then(() => { checkCancelled(); - templates = common.setNamesAndTitles(templates); + templates = common.setNamesAndTitles(templates, templateDictionary); buildOrder.forEach((id: string) => { // Get the item's template out of the list of templates From 31a625d10d1af698210f83c105aac0bc9374ef25 Mon Sep 17 00:00:00 2001 From: John Hauck Date: Thu, 25 Jun 2026 16:24:23 -0600 Subject: [PATCH 2/2] build --- demos/compareJSON/package-lock.json | 2 +- demos/compareSolutions/package-lock.json | 2 +- demos/copyItemInfo/package-lock.json | 2 +- demos/copySolutions/package-lock.json | 2 +- demos/createSolution/package-lock.json | 2 +- demos/deleteSolution/package-lock.json | 2 +- demos/deploySolution/package-lock.json | 2 +- demos/getItemInfo/package-lock.json | 2 +- demos/implementedTypes/package-lock.json | 2 +- demos/recreateSolution/package-lock.json | 2 +- demos/reuseDeployedItems/package-lock.json | 2 +- demos/verifySolution/package-lock.json | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/demos/compareJSON/package-lock.json b/demos/compareJSON/package-lock.json index 1ea0121ae..dd03973d6 100644 --- a/demos/compareJSON/package-lock.json +++ b/demos/compareJSON/package-lock.json @@ -2344,4 +2344,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/demos/compareSolutions/package-lock.json b/demos/compareSolutions/package-lock.json index 263545f9c..ecdb6dba0 100644 --- a/demos/compareSolutions/package-lock.json +++ b/demos/compareSolutions/package-lock.json @@ -2328,4 +2328,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/demos/copyItemInfo/package-lock.json b/demos/copyItemInfo/package-lock.json index 5e1854f06..d4c32d30e 100644 --- a/demos/copyItemInfo/package-lock.json +++ b/demos/copyItemInfo/package-lock.json @@ -2344,4 +2344,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/demos/copySolutions/package-lock.json b/demos/copySolutions/package-lock.json index 227315d4f..7e9baa21f 100644 --- a/demos/copySolutions/package-lock.json +++ b/demos/copySolutions/package-lock.json @@ -2719,4 +2719,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/demos/createSolution/package-lock.json b/demos/createSolution/package-lock.json index 5141cc7c9..8cea523ef 100644 --- a/demos/createSolution/package-lock.json +++ b/demos/createSolution/package-lock.json @@ -2350,4 +2350,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/demos/deleteSolution/package-lock.json b/demos/deleteSolution/package-lock.json index 390cd0fd4..09c8cd831 100644 --- a/demos/deleteSolution/package-lock.json +++ b/demos/deleteSolution/package-lock.json @@ -2351,4 +2351,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/demos/deploySolution/package-lock.json b/demos/deploySolution/package-lock.json index 023830982..45e67d6da 100644 --- a/demos/deploySolution/package-lock.json +++ b/demos/deploySolution/package-lock.json @@ -2357,4 +2357,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/demos/getItemInfo/package-lock.json b/demos/getItemInfo/package-lock.json index afce5f912..66bd239be 100644 --- a/demos/getItemInfo/package-lock.json +++ b/demos/getItemInfo/package-lock.json @@ -2344,4 +2344,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/demos/implementedTypes/package-lock.json b/demos/implementedTypes/package-lock.json index e80cc7e21..cdf9c0d6b 100644 --- a/demos/implementedTypes/package-lock.json +++ b/demos/implementedTypes/package-lock.json @@ -2336,4 +2336,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/demos/recreateSolution/package-lock.json b/demos/recreateSolution/package-lock.json index 4cd5f16a1..5846bdeab 100644 --- a/demos/recreateSolution/package-lock.json +++ b/demos/recreateSolution/package-lock.json @@ -2314,4 +2314,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/demos/reuseDeployedItems/package-lock.json b/demos/reuseDeployedItems/package-lock.json index 266e5aed5..974751a0e 100644 --- a/demos/reuseDeployedItems/package-lock.json +++ b/demos/reuseDeployedItems/package-lock.json @@ -2379,4 +2379,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/demos/verifySolution/package-lock.json b/demos/verifySolution/package-lock.json index ee83f4952..b4d9f350a 100644 --- a/demos/verifySolution/package-lock.json +++ b/demos/verifySolution/package-lock.json @@ -2350,4 +2350,4 @@ "license": "MIT" } } -} \ No newline at end of file +}