extension-create 4.1.19 → 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.
@@ -0,0 +1 @@
1
+ export declare function isUrlProjectName(projectNameInput: string): boolean;
package/dist/module.cjs CHANGED
@@ -358,6 +358,9 @@ function keepingExistingGitignore(projectName) {
358
358
  function existingRepositoryKept(projectName) {
359
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.`;
360
360
  }
361
+ function isUrlProjectName(projectNameInput) {
362
+ return /^https?:\/\//i.test(projectNameInput.trim());
363
+ }
361
364
  const promises_namespaceObject = require("node:fs/promises");
362
365
  async function copyDirectoryWithSymlinks(source, destination) {
363
366
  const entries = await promises_namespaceObject.readdir(source, {
@@ -609,7 +612,7 @@ const NETWORK_TIMEOUT_MS = (()=>{
609
612
  return Number.isFinite(raw) && raw > 0 ? raw : 60000;
610
613
  })();
611
614
  const CODELOAD_BASE = 'https://codeload.github.com/extension-js/examples/zip';
612
- const DEFAULT_TEMPLATES_REF = 'e552f6db6f2862093ba9d7026e0d376f002d95cc';
615
+ const DEFAULT_TEMPLATES_REF = '39448a235ea1b02516e6d555bcae409db2258b7f';
613
616
  const BUNDLED_TEMPLATES = [
614
617
  "javascript"
615
618
  ];
@@ -2135,7 +2138,7 @@ async function extensionCreate(projectNameInput, { cliVersion, template, install
2135
2138
  if (!projectNameInput) throw new Error(noProjectName());
2136
2139
  const templateWasOmitted = null == template || '' === String(template).trim();
2137
2140
  const effectiveTemplate = templateWasOmitted ? DEFAULT_TEMPLATE_NAME : String(template);
2138
- if (projectNameInput.startsWith('http')) throw new Error(noUrlAllowed());
2141
+ if (isUrlProjectName(projectNameInput)) throw new Error(noUrlAllowed());
2139
2142
  const projectPath = external_node_path_namespaceObject.isAbsolute(projectNameInput) ? projectNameInput : external_node_path_namespaceObject.join(process.cwd(), projectNameInput);
2140
2143
  const projectName = external_node_path_namespaceObject.basename(projectPath);
2141
2144
  const updateSuffix = process.env.EXTENSION_CLI_UPDATE_SUFFIX || '';
@@ -1,4 +1,4 @@
1
- export declare const DEFAULT_TEMPLATES_REF = "e552f6db6f2862093ba9d7026e0d376f002d95cc";
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";
@@ -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.18",
2
+ "createdWith": "extension-create@4.1.19",
3
3
  "template": "typescript",
4
- "source": "https://codeload.github.com/extension-js/examples/zip/e552f6db6f2862093ba9d7026e0d376f002d95cc",
5
- "ref": "e552f6db6f2862093ba9d7026e0d376f002d95cc"
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-15
12
+ Last updated: 2026-09-16
13
13
 
14
14
  ## Listing
15
15
 
@@ -8,7 +8,7 @@
8
8
  "devDependencies": {
9
9
  "@types/chrome": "^0.0.287",
10
10
  "typescript": "7.0.2",
11
- "extension": "^4.1.18"
11
+ "extension": "^4.1.19"
12
12
  },
13
13
  "scripts": {
14
14
  "dev": "extension dev",
@@ -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
- browser.runtime.onMessage.addListener((message: any) => {
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
- browser.sidebarAction.open()
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.setPanelBehavior({openPanelOnActionClick: true})
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.setPanelBehavior({openPanelOnActionClick: true})
74
+ chrome.sidePanel?.setPanelBehavior({openPanelOnActionClick: true})
36
75
 
37
76
  const tabId = sender.tab?.id
38
- if (!chrome.sidePanel.open || tabId === undefined) return
77
+ if (!chrome.sidePanel?.open || tabId === undefined) return
39
78
 
40
79
  try {
41
- chrome.sidePanel.open({tabId})
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
- const pill = document.createElement('button')
8
- pill.type = 'button'
9
- pill.className = 'content_pill'
10
- pill.setAttribute('aria-label', 'Open sidebar')
11
- pill.addEventListener('click', () => {
12
- if (import.meta.env.EXTENSION_PUBLIC_BROWSER === 'firefox') {
13
- browser.runtime.sendMessage({type: 'openSidebar'})
14
- } else {
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 = 'Open sidebar'
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)
@@ -40,5 +40,6 @@ async function fetchCSS() {
40
40
  const cssUrl = new URL('./styles.css', import.meta.url)
41
41
  const response = await fetch(cssUrl)
42
42
  const text = await response.text()
43
+
43
44
  return response.ok ? text : Promise.reject(text)
44
45
  }
@@ -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, "Segoe UI", Roboto, "Helvetica Neue",
44
- Arial, "Noto Sans", sans-serif;
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: 100vh;
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: 100vh;
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, "Segoe UI", Roboto, "Helvetica Neue",
43
- Arial, "Noto Sans", sans-serif;
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.19",
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
- browser.runtime.onMessage.addListener((message) => {
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
- browser.sidebarAction.open()
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.setPanelBehavior({openPanelOnActionClick: true})
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.setPanelBehavior({openPanelOnActionClick: true})
74
+ chrome.sidePanel?.setPanelBehavior({openPanelOnActionClick: true})
36
75
 
37
76
  const tabId = sender.tab?.id
38
- if (!chrome.sidePanel.open || tabId === undefined) return
77
+ if (!chrome.sidePanel?.open || tabId === undefined) return
39
78
 
40
79
  try {
41
- chrome.sidePanel.open({tabId})
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
- const pill = document.createElement('button')
8
- pill.type = 'button'
9
- pill.className = 'content_pill'
10
- pill.setAttribute('aria-label', 'Open sidebar')
11
- pill.addEventListener('click', () => {
12
- try {
13
- if (
14
- import.meta.env.EXTENSION_PUBLIC_BROWSER === 'firefox' ||
15
- import.meta.env.EXTENSION_PUBLIC_BROWSER === 'gecko-based'
16
- ) {
17
- browser.runtime.sendMessage({type: 'openSidebar'})
18
- } else {
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
- } catch (error) {
22
- console.error(error)
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 = 'Open sidebar'
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)
@@ -40,5 +40,6 @@ async function fetchCSS() {
40
40
  const cssUrl = new URL('./styles.css', import.meta.url)
41
41
  const response = await fetch(cssUrl)
42
42
  const text = await response.text()
43
+
43
44
  return response.ok ? text : Promise.reject(text)
44
45
  }
@@ -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, "Segoe UI", Roboto, "Helvetica Neue",
44
- Arial, "Noto Sans", sans-serif;
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: 100vh;
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: 100vh;
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, "Segoe UI", Roboto, "Helvetica Neue",
43
- Arial, "Noto Sans", sans-serif;
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;