From ecee0b6236b0cd8bfda063a8dd288a49dfc82ece Mon Sep 17 00:00:00 2001 From: harytfw Date: Sat, 7 Jan 2023 22:08:29 +0800 Subject: [PATCH 01/21] chore: refactor build process Signed-off-by: harytfw --- Makefile | 55 +++++++------------ scripts/gen_manifest.js | 11 +--- rollup.config.js => scripts/rollup.config.js | 40 +++++++++----- .../test_bootstrap.js | 5 +- scripts/utils.js | 17 ++++++ 5 files changed, 67 insertions(+), 61 deletions(-) rename rollup.config.js => scripts/rollup.config.js (63%) rename test/bootstrap.mjs => scripts/test_bootstrap.js (90%) create mode 100644 scripts/utils.js diff --git a/Makefile b/Makefile index 77069be..bb97034 100644 --- a/Makefile +++ b/Makefile @@ -1,46 +1,41 @@ export BUILD_VERSION ?= 2.1.4 -export SRC ?= $(shell realpath ./src) export BUILD_DIR ?= $(shell realpath ./build) -export BROWSER_LOCATION ?= $(shell which firefox-developer-edition) -export NODE_MODULES ?= $(shell realpath ./node_modules) +export SRC ?= $(shell realpath ./src) export BUILD_PROFILE ?= debug -export BUILD_COMMIT_ID ?= $(shell git rev-parse --short HEAD) -export BUILD_DATE ?= $(shell date --rfc-3339=seconds) -export BUILD_NODE_VERSION ?= $(shell node --version) -export BUILD_ROLLUP_VERSION ?= $(shell npx rollup --version) -export BUILD_OS ?= $(shell node -e "console.log(require('os').platform())") export BUILD_WEBSOCKET_SERVER = - -export TARGET_BROWSER = firefox -export TARGET_DIST = $(BUILD_DIR)/$(TARGET_BROWSER)/dist - export ENTRY_POINTS = background content_scripts options components +export TARGET_BROWSER = firefox +export TARGET_DIST = $(BUILD_DIR)/firefox/dist .PHONY: ext-firefox ext-firefox: TARGET_BROWSER = firefox -ext-firefox: setup-dist assets compile lint package +ext-firefox: TARGET_DIST = $(BUILD_DIR)/firefox/dist +ext-firefox: setup-dist assets compile lint + cd $(TARGET_DIST) && zip -r $(BUILD_DIR)/artifacts/glitterdrag-pro-$(BUILD_VERSION)-firefox.zip . .PHONY: ext-chromium -ext-chromium: TARGET_BROWSER = chromium -ext-chromium: setup-dist assets compile package +ext-test: TARGET_BROWSER = chromium +ext-chromium: TARGET_DIST = $(BUILD_DIR)/chromium/dist +ext-chromium: setup-dist assets compile + cd $(TARGET_DIST) && zip -r $(BUILD_DIR)/artifacts/glitterdrag-pro-$(BUILD_VERSION)-chromium.zip . .PHONY: test test: TARGET_BROWSER = firefox-test test: ENTRY_POINTS += test +test: TARGET_DIST = $(BUILD_DIR)/test/dist test: BUILD_WEBSOCKET_SERVER = ws://localhost:8000 test: setup-dist assets mocha-assets compile - @node test/bootstrap.mjs + @node scripts/test_bootstrap.js .PHONY: open-mocha -open-mocha: TARGET_BROWSER = firefox-test +open-mocha: TARGET_DIST = $(BUILD_DIR)/firefox/dist open-mocha: ENTRY_POINTS += test open-mocha: setup-dist assets mocha-assets compile web-ext run -v \ -s $(TARGET_DIST) \ - -f "$(BROWSER_LOCATION)" + -f "$(shell which firefox-developer-edition)" .PHONY: setup-dist -setup-dist: TARGET_DIST = $(BUILD_DIR)/$(TARGET_BROWSER)/dist setup-dist: $(shell [ ! -d $(TARGET_DIST) ] && mkdir -p $(TARGET_DIST)/) @@ -50,7 +45,7 @@ lint: .PHONY: compile compile: - @npx rollup -c; + @pnpm exec rollup -c "./scripts/rollup.config.js" .PHONY: manifest manifest: @@ -63,29 +58,19 @@ assets: manifest cp -f $(SRC)/options/options.html $(TARGET_DIST)/options/options.html @mkdir -p $(TARGET_DIST)/res && \ - cp -r -f $(NODE_MODULES)/simpledotcss/simple.min.css $(TARGET_DIST)/res/simple.min.css + cp -r -f node_modules/simpledotcss/simple.min.css $(TARGET_DIST)/res/simple.min.css @cp -r -f $(SRC)/icon/ $(SRC)/_locales/ $(TARGET_DIST)/ .PHONY: mocha-assets mocha-assets: @mkdir -p $(TARGET_DIST)/test && \ - cp $(NODE_MODULES)/mocha/mocha.js \ - $(NODE_MODULES)/mocha/mocha.css \ - $(NODE_MODULES)/chai/chai.js \ + cp node_modules/mocha/mocha.js \ + node_modules/mocha/mocha.css \ + node_modules/chai/chai.js \ $(SRC)/test/mocha.html \ $(TARGET_DIST)/test -.PHONY: package -package: - @mkdir -p $(BUILD_DIR)/artifacts - @cd $(TARGET_DIST) && zip -r $(BUILD_DIR)/artifacts/glitterdrag-pro-$(BUILD_VERSION)-$(TARGET_BROWSER).zip . - .PHONY: clean clean: - @rm -rf $(BUILD_DIR) - - -start-server: port ?= 8000 -start-server: - @python3 -m http.server $(port) + @rm -rf $(BUILD_DIR) \ No newline at end of file diff --git a/scripts/gen_manifest.js b/scripts/gen_manifest.js index 6c9600d..4b74b7b 100644 --- a/scripts/gen_manifest.js +++ b/scripts/gen_manifest.js @@ -1,13 +1,4 @@ - -function mustEnv(key) { - const value = process.env[key] - if (!value || value.length === 0) { - console.error("require environment: " + key) - process.exit(1) - } - return value -} - +import { mustEnv } from "./utils.js" const version = mustEnv("BUILD_VERSION") const targetBrowser = mustEnv("TARGET_BROWSER") diff --git a/rollup.config.js b/scripts/rollup.config.js similarity index 63% rename from rollup.config.js rename to scripts/rollup.config.js index 36787da..3da5734 100644 --- a/rollup.config.js +++ b/scripts/rollup.config.js @@ -1,3 +1,5 @@ +import { mustEnv, readStdout } from "./utils.js" + import svelte from 'rollup-plugin-svelte'; import resolve from '@rollup/plugin-node-resolve'; import typescript from '@rollup/plugin-typescript'; @@ -5,23 +7,32 @@ import replace from '@rollup/plugin-replace'; import { terser } from 'rollup-plugin-terser'; import autoPreprocess from 'svelte-preprocess'; import commonjs from '@rollup/plugin-commonjs'; -import pathLib from 'path' +import pathLib from 'node:path' +import os from "node:os" -function mustEnv(name) { - const value = process.env[name] - if (!value) { - console.error("require env: " + name) - process.exit(1) - } - return value -} +const BUILD_COMMIT_ID = readStdout("git rev-parse --short HEAD") +const BUILD_DATE = readStdout("date --rfc-3339=seconds") +const BUILD_NODE_VERSION = readStdout("node --version") +const BUILD_ROLLUP_VERSION = readStdout("npx rollup --version") +const BUILD_OS = os.platform() +const BUILD_PROFILE = mustEnv("BUILD_PROFILE") +const BUILD_WEBSOCKET_SERVER = mustEnv("BUILD_WEBSOCKET_SERVER") -const isProd = mustEnv("BUILD_PROFILE").toLowerCase() === 'prod'; +const isProd = BUILD_PROFILE.toLowerCase() === 'prod'; +const sourceMap = !isProd const src = mustEnv("SRC") const dist = mustEnv("TARGET_DIST") const entryPoints = mustEnv("ENTRY_POINTS").split(" ") -const safeEnvVar = Object.fromEntries(Object.entries(process.env).filter((entry) => entry[0].startsWith("BUILD"))) +const safeEnvVar = { + BUILD_COMMIT_ID, + BUILD_DATE, + BUILD_NODE_VERSION, + BUILD_ROLLUP_VERSION, + BUILD_OS, + BUILD_PROFILE, + BUILD_WEBSOCKET_SERVER, +} const useCustomElement = ["components"] @@ -38,7 +49,7 @@ function getPlugins(entrypoint) { }), replace({ __ENV: JSON.stringify(safeEnvVar), - __BUILD_PROFILE: JSON.stringify(process.env.BUILD_PROFILE), + __BUILD_PROFILE: JSON.stringify(BUILD_PROFILE), preventAssignment: true }), svelte({ @@ -58,15 +69,16 @@ function getPlugins(entrypoint) { return plugins } +console.log("sourcemap: ", sourceMap) + const output = [] -console.log("sourcemap: ", !isProd) for (const e of entryPoints) { output.push({ external: ["chai", "mocha", "Mocha"], input: pathLib.join(src, e, "main.ts"), output: { - sourcemap: !isProd, + sourcemap: sourceMap, globals: { "chai": "chai", "mocha": "mocha", diff --git a/test/bootstrap.mjs b/scripts/test_bootstrap.js similarity index 90% rename from test/bootstrap.mjs rename to scripts/test_bootstrap.js index ade0448..ffd3c86 100644 --- a/test/bootstrap.mjs +++ b/scripts/test_bootstrap.js @@ -1,16 +1,17 @@ import { WebSocketServer } from 'ws'; import { spawn } from 'node:child_process'; +import webExt from 'web-ext' const TEST_TIMEOUT_MS = 1000 * 60 * 5 -const { BROWSER_LOCATION, TARGET_DIST, BUILD_WEBSOCKET_SERVER } = process.env +const { TARGET_DIST, BUILD_WEBSOCKET_SERVER } = process.env async function runBrowser(signal) { return new Promise((resolve, reject) => { - const proc = spawn("web-ext", ["run", "-f", BROWSER_LOCATION, "-s", TARGET_DIST], { + const proc = spawn("pnpm", ["exec", "web-ext", "run", "-f", "firefox-developer-edition" , "-s", TARGET_DIST], { stdio: ["ignore", process.stdout, process.stderr], signal: signal }) diff --git a/scripts/utils.js b/scripts/utils.js new file mode 100644 index 0000000..6cc58d3 --- /dev/null +++ b/scripts/utils.js @@ -0,0 +1,17 @@ + +import { execSync } from 'node:child_process' + +export function mustEnv(key) { + const value = process.env[key] + if (!value || value.length === 0) { + console.error("require environment: " + key) + process.exit(1) + } + return value +} + + +export function readStdout(command) { + const result = execSync(command) + return result.toString("utf8") +} From e3a626ebd624f5390ce17a6176e995313b5f8063 Mon Sep 17 00:00:00 2001 From: harytfw Date: Sat, 7 Jan 2023 22:31:03 +0800 Subject: [PATCH 02/21] chore: fix build Signed-off-by: harytfw --- Makefile | 2 ++ scripts/rollup.config.js | 2 +- scripts/utils.js | 5 ++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index bb97034..cf1a5ff 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,8 @@ export ENTRY_POINTS = background content_scripts options components export TARGET_BROWSER = firefox export TARGET_DIST = $(BUILD_DIR)/firefox/dist +$(shell mkdir -p $(BUILD_DIR)/artifacts/) + .PHONY: ext-firefox ext-firefox: TARGET_BROWSER = firefox ext-firefox: TARGET_DIST = $(BUILD_DIR)/firefox/dist diff --git a/scripts/rollup.config.js b/scripts/rollup.config.js index 3da5734..560c9b8 100644 --- a/scripts/rollup.config.js +++ b/scripts/rollup.config.js @@ -16,7 +16,7 @@ const BUILD_NODE_VERSION = readStdout("node --version") const BUILD_ROLLUP_VERSION = readStdout("npx rollup --version") const BUILD_OS = os.platform() const BUILD_PROFILE = mustEnv("BUILD_PROFILE") -const BUILD_WEBSOCKET_SERVER = mustEnv("BUILD_WEBSOCKET_SERVER") +const BUILD_WEBSOCKET_SERVER = mustEnv("BUILD_WEBSOCKET_SERVER", "") const isProd = BUILD_PROFILE.toLowerCase() === 'prod'; const sourceMap = !isProd diff --git a/scripts/utils.js b/scripts/utils.js index 6cc58d3..ec7cab8 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -1,9 +1,12 @@ import { execSync } from 'node:child_process' -export function mustEnv(key) { +export function mustEnv(key, default_value) { const value = process.env[key] if (!value || value.length === 0) { + if (typeof default_value !== "undefined") { + return default_value + } console.error("require environment: " + key) process.exit(1) } From 1b46221ba26abd0cf25bbe217701053c0cd1f73d Mon Sep 17 00:00:00 2001 From: harytfw Date: Sat, 7 Jan 2023 22:32:55 +0800 Subject: [PATCH 03/21] fix: fix check defaultPrevented at dragstart event Signed-off-by: harytfw --- src/content_scripts/drag.ts | 27 +++++++++++++++------------ src/content_scripts/main.ts | 2 +- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/content_scripts/drag.ts b/src/content_scripts/drag.ts index 8b5b6b8..8b89ccb 100644 --- a/src/content_scripts/drag.ts +++ b/src/content_scripts/drag.ts @@ -46,15 +46,15 @@ export class DragController { return } for (const n of ["dragstart", "dragover", "dragenter", 'dragleave', "drop", "dragend"]) { - this.eventSource.addEventListener(n, this.handler, true) - this.eventSource.addEventListener(n, this.handler, false) + this.eventSource.addEventListener(n, this.handler, {capture: true}) + this.eventSource.addEventListener(n, this.handler, {capture: false}) } } stop() { for (const n of ["dragstart", "dragover", "dragenter", 'dragleave', "drop", "dragend"]) { - this.eventSource.removeEventListener(n, this.handler, true); - this.eventSource.removeEventListener(n, this.handler, false); + this.eventSource.removeEventListener(n, this.handler, {capture: true}); + this.eventSource.removeEventListener(n, this.handler, {capture: false}); } } @@ -169,14 +169,17 @@ export class DragController { } private checkDragStart(event: DragEvent) { - if (event.eventPhase !== EventPhase.capturing) { - return - } - this.sourceTarget = this.findSourceTarget(event) - if (this.sourceTarget != null) { - this.initFramePosition() - const op = this.makeOp(OpType.start, this.sourceTarget, event) - this.c.applyOp(op) + if (event.eventPhase === EventPhase.bubbling) { + if (!event.defaultPrevented) { + this.sourceTarget = this.findSourceTarget(event) + if (this.sourceTarget != null) { + this.initFramePosition() + const op = this.makeOp(OpType.start, this.sourceTarget, event) + this.c.applyOp(op) + } + } else { + log.V("host page cancel dragtart event") + } } } diff --git a/src/content_scripts/main.ts b/src/content_scripts/main.ts index 69f7634..ab88605 100644 --- a/src/content_scripts/main.ts +++ b/src/content_scripts/main.ts @@ -67,7 +67,7 @@ async function setup() { const opExecutor = new OpExecutor() opExecutor.reset() - controller = new DragController(document.documentElement, opExecutor); + controller = new DragController(window, opExecutor); onDocumentLoaded(async () => { await browser.runtime.sendMessage(buildRuntimeMessage(RuntimeMessageName.contextScriptLoaded, null)) From 9ebe3e4c3b57a4c3a40944c3f22066d5cc33f131 Mon Sep 17 00:00:00 2001 From: harytfw Date: Sun, 8 Jan 2023 12:47:07 +0800 Subject: [PATCH 04/21] docs: add limitation Signed-off-by: harytfw --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 62777b2..5c88faa 100644 --- a/README.md +++ b/README.md @@ -25,3 +25,12 @@ make ext-chromium ``` After the build process completed, `./build/firefox/dist` contains compiled result, `./build/firefox/artifacts` contains package file. + +## Limitation + +The extension has limited support at these situation: + +- Can not work on privilege pages or restricted pages, like , , +- Can not work when dragging any selection from page A and dropping at page B, and page A and page B have different origin. (For example, [host page](www.example.com) is A, it embeds [youtube video](https://www.youtube.com/embed/-88qGXDmh3E) as B) +- When host page explicitly uses drag-and-drop features, the extension will stop working temporarily +- Unable show icon of menu on when [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/Sources#sources) of host page prohibits *data:* URLs From 70c3b5bb4e0db6ba9d5e3d75fb5e32cc1c876b34 Mon Sep 17 00:00:00 2001 From: harytfw Date: Sun, 8 Jan 2023 13:25:47 +0800 Subject: [PATCH 05/21] feat: support other protocol uri Signed-off-by: harytfw --- src/utils/url.ts | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/utils/url.ts b/src/utils/url.ts index 3dc3f84..4446585 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -28,18 +28,18 @@ function matchRe(s: string, pat: RegExp): boolean { return r && r.length > 0 } -function isTopLevelDomain(url: URL): boolean { - return parse(url.hostname).isIcann -} - const log = rootLog.subLogger(LogLevel.VVV, "urlFixer") const urlLRU = new TinyLRU() export class URLFixer { private protocol = "https://" + private specialProtocols: Set = new Set() - constructor() { + constructor(extraProtocols?: string[]) { + if (Array.isArray(extraProtocols)) { + this.specialProtocols = new Set(extraProtocols) + } } fix(urlLike?: string): URL | null { @@ -52,7 +52,13 @@ export class URLFixer { return res } - internalFix(urlLike?: string): URL | null { + private isTopLevelDomain(url: URL): boolean { + if (parse(url.hostname).isIcann) { + return true + } + } + + private internalFix(urlLike?: string): URL | null { log.VVV("try fix url: ", urlLike) @@ -97,7 +103,12 @@ export class URLFixer { const url = this.tryBuildURL(urlLike) if (url) { - if (isTopLevelDomain(url)) { + if (this.isTopLevelDomain(url)) { + log.VVV(urlLike, "is top level domain") + return url + } + if (this.specialProtocols.has(url.protocol)) { + log.VVV(urlLike, "has special protocol", url.protocol) return url } return null From 24ed7e6603c664eea101364ce9ef72249084f885 Mon Sep 17 00:00:00 2001 From: harytfw Date: Sun, 8 Jan 2023 13:26:26 +0800 Subject: [PATCH 06/21] test: fix usage of assert Signed-off-by: harytfw --- src/context/context.test.ts | 17 ++++++++--------- src/utils/test_helper.ts | 12 ++++-------- src/utils/url.test.ts | 30 ++++++++++++++++++++++-------- src/utils/var_substitute.test.ts | 6 +++--- 4 files changed, 37 insertions(+), 28 deletions(-) diff --git a/src/context/context.test.ts b/src/context/context.test.ts index 85c2be9..c8b914e 100644 --- a/src/context/context.test.ts +++ b/src/context/context.test.ts @@ -1,21 +1,20 @@ import { blankExecuteContext } from "./test_helper" import { ContextType } from "../config/config" -import { assertEqual } from "../utils/test_helper" import { primaryContextType } from "./utils" - +import { assert } from 'chai' describe("test context", () => { - it("primary type",async () => { - let ctx = await blankExecuteContext() + it("primary type", async () => { + let ctx = await blankExecuteContext() ctx.data.selection = "hello" - assertEqual(primaryContextType(ctx), ContextType.selection) + assert.deepEqual(primaryContextType(ctx), ContextType.selection) - ctx = await blankExecuteContext() + ctx = await blankExecuteContext() ctx.data.link = "http://example.com/" - assertEqual(primaryContextType(ctx), ContextType.link) + assert.deepEqual(primaryContextType(ctx), ContextType.link) - ctx = await blankExecuteContext() + ctx = await blankExecuteContext() ctx.data.imageSource = "http://example.com/a.jpg" - assertEqual(primaryContextType(ctx), ContextType.image) + assert.deepEqual(primaryContextType(ctx), ContextType.image) }) }) \ No newline at end of file diff --git a/src/utils/test_helper.ts b/src/utils/test_helper.ts index 091cbb8..64906c3 100644 --- a/src/utils/test_helper.ts +++ b/src/utils/test_helper.ts @@ -1,21 +1,17 @@ import browser from 'webextension-polyfill'; -import isEqual from "lodash-es/isEqual" +import { assert } from 'chai' export function assertEqual(expected: any, actual: any, ...message: any[]) { - return assertOk(isEqual(actual, expected), ...message) + return assert.deepEqual(actual, expected, ...message) } export function assertOk(value: any, ...message: any[]) { - if (!value) { - throw new Error(message.join(" ")) - } + return assert.ok(value, ...message) } export function assertFail(value: any, ...message: any[]) { - if (!!value) { - throw new Error(message.join(" ")) - } + return assert.notOk(value, ...message) } export async function closeTab(tabIds: number | number[], delayMs?: number) { diff --git a/src/utils/url.test.ts b/src/utils/url.test.ts index d288ae2..42d5d7b 100644 --- a/src/utils/url.test.ts +++ b/src/utils/url.test.ts @@ -1,22 +1,23 @@ import { URLFixer } from './url' -import { assertEqual, assertFail, assertOk } from './test_helper'; import { rootLog } from './log'; +import { assert } from 'chai' describe('test url fixer', () => { - const fixer = new URLFixer() it('good url', () => { + const fixer = new URLFixer() const urls = [ "http://example.com/", "http://example.org/", "http://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/win-64" ] for (const c of urls) { - assertEqual(fixer.fix(c).toString(), c) + assert.deepEqual(fixer.fix(c).toString(), c) } }) it('bad url', () => { + const fixer = new URLFixer() const urls = [ "12345", @@ -24,35 +25,38 @@ describe('test url fixer', () => { ] for (const c of urls) { - assertFail(fixer.fix(c), c) + assert.notOk(fixer.fix(c), c) } }); it('valid ipv4', () => { + const fixer = new URLFixer() const urls = [ "8.8.8.8", "192.168.1.1", "127.0.0.1" ] for (const c of urls) { - assertOk(fixer.fix(c), c) + assert.ok(fixer.fix(c), c) } }); it('valid ipv6', () => { + const fixer = new URLFixer() const urls = [ "2001:db8:0:0:1:0:0:1", "2001:db8::1:0:0:1", "::1" ] for (const c of urls) { - assertFail(fixer.fix(c), "not support plain ipv6: ", c) - assertOk(fixer.fix("[" + c + "]"), "ipv6 url: ", c) + assert.notOk(fixer.fix(c), "not support plain ipv6: " + c) + assert.ok(fixer.fix("[" + c + "]"), "ipv6 url: " + c) } }); it('fix url', () => { + const fixer = new URLFixer() const urls = [ [ "www.example.com/a", @@ -79,7 +83,17 @@ describe('test url fixer', () => { for (const c of urls) { const fixed = fixer.fix(c[0]).toString() rootLog.VVV("origin: ", c[0], "fixed:", fixed, " expected: ", c[1], " same: ", fixed == c[1]) - assertEqual(fixed, c[1], "expected: ", c[1], "but got:", fixed) + assert.deepEqual(fixed, c[1]) } }); + it('special protocol url', function () { + const fixer = new URLFixer(["magnet:", "mailto:"]) + const urls = [ + "magnet:?xt=urn:btih:000000000000000000000000000&dn", + "mailto:someone@example.com" + ] + for (const url of urls) { + assert.ok(fixer.fix(url)) + } + }) }); \ No newline at end of file diff --git a/src/utils/var_substitute.test.ts b/src/utils/var_substitute.test.ts index e0439a3..d0ccb5c 100644 --- a/src/utils/var_substitute.test.ts +++ b/src/utils/var_substitute.test.ts @@ -1,6 +1,6 @@ import { assertEqual } from "./test_helper" import { VarSubstituteTemplate } from "./var_substitute" - +import { assert } from 'chai' describe('test variable substitute', () => { @@ -20,7 +20,7 @@ describe('test variable substitute', () => { for (const c of cases) { const template = new VarSubstituteTemplate(c) const result = template.substitute(m) - assertEqual(result, c, "expected: ", c, ", actually: ", result) + assert.deepEqual(result, c) } }) @@ -52,7 +52,7 @@ describe('test variable substitute', () => { for (const [s, expected] of cases) { const template = new VarSubstituteTemplate(s) const result = template.substitute(m) - assertEqual(result, expected, "expected: ", expected, ", actually: ", result) + assert.deepEqual(result, expected) } }) }) \ No newline at end of file From f3ed0fc1e85782ce1fd34f45dc1c3796ef8c265b Mon Sep 17 00:00:00 2001 From: harytfw Date: Sun, 8 Jan 2023 14:12:57 +0800 Subject: [PATCH 07/21] feat: add configuration for protocol of smartURL Signed-off-by: harytfw --- src/config/config.ts | 13 +++++++++++-- src/content_scripts/op_source.ts | 16 ++++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/config/config.ts b/src/config/config.ts index 7d44da3..5dc9484 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -107,13 +107,21 @@ export interface PlainLogConfig { } export class SmartURLConfig { - cfg: KVRecord - constructor(cfg: KVRecord) { + cfg: PlainSmartURLConfig + + constructor(cfg: PlainSmartURLConfig) { this.cfg = cfg } + + get protocols(): string[] { + return defaultTo(this.cfg.protocols, []) + } } +export interface PlainSmartURLConfig { + protocols?: string[] +} export class ModeConfig { @@ -653,6 +661,7 @@ export interface PlainConfiguration { assets?: PlainAsset[], scripts?: PlainScript[], compatibility?: PlainCompatibilityRule[], + smartURL?: PlainSmartURLConfig, } export class BroadcastEventTarget { diff --git a/src/content_scripts/op_source.ts b/src/content_scripts/op_source.ts index 99eb16d..050d900 100644 --- a/src/content_scripts/op_source.ts +++ b/src/content_scripts/op_source.ts @@ -1,8 +1,15 @@ -import { ContextType } from "../config/config" +import { configBroadcast, ContextType, type ReadonlyConfiguration } from "../config/config" import type { KVRecord } from "../types" import { URLFixer } from "../utils/url" import type { OpSummary } from "./op" + +let config: ReadonlyConfiguration | null = null + +configBroadcast.addListener((cfg) => { + config = cfg +}) + export class OpSource { private _isContentEditable: boolean = false private _isDraggable: boolean = false @@ -72,7 +79,12 @@ export class OpSource { } if (os.type === ContextType.selection) { - const url = new URLFixer().fix(os.text) + let protocols = [] + if (config) { + protocols = Array.from(config.smartURL.protocols) + } + const fixer = new URLFixer(protocols) + const url = fixer.fix(os.text) if (url) { os._link = url.toString() os._linkText = os.text From 79ea12a601f3f62cd45b630ad41265af3e58f05e Mon Sep 17 00:00:00 2001 From: harytfw Date: Sun, 8 Jan 2023 14:30:07 +0800 Subject: [PATCH 08/21] refactor: replace KVRecord with concrete type Signed-off-by: harytfw --- src/background/executor.test.ts | 4 +- src/config/config.ts | 183 ++++++++++++++------------------ 2 files changed, 84 insertions(+), 103 deletions(-) diff --git a/src/background/executor.test.ts b/src/background/executor.test.ts index 2e07e20..e84b3b6 100644 --- a/src/background/executor.test.ts +++ b/src/background/executor.test.ts @@ -29,14 +29,14 @@ describe("test executor", async () => { }) it("copy text", async () => { - const ctx = await blankExecuteContext(new ActionConfig({ "command": "copy" })) + const ctx = await blankExecuteContext(new ActionConfig({ command: CommandKind.copy })) ctx.data.selection = "copy demo" await executor.copyHandler(ctx) }) it("copy image", async () => { - const ctx = await blankExecuteContext(new ActionConfig({ "command": "copy" })) + const ctx = await blankExecuteContext(new ActionConfig({ command: CommandKind.copy })) ctx.data.imageSource = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAA8UlEQVQYlY3QoaqDYACG4dd5ysDgQBwz+QdZWFBYsTnwHryek3cVZi1isngHljVBUNAi/Ay3MIaWnXhY88tP+HiVqqo+rNhmDQL4aZoGx3FI05RpmhBCcDweKYoCXdeJooh5nlEty/rd7/ecTifatsV1XWzbZpomwjDkfr+TJAnK9Xr9qKpKEAR0XUdVVXieh2EY7Ha7/4/b7ZbX6wWAEAJN0+j7/gsBbN7vN5qmAfB8PjFNk3EcGYbhGw7DgOM4PB4Pbrcb5/OZy+VCWZZIKQFYloWN7/scDgfyPKeua6SU1HXNsixkWYaUkjiOUdYG/wONbmSLuq2dcQAAAABJRU5ErkJggg==" await executor.copyHandler(ctx) diff --git a/src/config/config.ts b/src/config/config.ts index 5dc9484..7bd4a7a 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -91,19 +91,19 @@ export enum Feature { export class LogConfig { - private cfg: KVRecord + private cfg: PlainLogConfig - constructor(cfg: KVRecord) { + constructor(cfg: PlainLogConfig) { this.cfg = cfg } get level(): LogLevel { - return defaultTo(this.cfg['level'], LogLevel.S) + return defaultTo(this.cfg.level, LogLevel.S) } } export interface PlainLogConfig { - level: number + level?: number } export class SmartURLConfig { @@ -125,66 +125,56 @@ export interface PlainSmartURLConfig { export class ModeConfig { - private cfg: KVRecord + private cfg: PlainModeConfig - constructor(cfg: KVRecord) { + constructor(cfg: PlainModeConfig) { this.cfg = cfg } get link(): OperationMode { - return defaultTo(this.cfg['link'], OperationMode.normal) + return defaultTo(this.cfg.link, OperationMode.normal) } get selection(): OperationMode { - return defaultTo(this.cfg['selection'], OperationMode.normal) + return defaultTo(this.cfg.selection, OperationMode.normal) } get image(): OperationMode { - return defaultTo(this.cfg['image'], OperationMode.normal) + return defaultTo(this.cfg.image, OperationMode.normal) } } export interface PlainModeConfig { - link?: string - selection?: string - image?: string + link?: OperationMode + selection?: OperationMode + image?: OperationMode } export class CommonConfig { - private cfg: KVRecord + private cfg: PlainCommonConfig private _mode: ModeConfig - constructor(cfg: KVRecord) { + constructor(cfg: PlainCommonConfig) { this.cfg = cfg - this._mode = new ModeConfig(defaultTo(this.cfg['mode'], {})) + this._mode = new ModeConfig(defaultTo(this.cfg.mode, {})) } get mode(): ModeConfig { return this._mode } - get allow(): string[] { - return defaultTo(this.cfg['allow'], []) - } - - get disallow(): string[] { - return defaultTo(this.cfg['disallow'], []) - } - get minDistance(): number { - return defaultTo(this.cfg['minDistance'], 0) + return defaultTo(this.cfg.minDistance, 0) } get maxDistance(): number { - return defaultTo(this.cfg['maxDistance'], 0) + return defaultTo(this.cfg.maxDistance, 0) } } export interface PlainCommonConfig { mode?: PlainModeConfig - allowHost?: string[] - disallowHost?: string[] minDistance?: number maxDistance?: number } @@ -195,9 +185,9 @@ export enum MenuLayout { } export class MenuConfig { - cfg: KVRecord + cfg: PlainMenuConfig - constructor(cfg: KVRecord) { + constructor(cfg: PlainMenuConfig) { this.cfg = cfg } @@ -207,19 +197,11 @@ export interface PlainMenuConfig { } -export class ListModeConfig { - cfg: KVRecord - - constructor(cfg: KVRecord) { - this.cfg = cfg - } -} - export class Script { - private cfg: KVRecord + private cfg: PlainScript - constructor(cfg: KVRecord) { + constructor(cfg: PlainScript) { this.cfg = cfg } @@ -231,15 +213,15 @@ export class Script { } get id() { - return defaultTo(this.cfg['id'], "") + return defaultTo(this.cfg.id, "") } get text() { - return defaultTo(this.cfg['text'], "") + return defaultTo(this.cfg.text, "") } get name() { - return defaultTo(this.cfg['name'], this.id) + return defaultTo(this.cfg.name, this.id) } } @@ -256,14 +238,14 @@ export class CommandRequest { private _url: string private _query: KVRecord private _protocol: string - private cfg: KVRecord + private cfg: PlainCommandRequest constructor(cfg: PlainCommandRequest) { this.cfg = cloneDeep(cfg) - const urlObj = new URL(cfg['url']) + const urlObj = new URL(cfg.url) - this._query = defaultTo(cfg['query'], {}) + this._query = defaultTo(cfg.query, {}) for (const [k, v] of urlObj.searchParams) { this._query[k] = cloneDeep(v) } @@ -289,11 +271,11 @@ export class CommandRequest { } get id() { - return defaultTo(this.cfg['id'], '') + return defaultTo(this.cfg.id, '') } get name() { - return defaultTo(this.cfg["name"], this.id) + return defaultTo(this.cfg.name, this.id) } get method() { @@ -331,41 +313,41 @@ export const enum AssetType { } export class Asset { - private cfg: KVRecord + private cfg: PlainAsset - constructor(cfg: KVRecord) { + constructor(cfg: PlainAsset) { this.cfg = cfg } get id(): string { - return defaultTo(this.cfg["id"], "") + return defaultTo(this.cfg.id, "") } get name(): string { - return defaultTo(this.cfg["name"], this.id) + return defaultTo(this.cfg.name, this.id) } get type(): AssetType { - return defaultTo(this.cfg["type"], AssetType.html) + return defaultTo(this.cfg.type, AssetType.html) } get data(): string { - return defaultTo(this.cfg["data"], "") + return defaultTo(this.cfg.data, "") } } export interface PlainAsset { id?: string name?: string - type?: string + type?: AssetType data?: string } export class CommandConfig { - private cfg: KVRecord + private cfg: PlainCommandConfig - constructor(cfg: KVRecord) { + constructor(cfg: PlainCommandConfig) { this.cfg = cfg } @@ -383,53 +365,53 @@ export class CommandConfig { } get activeTab(): boolean { - return defaultTo(this.cfg['activeTab'], true) + return defaultTo(this.cfg.activeTab, true) } get tabPosition(): TabPosition { - return defaultTo(this.cfg['tabPosition'], TabPosition.next) + return defaultTo(this.cfg.tabPosition, TabPosition.next) } get requestId(): string { - return defaultTo(this.cfg['requestId'], "") + return defaultTo(this.cfg.requestId, "") } get scriptId(): string { - return defaultTo(this.cfg['scriptId'], "") + return defaultTo(this.cfg.scriptId, "") } get directory(): string { - return defaultTo(this.cfg['directory'], "") + return defaultTo(this.cfg.directory, "") } get showSaveAsDialog(): boolean { - return defaultTo(this.cfg['showSaveAsDialog'], false) + return defaultTo(this.cfg.showSaveAsDialog, false) } get preferDataTypes(): ContextDataType[] { - return defaultTo(this.cfg['preferDataTypes'], []) + return defaultTo(this.cfg.preferDataTypes, []) } get container(): string { - return defaultTo(this.cfg['container'], "") + return defaultTo(this.cfg.container, "") } } export interface PlainCommandConfig { activeTab?: boolean - tabPosition?: string + tabPosition?: TabPosition requestId?: string scriptId?: string directory?: string showSaveAsDialog?: boolean - preferDataTypes?: string[] + preferDataTypes?: ContextDataType[] container?: string } export class ConditionConfig { - private cfg: KVRecord + private cfg: PlainConditionConfig - constructor(cfg: KVRecord) { + constructor(cfg: PlainConditionConfig) { this.cfg = cfg } @@ -442,39 +424,39 @@ export class ConditionConfig { } get modes(): OperationMode[] { - return defaultTo(this.cfg['modes'], []) + return defaultTo(this.cfg.modes, []) } get contextTypes(): ContextType[] { - return defaultTo(this.cfg['contextTypes'], []) + return defaultTo(this.cfg.contextTypes, []) } get directions(): Direction[] { - return defaultTo(this.cfg['directions'], []) + return defaultTo(this.cfg.directions, []) } get extra(): ContextType[] { - return defaultTo(this.cfg['extra'], []) + return defaultTo(this.cfg.extra, []) } } export interface PlainConditionConfig { - modes?: string[] - contextTypes?: string[], - directions?: string[], - extra?: string[] + modes?: OperationMode[] + contextTypes?: ContextType[], + directions?: Direction[], + extra?: ContextType[] } export class ActionConfig { - private cfg: KVRecord + private cfg: PlainActionConfig private _conditionConfig: ConditionConfig private _config: CommandConfig - constructor(cfg: KVRecord) { + constructor(cfg: PlainActionConfig) { this.cfg = cfg - this._conditionConfig = new ConditionConfig(defaultTo(cfg['condition'], {})) - this._config = new CommandConfig(defaultTo(cfg['config'], {})) + this._conditionConfig = new ConditionConfig(defaultTo(cfg.condition, {})) + this._config = new CommandConfig(defaultTo(cfg.config, {})) } toPlainObject(): PlainActionConfig { @@ -490,15 +472,15 @@ export class ActionConfig { } get id(): string { - return defaultTo(this.cfg['id'], "") + return defaultTo(this.cfg.id, "") } get name(): string { - return defaultTo(this.cfg['name'], this.id) + return defaultTo(this.cfg.name, this.id) } get prompt(): string { - return defaultTo(this.cfg['prompt'], "") + return defaultTo(this.cfg.prompt, "") } get condition(): Readonly { @@ -506,7 +488,7 @@ export class ActionConfig { } get command(): CommandKind { - return defaultTo(this.cfg['command'], CommandKind.invalid) + return defaultTo(this.cfg.command, CommandKind.invalid) } get config(): CommandConfig { @@ -514,11 +496,11 @@ export class ActionConfig { } get icon(): string { - return defaultTo(this.cfg['icon'], "") + return defaultTo(this.cfg.icon, "") } get iconAssetId(): string { - return defaultTo(this.cfg['iconAssetId'], "") + return defaultTo(this.cfg.iconAssetId, "") } } @@ -526,7 +508,7 @@ export interface PlainActionConfig { id?: string name?: string condition?: PlainConditionConfig - command?: string + command?: CommandKind config?: PlainCommandConfig icon?: string iconAssetId?: string @@ -544,13 +526,11 @@ export interface PlainCompatibilityRule { host?: string, regexp?: string, status export class CompatibilityRule { - private cfg: KVRecord private _host: string private _regexp: string private _status: CompatibilityStatus constructor(cfg: PlainCompatibilityRule) { - this.cfg = cfg this._host = defaultTo(cfg.host, "") this._regexp = defaultTo(cfg.regexp, "") this._status = defaultTo(cfg.status, CompatibilityStatus.enable) @@ -590,19 +570,19 @@ export class Configuration { private _scripts: Script[] private _compatibility: CompatibilityRule[] - constructor(data?: KVRecord) { - data = defaultTo(data, {}) + constructor(cfg?: PlainConfiguration) { + cfg = defaultTo(cfg, {}) - this._log = new LogConfig(defaultTo(data['log'], {})) - this._smartURL = new SmartURLConfig(defaultTo(data['smartURL'], {})) - this._common = new CommonConfig(defaultTo(data['common'], {})) - this._menu = new MenuConfig(defaultTo(data['grid'], {})) - this._actions = defaultTo(data['actions'], []).map((c: KVRecord) => new ActionConfig(c)) - this._features = new Set(defaultTo(data['features'], [])) - this._requests = defaultTo(data['requests'], []).map((r: KVRecord) => new CommandRequest(r)) - this._assets = defaultTo(data['assets'], []).map((r: KVRecord) => new Asset(r)) - this._scripts = defaultTo(data['scripts'], []).map((r: KVRecord) => new Script(r)) - this._compatibility = defaultTo(data["compatibility"], []).map((r: KVRecord) => new CompatibilityRule(r)) + this._log = new LogConfig(defaultTo(cfg.log, {})) + this._smartURL = new SmartURLConfig(defaultTo(cfg.smartURL, {})) + this._common = new CommonConfig(defaultTo(cfg.common, {})) + this._menu = new MenuConfig(defaultTo(cfg.menu, {})) + this._actions = defaultTo(cfg.actions, []).map((c: PlainActionConfig) => new ActionConfig(c)) + this._requests = defaultTo(cfg.requests, []).map((r: PlainCommandRequest) => new CommandRequest(r)) + this._assets = defaultTo(cfg.assets, []).map((r: PlainAsset) => new Asset(r)) + this._scripts = defaultTo(cfg.scripts, []).map((r: PlainScript) => new Script(r)) + this._compatibility = defaultTo(cfg.compatibility, []).map((r: PlainCompatibilityRule) => new CompatibilityRule(r)) + this._features = new Set(defaultTo(cfg.features as Feature[], [])) } get features(): Set { @@ -662,6 +642,7 @@ export interface PlainConfiguration { scripts?: PlainScript[], compatibility?: PlainCompatibilityRule[], smartURL?: PlainSmartURLConfig, + features?: string[] } export class BroadcastEventTarget { From 55ee756af717a6998b5bed85ce6c64ec9478d8a2 Mon Sep 17 00:00:00 2001 From: harytfw Date: Tue, 10 Jan 2023 14:30:07 +0800 Subject: [PATCH 09/21] chore: refactor build cli Signed-off-by: harytfw --- Makefile | 90 +++------- package.json | 8 +- pnpm-lock.yaml | 83 +++++---- scripts/build_rollup.mjs | 82 +++++++++ scripts/cli.mjs | 295 ++++++++++++++++++++++++++++++++ scripts/gen_manifest.js | 119 ------------- scripts/gen_manifest.mjs | 124 ++++++++++++++ scripts/rollup.config.js | 94 ---------- scripts/test_bootstrap.js | 95 ---------- scripts/{utils.js => utils.mjs} | 4 + src/build_info.ts | 10 +- src/global.d.ts | 12 +- src/test/mocha_init.ts | 2 +- 13 files changed, 598 insertions(+), 420 deletions(-) create mode 100644 scripts/build_rollup.mjs create mode 100644 scripts/cli.mjs delete mode 100644 scripts/gen_manifest.js create mode 100644 scripts/gen_manifest.mjs delete mode 100644 scripts/rollup.config.js delete mode 100644 scripts/test_bootstrap.js rename scripts/{utils.js => utils.mjs} (85%) diff --git a/Makefile b/Makefile index cf1a5ff..40d6f81 100644 --- a/Makefile +++ b/Makefile @@ -1,78 +1,38 @@ export BUILD_VERSION ?= 2.1.4 export BUILD_DIR ?= $(shell realpath ./build) -export SRC ?= $(shell realpath ./src) -export BUILD_PROFILE ?= debug -export BUILD_WEBSOCKET_SERVER = -export ENTRY_POINTS = background content_scripts options components -export TARGET_BROWSER = firefox -export TARGET_DIST = $(BUILD_DIR)/firefox/dist +export BUILD_SRC ?= $(shell realpath ./src) -$(shell mkdir -p $(BUILD_DIR)/artifacts/) +cli = node ./scripts/cli.mjs +.PHONY: build +build: TARGET ?= firefox +build: + $(cli) build --target $(TARGET) + +.PHONY: build-watch +build-watch: TARGET ?= firefox +build-watch: + $(cli) build --target $(TARGET) --watch .PHONY: ext-firefox -ext-firefox: TARGET_BROWSER = firefox -ext-firefox: TARGET_DIST = $(BUILD_DIR)/firefox/dist -ext-firefox: setup-dist assets compile lint - cd $(TARGET_DIST) && zip -r $(BUILD_DIR)/artifacts/glitterdrag-pro-$(BUILD_VERSION)-firefox.zip . +ext-firefox: BUILD_PROFILE ?= debug +ext-firefox: + $(cli) build --target firefox --lint --profile $(BUILD_PROFILE) .PHONY: ext-chromium -ext-test: TARGET_BROWSER = chromium -ext-chromium: TARGET_DIST = $(BUILD_DIR)/chromium/dist -ext-chromium: setup-dist assets compile - cd $(TARGET_DIST) && zip -r $(BUILD_DIR)/artifacts/glitterdrag-pro-$(BUILD_VERSION)-chromium.zip . +ext-chromium: BUILD_PROFILE ?= debug +ext-chromium: + $(cli) build --target chromium --profile $(BUILD_PROFILE) .PHONY: test -test: TARGET_BROWSER = firefox-test -test: ENTRY_POINTS += test -test: TARGET_DIST = $(BUILD_DIR)/test/dist -test: BUILD_WEBSOCKET_SERVER = ws://localhost:8000 -test: setup-dist assets mocha-assets compile - @node scripts/test_bootstrap.js - -.PHONY: open-mocha -open-mocha: TARGET_DIST = $(BUILD_DIR)/firefox/dist -open-mocha: ENTRY_POINTS += test -open-mocha: setup-dist assets mocha-assets compile - web-ext run -v \ - -s $(TARGET_DIST) \ - -f "$(shell which firefox-developer-edition)" - -.PHONY: setup-dist -setup-dist: - $(shell [ ! -d $(TARGET_DIST) ] && mkdir -p $(TARGET_DIST)/) - -.PHONY: lint -lint: - @pnpm exec web-ext lint -s $(TARGET_DIST) - -.PHONY: compile -compile: - @pnpm exec rollup -c "./scripts/rollup.config.js" - -.PHONY: manifest -manifest: - @node scripts/gen_manifest.js > $(TARGET_DIST)/manifest.json - -.PHONY: assets -assets: manifest - - @mkdir -p $(TARGET_DIST)/options && \ - cp -f $(SRC)/options/options.html $(TARGET_DIST)/options/options.html - - @mkdir -p $(TARGET_DIST)/res && \ - cp -r -f node_modules/simpledotcss/simple.min.css $(TARGET_DIST)/res/simple.min.css - - @cp -r -f $(SRC)/icon/ $(SRC)/_locales/ $(TARGET_DIST)/ +test: TARGET ?= firefox +test: + $(cli) test --target $(TARGET) -.PHONY: mocha-assets -mocha-assets: - @mkdir -p $(TARGET_DIST)/test && \ - cp node_modules/mocha/mocha.js \ - node_modules/mocha/mocha.css \ - node_modules/chai/chai.js \ - $(SRC)/test/mocha.html \ - $(TARGET_DIST)/test +.PHONY: watch +watch: TARGET ?= firefox +watch: + $(cli) watch .PHONY: clean clean: - @rm -rf $(BUILD_DIR) \ No newline at end of file + $(cli) clean \ No newline at end of file diff --git a/package.json b/package.json index a999ad2..2fe137b 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "@rollup/plugin-commonjs": "^23.0.2", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-replace": "^5.0.1", + "@rollup/plugin-terser": "^0.3.0", "@rollup/plugin-typescript": "^9.0.2", "@tsconfig/svelte": "^3.0.0", "@types/chai": "^4.3.4", @@ -13,14 +14,16 @@ "@types/node": "^18.11.9", "@types/uuid": "^8.3.4", "@types/webextension-polyfill": "^0.9.1", + "@types/ws": "^8.5.4", "chai": "^4.3.7", + "commander": "^9.5.0", + "fs-extra": "^11.1.0", "lodash": "^4.17.21", "lodash-core": "^4.17.19", "lodash-es": "^4.17.21", "mocha": "^10.1.0", "rollup": "^3.4.0", "rollup-plugin-svelte": "^7.1.0", - "rollup-plugin-terser": "^7.0.2", "simpledotcss": "^2.1.1", "svelte": "^3.53.1", "svelte-preprocess": "^4.10.7", @@ -30,7 +33,8 @@ "uuid": "^9.0.0", "web-ext": "7.4.0", "webextension-polyfill": "^0.10.0", - "ws": "^8.11.0" + "ws": "^8.11.0", + "zip-dir": "^2.0.0" }, "license": "MIT" } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9f8755..d24aa67 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,7 @@ specifiers: '@rollup/plugin-commonjs': ^23.0.2 '@rollup/plugin-node-resolve': ^15.0.1 '@rollup/plugin-replace': ^5.0.1 + '@rollup/plugin-terser': ^0.3.0 '@rollup/plugin-typescript': ^9.0.2 '@tsconfig/svelte': ^3.0.0 '@types/chai': ^4.3.4 @@ -12,14 +13,16 @@ specifiers: '@types/node': ^18.11.9 '@types/uuid': ^8.3.4 '@types/webextension-polyfill': ^0.9.1 + '@types/ws': ^8.5.4 chai: ^4.3.7 + commander: ^9.5.0 + fs-extra: ^11.1.0 lodash: ^4.17.21 lodash-core: ^4.17.19 lodash-es: ^4.17.21 mocha: ^10.1.0 rollup: ^3.4.0 rollup-plugin-svelte: ^7.1.0 - rollup-plugin-terser: ^7.0.2 simpledotcss: ^2.1.1 svelte: ^3.53.1 svelte-preprocess: ^4.10.7 @@ -30,11 +33,13 @@ specifiers: web-ext: 7.4.0 webextension-polyfill: ^0.10.0 ws: ^8.11.0 + zip-dir: ^2.0.0 devDependencies: '@rollup/plugin-commonjs': 23.0.2_rollup@3.4.0 '@rollup/plugin-node-resolve': 15.0.1_rollup@3.4.0 '@rollup/plugin-replace': 5.0.1_rollup@3.4.0 + '@rollup/plugin-terser': 0.3.0_rollup@3.4.0 '@rollup/plugin-typescript': 9.0.2_vmrh7niimjcuan47h4hgsgyxjy '@tsconfig/svelte': 3.0.0 '@types/chai': 4.3.4 @@ -43,14 +48,16 @@ devDependencies: '@types/node': 18.11.9 '@types/uuid': 8.3.4 '@types/webextension-polyfill': 0.9.1 + '@types/ws': 8.5.4 chai: 4.3.7 + commander: 9.5.0 + fs-extra: 11.1.0 lodash: 4.17.21 lodash-core: 4.17.19 lodash-es: 4.17.21 mocha: 10.1.0 rollup: 3.4.0 rollup-plugin-svelte: 7.1.0_rollup@3.4.0+svelte@3.53.1 - rollup-plugin-terser: 7.0.2_rollup@3.4.0 simpledotcss: 2.1.1 svelte: 3.53.1 svelte-preprocess: 4.10.7_7dvewpees4iyn2tkw2qzal77a4 @@ -61,6 +68,7 @@ devDependencies: web-ext: 7.4.0 webextension-polyfill: 0.10.0 ws: 8.11.0 + zip-dir: 2.0.0 packages: @@ -110,7 +118,7 @@ packages: '@devicefarmer/adbkit-logcat': 2.1.2 '@devicefarmer/adbkit-monkey': 1.2.1 bluebird: 3.7.2 - commander: 9.4.1 + commander: 9.5.0 debug: 4.3.4 node-forge: 1.3.1 split: 1.0.1 @@ -282,6 +290,21 @@ packages: rollup: 3.4.0 dev: true + /@rollup/plugin-terser/0.3.0_rollup@3.4.0: + resolution: {integrity: sha512-mYTkNW9KjOscS/3QWU5LfOKsR3/fAAVDaqcAe2TZ7ng6pN46f+C7FOZbITuIW/neA+PhcjoKl7yMyB3XcmA4gw==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.x || ^3.x + peerDependenciesMeta: + rollup: + optional: true + dependencies: + rollup: 3.4.0 + serialize-javascript: 6.0.0 + smob: 0.0.6 + terser: 5.16.1 + dev: true + /@rollup/plugin-typescript/9.0.2_vmrh7niimjcuan47h4hgsgyxjy: resolution: {integrity: sha512-/sS93vmHUMjzDUsl5scNQr1mUlNE1QjBBvOhmRwJCH8k2RRhDIm3c977B3wdu3t3Ap17W6dDeXP3hj1P1Un1bA==} engines: {node: '>=14.0.0'} @@ -389,6 +412,12 @@ packages: resolution: {integrity: sha512-6aNzPIhqKlAV9t06nwSH3/veAceYE2dS2RVFZI8V1+UXHqsFNB6cRwxNmheiBvEGRc45E/gyZNzH0xAYIC27KA==} dev: true + /@types/ws/8.5.4: + resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==} + dependencies: + '@types/node': 18.11.9 + dev: true + /@types/yauzl/2.10.0: resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} dependencies: @@ -937,8 +966,8 @@ packages: graceful-readlink: 1.0.1 dev: true - /commander/9.4.1: - resolution: {integrity: sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==} + /commander/9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} dev: true @@ -1586,6 +1615,15 @@ packages: universalify: 2.0.0 dev: true + /fs-extra/11.1.0: + resolution: {integrity: sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==} + engines: {node: '>=14.14'} + dependencies: + graceful-fs: 4.2.10 + jsonfile: 6.1.0 + universalify: 2.0.0 + dev: true + /fs-extra/9.0.1: resolution: {integrity: sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==} engines: {node: '>=10'} @@ -2063,15 +2101,6 @@ packages: resolution: {integrity: sha1-elSbvZ/+FYWwzQoZHiAwVb7ldLQ=} dev: true - /jest-worker/26.6.2: - resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} - engines: {node: '>= 10.13.0'} - dependencies: - '@types/node': 18.11.9 - merge-stream: 2.0.0 - supports-color: 7.2.0 - dev: true - /jose/4.11.1: resolution: {integrity: sha512-YRv4Tk/Wlug8qicwqFNFVEZSdbROCHRAC6qu/i0dyNKr5JQdoa2pIGoS04lLO/jXQX7Z9omoNewYIVIxqZBd9Q==} dev: true @@ -3052,18 +3081,6 @@ packages: svelte: 3.53.1 dev: true - /rollup-plugin-terser/7.0.2_rollup@3.4.0: - resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==} - peerDependencies: - rollup: ^2.0.0 - dependencies: - '@babel/code-frame': 7.18.6 - jest-worker: 26.6.2 - rollup: 3.4.0 - serialize-javascript: 4.0.0 - terser: 5.16.0 - dev: true - /rollup-pluginutils/2.8.2: resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} dependencies: @@ -3140,12 +3157,6 @@ packages: lru-cache: 6.0.0 dev: true - /serialize-javascript/4.0.0: - resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==} - dependencies: - randombytes: 2.1.0 - dev: true - /serialize-javascript/6.0.0: resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} dependencies: @@ -3207,6 +3218,10 @@ packages: resolution: {integrity: sha512-BhqPaXVd8yGiZucZzn8rRV+1q19iVUvkv4mVXvtc4KMuf8H7/FrreTqzDcTfOl8X44DftHdn/laMkAKtX/itcw==} dev: true + /smob/0.0.6: + resolution: {integrity: sha512-V21+XeNni+tTyiST1MHsa84AQhT1aFZipzPpOFAVB8DkHzwJyjjAmt9bgwnuZiZWnIbMo2duE29wybxv/7HWUw==} + dev: true + /sonic-boom/3.2.0: resolution: {integrity: sha512-SbbZ+Kqj/XIunvIAgUZRlqd6CGQYq71tRRbXR92Za8J/R3Yh4Av+TWENiSiEgnlwckYLyP0YZQWVfyNC0dzLaA==} dependencies: @@ -3461,8 +3476,8 @@ packages: engines: {node: '>= 8'} dev: true - /terser/5.16.0: - resolution: {integrity: sha512-KjTV81QKStSfwbNiwlBXfcgMcOloyuRdb62/iLFPGBcVNF4EXjhdYBhYHmbJpiBrVxZhDvltE11j+LBQUxEEJg==} + /terser/5.16.1: + resolution: {integrity: sha512-xvQfyfA1ayT0qdK47zskQgRZeWLoOQ8JQ6mIgRGVNwZKdQMU+5FkCBjmv4QjcrTzyZquRw2FVtlJSRUmMKQslw==} engines: {node: '>=10'} hasBin: true dependencies: diff --git a/scripts/build_rollup.mjs b/scripts/build_rollup.mjs new file mode 100644 index 0000000..361c2b4 --- /dev/null +++ b/scripts/build_rollup.mjs @@ -0,0 +1,82 @@ +import { readStdout } from "./utils.mjs"; + +import commonjs from '@rollup/plugin-commonjs'; +import resolve from '@rollup/plugin-node-resolve'; +import replace from '@rollup/plugin-replace'; +import terser from '@rollup/plugin-terser'; +import typescript from '@rollup/plugin-typescript'; +import os from "node:os"; +import pathLib from 'node:path'; +import svelte from 'rollup-plugin-svelte'; +import autoPreprocess from 'svelte-preprocess'; + + +const useCustomElement = ["components"] + +export default function (opts) { + const { profile, websocketServer, src, dist, entryPoints, target } = opts + + const isProd = profile.toLowerCase() === 'prod'; + const sourceMap = !isProd + + const safeEnvVar = { + commitId: readStdout("git rev-parse --short HEAD"), + date: readStdout("date --rfc-3339=seconds"), + nodeVersion: readStdout("node --version"), + rollupVersion: readStdout("npx rollup --version"), + os: os.platform(), + profile, + websocketServer, + target, + } + + function getPlugins(entrypoint) { + return [ + commonjs(), + replace({ + values: { + // avoid "Function constructor" warning + "Function('return this')()": "globalThis", + }, + delimiters: ["", ""], + preventAssignment: true + }), + replace({ + __ENV: JSON.stringify(safeEnvVar), + __BUILD_PROFILE: JSON.stringify(profile), + preventAssignment: true + }), + svelte({ + preprocess: autoPreprocess(), + compilerOptions: { + customElement: useCustomElement.includes(entrypoint) + } + }), + typescript({ sourceMap: false }), + resolve({ browser: true }), + isProd && terser(), + ] + } + + const output = [] + + for (const e of entryPoints) { + output.push({ + external: ["chai", "mocha", "Mocha"], + input: pathLib.join(src, e, "main.ts"), + output: { + sourcemap: sourceMap, + globals: { + "chai": "chai", + "mocha": "mocha", + "Mocha": "Mocha" + }, + file: pathLib.join(dist, e, "main.js"), + format: 'iife' + }, + plugins: getPlugins(e) + }) + } + + return output +} \ No newline at end of file diff --git a/scripts/cli.mjs b/scripts/cli.mjs new file mode 100644 index 0000000..a486844 --- /dev/null +++ b/scripts/cli.mjs @@ -0,0 +1,295 @@ +import { program } from 'commander' +import fs from 'fs-extra' +import { spawn } from 'node:child_process' +import pathLib from 'node:path' +import * as rollup from 'rollup' +import webExt from 'web-ext' +import { WebSocketServer } from 'ws' +import zipDir from 'zip-dir' +import buildRollupConfig from './build_rollup.mjs' +import genManifest from './gen_manifest.mjs' +import { isTestTarget, mustEnv } from './utils.mjs' + +const BUILD_DIR = mustEnv("BUILD_DIR", "./build") +const BUILD_SRC = mustEnv("BUILD_SRC", "./src") +const BUILD_VERSION = mustEnv("BUILD_VERSION", "2.1.0") + +const FIREFOX = "firefox-developer-edition" +function copyAssets(destDir) { + const assets = [ + { + src: pathLib.join(BUILD_SRC, "options/options.html"), + dest: pathLib.join(destDir, "options/options.html") + }, + { + src: pathLib.join(BUILD_SRC, "/icon"), + dest: pathLib.join(destDir, "/icon") + }, + { + src: pathLib.join(BUILD_SRC, "/_locales"), + dest: pathLib.join(destDir, "/_locales") + }, + + { + src: "node_modules/simpledotcss/simple.min.css", + dest: pathLib.join(destDir, "res/simple.min.css") + }, + ] + for (const entry of assets) { + fs.copySync(entry.src, entry.dest) + } +} + +function copyTestAssets(dest) { + dest = pathLib.join(dest, "/test") + const assets = [ + { + src: pathLib.join("node_modules/mocha/mocha.js"), + dest: pathLib.join(dest, "mocha.js") + }, + { + src: pathLib.join("node_modules/mocha/mocha.css"), + dest: pathLib.join(dest, "mocha.css") + }, + { + src: pathLib.join("node_modules/chai/chai.js"), + dest: pathLib.join(dest, "chai.js") + }, + { + src: pathLib.join(BUILD_SRC, "/test/mocha.html"), + dest: pathLib.join(dest, "mocha.html") + } + ] + for (const entry of assets) { + fs.copySync(entry.src, entry.dest) + } +} + +function buildDist(target) { + return pathLib.join(BUILD_DIR, target, "dist") +} + +function buildManifest(dist, version, target) { + const manifest = genManifest({ + version: version, + target: target, + }) + fs.writeFileSync(pathLib.join(dist, "manifest.json"), JSON.stringify(manifest, null, " ")) +} + +async function buildWithRollup(config) { + for (const optionsObj of config) { + try { + const bundle = await rollup.rollup(optionsObj); + await bundle.write(optionsObj.output); + if (bundle) { + // closes the bundle + await bundle.close(); + } + } catch (err) { + console.error(err) + } + } +} + +function validateTarget(target) { + if (!["firefox", "chromium", "firefox-test"].includes(target)) { + throw new Error("not support build target: " + target) + } +} + +const defaultEntryPoints = ["background", "content_scripts", "options", "components"] + +program.command('build') + .description('Build extension') + .option('-t, --target ', "The browser target to build", "firefox") + .option('--profile ', "The profile of compilation", "debug") + .option('--artifacts ', "The directory to store artifacts", "artifacts") + .option('--lint', "Use web-ext the validate extension source", false) + .option('--watch', "Watch source file change", false) + .option('--websocket-server ', "The address of websocket server for capture event of unit test", "ws://localhost:8000") + .action(async (args, options) => { + validateTarget(args.target) + const dist = pathLib.join(BUILD_DIR, args.target, "dist") + copyAssets(dist) + buildManifest(dist, BUILD_VERSION, args.target) + + const entryPoints = [...defaultEntryPoints] + if (isTestTarget(args.target)) { + entryPoints.push("test") + copyTestAssets() + } + console.debug("args:", args) + console.debug("entryPoints: ", entryPoints) + + const config = buildRollupConfig({ + profile: args.profile, + src: BUILD_SRC, + dist: dist, + entryPoints: entryPoints, + watch: args.watch, + target: args.target, + websocketServer: args.websocketServer, + }) + const artifacts = pathLib.join( + BUILD_DIR, + args.artifacts + ) + fs.ensureDirSync(artifacts) + + if (args.watch) { + const watcher = rollup.watch(config) + watcher.on('event', (event) => { + if (event.code === "BUNDLE_END") { + console.log("code: ", event.code, "input:", event.input, "output:", event.output) + } + const { result } = event; + if (result) { + result.close(); + } + }); + } else { + await buildWithRollup(config) + + if (args.lint) { + webExt.cmd.lint({ sourceDir: dist }) + } + await zipDir( + dist, + { + saveTo: pathLib.join( + artifacts, + `glitterdrag-pro-${BUILD_VERSION}-${args.target}.zip` + ) + } + ); + } + + }); + +program.command('test') + .description('Start Firefox browser and run unit tests') + .option('-t, --target ', "The target to build", "firefox-test") + .option('-s, --websocket-server', "The address of websocket server for capture event of unit test", "ws://localhost:8000") + .action(async (args) => { + validateTarget(args.target) + const dist = buildDist(args.target) + + async function runBrowser(signal) { + + return new Promise((resolve, reject) => { + + const proc = spawn("pnpm", ["exec", "web-ext", "run", "-f", FIREFOX, "-s", dist], { + stdio: ["ignore", process.stdout, process.stderr], + signal: signal + }) + + proc.on("error", (err) => { + console.error(err) + resolve() + }) + + proc.on("close", (code) => { + if (code === 0) { + resolve() + } else { + reject("code: " + code) + } + }) + }) + } + + + async function waitTestComplete() { + const url = new URL(args.websocketServer) + let port = undefined + if (url.port.length > 0) { + port = Number.parseInt(url.port) + } + const wss = new WebSocketServer({ host: url.hostname, port, }); + + return new Promise((resolve) => { + wss.on('connection', (ws) => { + ws.on('message', (data) => { + const [type, payload] = JSON.parse(data) + switch (type) { + case "start": { + ws.send("ok") + break + } + case "pass": { + ws.send("ok") + console.info("pass: %s", payload.fullTitle) + break + } + case "fail": { + ws.send("ok") + console.error("fail: %s", payload.fullTitle) + break + } + case "end": { + ws.close() + wss.close() + resolve(payload) + break + } + default: { + throw new Error("unknown event type: ", type) + } + } + }); + ws.send("ok") + }); + }) + } + + const controller = new AbortController() + const resultPromise = waitTestComplete() + const browser = runBrowser(controller.signal) + + const result = await resultPromise + await browser + console.log("tests: %d, passes: %d, failures: %d", result.tests, result.passes, result.failures) + if (result.failure > 0) { + process.exit(1) + } else { + process.exit(0) + } + }); + +program.command('watch') + .description('detect dist directory change and reload extension') + .option('-t, --target', "The target to build", "firefox") + .action((args) => { + + validateTarget(args.target) + + const dist = buildDist(args.target) + + return new Promise((resolve, reject) => { + const proc = spawn("pnpm", ["exec", "web-ext", "run", "-f", FIREFOX, "-s", dist], { + stdio: ["ignore", process.stdout, process.stderr], + }) + + proc.on("error", (err) => { + console.error(err) + resolve() + }) + + proc.on("close", (code) => { + if (code === 0) { + resolve() + } else { + reject("code: " + code) + } + }) + }) + }); + +program.command('clean') + .description('clean build directory') + .action(() => { + fs.removeSync(BUILD_DIR) + }); + +await program.parseAsync(); \ No newline at end of file diff --git a/scripts/gen_manifest.js b/scripts/gen_manifest.js deleted file mode 100644 index 4b74b7b..0000000 --- a/scripts/gen_manifest.js +++ /dev/null @@ -1,119 +0,0 @@ -import { mustEnv } from "./utils.js" - -const version = mustEnv("BUILD_VERSION") -const targetBrowser = mustEnv("TARGET_BROWSER") -const isFirefox = targetBrowser.startsWith("firefox") -const isChromium = targetBrowser.startsWith("chromium") -const isTest = targetBrowser.endsWith("test") - -function contentSecurityPolicy() { - if (isFirefox && isTest) { - // override CSP to allow mocha test connect to a insecure websocket - // https://bugzilla.mozilla.org/show_bug.cgi?id=1797086 - return { - "extension_pages": "script-src 'self'" - } - } - return undefined -} - -function manifestPermissions() { - const permissions = [ - "activeTab", - "storage", - "tabs", - "clipboardWrite", - "downloads", - "search", - "scripting", - "contextMenus", - "alarms" - ] - - if (isFirefox) { - permissions.push(...[ - "contextualIdentities", - "cookies" - ]) - } - - return permissions -} - -function manifestBackground() { - if (isChromium) { - return { - "service_worker": "background/main.js" - } - } - return { - "scripts": [ - "background/main.js" - ] - } -} - -function manifestVersion() { - return version -} - -function browserSpecificSetting() { - if (isFirefox) { - return { - "gecko": { - "id": "glitterdragpro@harytfw", - "strict_min_version": "106.0" - } - } - } - return undefined -} - -const manifest = { - "manifest_version": 3, - "name": "__MSG_extensionName__", - "description": "__MSG_extensionDescription__", - "version": manifestVersion(), - "homepage_url": "https://github.com/harytfw/GlitterDrag", - "icons": { - "128": "/icon/drag.png" - }, - "author": "harytfw", - "permissions": manifestPermissions(), - "host_permissions": [ - "*://*/*" - ], - "background": manifestBackground(), - "content_scripts": [ - { - "run_at": "document_end", - "all_frames": true, - "matches": [ - "*://*/*" - ], - "js": [ - "content_scripts/main.js" - ] - } - ], - "options_ui": { - "page": "options/options.html", - "browser_style": true, - "open_in_tab": true - }, - "web_accessible_resources": [ - { - "resources": [ - "components/main.js" - ], - "matches": [ - "*://*/*" - ] - } - ], - "default_locale": "en", - "browser_specific_settings": browserSpecificSetting(), - "content_security_policy": contentSecurityPolicy() -} - -console.log(JSON.stringify(manifest, null, " ")) \ No newline at end of file diff --git a/scripts/gen_manifest.mjs b/scripts/gen_manifest.mjs new file mode 100644 index 0000000..a9c9a56 --- /dev/null +++ b/scripts/gen_manifest.mjs @@ -0,0 +1,124 @@ +import assert from 'node:assert/strict' +export default function (opt) { + + const { version, target } = opt + + { + assert.ok(typeof version === 'string') + assert.ok(version.length > 0) + + assert.ok(typeof target === 'string') + assert.ok(target.length > 0) + } + + const isFirefox = target.startsWith("firefox") + const isChromium = target.startsWith("chromium") + const isTest = target.endsWith("test") + + function contentSecurityPolicy(opt) { + if (isFirefox && isTest) { + // override CSP to allow mocha test connect to a insecure websocket + // https://bugzilla.mozilla.org/show_bug.cgi?id=1797086 + return { + "extension_pages": "script-src 'self'" + } + } + return undefined + } + + function manifestPermissions() { + const permissions = [ + "activeTab", + "storage", + "tabs", + "clipboardWrite", + "downloads", + "search", + "scripting", + "contextMenus", + "alarms" + ] + + if (isFirefox) { + permissions.push(...[ + "contextualIdentities", + "cookies" + ]) + } + + return permissions + } + + function manifestBackground() { + if (isChromium) { + return { + "service_worker": "background/main.js" + } + } + return { + "scripts": [ + "background/main.js" + ] + } + } + + function browserSpecificSetting() { + if (isFirefox) { + return { + "gecko": { + "id": "glitterdragpro@harytfw", + "strict_min_version": "106.0" + } + } + } + return undefined + } + + const manifest = { + "manifest_version": 3, + "name": "__MSG_extensionName__", + "description": "__MSG_extensionDescription__", + "version": version, + "homepage_url": "https://github.com/harytfw/GlitterDrag", + "icons": { + "128": "/icon/drag.png" + }, + "author": "harytfw", + "permissions": manifestPermissions(), + "host_permissions": [ + "*://*/*" + ], + "background": manifestBackground(), + "content_scripts": [ + { + "run_at": "document_end", + "all_frames": true, + "matches": [ + "*://*/*" + ], + "js": [ + "content_scripts/main.js" + ] + } + ], + "options_ui": { + "page": "options/options.html", + "browser_style": true, + "open_in_tab": true + }, + "web_accessible_resources": [ + { + "resources": [ + "components/main.js" + ], + "matches": [ + "*://*/*" + ] + } + ], + "default_locale": "en", + "browser_specific_settings": browserSpecificSetting(), + "content_security_policy": contentSecurityPolicy() + } + return manifest +} diff --git a/scripts/rollup.config.js b/scripts/rollup.config.js deleted file mode 100644 index 560c9b8..0000000 --- a/scripts/rollup.config.js +++ /dev/null @@ -1,94 +0,0 @@ -import { mustEnv, readStdout } from "./utils.js" - -import svelte from 'rollup-plugin-svelte'; -import resolve from '@rollup/plugin-node-resolve'; -import typescript from '@rollup/plugin-typescript'; -import replace from '@rollup/plugin-replace'; -import { terser } from 'rollup-plugin-terser'; -import autoPreprocess from 'svelte-preprocess'; -import commonjs from '@rollup/plugin-commonjs'; -import pathLib from 'node:path' -import os from "node:os" - -const BUILD_COMMIT_ID = readStdout("git rev-parse --short HEAD") -const BUILD_DATE = readStdout("date --rfc-3339=seconds") -const BUILD_NODE_VERSION = readStdout("node --version") -const BUILD_ROLLUP_VERSION = readStdout("npx rollup --version") -const BUILD_OS = os.platform() -const BUILD_PROFILE = mustEnv("BUILD_PROFILE") -const BUILD_WEBSOCKET_SERVER = mustEnv("BUILD_WEBSOCKET_SERVER", "") - -const isProd = BUILD_PROFILE.toLowerCase() === 'prod'; -const sourceMap = !isProd -const src = mustEnv("SRC") -const dist = mustEnv("TARGET_DIST") -const entryPoints = mustEnv("ENTRY_POINTS").split(" ") - -const safeEnvVar = { - BUILD_COMMIT_ID, - BUILD_DATE, - BUILD_NODE_VERSION, - BUILD_ROLLUP_VERSION, - BUILD_OS, - BUILD_PROFILE, - BUILD_WEBSOCKET_SERVER, -} - -const useCustomElement = ["components"] - -function getPlugins(entrypoint) { - const plugins = [ - commonjs(), - replace({ - values: { - // avoid "Function constructor" warning - "Function('return this')()": "globalThis", - }, - delimiters: ["", ""], - preventAssignment: true - }), - replace({ - __ENV: JSON.stringify(safeEnvVar), - __BUILD_PROFILE: JSON.stringify(BUILD_PROFILE), - preventAssignment: true - }), - svelte({ - preprocess: autoPreprocess(), - compilerOptions: { - customElement: useCustomElement.includes(entrypoint) - } - }), - typescript({ sourceMap: false }), - resolve({ browser: true }), - ] - - if (isProd) { - plugins.push(terser()) - } - - return plugins -} - -console.log("sourcemap: ", sourceMap) - -const output = [] - -for (const e of entryPoints) { - output.push({ - external: ["chai", "mocha", "Mocha"], - input: pathLib.join(src, e, "main.ts"), - output: { - sourcemap: sourceMap, - globals: { - "chai": "chai", - "mocha": "mocha", - "Mocha": "Mocha" - }, - file: pathLib.join(dist, e, "main.js"), - format: 'iife' - }, - plugins: getPlugins(e) - }) -} - -export default output diff --git a/scripts/test_bootstrap.js b/scripts/test_bootstrap.js deleted file mode 100644 index ffd3c86..0000000 --- a/scripts/test_bootstrap.js +++ /dev/null @@ -1,95 +0,0 @@ -import { WebSocketServer } from 'ws'; -import { spawn } from 'node:child_process'; -import webExt from 'web-ext' - - -const TEST_TIMEOUT_MS = 1000 * 60 * 5 - -const { TARGET_DIST, BUILD_WEBSOCKET_SERVER } = process.env - -async function runBrowser(signal) { - - return new Promise((resolve, reject) => { - - const proc = spawn("pnpm", ["exec", "web-ext", "run", "-f", "firefox-developer-edition" , "-s", TARGET_DIST], { - stdio: ["ignore", process.stdout, process.stderr], - signal: signal - }) - - proc.on("error", (err) => { - console.error(err) - resolve() - }) - - proc.on("close", (code) => { - if (code === 0) { - resolve() - } else { - reject("code: " + code) - } - }) - }) -} - -async function waitTestComplete() { - const url = new URL(BUILD_WEBSOCKET_SERVER) - let port = undefined - if (url.port.length > 0) { - port = Number.parseInt(url.port) - } - const wss = new WebSocketServer({ host: url.hostname, port, }); - - return new Promise((resolve) => { - wss.on('connection', (ws) => { - ws.on('message', (data) => { - const [type, payload] = JSON.parse(data) - switch (type) { - case "start": { - ws.send("ok") - break - } - case "pass": { - ws.send("ok") - console.info("pass: %s", payload.fullTitle) - break - } - case "fail": { - ws.send("ok") - console.error("fail: %s", payload.fullTitle) - break - } - case "end": { - ws.close() - wss.close() - resolve(payload) - break - } - default: { - throw new Error("unknown event type: ", type) - } - } - }); - ws.send("ok") - }); - }) -} - -async function main() { - const controller = new AbortController() - - const browser = runBrowser(controller.signal) - - const summary = await waitTestComplete() - - await browser - - console.log("tests: %d, passes: %d, failures: %d", summary.tests, summary.passes, summary.failures) - // console.log(summary) - if (summary.failure > 0) { - process.exit(1) - } else { - process.exit(0) - } -} - -main() \ No newline at end of file diff --git a/scripts/utils.js b/scripts/utils.mjs similarity index 85% rename from scripts/utils.js rename to scripts/utils.mjs index ec7cab8..caa21af 100644 --- a/scripts/utils.js +++ b/scripts/utils.mjs @@ -18,3 +18,7 @@ export function readStdout(command) { const result = execSync(command) return result.toString("utf8") } + +export function isTestTarget(target) { + return target.endsWith("test") +} diff --git a/src/build_info.ts b/src/build_info.ts index 368d511..fb0e4f1 100644 --- a/src/build_info.ts +++ b/src/build_info.ts @@ -1,12 +1,4 @@ const env = __ENV -export default { - commitId: env["BUILD_COMMIT_ID"], - date: env["BUILD_DATE"], - nodeVersion: env["BUILD_NODE_VERSION"], - rollupVersion: env["BUILD_ROLLUP_VERSION"], - os: env["BUILD_OS"], - websocketServer: env["BUILD_WEBSOCKET_SERVER"], - profile: env["BUILD_PROFILE"], -} as const +export default env diff --git a/src/global.d.ts b/src/global.d.ts index 2f1a245..676b83f 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,8 +1,18 @@ import { Mocha } from 'mocha' +type EnvVariable = { + commitId: string + date: string + nodeVersion: string + rollupVersion: string + os: string + websocketServer: string + profile: string + target: string +} export declare global { - export const __ENV: Record + export const __ENV: EnvVariable export const __BUILD_PROFILE: 'prod' | 'debug' export const chrome: any export const Mocha: Mocha diff --git a/src/test/mocha_init.ts b/src/test/mocha_init.ts index 45988f9..f9511ff 100644 --- a/src/test/mocha_init.ts +++ b/src/test/mocha_init.ts @@ -67,7 +67,7 @@ async function closeInstance() { } export let ws = null -if (buildInfo.websocketServer) { +if (buildInfo.target.endsWith("test") && buildInfo.websocketServer) { ws = new WebSocket(buildInfo.websocketServer) ws.addEventListener("open", (event) => { console.log("open", event) From 1165bdac9b58365bdc848f3f1ad5210c4cff35dd Mon Sep 17 00:00:00 2001 From: harytfw Date: Tue, 10 Jan 2023 14:31:27 +0800 Subject: [PATCH 10/21] refactor: clean code about menu Signed-off-by: harytfw --- src/components/menu/menu.svelte | 29 +++++------- src/components/menu/menu.ts | 31 +++++++------ src/components/menu/menu_builder.test.ts | 5 ++- src/components/menu/menu_builder.ts | 56 +++++++++++++----------- src/components/types.ts | 3 ++ 5 files changed, 66 insertions(+), 58 deletions(-) diff --git a/src/components/menu/menu.svelte b/src/components/menu/menu.svelte index 9f75599..eb44c77 100644 --- a/src/components/menu/menu.svelte +++ b/src/components/menu/menu.svelte @@ -3,11 +3,7 @@ @@ -99,14 +95,13 @@ id="container" on:dragover={dragover} on:dragleave={dragleave} - width={box()[0]} - height={box()[1]} + style="width: {box[0]}px; height: {box[1]}px;" bind:this={container} > diff --git a/src/components/menu/menu.ts b/src/components/menu/menu.ts index cb3d3d6..cc2b727 100644 --- a/src/components/menu/menu.ts +++ b/src/components/menu/menu.ts @@ -8,35 +8,27 @@ import { LogLevel } from '../../config/config'; const log = rootLog.subLogger(LogLevel.VVV, "menu") const tag = "glitterdrag-menu" -function computeStyle(x: number, y: number, width: number, height: number) { - const style = `position: absolute; - left: ${x}px; - top: ${y}px; - z-index: 2147483647; - width: ${width}px; - height: ${height}px; - user-select: none; - ` - return style -} class MenuImpl extends MessageTarget implements MenuInterface { + elem: MenuElement & HTMLElement constructor() { super(ProxyEventType.Menu) customElements.define(tag, MenuElement as any); this.elem = document.createElement(tag) as HTMLElement & MenuElement - this.elem.setAttribute("style", computeStyle(0, 0, 0, 0)); + this.elem.setAttribute("style", this.computeStyle(0, 0, 0, 0)); } show(opts: ShowMenuOptions) { log.V("show menu: ", opts) - const [width, height] = this.elem.box() + const [width, height] = this.elem.box + const x = opts.position.x - width / 2 const y = opts.position.y - height / 2 - this.elem.setAttribute("style", computeStyle(x, y, width, height)); + + this.elem.setAttribute("style", this.computeStyle(x, y, width, height)); this.elem.show(opts) !this.elem.parentElement && document.body.append(this.elem) } @@ -50,6 +42,17 @@ class MenuImpl extends MessageTarget implements MenuInterface { this.elem.remove() } + computeStyle(x: number, y: number, width: number, height: number) { + const style = `position: absolute; + left: ${x}px; + top: ${y}px; + z-index: 2147483647; + width: ${width}px; + height: ${height}px; + user-select: none; + ` + return style + } } export const menuImpl = new MenuImpl() \ No newline at end of file diff --git a/src/components/menu/menu_builder.test.ts b/src/components/menu/menu_builder.test.ts index 119c4d8..d86af46 100644 --- a/src/components/menu/menu_builder.test.ts +++ b/src/components/menu/menu_builder.test.ts @@ -56,7 +56,10 @@ describe("menu builder", () => { dividerLineLength: 10, iconOffset: 10, iconSize: 10, - textOffset: 10 + textOffset: 10, + fontSize: 4, + width: 100, + height: 100 }) assert.equal(svg.querySelectorAll("g").length, 2) }) diff --git a/src/components/menu/menu_builder.ts b/src/components/menu/menu_builder.ts index aa27ed0..731cfa7 100644 --- a/src/components/menu/menu_builder.ts +++ b/src/components/menu/menu_builder.ts @@ -17,31 +17,6 @@ const colorDef = ` id="stop14618" /> `; -const styleContent = ` - g .sector-fill { - fill: #fff0; - stroke: none; - } - - g:hover .sector-fill { - fill: url("#sector-color"); - } - - g.hover .sector-fill { - fill: url("#sector-color"); - } - - g.hover .sector-fill text { - fill: white; - } - - g text { - font-size: 4px; - fill: white; - text-shadow: #000 0px 0 4px; - font-weight: bold; - } -`; export function polar2cartesian( r: number, @@ -80,7 +55,7 @@ export async function toDataURL(text: string, type: string): Promise { export async function rebuildMenu(svg: SVGSVGElement, opt: MenuOptions) { - const [width, height] = [200, 200]; + const { width, height } = opt const center = [width / 2, height / 2]; const arcRadius = opt.dividerLineLength + opt.circleRadius; const angleUnit = 360 / opt.items.length; @@ -96,6 +71,35 @@ export async function rebuildMenu(svg: SVGSVGElement, opt: MenuOptions) { svg.dataset["iconOffset"] = `${opt.iconOffset}`; svg.dataset["iconSize"] = `${opt.iconSize}`; svg.dataset["textOffset"] = `${opt.textOffset}`; + svg.dataset["fontSize"] = `${opt.fontSize}`; + + + const styleContent = ` + g .sector-fill { + fill: #fff0; + stroke: none; + } + + g:hover .sector-fill { + fill: url("#sector-color"); + } + + g.hover .sector-fill { + fill: url("#sector-color"); + } + + g.hover .sector-fill text { + fill: white; + } + + g text { + font-size: ${opt.fontSize}px; + fill: white; + text-shadow: #000 0px 0 4px; + font-weight: bold; + } +`; + { const defs = document.createElementNS(SVG_NAMESPACE, "defs"); diff --git a/src/components/types.ts b/src/components/types.ts index 92b574b..e78c96c 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -75,4 +75,7 @@ export interface MenuOptions { iconOffset: number; iconSize: number; textOffset: number; + fontSize: number; + width: number; + height: number; } From 66aa94385deffd0e5354b2e87aedf55946c83ca5 Mon Sep 17 00:00:00 2001 From: harytfw Date: Wed, 18 Jan 2023 18:25:58 +0800 Subject: [PATCH 11/21] refactor: clean code about cli.mjs Signed-off-by: harytfw --- scripts/cli.mjs | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/scripts/cli.mjs b/scripts/cli.mjs index a486844..ddc05ed 100644 --- a/scripts/cli.mjs +++ b/scripts/cli.mjs @@ -15,6 +15,7 @@ const BUILD_SRC = mustEnv("BUILD_SRC", "./src") const BUILD_VERSION = mustEnv("BUILD_VERSION", "2.1.0") const FIREFOX = "firefox-developer-edition" + function copyAssets(destDir) { const assets = [ { @@ -79,23 +80,19 @@ function buildManifest(dist, version, target) { async function buildWithRollup(config) { for (const optionsObj of config) { - try { - const bundle = await rollup.rollup(optionsObj); - await bundle.write(optionsObj.output); - if (bundle) { - // closes the bundle - await bundle.close(); - } - } catch (err) { - console.error(err) + const bundle = await rollup.rollup(optionsObj); + await bundle.write(optionsObj.output); + if (bundle) { + await bundle.close(); } } } function validateTarget(target) { - if (!["firefox", "chromium", "firefox-test"].includes(target)) { - throw new Error("not support build target: " + target) + if (["firefox", "chromium", "firefox-test"].includes(target)) { + return } + throw new Error("not support build target: " + target) } const defaultEntryPoints = ["background", "content_scripts", "options", "components"] @@ -105,7 +102,7 @@ program.command('build') .option('-t, --target ', "The browser target to build", "firefox") .option('--profile ', "The profile of compilation", "debug") .option('--artifacts ', "The directory to store artifacts", "artifacts") - .option('--lint', "Use web-ext the validate extension source", false) + .option('--lint', "Use web-ext validates extension source", false) .option('--watch', "Watch source file change", false) .option('--websocket-server ', "The address of websocket server for capture event of unit test", "ws://localhost:8000") .action(async (args, options) => { @@ -263,7 +260,7 @@ program.command('watch') .action((args) => { validateTarget(args.target) - + const dist = buildDist(args.target) return new Promise((resolve, reject) => { From 26a48df37217fa0b42698e014aec65cd15d17ef8 Mon Sep 17 00:00:00 2001 From: harytfw Date: Wed, 18 Jan 2023 18:32:32 +0800 Subject: [PATCH 12/21] test: clean code about unit test Signed-off-by: harytfw --- Makefile | 2 +- src/background/executor.test.ts | 2 +- src/background/search.test.ts | 2 +- src/background/utils.test.ts | 16 ++++++++-------- src/config/config.test.ts | 9 +++------ src/content_scripts/utils.test.ts | 20 ++++++++++---------- src/utils/test.ts | 8 ++++++++ src/utils/test_helper.ts | 22 ---------------------- src/utils/var_substitute.test.ts | 1 - 9 files changed, 32 insertions(+), 50 deletions(-) create mode 100644 src/utils/test.ts delete mode 100644 src/utils/test_helper.ts diff --git a/Makefile b/Makefile index 40d6f81..3fb7c19 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ ext-chromium: $(cli) build --target chromium --profile $(BUILD_PROFILE) .PHONY: test -test: TARGET ?= firefox +test: TARGET ?= firefox-test test: $(cli) test --target $(TARGET) diff --git a/src/background/executor.test.ts b/src/background/executor.test.ts index e84b3b6..88f0d30 100644 --- a/src/background/executor.test.ts +++ b/src/background/executor.test.ts @@ -1,7 +1,7 @@ import browser, { tabs } from 'webextension-polyfill'; import { ActionConfig, CommandKind } from "../config/config" -import { closeTab } from '../utils/test_helper'; +import { closeTab } from '../utils/test'; import { Executor } from "./executor" import { blankExecuteContext } from "../context/test_helper" diff --git a/src/background/search.test.ts b/src/background/search.test.ts index ceb6ac8..bbf17ee 100644 --- a/src/background/search.test.ts +++ b/src/background/search.test.ts @@ -1,5 +1,5 @@ import browser from 'webextension-polyfill' -import { closeTab } from '../utils/test_helper' +import { closeTab } from '../utils/test' import { searchText } from './search' describe("test browser search", () => { diff --git a/src/background/utils.test.ts b/src/background/utils.test.ts index 47fe91a..d0d874f 100644 --- a/src/background/utils.test.ts +++ b/src/background/utils.test.ts @@ -1,5 +1,5 @@ import browser from 'webextension-polyfill' -import { assertOk } from "../utils/test_helper" +import { assert } from "chai" import { blankExecuteContext } from "../context/test_helper" import { bufferToObjectURL, buildDownloadableURL, generatedDownloadFileName } from "./utils" @@ -9,44 +9,44 @@ describe("background utils", () => { it("text", async () => { const ctx = await blankExecuteContext() ctx.data.selection = "hello world" - assertOk(generatedDownloadFileName(ctx, await buildDownloadableURL(ctx))) + assert.ok(generatedDownloadFileName(ctx, await buildDownloadableURL(ctx))) }) it("image", async () => { const ctx = await blankExecuteContext() ctx.data.imageSource = "http://example.com/a.jpg" - assertOk(generatedDownloadFileName(ctx, await buildDownloadableURL(ctx))) + assert.ok(generatedDownloadFileName(ctx, await buildDownloadableURL(ctx))) }) it("link", async () => { const ctx = await blankExecuteContext() ctx.data.link = "http://example.com/" - assertOk(generatedDownloadFileName(ctx, await buildDownloadableURL(ctx))) + assert.ok(generatedDownloadFileName(ctx, await buildDownloadableURL(ctx))) }) }) it("buffer to object url", () => { - assertOk(bufferToObjectURL(new ArrayBuffer(10))) + assert.ok(bufferToObjectURL(new ArrayBuffer(10))) }) it("build url for text", async () => { let ctx = await blankExecuteContext() ctx.data.selection = "hello" - assertOk(buildDownloadableURL(ctx)) + assert.ok(buildDownloadableURL(ctx)) }) it("build url for image", async () => { let ctx = await blankExecuteContext() ctx.data.imageSource = browser.runtime.getURL("icon/drag.png") - assertOk(buildDownloadableURL(ctx)) + assert.ok(buildDownloadableURL(ctx)) }) it("build url for link", async () => { let ctx = await blankExecuteContext() ctx.data.link = "http://www.example.com" - assertOk(buildDownloadableURL(ctx)) + assert.ok(buildDownloadableURL(ctx)) }) }) \ No newline at end of file diff --git a/src/config/config.test.ts b/src/config/config.test.ts index fc514db..a9ce249 100644 --- a/src/config/config.test.ts +++ b/src/config/config.test.ts @@ -1,9 +1,6 @@ -import { assertOk } from "../utils/test_helper" +import { assert } from "chai" import { ActionConfig, BroadcastEventTarget, CommandRequest, configBroadcast as configBroadcast, Configuration, type ReadonlyConfiguration } from "./config" -import chai from "chai" -const assert = chai.assert - describe("test configuration", () => { it("empty config", () => { new Configuration() @@ -11,14 +8,14 @@ describe("test configuration", () => { it("action config", () => { const action = new ActionConfig({}) - assertOk(action.toPlainObject()) + assert.ok(action.toPlainObject()) }) it("request config", () => { const req = new CommandRequest({ "url": "http://example.com" }) - assertOk(req.toPlainObject()) + assert.ok(req.toPlainObject()) }) it("broadcast", () => { diff --git a/src/content_scripts/utils.test.ts b/src/content_scripts/utils.test.ts index e617d59..cfb725a 100644 --- a/src/content_scripts/utils.test.ts +++ b/src/content_scripts/utils.test.ts @@ -1,4 +1,4 @@ -import { assertOk } from "../utils/test_helper" +import { assert } from "chai" import { getAngle, TinyLRU } from "./utils" describe("content script utils", () => { @@ -27,28 +27,28 @@ describe("content script utils", () => { ] for (const tt of testCases) { let a = getAngle(tt.args[0], tt.args[1]) - assertOk(tt.angle === a, tt.angle, " != ", a) + assert.ok(tt.angle === a) } }) it('tiny lru', () => { const lru = new TinyLRU() - assertOk(lru.get(1) === undefined) - assertOk(lru.get(2) === undefined) - assertOk(lru.get(undefined) === undefined) + assert.ok(lru.get(1) === undefined) + assert.ok(lru.get(2) === undefined) + assert.ok(lru.get(undefined) === undefined) lru.put(1, 1) lru.put(2, 2) - assertOk(lru.get(1) === 1) - assertOk(lru.get(2) === 2) + assert.ok(lru.get(1) === 1) + assert.ok(lru.get(2) === 2) - assertOk(lru.get(0) === undefined) - assertOk(lru.size() === 2) + assert.ok(lru.get(0) === undefined) + assert.ok(lru.size() === 2) lru.clear() - assertOk(lru.size() === 0) + assert.ok(lru.size() === 0) }) }) \ No newline at end of file diff --git a/src/utils/test.ts b/src/utils/test.ts new file mode 100644 index 0000000..9a00a4f --- /dev/null +++ b/src/utils/test.ts @@ -0,0 +1,8 @@ +import browser from 'webextension-polyfill'; + +export async function closeTab(tabIds: number | number[], delayMs?: number) { + if (typeof delayMs === 'number') { + await new Promise(r => setTimeout(r, delayMs)) + } + return browser.tabs.remove(tabIds) +} \ No newline at end of file diff --git a/src/utils/test_helper.ts b/src/utils/test_helper.ts deleted file mode 100644 index 64906c3..0000000 --- a/src/utils/test_helper.ts +++ /dev/null @@ -1,22 +0,0 @@ - -import browser from 'webextension-polyfill'; -import { assert } from 'chai' - -export function assertEqual(expected: any, actual: any, ...message: any[]) { - return assert.deepEqual(actual, expected, ...message) -} - -export function assertOk(value: any, ...message: any[]) { - return assert.ok(value, ...message) -} - -export function assertFail(value: any, ...message: any[]) { - return assert.notOk(value, ...message) -} - -export async function closeTab(tabIds: number | number[], delayMs?: number) { - if (typeof delayMs === 'number') { - await new Promise(r => setTimeout(r, delayMs)) - } - return browser.tabs.remove(tabIds) -} \ No newline at end of file diff --git a/src/utils/var_substitute.test.ts b/src/utils/var_substitute.test.ts index d0ccb5c..f292d5c 100644 --- a/src/utils/var_substitute.test.ts +++ b/src/utils/var_substitute.test.ts @@ -1,4 +1,3 @@ -import { assertEqual } from "./test_helper" import { VarSubstituteTemplate } from "./var_substitute" import { assert } from 'chai' From 595845daf035fc22efc100bc83a352c99c8e0537 Mon Sep 17 00:00:00 2001 From: harytfw Date: Wed, 18 Jan 2023 19:43:07 +0800 Subject: [PATCH 13/21] chore: fix archive file was not generated when enable lint flag Signed-off-by: harytfw --- scripts/cli.mjs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/scripts/cli.mjs b/scripts/cli.mjs index ddc05ed..e7f425e 100644 --- a/scripts/cli.mjs +++ b/scripts/cli.mjs @@ -147,10 +147,6 @@ program.command('build') }); } else { await buildWithRollup(config) - - if (args.lint) { - webExt.cmd.lint({ sourceDir: dist }) - } await zipDir( dist, { @@ -161,7 +157,10 @@ program.command('build') } ); } - + if (args.lint) { + // linter will terminate program with proper exit code + await webExt.cmd.lint({ sourceDir: dist }, { shouldExitProgram: true }) + } }); program.command('test') From 62374fc0a9dd6a3d0d213083713defd5e2ef07fa Mon Sep 17 00:00:00 2001 From: harytfw Date: Thu, 19 Jan 2023 11:38:33 +0800 Subject: [PATCH 14/21] feat: add feature tab to manage features Signed-off-by: harytfw --- src/config/config.ts | 1 + src/options/common.ts | 12 +++++-- src/options/feature.svelte | 53 +++++++++++++++++++++++++++ src/options/nav.svelte | 73 ++++++++++++++++++++++++++++++-------- src/options/options.svelte | 3 ++ src/options/store.ts | 1 + 6 files changed, 127 insertions(+), 16 deletions(-) create mode 100644 src/options/feature.svelte diff --git a/src/config/config.ts b/src/config/config.ts index 7bd4a7a..fb7d1ea 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -84,6 +84,7 @@ export type ContextData = { }; export enum Feature { + featureTab = "featureTab", middleButtonSelector = "middleButtonSelector", retainComponent = "retainComponent", auxClose = "auxClose", diff --git a/src/options/common.ts b/src/options/common.ts index 4f41775..95889b0 100644 --- a/src/options/common.ts +++ b/src/options/common.ts @@ -1,6 +1,5 @@ -import { CommandKind, Direction, OperationMode, TabPosition, ContextType, ContextDataType, CompatibilityStatus } from "../config/config"; +import { CommandKind, Direction, OperationMode, TabPosition, ContextType, ContextDataType, CompatibilityStatus, Feature } from "../config/config"; import { LocaleMessageHelper } from "../locale"; -import { titleCase } from "./utils"; const localeHelper = new LocaleMessageHelper() @@ -14,6 +13,7 @@ export enum Tab { common = "common", configEditor = "configEditor", compatibility = "compatibility", + feature = "feature", } @@ -100,4 +100,12 @@ export const compatibilityStatusOptions: Readonly> = Object.values(Feature) + .map(s => { + return { + label: s as string, + value: s, + } }) \ No newline at end of file diff --git a/src/options/feature.svelte b/src/options/feature.svelte new file mode 100644 index 0000000..b0568b0 --- /dev/null +++ b/src/options/feature.svelte @@ -0,0 +1,53 @@ + + + + +
+
+ {#each featureOptions as featureOption} + + {/each} +
+
diff --git a/src/options/nav.svelte b/src/options/nav.svelte index d913f3b..662a4ca 100644 --- a/src/options/nav.svelte +++ b/src/options/nav.svelte @@ -1,21 +1,23 @@ -