extension-create 4.1.18 → 4.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/lib/messages.d.ts +1 -0
- package/dist/lib/project-name.d.ts +1 -0
- package/dist/module.cjs +43 -5
- package/dist/steps/import-external-template.d.ts +10 -1
- package/dist/steps/write-package-json.d.ts +0 -2
- package/dist/test-template-javascript/.extension-create.json +3 -3
- package/dist/test-template-javascript/STORE.md +1 -1
- package/dist/test-template-javascript/package.json +1 -1
- package/dist/test-template-javascript/src/background.ts +46 -7
- package/dist/test-template-javascript/src/content/ContentApp.ts +20 -11
- package/dist/test-template-javascript/src/content/scripts.ts +1 -0
- package/dist/test-template-javascript/src/content/styles.css +9 -3
- package/dist/test-template-javascript/src/sidebar/styles.css +6 -4
- package/package.json +2 -2
- package/templates/javascript/src/background.js +46 -7
- package/templates/javascript/src/content/ContentApp.js +23 -17
- package/templates/javascript/src/content/scripts.js +1 -0
- package/templates/javascript/src/content/styles.css +9 -3
- package/templates/javascript/src/sidebar/styles.css +6 -4
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ Creates a new extension project with the specified configuration.
|
|
|
47
47
|
|
|
48
48
|
- `projectName` (string, required) - The name of your extension project
|
|
49
49
|
- `options` (object) - Configuration options
|
|
50
|
-
- `template` (string, optional) - Template name or URL. Defaults to `'javascript'` (`init` is an alias)
|
|
50
|
+
- `template` (string, optional) - Template name or URL. Defaults to `'javascript'` (`init` is an alias). A URL must use `https://`. A plain `http://` URL, and an `http://` value in `EXTENSION_CREATE_TEMPLATE_URL`, fails unless `EXTENSION_ALLOW_HTTP_TEMPLATE=true` is set
|
|
51
51
|
- `install` (boolean, optional) - Whether to install dependencies after scaffolding. Defaults to `false` so project creation is fast and users see the familiar `npm install` output on their own.
|
|
52
52
|
- `cliVersion` (string, optional) - CLI version for package.json
|
|
53
53
|
|
package/dist/lib/messages.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare function writingTypeDefinitionsError(error: unknown): string;
|
|
|
11
11
|
export declare function installingFromTemplate(projectName: string, templateName: string): string;
|
|
12
12
|
export declare function usingTemplate(templateName: string, source: string): string;
|
|
13
13
|
export declare function installingFromTemplateError(template: string, error: unknown): string;
|
|
14
|
+
export declare function templateUrlNotHttps(url: string): string;
|
|
14
15
|
export declare function templateFetchTimedOut(templateName: string, ms: number): string;
|
|
15
16
|
export declare function templateNotFoundInCatalog(templateName: string, error?: unknown): string;
|
|
16
17
|
export declare function templateDownloadFailed(templateName: string, error: unknown): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isUrlProjectName(projectNameInput: string): boolean;
|
package/dist/module.cjs
CHANGED
|
@@ -252,6 +252,9 @@ function usingTemplate(templateName, source) {
|
|
|
252
252
|
function installingFromTemplateError(template, error) {
|
|
253
253
|
return `${prefix('error')} Couldn't find the template ${external_pintor_default().blue(template)}.\n${fmt.label('REASON')} ${fmt.val(fmt.truncate(String(error)))}\n${external_pintor_default().red('Choose a template name from')} ${external_pintor_default().blue('extension create --help')}${external_pintor_default().red(', or pass a GitHub URL.')}`;
|
|
254
254
|
}
|
|
255
|
+
function templateUrlNotHttps(url) {
|
|
256
|
+
return `${prefix('error')} Can't download a template over plain HTTP.\n${fmt.label('GOT')} ${fmt.val(fmt.truncate(url, 120))}\n${external_pintor_default().red('- Use the')} ${external_pintor_default().blue('https://')} ${external_pintor_default().red('address of the template.')}\n${external_pintor_default().red('- Set')} ${external_pintor_default().blue('EXTENSION_ALLOW_HTTP_TEMPLATE=true')} ${external_pintor_default().red('to allow plain HTTP.')}`;
|
|
257
|
+
}
|
|
255
258
|
function templateFetchTimedOut(templateName, ms) {
|
|
256
259
|
return `${prefix('error')} Couldn't fetch the template ${external_pintor_default().blue(templateName)} within ${Math.round(ms / 1000)}s.\n${external_pintor_default().red('- Check your network connection.')}\n${external_pintor_default().red('- Set')} ${external_pintor_default().blue('EXTENSION_CREATE_TIMEOUT_MS')} ${external_pintor_default().red('to allow more time.')}`;
|
|
257
260
|
}
|
|
@@ -355,6 +358,9 @@ function keepingExistingGitignore(projectName) {
|
|
|
355
358
|
function existingRepositoryKept(projectName) {
|
|
356
359
|
return `${prefix('info')} ${external_pintor_default().blue(projectName)} is already a git repository with history.\nLeft it uncommitted. Review the scaffold and run ${external_pintor_default().blue('git add -A && git commit')} yourself.`;
|
|
357
360
|
}
|
|
361
|
+
function isUrlProjectName(projectNameInput) {
|
|
362
|
+
return /^https?:\/\//i.test(projectNameInput.trim());
|
|
363
|
+
}
|
|
358
364
|
const promises_namespaceObject = require("node:fs/promises");
|
|
359
365
|
async function copyDirectoryWithSymlinks(source, destination) {
|
|
360
366
|
const entries = await promises_namespaceObject.readdir(source, {
|
|
@@ -606,7 +612,7 @@ const NETWORK_TIMEOUT_MS = (()=>{
|
|
|
606
612
|
return Number.isFinite(raw) && raw > 0 ? raw : 60000;
|
|
607
613
|
})();
|
|
608
614
|
const CODELOAD_BASE = 'https://codeload.github.com/extension-js/examples/zip';
|
|
609
|
-
const DEFAULT_TEMPLATES_REF = '
|
|
615
|
+
const DEFAULT_TEMPLATES_REF = '39448a235ea1b02516e6d555bcae409db2258b7f';
|
|
610
616
|
const BUNDLED_TEMPLATES = [
|
|
611
617
|
"javascript"
|
|
612
618
|
];
|
|
@@ -655,6 +661,29 @@ class TemplateNotFoundError extends Error {
|
|
|
655
661
|
if (cause) this.cause = cause;
|
|
656
662
|
}
|
|
657
663
|
}
|
|
664
|
+
class InsecureTemplateUrlError extends Error {
|
|
665
|
+
constructor(url){
|
|
666
|
+
super(`template URL is not https: ${url}`), _define_property(this, "url", void 0);
|
|
667
|
+
this.name = 'InsecureTemplateUrlError';
|
|
668
|
+
this.url = url;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
function isRefusedHttpTemplateUrl(url) {
|
|
672
|
+
if ('true' === process.env.EXTENSION_ALLOW_HTTP_TEMPLATE) return false;
|
|
673
|
+
return /^http:\/\//i.test(url);
|
|
674
|
+
}
|
|
675
|
+
function refuseHttpRedirect(options) {
|
|
676
|
+
const target = String(options.href || `${options.protocol || ''}//`);
|
|
677
|
+
if (isRefusedHttpTemplateUrl(target)) throw new InsecureTemplateUrlError(target);
|
|
678
|
+
}
|
|
679
|
+
function findInsecureTemplateUrlError(error) {
|
|
680
|
+
let current = error;
|
|
681
|
+
for(let depth = 0; current && depth < 4; depth++){
|
|
682
|
+
if (current instanceof InsecureTemplateUrlError) return current;
|
|
683
|
+
current = current.cause;
|
|
684
|
+
}
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
658
687
|
class TemplateDownloadError extends Error {
|
|
659
688
|
constructor(templateName, cause){
|
|
660
689
|
const msg = cause?.message ?? String(cause);
|
|
@@ -674,11 +703,13 @@ async function downloadArchive(url, timeoutMs, attempts = 2) {
|
|
|
674
703
|
timeout: timeoutMs,
|
|
675
704
|
headers: {
|
|
676
705
|
'User-Agent': 'extension-create'
|
|
677
|
-
}
|
|
706
|
+
},
|
|
707
|
+
beforeRedirect: refuseHttpRedirect
|
|
678
708
|
});
|
|
679
709
|
return Buffer.from(data);
|
|
680
710
|
} catch (error) {
|
|
681
711
|
lastError = error;
|
|
712
|
+
if (findInsecureTemplateUrlError(error)) break;
|
|
682
713
|
const status = error?.response?.status;
|
|
683
714
|
const retriable = void 0 === status || 429 === status || status >= 500;
|
|
684
715
|
if (attempt >= attempts || !retriable) break;
|
|
@@ -712,6 +743,7 @@ async function extractExamplesTemplateFromZip(zipBuffer, templateName, projectPa
|
|
|
712
743
|
async function importFromExamplesCatalog(templateName, projectPath) {
|
|
713
744
|
const ref = process.env.EXTENSION_CREATE_TEMPLATE_REF || DEFAULT_TEMPLATES_REF;
|
|
714
745
|
const overrideUrl = process.env.EXTENSION_CREATE_TEMPLATE_URL || void 0;
|
|
746
|
+
if (overrideUrl && isRefusedHttpTemplateUrl(overrideUrl)) throw new InsecureTemplateUrlError(overrideUrl);
|
|
715
747
|
const urls = resolveCatalogUrls(ref, overrideUrl);
|
|
716
748
|
let buffer;
|
|
717
749
|
let source;
|
|
@@ -721,6 +753,8 @@ async function importFromExamplesCatalog(templateName, projectPath) {
|
|
|
721
753
|
source = candidate;
|
|
722
754
|
break;
|
|
723
755
|
} catch (error) {
|
|
756
|
+
const insecure = findInsecureTemplateUrlError(error);
|
|
757
|
+
if (insecure) throw insecure;
|
|
724
758
|
lastError = error;
|
|
725
759
|
}
|
|
726
760
|
if (!buffer || !source) throw new TemplateDownloadError(templateName, lastError);
|
|
@@ -884,6 +918,7 @@ async function importExternalTemplate(projectPath, projectName, template, logger
|
|
|
884
918
|
} catch {}
|
|
885
919
|
const ownerGitignore = dirExistedBeforeImport ? await readOwnerGitignore(projectPath) : null;
|
|
886
920
|
try {
|
|
921
|
+
if (isRefusedHttpTemplateUrl(template)) throw new InsecureTemplateUrlError(template);
|
|
887
922
|
await promises_namespaceObject.mkdir(projectPath, {
|
|
888
923
|
recursive: true
|
|
889
924
|
});
|
|
@@ -931,7 +966,8 @@ async function importExternalTemplate(projectPath, projectName, template, logger
|
|
|
931
966
|
const { data, headers } = await external_axios_default().get(template, {
|
|
932
967
|
responseType: 'arraybuffer',
|
|
933
968
|
maxRedirects: 5,
|
|
934
|
-
timeout: NETWORK_TIMEOUT_MS
|
|
969
|
+
timeout: NETWORK_TIMEOUT_MS,
|
|
970
|
+
beforeRedirect: refuseHttpRedirect
|
|
935
971
|
});
|
|
936
972
|
const contentType = String(headers?.['content-type'] || '');
|
|
937
973
|
const looksZip = /zip|octet-stream/i.test(contentType) || template.toLowerCase().endsWith('.zip');
|
|
@@ -974,7 +1010,9 @@ async function importExternalTemplate(projectPath, projectName, template, logger
|
|
|
974
1010
|
return fallback;
|
|
975
1011
|
}
|
|
976
1012
|
}
|
|
977
|
-
|
|
1013
|
+
const insecureUrl = findInsecureTemplateUrlError(error);
|
|
1014
|
+
if (insecureUrl) logger.error(templateUrlNotHttps(insecureUrl.url));
|
|
1015
|
+
else if (error instanceof TemplateNotFoundError) logger.error(templateNotFoundInCatalog(templateName, error.cause));
|
|
978
1016
|
else if (error instanceof TemplateDownloadError) logger.error(templateDownloadFailed(templateName, error));
|
|
979
1017
|
else logger.error(installingFromTemplateError(templateName, error));
|
|
980
1018
|
await cleanupFailedImport(projectPath, ownsProjectDir, preExistingEntries);
|
|
@@ -2100,7 +2138,7 @@ async function extensionCreate(projectNameInput, { cliVersion, template, install
|
|
|
2100
2138
|
if (!projectNameInput) throw new Error(noProjectName());
|
|
2101
2139
|
const templateWasOmitted = null == template || '' === String(template).trim();
|
|
2102
2140
|
const effectiveTemplate = templateWasOmitted ? DEFAULT_TEMPLATE_NAME : String(template);
|
|
2103
|
-
if (projectNameInput
|
|
2141
|
+
if (isUrlProjectName(projectNameInput)) throw new Error(noUrlAllowed());
|
|
2104
2142
|
const projectPath = external_node_path_namespaceObject.isAbsolute(projectNameInput) ? projectNameInput : external_node_path_namespaceObject.join(process.cwd(), projectNameInput);
|
|
2105
2143
|
const projectName = external_node_path_namespaceObject.basename(projectPath);
|
|
2106
2144
|
const updateSuffix = process.env.EXTENSION_CLI_UPDATE_SUFFIX || '';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const DEFAULT_TEMPLATES_REF = "
|
|
1
|
+
export declare const DEFAULT_TEMPLATES_REF = "39448a235ea1b02516e6d555bcae409db2258b7f";
|
|
2
2
|
export declare const BUNDLED_TEMPLATES: readonly string[];
|
|
3
3
|
export declare const DEFAULT_TEMPLATE_NAME = "typescript";
|
|
4
4
|
export declare const OFFLINE_FALLBACK_TEMPLATE = "javascript";
|
|
@@ -14,6 +14,15 @@ export declare class TemplateNotFoundError extends Error {
|
|
|
14
14
|
readonly templateName: string;
|
|
15
15
|
constructor(templateName: string, cause?: unknown);
|
|
16
16
|
}
|
|
17
|
+
export declare class InsecureTemplateUrlError extends Error {
|
|
18
|
+
readonly url: string;
|
|
19
|
+
constructor(url: string);
|
|
20
|
+
}
|
|
21
|
+
export declare function isRefusedHttpTemplateUrl(url: string): boolean;
|
|
22
|
+
export declare function refuseHttpRedirect(options: {
|
|
23
|
+
protocol?: string;
|
|
24
|
+
href?: string;
|
|
25
|
+
}): void;
|
|
17
26
|
export declare class TemplateDownloadError extends Error {
|
|
18
27
|
readonly templateName: string;
|
|
19
28
|
constructor(templateName: string, cause: unknown);
|
|
@@ -2,10 +2,8 @@ import { type ScaffoldPackageManager } from '../lib/package-manager.js';
|
|
|
2
2
|
export declare function resolveExtensionBinary(): Promise<string>;
|
|
3
3
|
export declare function getTemplateAwareScripts(template: string, extensionBinary: string): Record<string, string>;
|
|
4
4
|
interface OverridePackageJsonOptions {
|
|
5
|
-
/** Defaults to `javascript` when omitted (same as `extensionCreate`). */
|
|
6
5
|
template?: string;
|
|
7
6
|
cliVersion?: string;
|
|
8
|
-
/** The one manager the project uses; resolved from the scaffold when omitted. */
|
|
9
7
|
packageManager?: ScaffoldPackageManager;
|
|
10
8
|
}
|
|
11
9
|
export declare function resolveExtensionDevDependencyVersion(cliVersion?: string): string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"createdWith": "extension-create@4.1.
|
|
2
|
+
"createdWith": "extension-create@4.1.19",
|
|
3
3
|
"template": "typescript",
|
|
4
|
-
"source": "https://codeload.github.com/extension-js/examples/zip/
|
|
5
|
-
"ref": "
|
|
4
|
+
"source": "https://codeload.github.com/extension-js/examples/zip/39448a235ea1b02516e6d555bcae409db2258b7f",
|
|
5
|
+
"ref": "39448a235ea1b02516e6d555bcae409db2258b7f"
|
|
6
6
|
}
|
|
@@ -9,7 +9,7 @@ Packaging your extension is local and free. Submitting the result to a
|
|
|
9
9
|
store is what [extension.dev](https://docs.extension.dev/publish/overview?utm_source=store-md)
|
|
10
10
|
does, and it sponsors Extension.js.
|
|
11
11
|
|
|
12
|
-
Last updated: 2026-09-
|
|
12
|
+
Last updated: 2026-09-16
|
|
13
13
|
|
|
14
14
|
## Listing
|
|
15
15
|
|
|
@@ -6,22 +6,61 @@ const isFirefoxLike =
|
|
|
6
6
|
import.meta.env.EXTENSION_PUBLIC_BROWSER === 'firefox' ||
|
|
7
7
|
import.meta.env.EXTENSION_PUBLIC_BROWSER === 'gecko-based'
|
|
8
8
|
|
|
9
|
+
const isSafariLike =
|
|
10
|
+
import.meta.env.EXTENSION_PUBLIC_BROWSER === 'safari' ||
|
|
11
|
+
import.meta.env.EXTENSION_PUBLIC_BROWSER === 'webkit-based'
|
|
12
|
+
|
|
13
|
+
// Safari has no side panel surface, so the sidebar page opens in a tab.
|
|
14
|
+
let sidebarTabId: number | undefined
|
|
15
|
+
|
|
16
|
+
function openSidebarTab() {
|
|
17
|
+
const url = chrome.runtime.getURL('sidebar/index.html')
|
|
18
|
+
|
|
19
|
+
const openNewTab = () => {
|
|
20
|
+
chrome.tabs.create({url}, (tab) => {
|
|
21
|
+
sidebarTabId = tab?.id
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// A repeat click focuses the tab already opened instead of a new copy.
|
|
26
|
+
const knownTabId = sidebarTabId
|
|
27
|
+
|
|
28
|
+
if (knownTabId === undefined) {
|
|
29
|
+
openNewTab()
|
|
30
|
+
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
chrome.tabs.update(knownTabId, {active: true}, () => {
|
|
35
|
+
if (chrome.runtime.lastError) openNewTab()
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
9
39
|
if (isFirefoxLike) {
|
|
40
|
+
// Firefox refuses sidebarAction.open() outside a user input handler, and a
|
|
41
|
+
// message listener is not one, so the toolbar click is the only route.
|
|
10
42
|
browser.browserAction.onClicked.addListener(() => {
|
|
11
43
|
browser.sidebarAction.open()
|
|
12
44
|
})
|
|
45
|
+
}
|
|
13
46
|
|
|
14
|
-
|
|
47
|
+
if (isSafariLike) {
|
|
48
|
+
// Safari never had setPanelBehavior, so the toolbar click needs a listener.
|
|
49
|
+
chrome.action?.onClicked.addListener(() => {
|
|
50
|
+
openSidebarTab()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
chrome.runtime.onMessage.addListener((message) => {
|
|
15
54
|
if (!message || message.type !== 'openSidebar') return
|
|
16
55
|
|
|
17
|
-
|
|
56
|
+
openSidebarTab()
|
|
18
57
|
})
|
|
19
58
|
}
|
|
20
59
|
|
|
21
|
-
if (!isFirefoxLike) {
|
|
60
|
+
if (!isFirefoxLike && !isSafariLike) {
|
|
22
61
|
// setPanelBehavior only affects FUTURE action clicks, registering it
|
|
23
62
|
// inside onClicked would swallow the first toolbar click.
|
|
24
|
-
chrome.sidePanel
|
|
63
|
+
chrome.sidePanel?.setPanelBehavior({openPanelOnActionClick: true})
|
|
25
64
|
|
|
26
65
|
// The side panel API only exists in Chromium. Firefox opens the sidebar in
|
|
27
66
|
// the listener above, so this listener is compiled out of gecko builds.
|
|
@@ -32,13 +71,13 @@ if (!isFirefoxLike) {
|
|
|
32
71
|
// allowed inside the user gesture that the content-script click carries, and
|
|
33
72
|
// a tabs.query callback outlives it: the panel then silently refuses to open.
|
|
34
73
|
// sender.tab is the tab the click came from, so no lookup is needed at all.
|
|
35
|
-
chrome.sidePanel
|
|
74
|
+
chrome.sidePanel?.setPanelBehavior({openPanelOnActionClick: true})
|
|
36
75
|
|
|
37
76
|
const tabId = sender.tab?.id
|
|
38
|
-
if (!chrome.sidePanel
|
|
77
|
+
if (!chrome.sidePanel?.open || tabId === undefined) return
|
|
39
78
|
|
|
40
79
|
try {
|
|
41
|
-
chrome.sidePanel
|
|
80
|
+
chrome.sidePanel?.open({tabId})
|
|
42
81
|
} catch (error) {
|
|
43
82
|
console.error(error)
|
|
44
83
|
}
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import logo from '../images/icon.png'
|
|
2
2
|
|
|
3
|
+
const isFirefoxLike =
|
|
4
|
+
import.meta.env.EXTENSION_PUBLIC_BROWSER === 'firefox' ||
|
|
5
|
+
import.meta.env.EXTENSION_PUBLIC_BROWSER === 'gecko-based'
|
|
6
|
+
|
|
3
7
|
export default function createContentApp(): HTMLDivElement {
|
|
4
8
|
const container = document.createElement('div')
|
|
5
9
|
container.className = 'content_script'
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
pill
|
|
10
|
-
pill.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
// Firefox cannot open a sidebar from a message listener, so the gecko build
|
|
12
|
+
// renders a hint naming the toolbar action instead of a dead control.
|
|
13
|
+
const pill = document.createElement(isFirefoxLike ? 'div' : 'button')
|
|
14
|
+
pill.className = isFirefoxLike
|
|
15
|
+
? 'content_pill content_pill_static'
|
|
16
|
+
: 'content_pill'
|
|
17
|
+
|
|
18
|
+
if (!isFirefoxLike) {
|
|
19
|
+
;(pill as HTMLButtonElement).type = 'button'
|
|
20
|
+
pill.setAttribute('aria-label', 'Open sidebar')
|
|
21
|
+
pill.addEventListener('click', () => {
|
|
15
22
|
chrome.runtime.sendMessage({type: 'openSidebar'})
|
|
16
|
-
}
|
|
17
|
-
}
|
|
23
|
+
})
|
|
24
|
+
}
|
|
18
25
|
|
|
19
26
|
const img = document.createElement('img')
|
|
20
27
|
img.className = 'content_pill_logo'
|
|
@@ -26,7 +33,9 @@ export default function createContentApp(): HTMLDivElement {
|
|
|
26
33
|
|
|
27
34
|
const text = document.createElement('span')
|
|
28
35
|
text.className = 'content_pill_text'
|
|
29
|
-
text.textContent =
|
|
36
|
+
text.textContent = isFirefoxLike
|
|
37
|
+
? 'Use the toolbar icon to open the sidebar'
|
|
38
|
+
: 'Open sidebar'
|
|
30
39
|
|
|
31
40
|
pill.appendChild(img)
|
|
32
41
|
pill.appendChild(text)
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
.content_pill:hover {
|
|
25
|
+
.content_pill:not(.content_pill_static):hover {
|
|
26
26
|
background: #11151c;
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -40,6 +40,12 @@
|
|
|
40
40
|
font-weight: 600;
|
|
41
41
|
line-height: 1;
|
|
42
42
|
font-family:
|
|
43
|
-
-apple-system, BlinkMacSystemFont,
|
|
44
|
-
Arial,
|
|
43
|
+
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue',
|
|
44
|
+
Arial, 'Noto Sans', sans-serif;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* The gecko build renders this hint in place of the pill, so it must not read
|
|
48
|
+
as a control: no pointer cursor and no hover response. */
|
|
49
|
+
.content_pill_static {
|
|
50
|
+
cursor: default;
|
|
45
51
|
}
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
body {
|
|
12
12
|
background-color: var(--sidebar-bg);
|
|
13
13
|
color: var(--sidebar-text);
|
|
14
|
-
height
|
|
14
|
+
/* The margin insets the panel, so a full-viewport height would push the
|
|
15
|
+
content below centre and overflow the panel by twice the margin. */
|
|
16
|
+
height: calc(100vh - 2 * var(--sidebar-margin));
|
|
15
17
|
margin: var(--sidebar-margin);
|
|
16
18
|
border-radius: 6px;
|
|
17
19
|
display: flex;
|
|
@@ -26,7 +28,7 @@ body {
|
|
|
26
28
|
align-items: center;
|
|
27
29
|
padding: 0 1rem;
|
|
28
30
|
text-align: center;
|
|
29
|
-
max-height:
|
|
31
|
+
max-height: 100%;
|
|
30
32
|
overflow-y: auto;
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -39,8 +41,8 @@ body {
|
|
|
39
41
|
font-size: 1.85em;
|
|
40
42
|
line-height: 1.1;
|
|
41
43
|
font-family:
|
|
42
|
-
-apple-system, BlinkMacSystemFont,
|
|
43
|
-
Arial,
|
|
44
|
+
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue',
|
|
45
|
+
Arial, 'Noto Sans', sans-serif;
|
|
44
46
|
font-weight: 700;
|
|
45
47
|
margin: 0;
|
|
46
48
|
text-align: center;
|
package/package.json
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"templates"
|
|
26
26
|
],
|
|
27
27
|
"name": "extension-create",
|
|
28
|
-
"version": "4.1.
|
|
28
|
+
"version": "4.1.20",
|
|
29
29
|
"description": "The standalone extension creation engine for Extension.js",
|
|
30
30
|
"author": {
|
|
31
31
|
"name": "Cezar Augusto",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"watch": "rslib build --watch",
|
|
46
46
|
"compile": "rslib build",
|
|
47
47
|
"format": "biome format --write .",
|
|
48
|
-
"lint": "biome lint .",
|
|
48
|
+
"lint": "biome lint . && eslint .",
|
|
49
49
|
"pretest:create": "pnpm compile",
|
|
50
50
|
"pretest": "pnpm compile",
|
|
51
51
|
"test": "vitest run",
|
|
@@ -6,22 +6,61 @@ const isFirefoxLike =
|
|
|
6
6
|
import.meta.env.EXTENSION_PUBLIC_BROWSER === 'firefox' ||
|
|
7
7
|
import.meta.env.EXTENSION_PUBLIC_BROWSER === 'gecko-based'
|
|
8
8
|
|
|
9
|
+
const isSafariLike =
|
|
10
|
+
import.meta.env.EXTENSION_PUBLIC_BROWSER === 'safari' ||
|
|
11
|
+
import.meta.env.EXTENSION_PUBLIC_BROWSER === 'webkit-based'
|
|
12
|
+
|
|
13
|
+
// Safari has no side panel surface, so the sidebar page opens in a tab.
|
|
14
|
+
let sidebarTabId
|
|
15
|
+
|
|
16
|
+
function openSidebarTab() {
|
|
17
|
+
const url = chrome.runtime.getURL('sidebar/index.html')
|
|
18
|
+
|
|
19
|
+
const openNewTab = () => {
|
|
20
|
+
chrome.tabs.create({url}, (tab) => {
|
|
21
|
+
sidebarTabId = tab?.id
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// A repeat click focuses the tab already opened instead of a new copy.
|
|
26
|
+
const knownTabId = sidebarTabId
|
|
27
|
+
|
|
28
|
+
if (knownTabId === undefined) {
|
|
29
|
+
openNewTab()
|
|
30
|
+
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
chrome.tabs.update(knownTabId, {active: true}, () => {
|
|
35
|
+
if (chrome.runtime.lastError) openNewTab()
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
9
39
|
if (isFirefoxLike) {
|
|
40
|
+
// Firefox refuses sidebarAction.open() outside a user input handler, and a
|
|
41
|
+
// message listener is not one, so the toolbar click is the only route.
|
|
10
42
|
browser.browserAction.onClicked.addListener(() => {
|
|
11
43
|
browser.sidebarAction.open()
|
|
12
44
|
})
|
|
45
|
+
}
|
|
13
46
|
|
|
14
|
-
|
|
47
|
+
if (isSafariLike) {
|
|
48
|
+
// Safari never had setPanelBehavior, so the toolbar click needs a listener.
|
|
49
|
+
chrome.action?.onClicked.addListener(() => {
|
|
50
|
+
openSidebarTab()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
chrome.runtime.onMessage.addListener((message) => {
|
|
15
54
|
if (!message || message.type !== 'openSidebar') return
|
|
16
55
|
|
|
17
|
-
|
|
56
|
+
openSidebarTab()
|
|
18
57
|
})
|
|
19
58
|
}
|
|
20
59
|
|
|
21
|
-
if (!isFirefoxLike) {
|
|
60
|
+
if (!isFirefoxLike && !isSafariLike) {
|
|
22
61
|
// setPanelBehavior only affects FUTURE action clicks, registering it
|
|
23
62
|
// inside onClicked would swallow the first toolbar click.
|
|
24
|
-
chrome.sidePanel
|
|
63
|
+
chrome.sidePanel?.setPanelBehavior({openPanelOnActionClick: true})
|
|
25
64
|
|
|
26
65
|
// The side panel API only exists in Chromium. Firefox opens the sidebar in
|
|
27
66
|
// the listener above, so this listener is compiled out of gecko builds.
|
|
@@ -32,13 +71,13 @@ if (!isFirefoxLike) {
|
|
|
32
71
|
// allowed inside the user gesture that the content-script click carries, and
|
|
33
72
|
// a tabs.query callback outlives it: the panel then silently refuses to open.
|
|
34
73
|
// sender.tab is the tab the click came from, so no lookup is needed at all.
|
|
35
|
-
chrome.sidePanel
|
|
74
|
+
chrome.sidePanel?.setPanelBehavior({openPanelOnActionClick: true})
|
|
36
75
|
|
|
37
76
|
const tabId = sender.tab?.id
|
|
38
|
-
if (!chrome.sidePanel
|
|
77
|
+
if (!chrome.sidePanel?.open || tabId === undefined) return
|
|
39
78
|
|
|
40
79
|
try {
|
|
41
|
-
chrome.sidePanel
|
|
80
|
+
chrome.sidePanel?.open({tabId})
|
|
42
81
|
} catch (error) {
|
|
43
82
|
console.error(error)
|
|
44
83
|
}
|
|
@@ -1,27 +1,31 @@
|
|
|
1
1
|
import logo from '../images/icon.png'
|
|
2
2
|
|
|
3
|
+
const isFirefoxLike =
|
|
4
|
+
import.meta.env.EXTENSION_PUBLIC_BROWSER === 'firefox' ||
|
|
5
|
+
import.meta.env.EXTENSION_PUBLIC_BROWSER === 'gecko-based'
|
|
6
|
+
|
|
3
7
|
export default function createContentApp() {
|
|
4
8
|
const container = document.createElement('div')
|
|
5
9
|
container.className = 'content_script'
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
pill
|
|
10
|
-
pill.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
// Firefox cannot open a sidebar from a message listener, so the gecko build
|
|
12
|
+
// renders a hint naming the toolbar action instead of a dead control.
|
|
13
|
+
const pill = document.createElement(isFirefoxLike ? 'div' : 'button')
|
|
14
|
+
pill.className = isFirefoxLike
|
|
15
|
+
? 'content_pill content_pill_static'
|
|
16
|
+
: 'content_pill'
|
|
17
|
+
|
|
18
|
+
if (!isFirefoxLike) {
|
|
19
|
+
pill.type = 'button'
|
|
20
|
+
pill.setAttribute('aria-label', 'Open sidebar')
|
|
21
|
+
pill.addEventListener('click', () => {
|
|
22
|
+
try {
|
|
19
23
|
chrome.runtime.sendMessage({type: 'openSidebar'})
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error(error)
|
|
20
26
|
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
})
|
|
27
|
+
})
|
|
28
|
+
}
|
|
25
29
|
|
|
26
30
|
const img = document.createElement('img')
|
|
27
31
|
img.className = 'content_pill_logo'
|
|
@@ -31,7 +35,9 @@ export default function createContentApp() {
|
|
|
31
35
|
|
|
32
36
|
const text = document.createElement('span')
|
|
33
37
|
text.className = 'content_pill_text'
|
|
34
|
-
text.textContent =
|
|
38
|
+
text.textContent = isFirefoxLike
|
|
39
|
+
? 'Use the toolbar icon to open the sidebar'
|
|
40
|
+
: 'Open sidebar'
|
|
35
41
|
|
|
36
42
|
pill.appendChild(img)
|
|
37
43
|
pill.appendChild(text)
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
.content_pill:hover {
|
|
25
|
+
.content_pill:not(.content_pill_static):hover {
|
|
26
26
|
background: #11151c;
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -40,6 +40,12 @@
|
|
|
40
40
|
font-weight: 600;
|
|
41
41
|
line-height: 1;
|
|
42
42
|
font-family:
|
|
43
|
-
-apple-system, BlinkMacSystemFont,
|
|
44
|
-
Arial,
|
|
43
|
+
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue',
|
|
44
|
+
Arial, 'Noto Sans', sans-serif;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* The gecko build renders this hint in place of the pill, so it must not read
|
|
48
|
+
as a control: no pointer cursor and no hover response. */
|
|
49
|
+
.content_pill_static {
|
|
50
|
+
cursor: default;
|
|
45
51
|
}
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
body {
|
|
12
12
|
background-color: var(--sidebar-bg);
|
|
13
13
|
color: var(--sidebar-text);
|
|
14
|
-
height
|
|
14
|
+
/* The margin insets the panel, so a full-viewport height would push the
|
|
15
|
+
content below centre and overflow the panel by twice the margin. */
|
|
16
|
+
height: calc(100vh - 2 * var(--sidebar-margin));
|
|
15
17
|
margin: var(--sidebar-margin);
|
|
16
18
|
border-radius: 6px;
|
|
17
19
|
display: flex;
|
|
@@ -26,7 +28,7 @@ body {
|
|
|
26
28
|
align-items: center;
|
|
27
29
|
padding: 0 1rem;
|
|
28
30
|
text-align: center;
|
|
29
|
-
max-height:
|
|
31
|
+
max-height: 100%;
|
|
30
32
|
overflow-y: auto;
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -39,8 +41,8 @@ body {
|
|
|
39
41
|
font-size: 1.85em;
|
|
40
42
|
line-height: 1.1;
|
|
41
43
|
font-family:
|
|
42
|
-
-apple-system, BlinkMacSystemFont,
|
|
43
|
-
Arial,
|
|
44
|
+
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue',
|
|
45
|
+
Arial, 'Noto Sans', sans-serif;
|
|
44
46
|
font-weight: 700;
|
|
45
47
|
margin: 0;
|
|
46
48
|
text-align: center;
|