From 2ab9feed991bb7ea9e311a68ab9420bde4c3a7b7 Mon Sep 17 00:00:00 2001 From: Erin Millard Date: Sat, 2 Nov 2024 14:18:39 +1000 Subject: [PATCH] Implement the loader --- .gitignore | 4 + .nvmrc | 1 + .prettierignore | 5 + .prettierrc.json | 12 ++ CHANGELOG.md | 15 ++ Makefile | 64 +++++++ README.md | 62 ++++++- eslint.config.js | 50 ++++++ package.json | 83 +++++++++ src/index.ts | 81 +++++++++ test/compiler.ts | 30 ++++ test/create-webpack-config.d.ts | 3 + test/create-webpack-config.js | 61 +++++++ .../invalid/malformed-xml/browserconfig.xml | 1 + test/fixture/invalid/malformed-xml/index.html | 7 + .../unresolved-image/browserconfig.xml | 9 + .../invalid/unresolved-image/index.html | 9 + test/fixture/valid/comprehensive/badge.xml | 11 ++ .../valid/comprehensive/browserconfig.xml | 26 +++ test/fixture/valid/comprehensive/index.html | 9 + .../valid/comprehensive/notification-1.xml | 11 ++ .../valid/comprehensive/notification-2.xml | 11 ++ .../valid/comprehensive/notification-3.xml | 11 ++ .../valid/comprehensive/notification-4.xml | 11 ++ .../valid/comprehensive/notification-5.xml | 11 ++ .../valid/comprehensive/tile-140x140.png | Bin 0 -> 440 bytes .../valid/comprehensive/tile-300x300.png | Bin 0 -> 666 bytes .../valid/comprehensive/tile-620x300.png | Bin 0 -> 757 bytes .../valid/comprehensive/tile-620x620.png | Bin 0 -> 1338 bytes .../empty-msapplication/browserconfig.xml | 4 + .../valid/empty-msapplication/index.html | 7 + .../missing-msapplication/browserconfig.xml | 2 + .../valid/missing-msapplication/index.html | 7 + .../partially-malformed/browserconfig.xml | 26 +++ .../valid/partially-malformed/index.html | 7 + .../partially-malformed/notification-2.xml | 11 ++ .../partially-malformed/notification-4.xml | 11 ++ .../partially-malformed/tile-140x140.png | Bin 0 -> 440 bytes .../partially-malformed/tile-620x620.png | Bin 0 -> 1338 bytes test/suite/output.spec.ts | 164 ++++++++++++++++++ test/webpack.config.js | 7 + tsconfig.build.cjs.json | 10 ++ tsconfig.build.esm.json | 8 + tsconfig.json | 13 ++ vitest.config.ts | 11 ++ 45 files changed, 884 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 .nvmrc create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 CHANGELOG.md create mode 100644 Makefile create mode 100644 eslint.config.js create mode 100644 package.json create mode 100644 src/index.ts create mode 100644 test/compiler.ts create mode 100644 test/create-webpack-config.d.ts create mode 100644 test/create-webpack-config.js create mode 100644 test/fixture/invalid/malformed-xml/browserconfig.xml create mode 100644 test/fixture/invalid/malformed-xml/index.html create mode 100644 test/fixture/invalid/unresolved-image/browserconfig.xml create mode 100644 test/fixture/invalid/unresolved-image/index.html create mode 100644 test/fixture/valid/comprehensive/badge.xml create mode 100644 test/fixture/valid/comprehensive/browserconfig.xml create mode 100644 test/fixture/valid/comprehensive/index.html create mode 100644 test/fixture/valid/comprehensive/notification-1.xml create mode 100644 test/fixture/valid/comprehensive/notification-2.xml create mode 100644 test/fixture/valid/comprehensive/notification-3.xml create mode 100644 test/fixture/valid/comprehensive/notification-4.xml create mode 100644 test/fixture/valid/comprehensive/notification-5.xml create mode 100644 test/fixture/valid/comprehensive/tile-140x140.png create mode 100644 test/fixture/valid/comprehensive/tile-300x300.png create mode 100644 test/fixture/valid/comprehensive/tile-620x300.png create mode 100644 test/fixture/valid/comprehensive/tile-620x620.png create mode 100644 test/fixture/valid/empty-msapplication/browserconfig.xml create mode 100644 test/fixture/valid/empty-msapplication/index.html create mode 100644 test/fixture/valid/missing-msapplication/browserconfig.xml create mode 100644 test/fixture/valid/missing-msapplication/index.html create mode 100644 test/fixture/valid/partially-malformed/browserconfig.xml create mode 100644 test/fixture/valid/partially-malformed/index.html create mode 100644 test/fixture/valid/partially-malformed/notification-2.xml create mode 100644 test/fixture/valid/partially-malformed/notification-4.xml create mode 100644 test/fixture/valid/partially-malformed/tile-140x140.png create mode 100644 test/fixture/valid/partially-malformed/tile-620x620.png create mode 100644 test/suite/output.spec.ts create mode 100644 test/webpack.config.js create mode 100644 tsconfig.build.cjs.json create mode 100644 tsconfig.build.esm.json create mode 100644 tsconfig.json create mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..12a2740 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.makefiles/ +/artifacts/ +/node_modules/ +/package-lock.json diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..1e073e9 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +/.github/ +/.makefiles/ +/artifacts/ +/test/fixture/ +/CHANGELOG.md diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..4de95c7 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,12 @@ +{ + "plugins": ["prettier-plugin-organize-imports"], + "overrides": [ + { + "files": "*.md", + "options": { + "printWidth": 80, + "proseWrap": "always" + } + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bc59c27 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog], and this project adheres to [Semantic +Versioning]. + +[keep a changelog]: https://keepachangelog.com/en/1.0.0/ +[semantic versioning]: https://semver.org/spec/v2.0.0.html + +## Unreleased + +### Added + +- Initial release. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..88a4555 --- /dev/null +++ b/Makefile @@ -0,0 +1,64 @@ +CHANGELOG_TAG_URL_PREFIX := https://github.com/iconduit/browserconfig-loader/releases/tag/ +JS_ARETHETYPESWRONG_REQ += artifacts/dist +JS_PUBLINT_REQ += artifacts/dist +JS_SKYPACK_PACKAGE_CHECK_REQ += artifacts/dist +JS_VITEST_REQ := artifacts/dist + +-include .makefiles/Makefile +-include .makefiles/pkg/js/v1/Makefile +-include .makefiles/pkg/js/v1/with-npm.mk +-include .makefiles/pkg/js/v1/with-arethetypeswrong.mk +-include .makefiles/pkg/js/v1/with-publint.mk +-include .makefiles/pkg/js/v1/with-skypack-package-check.mk +-include .makefiles/pkg/js/v1/with-tsc.mk +-include .makefiles/pkg/changelog/v1/Makefile + +.makefiles/%: + @curl -sfL https://makefiles.dev/v1 | bash /dev/stdin "$@" + +################################################################################ + +.PHONY: precommit +precommit:: run-fixtures + +.PHONY: ci +ci:: run-fixtures + +################################################################################ + +VALID_FIXTURES := $(wildcard test/fixture/valid/*) +INVALID_FIXTURES := $(wildcard test/fixture/invalid/*) + +.PHONY: run-fixtures +run-fixtures: run-fixtures-valid run-fixtures-invalid + +.PHONY: run-fixtures-valid +run-fixtures-valid: $(addprefix run-fixture-valid-,$(notdir $(VALID_FIXTURES))) + +.PHONY: run-fixtures-invalid +run-fixtures-invalid: $(addprefix run-fixture-invalid-,$(notdir $(INVALID_FIXTURES))) + +.PHONY: run-fixture-valid-% +run-fixture-valid-%: artifacts/link-dependencies.touch artifacts/dist + FIXTURE="valid/$*" $(JS_EXEC) webpack --config test/webpack.config.js + +.PHONY: run-fixture-invalid-% +run-fixture-invalid-%: artifacts/link-dependencies.touch artifacts/dist + if FIXTURE="invalid/$*" $(JS_EXEC) webpack --config test/webpack.config.js; then exit 1; else exit 0; fi + +################################################################################ + +artifacts/dist: artifacts/dist/cjs artifacts/dist/esm + @touch "$@" + +artifacts/dist/cjs: tsconfig.build.cjs.json tsconfig.json artifacts/link-dependencies.touch $(JS_SOURCE_FILES) + @rm -rf "$@" + $(JS_EXEC) tsc -p "$<" + echo '{"type":"commonjs"}' > "$@/package.json" + @touch "$@" + +artifacts/dist/esm: tsconfig.build.esm.json tsconfig.json artifacts/link-dependencies.touch $(JS_SOURCE_FILES) + @rm -rf "$@" + $(JS_EXEC) tsc -p "$<" + echo '{"type":"module"}' > "$@/package.json" + @touch "$@" diff --git a/README.md b/README.md index 5cdfa5f..cc8424e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,60 @@ -# browserconfig-loader -A Webpack loader for browserconfig.xml files +# Iconduit browser configuration loader + +_A Webpack loader for `browserconfig.xml` files_ + +[![Current NPM version][badge-npm-version-image]][badge-npm-version-link] +[![Build status][badge-build-image]][badge-build-link] +[![Test coverage][badge-coverage-image]][badge-coverage-link] + +[badge-build-image]: + https://img.shields.io/github/actions/workflow/status/iconduit/browserconfig-loader/ci-library.yml?branch=main&style=for-the-badge +[badge-build-link]: + https://github.com/iconduit/browserconfig-loader/actions/workflows/ci-library.yml +[badge-coverage-image]: + https://img.shields.io/codecov/c/gh/iconduit/browserconfig-loader?style=for-the-badge +[badge-coverage-link]: https://codecov.io/gh/iconduit/browserconfig-loader +[badge-npm-version-image]: + https://img.shields.io/npm/v/%40iconduit%2Fbrowserconfig-loader?label=%40iconduit%2Fbrowserconfig-loader&logo=npm&style=for-the-badge +[badge-npm-version-link]: + https://npmjs.com/package/@iconduit/browserconfig-loader + +This loader processes [browser configuration] files, adding any images or polling +URI assets to the bundle and resolving their URLs in the output browser configuration +file. + +[browser configuration]: https://msdn.microsoft.com/library/dn320426(v%3Dvs.85) + +It supports: + +- Resolving [tile images] +- Resolving [badge polling URI assets] +- Resolving [notification polling URI assets] + +[tile images]: + https://msdn.microsoft.com/library/dn320426(v%3Dvs.85)#specifying-tile-images-and-assets +[badge polling uri assets]: + https://msdn.microsoft.com/library/dn320426(v%3Dvs.85)#defining-badge-polling +[notification polling uri assets]: + https://msdn.microsoft.com/library/dn320426(v%3Dvs.85)#defining-notification-polling + +## Usage + +```js +// webpack.config.js +export default { + // ... + module: { + rules: [ + { + test: /\.(png|jpg|gif|xml)$/i, + type: "asset/resource", + }, + { + test: /\/browserconfig\.xml$/i, + type: "asset/resource", + use: "@iconduit/browserconfig-loader", + }, + ], + }, +}; +``` diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..2700ed7 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,50 @@ +import js from "@eslint/js"; +import import_ from "eslint-plugin-import"; +import node from "eslint-plugin-n"; +import promise from "eslint-plugin-promise"; +import globals from "globals"; +import ts from "typescript-eslint"; + +export default ts.config( + { + ignores: [".makefiles", "artifacts", "test/fixture"], + }, + js.configs.recommended, + // eslint-disable-next-line import/no-named-as-default-member + ...ts.configs.recommended, + node.configs["flat/recommended-module"], + import_.flatConfigs.recommended, + import_.flatConfigs.typescript, + promise.configs["flat/recommended"], + { + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + globals: { + ...globals.es2022, + ...globals.node, + }, + }, + settings: { + "import/resolver": { + typescript: true, + node: true, + }, + }, + rules: { + "@typescript-eslint/no-unused-vars": [ + "error", + { + // allow unused args if they start with _ + argsIgnorePattern: "^_", + }, + ], + // handled by import/no-unresolved + "n/no-missing-import": "off", + // don't check for unsupported features - too much config to make this work + "n/no-unsupported-features/es-builtins": "off", + "n/no-unsupported-features/es-syntax": "off", + "n/no-unsupported-features/node-builtins": "off", + }, + }, +); diff --git a/package.json b/package.json new file mode 100644 index 0000000..1df1713 --- /dev/null +++ b/package.json @@ -0,0 +1,83 @@ +{ + "name": "@iconduit/browserconfig-loader", + "version": "0.0.0", + "description": "A Webpack loader for browserconfig.xml files", + "keywords": [ + "app", + "browser", + "browserconfig", + "config", + "iconduit", + "import", + "loader", + "web", + "webpack" + ], + "repository": "iconduit/browserconfig-loader", + "bugs": "https://github.com/iconduit/browserconfig-loader/issues", + "homepage": "https://github.com/iconduit/browserconfig-loader", + "author": "Erin Millard ", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "type": "module", + "types": "artifacts/dist/esm/index.d.ts", + "main": "artifacts/dist/esm/index.js", + "exports": { + ".": { + "import": { + "types": "./artifacts/dist/esm/index.d.ts", + "default": "./artifacts/dist/esm/index.js" + }, + "require": { + "types": "./artifacts/dist/cjs/index.d.ts", + "default": "./artifacts/dist/cjs/index.js" + }, + "default": { + "types": "./artifacts/dist/esm/index.d.ts", + "default": "./artifacts/dist/esm/index.js" + } + } + }, + "sideEffects": false, + "files": [ + "/artifacts/dist/" + ], + "engines": { + "node": ">=18" + }, + "scripts": { + "prepublishOnly": "make artifacts/dist" + }, + "dependencies": { + "fast-xml-parser": "^4.5.0", + "loader-utils": "^3.3.1" + }, + "devDependencies": { + "@arethetypeswrong/cli": "^0.16.4", + "@eslint/js": "^9.13.0", + "@skypack/package-check": "^0.2.2", + "@types/eslint__js": "^8.42.3", + "@types/loader-utils": "^2.0.6", + "@types/node": "^22.8.5", + "@vitest/coverage-v8": "^2.1.4", + "@vitest/eslint-plugin": "^1.1.7", + "eslint": "^9.13.0", + "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.6.3", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-n": "^17.12.0", + "eslint-plugin-promise": "^7.1.0", + "globals": "^15.11.0", + "html-bundler-webpack-plugin": "^4.1.4", + "prettier": "^3.3.3", + "prettier-plugin-organize-imports": "^4.1.0", + "publint": "^0.2.12", + "typescript": "^5.6.3", + "typescript-eslint": "^8.12.2", + "vitest": "^2.1.4", + "webpack": "^5 <5.96.0", + "webpack-cli": "^5.1.4" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..4da983c --- /dev/null +++ b/src/index.ts @@ -0,0 +1,81 @@ +import { XMLBuilder, XMLParser } from "fast-xml-parser"; +import { resolve } from "path"; +import { callbackify } from "util"; +import type { LoaderDefinitionFunction } from "webpack"; + +const browserConfigLoader: LoaderDefinitionFunction = function (source) { + const XMLOptions = { + attributeNamePrefix: "@_", + ignoreAttributes: false, + suppressEmptyNode: true, + }; + const config = new XMLParser(XMLOptions).parse(source) as object; + + if ( + !hasProperty(config, "browserconfig") || + !hasProperty(config.browserconfig, "msapplication") + ) { + return source; + } + + const { msapplication } = config.browserconfig; + const tasks: Promise[] = []; + + const loadSrc = async (obj: unknown): Promise => { + if (!hasProperty(obj, "@_src") || typeof obj["@_src"] !== "string") return; + + obj["@_src"] = await this.importModule(resolve(this.context, obj["@_src"])); + }; + + if (hasProperty(msapplication, "tile")) { + const { tile } = msapplication; + + for (const tag of [ + "TileImage", + "square70x70logo", + "square150x150logo", + "square310x310logo", + "wide310x150logo", + ]) { + if (hasProperty(tile, tag)) tasks.push(loadSrc(tile[tag])); + } + } + + if (hasProperty(msapplication, "badge")) { + const { badge } = msapplication; + + if (hasProperty(badge, "polling-uri")) { + tasks.push(loadSrc(badge["polling-uri"])); + } + } + + if (hasProperty(msapplication, "notification")) { + const { notification } = msapplication; + + for (const tag of [ + "polling-uri", + "polling-uri2", + "polling-uri3", + "polling-uri4", + "polling-uri5", + ]) { + if (hasProperty(notification, tag)) + tasks.push(loadSrc(notification[tag])); + } + } + + callbackify(async () => { + await Promise.all(tasks); + + return new XMLBuilder(XMLOptions).build(config); + })(this.async()); +}; + +export default browserConfigLoader; + +function hasProperty( + obj: unknown, + tag: T, +): obj is { [K in T]: unknown } { + return typeof obj === "object" && obj != null && tag in obj; +} diff --git a/test/compiler.ts b/test/compiler.ts new file mode 100644 index 0000000..e1bfa80 --- /dev/null +++ b/test/compiler.ts @@ -0,0 +1,30 @@ +import webpack, { type Stats } from "webpack"; +import { createWebpackConfig } from "./create-webpack-config.js"; + +export default (fixture: string): Promise => { + const compiler = webpack(createWebpackConfig(fixture)); + + return new Promise((resolve, reject) => { + compiler.run((err, stats) => { + if (err) { + reject(err); + + return; + } + + if (!stats) { + reject(new Error("Missing stats")); + + return; + } + + if (stats.hasErrors()) { + reject(stats.toJson().errors); + + return; + } + + resolve(stats); + }); + }); +}; diff --git a/test/create-webpack-config.d.ts b/test/create-webpack-config.d.ts new file mode 100644 index 0000000..4ecf38d --- /dev/null +++ b/test/create-webpack-config.d.ts @@ -0,0 +1,3 @@ +import { Configuration } from "webpack"; + +declare function createWebpackConfig(fixture: string): Configuration; diff --git a/test/create-webpack-config.js b/test/create-webpack-config.js new file mode 100644 index 0000000..819597e --- /dev/null +++ b/test/create-webpack-config.js @@ -0,0 +1,61 @@ +import HtmlBundlerPlugin from "html-bundler-webpack-plugin"; +import { resolve } from "path"; + +/** + * @param {string} fixture + * @returns {import('webpack').Configuration} + */ +export function createWebpackConfig(fixture) { + return { + mode: "production", + devtool: "source-map", + context: resolve(import.meta.dirname, "fixture", fixture), + entry: "./index.html", + output: { + path: resolve(import.meta.dirname, "../artifacts/test/output", fixture), + filename: "bundle.js", + publicPath: "/path/to/public/", + assetModuleFilename: "[name].public[ext][query]", + }, + plugins: [ + new HtmlBundlerPlugin({ + entry: { + index: "index.html", + }, + loaderOptions: { + sources: [ + { + tag: "meta", + attributes: ["content"], + filter: ({ attributes: { name } }) => + name === "msapplication-config" || + name === "msapplication-TileImage", + }, + ], + }, + }), + ], + module: { + rules: [ + { + test: /\.(png|xml)$/, + type: "asset/resource", + }, + { + test: /\/browserconfig\.xml$/i, + type: "asset/resource", + use: resolve(import.meta.dirname, "../artifacts/dist/esm/index.js"), + }, + { + test: /\/notification-[135]\.xml$/i, + type: "asset/resource", + generator: { + emit: false, + filename: "[name][ext]", + publicPath: "https://cdn.example.com/", + }, + }, + ], + }, + }; +} diff --git a/test/fixture/invalid/malformed-xml/browserconfig.xml b/test/fixture/invalid/malformed-xml/browserconfig.xml new file mode 100644 index 0000000..9318418 --- /dev/null +++ b/test/fixture/invalid/malformed-xml/browserconfig.xml @@ -0,0 +1 @@ +< diff --git a/test/fixture/invalid/malformed-xml/index.html b/test/fixture/invalid/malformed-xml/index.html new file mode 100644 index 0000000..7e64998 --- /dev/null +++ b/test/fixture/invalid/malformed-xml/index.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/fixture/invalid/unresolved-image/browserconfig.xml b/test/fixture/invalid/unresolved-image/browserconfig.xml new file mode 100644 index 0000000..9f04114 --- /dev/null +++ b/test/fixture/invalid/unresolved-image/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + #D5415C + + + + diff --git a/test/fixture/invalid/unresolved-image/index.html b/test/fixture/invalid/unresolved-image/index.html new file mode 100644 index 0000000..9ac072c --- /dev/null +++ b/test/fixture/invalid/unresolved-image/index.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/test/fixture/valid/comprehensive/badge.xml b/test/fixture/valid/comprehensive/badge.xml new file mode 100644 index 0000000..3ce2ab8 --- /dev/null +++ b/test/fixture/valid/comprehensive/badge.xml @@ -0,0 +1,11 @@ + + + + + Badge image + + + diff --git a/test/fixture/valid/comprehensive/browserconfig.xml b/test/fixture/valid/comprehensive/browserconfig.xml new file mode 100644 index 0000000..0228375 --- /dev/null +++ b/test/fixture/valid/comprehensive/browserconfig.xml @@ -0,0 +1,26 @@ + + + + + #D5415C + + + + + + + + + 30 + + + + + + + + 30 + 1 + + + diff --git a/test/fixture/valid/comprehensive/index.html b/test/fixture/valid/comprehensive/index.html new file mode 100644 index 0000000..9ac072c --- /dev/null +++ b/test/fixture/valid/comprehensive/index.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/test/fixture/valid/comprehensive/notification-1.xml b/test/fixture/valid/comprehensive/notification-1.xml new file mode 100644 index 0000000..7e045ec --- /dev/null +++ b/test/fixture/valid/comprehensive/notification-1.xml @@ -0,0 +1,11 @@ + + + + + Notification image 1 + + + diff --git a/test/fixture/valid/comprehensive/notification-2.xml b/test/fixture/valid/comprehensive/notification-2.xml new file mode 100644 index 0000000..67cf36b --- /dev/null +++ b/test/fixture/valid/comprehensive/notification-2.xml @@ -0,0 +1,11 @@ + + + + + Notification image 2 + + + diff --git a/test/fixture/valid/comprehensive/notification-3.xml b/test/fixture/valid/comprehensive/notification-3.xml new file mode 100644 index 0000000..fe8c069 --- /dev/null +++ b/test/fixture/valid/comprehensive/notification-3.xml @@ -0,0 +1,11 @@ + + + + + Notification image 3 + + + diff --git a/test/fixture/valid/comprehensive/notification-4.xml b/test/fixture/valid/comprehensive/notification-4.xml new file mode 100644 index 0000000..ac77414 --- /dev/null +++ b/test/fixture/valid/comprehensive/notification-4.xml @@ -0,0 +1,11 @@ + + + + + Notification image 4 + + + diff --git a/test/fixture/valid/comprehensive/notification-5.xml b/test/fixture/valid/comprehensive/notification-5.xml new file mode 100644 index 0000000..2d38c71 --- /dev/null +++ b/test/fixture/valid/comprehensive/notification-5.xml @@ -0,0 +1,11 @@ + + + + + Notification image 5 + + + diff --git a/test/fixture/valid/comprehensive/tile-140x140.png b/test/fixture/valid/comprehensive/tile-140x140.png new file mode 100644 index 0000000000000000000000000000000000000000..7e08c54b7d1f5fe030d767f71739f6cf09579c80 GIT binary patch literal 440 zcmeAS@N?(olHy`uVBq!ia0vp^Js`}&3?yUT#qa_tmUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIID%0X`wF?gc*ok$~_6Ln)BHk|4iehKBtP?&^Jif!fSHT^vI)?!BEJo7e2X z)AGHYIp8XL1}D466&ZyC_x}6za=dA}x_skOuZuPBd&=HyEaz+#aNPTioaGNKKkTU{@2poUS)20CZduaLM}9vi-~RAu_x5w& z+EUGSt9>?TUAF1ZexEF(X#&p*!)N-w&c8l$-80*br&vw}v>E9c{cgD}AEmu)>b-0C z9|g{OeyeIeciPvIos!Y>w#Rj!4ZYJhF~__or}Ly)_}0rwoJDJP8tRvIOjNyQv!N}!D(ALFT8YVdhwLxTbJO>ISUwZ%`8}HZ ZzpR@Uuk!m;6fj5`JYD@<);T3K0RXlr#GL>D literal 0 HcmV?d00001 diff --git a/test/fixture/valid/comprehensive/tile-300x300.png b/test/fixture/valid/comprehensive/tile-300x300.png new file mode 100644 index 0000000000000000000000000000000000000000..d516b3c8852d0f55f1cf12e905cd98d1cf663721 GIT binary patch literal 666 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 literal 0 HcmV?d00001 diff --git a/test/fixture/valid/empty-msapplication/browserconfig.xml b/test/fixture/valid/empty-msapplication/browserconfig.xml new file mode 100644 index 0000000..190c957 --- /dev/null +++ b/test/fixture/valid/empty-msapplication/browserconfig.xml @@ -0,0 +1,4 @@ + + + + diff --git a/test/fixture/valid/empty-msapplication/index.html b/test/fixture/valid/empty-msapplication/index.html new file mode 100644 index 0000000..7e64998 --- /dev/null +++ b/test/fixture/valid/empty-msapplication/index.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/fixture/valid/missing-msapplication/browserconfig.xml b/test/fixture/valid/missing-msapplication/browserconfig.xml new file mode 100644 index 0000000..11c6a7c --- /dev/null +++ b/test/fixture/valid/missing-msapplication/browserconfig.xml @@ -0,0 +1,2 @@ + + diff --git a/test/fixture/valid/missing-msapplication/index.html b/test/fixture/valid/missing-msapplication/index.html new file mode 100644 index 0000000..7e64998 --- /dev/null +++ b/test/fixture/valid/missing-msapplication/index.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/fixture/valid/partially-malformed/browserconfig.xml b/test/fixture/valid/partially-malformed/browserconfig.xml new file mode 100644 index 0000000..af150ba --- /dev/null +++ b/test/fixture/valid/partially-malformed/browserconfig.xml @@ -0,0 +1,26 @@ + + + + + #D5415C + + + + + + + + + 30 + + + + + + + + 30 + 1 + + + diff --git a/test/fixture/valid/partially-malformed/index.html b/test/fixture/valid/partially-malformed/index.html new file mode 100644 index 0000000..7e64998 --- /dev/null +++ b/test/fixture/valid/partially-malformed/index.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/fixture/valid/partially-malformed/notification-2.xml b/test/fixture/valid/partially-malformed/notification-2.xml new file mode 100644 index 0000000..67cf36b --- /dev/null +++ b/test/fixture/valid/partially-malformed/notification-2.xml @@ -0,0 +1,11 @@ + + + + + Notification image 2 + + + diff --git a/test/fixture/valid/partially-malformed/notification-4.xml b/test/fixture/valid/partially-malformed/notification-4.xml new file mode 100644 index 0000000..ac77414 --- /dev/null +++ b/test/fixture/valid/partially-malformed/notification-4.xml @@ -0,0 +1,11 @@ + + + + + Notification image 4 + + + diff --git a/test/fixture/valid/partially-malformed/tile-140x140.png b/test/fixture/valid/partially-malformed/tile-140x140.png new file mode 100644 index 0000000000000000000000000000000000000000..7e08c54b7d1f5fe030d767f71739f6cf09579c80 GIT binary patch literal 440 zcmeAS@N?(olHy`uVBq!ia0vp^Js`}&3?yUT#qa_tmUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIID%0X`wF?gc*ok$~_6Ln)BHk|4iehKBtP?&^Jif!fSHT^vI)?!BEJo7e2X z)AGHYIp8XL1}D466&ZyC_x}6za=dA}x_skOuZuPBd&=HyEaz+#aNPTioaGNKKkTU{@2poUS)20CZduaLM}9vi-~RAu_x5w& z+EUGSt9>?TUAF1ZexEF(X#&p*!)N-w&c8l$-80*br&vw}v>E9c{cgD}AEmu)>b-0C z9|g{OeyeIeciPvIos!Y>w#Rj!4ZYJhF~__or}Ly)_}0rwoJDJP8tRvIOjNyQv!N}!D(ALFT8YVdhwLxTbJO>ISUwZ%`8}HZ ZzpR@Uuk!m;6fj5`JYD@<);T3K0RXlr#GL>D literal 0 HcmV?d00001 diff --git a/test/fixture/valid/partially-malformed/tile-620x620.png b/test/fixture/valid/partially-malformed/tile-620x620.png new file mode 100644 index 0000000000000000000000000000000000000000..5a6b1cd810fa56f9d426f4561fcd7ec5173d7af3 GIT binary patch literal 1338 zcmeAS@N?(olHy`uVBq!ia0y~yV9Ehu7G|KxrrnRkfD}u*qpu?a!^VE@KZ&eBK4Wo^ zyA#8@b22Z19R2{G5LfpCpZ`ce_<^AmNMA{iUob<1!~UO)bC`hKot`d^Ar-gY-pvh? z@)luBNJwK`u$jS7hhZw~f;0cFPl@pKaXRn*y>UV4UmN@UpU?N1=6|>Bn0yqdmpt%5 zc+EcByPgZLZ2JE&l&|&tt%tVT{zVG8?)tZUtA>}{w)mA_^EYW+_|x}RrQ5-M>y{Jl zS$_j1p1RLIytX1k_%`dtox)7wD{nMyyt&>fyy?cBCC|*Fo9145_$*`hf~8hb-*)^g zG2a(2d8^1~-yF00dE3vPUhH9$@c+9-F=tDf>Eu~654ZfDs?Vakwe6nE?~e=Hb-3Qx zB~=T!shCA-tkiy(eLi>FB_;O#B|D$^ZHRHb#v;r6_S3R$9(mP1CE1 ze>N?>kyko#m)Fs<)04W+-YnbA_(Ws5ac`k{QtHl4r|MTud-bB>a7@-dE4g1hM-H21 z7|)%x-MGg#|NCNBTieR>4iDmPoOa7e5}tkIh??2PPG2lcs6+=uP5`1(5w7@O^SSxe z?mu4d>sV_4zB^;@l!vR_XHPq3^-A87v-Apw(5n^KjaZKH>-Ng!MoGO|A*gaKaQ!Qw z%p!xFZ@hU?QWm##xHk4lId+A!=yUUw1H~0y=Qc)F>Ak)?MXVJVS0?GM6}Rq1i*z0D z|33FYoHw`iFPk6pb-Dgq@x;&a?sf`vk9U8x<(&UIj@J|a-dki)x@GP$IcMFuuU_1K zu`oUG#EXnuLCPL(?w8pwE;87sB%|&otSf7K>z|zQ7L)CZ*9Xsx5-GBpwdD%Oq{|n# zEEQ{g`0??ZO*vakUT0YQl}C6NTg}=Al)DI&bDOZR@7?pfsP01-eoQP2@P6LEtufux zTUpju&O_Pb*2RVMZ%R6zvIeSOG-10{=F2k|r-{Dvbl;q|=@^I6PNkkXQQG^zbC;O> z+<8j8HT2!RB};?dZ(Et)-`kf~Tff%0$>rI`W8y5Qtye7-K5H%dC+DoLt%%FT2oaEx zTke(a)18%Q-R-2pq03uhlAF1<+bMWQdYRjV6Ny(Np0kIw+*}>#&EB`|`lI8;>la5d zyCw%}ywA9FX>#8)*$;DF7xK!4L{$>*_lkVTJ-`d-Dop_xXzW0XE?BkNRn{WJDy+U`}-O{9L;b@oK$9dY@I-Xf=yt-|& z-g@ENyEfejyWY8W+xgp9DFjvIGfuy85}Sb4q9e0Iv>O)&Kwi literal 0 HcmV?d00001 diff --git a/test/suite/output.spec.ts b/test/suite/output.spec.ts new file mode 100644 index 0000000..e5c7792 --- /dev/null +++ b/test/suite/output.spec.ts @@ -0,0 +1,164 @@ +import { XMLBuilder, XMLParser } from "fast-xml-parser"; +import { rm } from "fs/promises"; +import { resolve } from "path"; +import { beforeAll, expect, it } from "vitest"; +import type { Stats } from "webpack"; +import compiler from "../compiler.js"; + +const outputPath = resolve(import.meta.dirname, "../../artifacts/test/output"); + +const XMLOptions = { + attributeNamePrefix: "@_", + format: true, + ignoreAttributes: false, + suppressEmptyNode: true, +}; +const parser = new XMLParser(XMLOptions); +const builder = new XMLBuilder(XMLOptions); + +beforeAll(async () => { + try { + await rm(outputPath, { recursive: true }); + } catch (error) { + if (error instanceof Error && "code" in error && error.code === "ENOENT") { + return; + } + + throw error; + } +}); + +it("errors for browser configs with malformed XML", async () => { + let error: unknown; + + try { + await compiler("invalid/malformed-xml"); + } catch (err) { + error = err; + } + + expect(getErrorMessage(error)).toMatch("XMLParser.parse"); +}); + +it("errors for browser configs with unresolved images", async () => { + let error: unknown; + + try { + await compiler("invalid/unresolved-image"); + } catch (err) { + error = err; + } + + expect(getErrorMessage(error)).toMatch("Can't resolve"); +}); + +it("outputs comprehensive browser configs", async () => { + const stats = await compiler("valid/comprehensive"); + + expect(parseBrowserConfigXML(stats)).toMatchInlineSnapshot(` + " + + + + #D5415C + + + + + + + + + 30 + + + + + + + + 30 + 1 + + + + " + `); +}); + +it("outputs partially malformed browser configs", async () => { + const stats = await compiler("valid/partially-malformed"); + + expect(parseBrowserConfigXML(stats)).toMatchInlineSnapshot(` + " + + + + #D5415C + + + + + + + + + 30 + + + + + + + + 30 + 1 + + + + " + `); +}); + +it("outputs browser configs with an empty msapplication tag", async () => { + const stats = await compiler("valid/empty-msapplication"); + + expect(parseBrowserConfigXML(stats)).toMatchInlineSnapshot(` + " + + + + " + `); +}); + +it("outputs browser configs with no msapplication tag", async () => { + const stats = await compiler("valid/missing-msapplication"); + + expect(parseBrowserConfigXML(stats)).toMatchInlineSnapshot(` + " + + " + `); +}); + +function parseBrowserConfigXML(stats: Stats): string { + const { modules = [] } = stats.toJson({ source: true }); + + for (const { name, source } of modules) { + if (name === "./browserconfig.xml") { + return builder.build(parser.parse(String(source))); + } + } + + throw new Error("No browser config found in compilation"); +} + +function getErrorMessage(err: unknown): string { + return Array.isArray(err) && + typeof err[0] === "object" && + err[0] != null && + "message" in err[0] + ? err[0].message + : ""; +} diff --git a/test/webpack.config.js b/test/webpack.config.js new file mode 100644 index 0000000..2206546 --- /dev/null +++ b/test/webpack.config.js @@ -0,0 +1,7 @@ +import { createWebpackConfig } from "./create-webpack-config.js"; + +const fixture = process.env.FIXTURE; + +if (!fixture) throw new Error("FIXTURE is required"); + +export default createWebpackConfig(fixture); diff --git a/tsconfig.build.cjs.json b/tsconfig.build.cjs.json new file mode 100644 index 0000000..81cf4ab --- /dev/null +++ b/tsconfig.build.cjs.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declarationDir": "artifacts/dist/cjs", + "module": "CommonJS", + "moduleResolution": "node", + "outDir": "artifacts/dist/cjs" + }, + "include": ["src/**/*"] +} diff --git a/tsconfig.build.esm.json b/tsconfig.build.esm.json new file mode 100644 index 0000000..9a334f8 --- /dev/null +++ b/tsconfig.build.esm.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declarationDir": "artifacts/dist/esm", + "outDir": "artifacts/dist/esm" + }, + "include": ["src/**/*"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2f65cb2 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "declaration": true, + "esModuleInterop": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmitOnError": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "ESNext" + } +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..522683f --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + watch: false, + include: ["test/suite/**/*.spec.ts"], + coverage: { + include: ["artifacts/dist/esm/**/*.js"], + }, + }, +});