diff --git a/README.md b/README.md index 03485d0..1a9cfa7 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ Here are the properties available in the config file: own way of writing the import map. The function must return a Promise that resolves with the import map as an object. Since javascript functions are not part of JSON, this option is only available if you provide a config.js file (instead of config.json). - `cacheControl` (optional): Cache-control header that will be set on the import map file when the import-map-deployer is called. Defaults to `public, must-revalidate, max-age=0`. +- `alphabetical` (optional, defaults to false): A boolean that indicates whether to sort the import-map alphabetically by service/key/name. ### Option 1: json file diff --git a/src/modify.js b/src/modify.js index 41cfb74..a79f9e5 100644 --- a/src/modify.js +++ b/src/modify.js @@ -83,7 +83,17 @@ function modifyLock(env, modifierFunc) { } exports.modifyImportMap = function (env, newValues) { - const { services: newImports, scopes: newScopes } = newValues; + const { services, scopes } = newValues; + + const alphabetical = !!getConfig().alphabetical; + const newImports = + services && typeof services === "object" && alphabetical + ? sortObjectAlphabeticallyByKeys(services) + : services; + const newScopes = + scopes && typeof scopes === "object" && alphabetical + ? sortObjectAlphabeticallyByKeys(scopes) + : scopes; // either imports or scopes have to be defined if (newImports || newScopes) { @@ -135,9 +145,25 @@ exports.modifyService = function ( map[serviceName + "/"] = address; } } - - return json; + const alphabetical = !!getConfig().alphabetical; + if (alphabetical) { + return { + imports: sortObjectAlphabeticallyByKeys(json.imports), + scopes: sortObjectAlphabeticallyByKeys(json.scopes), + }; + } else { + return json; + } }); }; exports.getEmptyManifest = getEmptyManifest; + +function sortObjectAlphabeticallyByKeys(unordered) { + return Object.keys(unordered) + .sort() + .reduce((obj, key) => { + obj[key] = unordered[key]; + return obj; + }, {}); +} diff --git a/test/alphabetical-import-map.test.js b/test/alphabetical-import-map.test.js new file mode 100644 index 0000000..2300d85 --- /dev/null +++ b/test/alphabetical-import-map.test.js @@ -0,0 +1,116 @@ +const request = require("supertest"); +const { app, setConfig } = require("../src/web-server"); +const { + resetManifest: resetMemoryManifest, +} = require("../src/io-methods/memory"); + +describe(`alphabetically sorted`, () => { + beforeAll(() => { + setConfig({ + manifestFormat: "importmap", + alphabetical: true, + packagesViaTrailingSlashes: true, + locations: { + prod: "memory://prod", + }, + }); + }); + + beforeEach(() => { + // assure we have a clean import map every test + resetMemoryManifest(); + const setupRequest = request(app) + .patch("/import-map.json") + .query({ + skip_url_check: true, + }) + .set("accept", "json") + .send({ + imports: { + c: "/c-1.mjs", + b: "/b-1.mjs", + }, + }) + .expect(200) + .expect("Content-Type", /json/); + return setupRequest.then((response) => { + expect(JSON.stringify(response.body)).toBe( + `{"imports":{"b":"/b-1.mjs","c":"/c-1.mjs"},"scopes":{}}` + ); + }); + }); + + it(`should place the import into the map alphabetically instead of just at the end`, async () => { + const response = await request(app) + .patch("/services") + .query({ + skip_url_check: true, + }) + .set("accept", "json") + .send({ + service: "a", + url: "/a-1-updated.mjs", + }) + .expect(200) + .expect("Content-Type", /json/); + + expect(JSON.stringify(response.body.imports)).toBe( + `{"a":"/a-1-updated.mjs","b":"/b-1.mjs","c":"/c-1.mjs"}` + ); + }); +}); + +describe(`not alphabetically sorted`, () => { + beforeAll(() => { + setConfig({ + manifestFormat: "importmap", + packagesViaTrailingSlashes: true, + locations: { + prod: "memory://prod", + }, + }); + }); + + beforeEach(() => { + // assure we have a clean import map every test + resetMemoryManifest(); + const setupRequest = request(app) + .patch("/import-map.json") + .query({ + skip_url_check: true, + }) + .set("accept", "json") + .send({ + imports: { + c: "/c-1.mjs", + b: "/b-1.mjs", + }, + }) + .expect(200) + .expect("Content-Type", /json/); + return setupRequest.then((response) => { + expect(JSON.stringify(response.body)).toBe( + `{"imports":{"c":"/c-1.mjs","b":"/b-1.mjs"},"scopes":{}}` + ); + }); + }); + + it(`should not place things alphabetically and should just append to the end`, async () => { + const response = await request(app) + .patch("/services") + .query({ + skip_url_check: true, + }) + .set("accept", "json") + .send({ + service: "a", + url: "/a-1-updated.mjs", + }) + .expect(200) + .expect("Content-Type", /json/); + + expect(JSON.stringify(response.body.imports)).toBe( + `{"c":"/c-1.mjs","b":"/b-1.mjs","a":"/a-1-updated.mjs"}` + ); + }); +}); diff --git a/test/import-map.test.js b/test/import-map.test.js index 75f42fc..36faef1 100644 --- a/test/import-map.test.js +++ b/test/import-map.test.js @@ -4,22 +4,24 @@ const { resetManifest: resetMemoryManifest, } = require("../src/io-methods/memory"); -beforeAll(() => { - setConfig({ - manifestFormat: "importmap", - packagesViaTrailingSlashes: true, - locations: { - prod: "memory://prod", - }, +describe(`/import-map.json`, () => { + let errorSpy; + beforeAll(() => { + setConfig({ + manifestFormat: "importmap", + packagesViaTrailingSlashes: true, + locations: { + prod: "memory://prod", + }, + }); + }); + beforeEach(() => { + // assure we have a clean import map every test + resetMemoryManifest(); + errorSpy = jest.spyOn(console, "error").mockImplementation(() => {}); + errorSpy.mockClear(); }); -}); - -beforeEach(() => { - // assure we have a clean import map every test - resetMemoryManifest(); -}); -describe(`/import-map.json`, () => { it(`does not return anything when it's not setup yet.`, async () => { const response = await request(app) .get("/import-map.json")