extension-create 4.1.11 → 4.1.13

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.
@@ -1,8 +1,9 @@
1
+ import { type ScaffoldPackageManager } from './package-manager';
1
2
  export declare function destinationNotWriteable(workingDir: string): string;
2
3
  export declare function directoryHasConflicts(projectPath: string, conflictingFiles: string[]): Promise<string>;
3
4
  export declare function noProjectName(): string;
4
5
  export declare function noUrlAllowed(): string;
5
- export declare function scaffoldReady(projectPath: string, projectName: string, depsInstalled: boolean): Promise<string>;
6
+ export declare function scaffoldReady(projectPath: string, projectName: string, depsInstalled: boolean, packageManager?: ScaffoldPackageManager): Promise<string>;
6
7
  export declare function startingNewExtension(projectName: string): string;
7
8
  export declare function createDirectoryError(projectName: string, error: unknown): string;
8
9
  export declare function writingTypeDefinitions(projectName: string): string;
@@ -13,6 +14,7 @@ export declare function installingFromTemplateError(template: string, error: unk
13
14
  export declare function templateFetchTimedOut(templateName: string, ms: number): string;
14
15
  export declare function templateNotFoundInCatalog(templateName: string, error?: unknown): string;
15
16
  export declare function templateDownloadFailed(templateName: string, error: unknown): string;
17
+ export declare function templateOfflineFallback(requestedTemplate: string, fallbackTemplate: string, error: unknown): string;
16
18
  export declare function removedStaleTemplateLockfiles(fileNames: string[]): string;
17
19
  export declare function initializingGitForRepository(projectName: string): string;
18
20
  export declare function initializingGitSkipped(projectName: string, reason: string): string;
@@ -39,3 +41,5 @@ export declare function writingGitIgnore(): string;
39
41
  export declare function writingReadmeMetaDataError(error: unknown): string;
40
42
  export declare function writingDirectoryError(error: unknown): string;
41
43
  export declare function cantSetupBuiltInTests(error: unknown): string;
44
+ export declare function keepingExistingGitignore(projectName: string): string;
45
+ export declare function existingRepositoryKept(projectName: string): string;
@@ -5,3 +5,6 @@ export declare function detectPackageManagerFromEnv(): NodePackageManager;
5
5
  export declare function isDenoRuntime(): boolean;
6
6
  export type ScaffoldPackageManager = 'npm' | 'yarn' | 'pnpm' | 'bun' | 'deno';
7
7
  export declare function resolveScaffoldPackageManager(): ScaffoldPackageManager;
8
+ export declare function resolveProjectPackageManager(projectPath: string): ScaffoldPackageManager;
9
+ export declare function readPinnedPackageManager(projectPath: string): NodePackageManager | undefined;
10
+ export declare function resolvePackageManagerSpec(projectPath: string, manager: ScaffoldPackageManager): string | undefined;
package/dist/module.cjs CHANGED
@@ -117,6 +117,8 @@ new Set([
117
117
  'librewolf',
118
118
  'waterfox'
119
119
  ]);
120
+ const external_node_child_process_namespaceObject = require("node:child_process");
121
+ const external_node_fs_namespaceObject = require("node:fs");
120
122
  const external_prefers_yarn_namespaceObject = require("prefers-yarn");
121
123
  const NODE_PACKAGE_MANAGERS = [
122
124
  'pnpm',
@@ -138,6 +140,48 @@ function resolveScaffoldPackageManager() {
138
140
  if (isDenoRuntime()) return 'deno';
139
141
  return detectPackageManagerFromEnv();
140
142
  }
143
+ function resolveProjectPackageManager(projectPath) {
144
+ if (isDenoRuntime()) return 'deno';
145
+ const pinned = readPinnedPackageManager(projectPath);
146
+ if (pinned) return pinned;
147
+ if (external_node_fs_namespaceObject.existsSync(external_node_path_namespaceObject.join(projectPath, 'pnpm-workspace.yaml'))) return 'pnpm';
148
+ return detectPackageManagerFromEnv();
149
+ }
150
+ function readPinnedPackageManager(projectPath) {
151
+ try {
152
+ const raw = external_node_fs_namespaceObject.readFileSync(external_node_path_namespaceObject.join(projectPath, 'package.json'), 'utf8');
153
+ const pin = String(JSON.parse(raw)?.packageManager || '');
154
+ const name = pin.split('@')[0].toLowerCase();
155
+ return NODE_PACKAGE_MANAGERS.includes(name) ? name : void 0;
156
+ } catch {
157
+ return;
158
+ }
159
+ }
160
+ function resolvePackageManagerSpec(projectPath, manager) {
161
+ if ('deno' === manager) return;
162
+ try {
163
+ const raw = external_node_fs_namespaceObject.readFileSync(external_node_path_namespaceObject.join(projectPath, 'package.json'), 'utf8');
164
+ const pin = String(JSON.parse(raw)?.packageManager || '');
165
+ if (pin.toLowerCase().startsWith(`${manager}@`)) return pin;
166
+ } catch {}
167
+ const fromEnv = (0, external_prefers_yarn_namespaceObject.getPackageManagerSpec)();
168
+ if (fromEnv && fromEnv.startsWith(`${manager}@`)) return fromEnv;
169
+ try {
170
+ const version = (0, external_node_child_process_namespaceObject.execFileSync)(manager, [
171
+ '--version'
172
+ ], {
173
+ encoding: 'utf8',
174
+ stdio: [
175
+ 'ignore',
176
+ 'pipe',
177
+ 'ignore'
178
+ ],
179
+ timeout: 5000,
180
+ shell: 'win32' === process.platform
181
+ }).trim().replace(/^v/, '');
182
+ if (/^\d+\.\d+\.\d+/.test(version)) return `${manager}@${version}`;
183
+ } catch {}
184
+ }
141
185
  function destinationNotWriteable(workingDir) {
142
186
  const workingDirFolder = external_node_path_namespaceObject.basename(workingDir);
143
187
  return `${prefix('error')} Couldn't write to the destination directory.\n${fmt.label('PATH')} ${fmt.val(workingDirFolder)}\n${external_pintor_default().red('Choose a writable path, or update the folder permissions.')}`;
@@ -155,9 +199,9 @@ function noProjectName() {
155
199
  function noUrlAllowed() {
156
200
  return `${prefix('error')} A URL is not a valid project path.\n${external_pintor_default().red('Pass a project name or a local directory path.')}`;
157
201
  }
158
- async function scaffoldReady(projectPath, projectName, depsInstalled) {
202
+ async function scaffoldReady(projectPath, projectName, depsInstalled, packageManager) {
159
203
  const relativePath = external_node_path_namespaceObject.relative(process.cwd(), projectPath);
160
- const pm = resolveScaffoldPackageManager();
204
+ const pm = packageManager ?? resolveScaffoldPackageManager();
161
205
  let command = 'npm run dev';
162
206
  let installCmd = 'npm install';
163
207
  switch(pm){
@@ -217,6 +261,9 @@ function templateNotFoundInCatalog(templateName, error) {
217
261
  function templateDownloadFailed(templateName, error) {
218
262
  return `${prefix('error')} Couldn't download the template ${external_pintor_default().blue(templateName)} from ${fmt.val('github.com/extension-js/examples')}.\n${fmt.label('REASON')} ${fmt.val(fmt.truncate(String(error?.message || error)))}\n${external_pintor_default().red('- Check your network connection and any GitHub rate limit, then try again.')}\n${external_pintor_default().red('- Set')} ${external_pintor_default().blue('EXTENSION_CREATE_TIMEOUT_MS')} ${external_pintor_default().red('to allow more time.')}`;
219
263
  }
264
+ function templateOfflineFallback(requestedTemplate, fallbackTemplate, error) {
265
+ return `${prefix('info')} Couldn't download the template ${external_pintor_default().blue(requestedTemplate)} from ${fmt.val('github.com/extension-js/examples')}, so this scaffold uses the offline ${external_pintor_default().blue(fallbackTemplate)} template instead.\n${fmt.label('REASON')} ${fmt.val(fmt.truncate(String(error?.message || error)))}\n${external_pintor_default().gray('- Reconnect and run')} ${external_pintor_default().blue(`extension create --template ${requestedTemplate}`)} ${external_pintor_default().gray('to scaffold it.')}`;
266
+ }
220
267
  function removedStaleTemplateLockfiles(fileNames) {
221
268
  return `${prefix('info')} Removed the template ${fileNames.map((name)=>fmt.val(name)).join(', ')}. The first install writes a fresh one.`;
222
269
  }
@@ -302,6 +349,12 @@ function writingDirectoryError(error) {
302
349
  function cantSetupBuiltInTests(error) {
303
350
  return `${prefix('error')} Couldn't set up the built-in tests.\n${fmt.label('REASON')} ${fmt.val(fmt.truncate(String(error)))}\n${external_pintor_default().red('The extension itself is fine. Re-run')} ${external_pintor_default().blue('extension create')}${external_pintor_default().red(', or skip the tests.')}`;
304
351
  }
352
+ function keepingExistingGitignore(projectName) {
353
+ return `${prefix('info')} ${external_pintor_default().blue(projectName)} already has a ${external_pintor_default().blue('.gitignore')}.\nKeeping its rules and adding the template rules it lacks.`;
354
+ }
355
+ function existingRepositoryKept(projectName) {
356
+ 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
+ }
305
358
  const promises_namespaceObject = require("node:fs/promises");
306
359
  async function copyDirectoryWithSymlinks(source, destination) {
307
360
  const entries = await promises_namespaceObject.readdir(source, {
@@ -386,11 +439,13 @@ async function isDirectoryWriteable(directory, logger) {
386
439
  function isTypeScriptTemplate(templateName) {
387
440
  return templateName.includes("typescript") || templateName.includes('react') || templateName.includes('preact') || templateName.includes('svelte') || templateName.includes('solid');
388
441
  }
389
- const external_node_fs_namespaceObject = require("node:fs");
390
442
  const allowlist = [
391
443
  'LICENSE',
392
444
  'node_modules'
393
445
  ];
446
+ const HIDDEN_FILES_TEMPLATES_SHIP = [
447
+ '.gitignore'
448
+ ];
394
449
  async function createDirectory(projectPath, projectName, logger) {
395
450
  logger.log(startingNewExtension(projectName));
396
451
  const directoryPreExisted = (0, external_node_fs_namespaceObject.existsSync)(projectPath);
@@ -409,6 +464,7 @@ async function createDirectory(projectPath, projectName, logger) {
409
464
  const conflictMessage = await directoryHasConflicts(projectPath, conflictingFiles);
410
465
  throw new Error(conflictMessage);
411
466
  }
467
+ for (const hidden of HIDDEN_FILES_TEMPLATES_SHIP)if (currentDir.includes(hidden)) logger.log(keepingExistingGitignore(projectName));
412
468
  } catch (error) {
413
469
  throw new Error(createDirectoryError(projectName, error));
414
470
  }
@@ -416,10 +472,67 @@ async function createDirectory(projectPath, projectName, logger) {
416
472
  directoryCreated: !directoryPreExisted
417
473
  };
418
474
  }
419
- async function generateExtensionTypes(projectPath, projectName, logger) {
420
- const extensionEnvFile = external_node_path_namespaceObject.join(projectPath, 'extension-env.d.ts');
421
- const typePath = 'extension';
422
- const fileContent = `\
475
+ const EXTENSION_ENV_TYPES_PACKAGE = 'extension';
476
+ const STYLE_TYPE = 'Readonly<Record<string, string>>';
477
+ const EXTENSION_ENV_WILDCARD_MODULES = Object.freeze([
478
+ {
479
+ pattern: '*.css',
480
+ type: STYLE_TYPE
481
+ },
482
+ {
483
+ pattern: '*.module.css',
484
+ type: STYLE_TYPE
485
+ },
486
+ {
487
+ pattern: '*.module.scss',
488
+ type: STYLE_TYPE
489
+ },
490
+ {
491
+ pattern: '*.module.sass',
492
+ type: STYLE_TYPE
493
+ },
494
+ {
495
+ pattern: '*.png',
496
+ type: 'string'
497
+ },
498
+ {
499
+ pattern: '*.jpg',
500
+ type: 'string'
501
+ },
502
+ {
503
+ pattern: '*.jpeg',
504
+ type: 'string'
505
+ },
506
+ {
507
+ pattern: '*.gif',
508
+ type: 'string'
509
+ },
510
+ {
511
+ pattern: '*.webp',
512
+ type: 'string'
513
+ },
514
+ {
515
+ pattern: '*.avif',
516
+ type: 'string'
517
+ },
518
+ {
519
+ pattern: '*.ico',
520
+ type: 'string'
521
+ },
522
+ {
523
+ pattern: '*.bmp',
524
+ type: 'string'
525
+ },
526
+ {
527
+ pattern: '*.svg',
528
+ type: 'any'
529
+ }
530
+ ]);
531
+ function renderWildcardModuleDeclarations(modules = EXTENSION_ENV_WILDCARD_MODULES) {
532
+ return modules.map(({ pattern, type })=>`declare module '${pattern}' {\n const content: ${type}\n export default content\n}\n`).join('');
533
+ }
534
+ function renderExtensionEnvTypes(typePath = EXTENSION_ENV_TYPES_PACKAGE) {
535
+ return `\
423
536
  // Required Extension.js types for TypeScript projects.
424
537
  // This file is auto-generated and should not be excluded.
425
538
  // If you need additional types, consider creating a new *.d.ts file and
@@ -429,7 +542,15 @@ async function generateExtensionTypes(projectPath, projectName, logger) {
429
542
 
430
543
  // Polyfill types for browser.* APIs
431
544
  /// <reference types="${typePath}/types/polyfill" />
432
- `;
545
+
546
+ // Asset and stylesheet imports. These wildcard declarations also live in
547
+ // ${typePath}/types, but TypeScript 7 native does not apply them through the
548
+ // reference above, so they are emitted here as well.
549
+ ${renderWildcardModuleDeclarations()}`;
550
+ }
551
+ async function generateExtensionTypes(projectPath, projectName, logger) {
552
+ const extensionEnvFile = external_node_path_namespaceObject.join(projectPath, 'extension-env.d.ts');
553
+ const fileContent = renderExtensionEnvTypes();
433
554
  try {
434
555
  await promises_namespaceObject.mkdir(projectPath, {
435
556
  recursive: true
@@ -489,6 +610,8 @@ const DEFAULT_TEMPLATES_REF = 'cb6a25377bd9516a1e55447a2010537019851ab2';
489
610
  const BUNDLED_TEMPLATES = [
490
611
  "javascript"
491
612
  ];
613
+ const DEFAULT_TEMPLATE_NAME = "typescript";
614
+ const OFFLINE_FALLBACK_TEMPLATE = "javascript";
492
615
  function resolveCatalogUrls(ref, overrideUrl) {
493
616
  if (overrideUrl) return [
494
617
  overrideUrl
@@ -640,6 +763,19 @@ async function withSuppressedOutput(task) {
640
763
  function bundledTemplateDir(templateName) {
641
764
  return external_node_path_namespaceObject.join(__dirname, '..', 'templates', templateName);
642
765
  }
766
+ async function copyBundledTemplate(templateName, projectPath, logger, ownerGitignore = null) {
767
+ const localTemplate = bundledTemplateDir(templateName);
768
+ if (!(0, external_node_fs_namespaceObject.existsSync)(localTemplate)) return;
769
+ await copyDirectoryWithSymlinks(localTemplate, projectPath);
770
+ await restoreOwnerGitignore(projectPath, ownerGitignore);
771
+ await removeTemplateScaffoldingFiles(projectPath);
772
+ const dropped = await removeStaleTemplateLockfiles(projectPath);
773
+ if (dropped.length) logger.log(removedStaleTemplateLockfiles(dropped));
774
+ return {
775
+ template: templateName,
776
+ source: 'bundled'
777
+ };
778
+ }
643
779
  const TEMPLATE_SCAFFOLDING_FILES = [
644
780
  'template.meta.json',
645
781
  'template.spec.ts',
@@ -711,6 +847,28 @@ async function cleanupFailedImport(projectPath, ownsProjectDir, preExistingEntri
711
847
  force: true
712
848
  }).catch(()=>{})));
713
849
  }
850
+ async function readOwnerGitignore(projectPath) {
851
+ try {
852
+ return await promises_namespaceObject.readFile(external_node_path_namespaceObject.join(projectPath, '.gitignore'), 'utf8');
853
+ } catch {
854
+ return null;
855
+ }
856
+ }
857
+ async function restoreOwnerGitignore(projectPath, ownerContents) {
858
+ if (null === ownerContents) return;
859
+ const target = external_node_path_namespaceObject.join(projectPath, '.gitignore');
860
+ let templateContents = '';
861
+ try {
862
+ templateContents = await promises_namespaceObject.readFile(target, 'utf8');
863
+ } catch {
864
+ await promises_namespaceObject.writeFile(target, ownerContents);
865
+ return;
866
+ }
867
+ const ownerLines = new Set(ownerContents.split(/\r?\n/).map((line)=>line.trim()));
868
+ const added = templateContents.split(/\r?\n/).filter((line)=>line.trim().length > 0 && !ownerLines.has(line.trim()));
869
+ const merged = 0 === added.length ? ownerContents : `${ownerContents.replace(/\n?$/, '\n')}\n# Extension.js template rules\n${added.join('\n')}\n`;
870
+ await promises_namespaceObject.writeFile(target, merged);
871
+ }
714
872
  async function importExternalTemplate(projectPath, projectName, template, logger, options) {
715
873
  const templateName = external_node_path_namespaceObject.basename(template);
716
874
  const resolvedTemplate = template;
@@ -724,22 +882,14 @@ async function importExternalTemplate(projectPath, projectName, template, logger
724
882
  if (dirExistedBeforeImport) try {
725
883
  preExistingEntries = await promises_namespaceObject.readdir(projectPath);
726
884
  } catch {}
885
+ const ownerGitignore = dirExistedBeforeImport ? await readOwnerGitignore(projectPath) : null;
727
886
  try {
728
887
  await promises_namespaceObject.mkdir(projectPath, {
729
888
  recursive: true
730
889
  });
731
890
  if (!isHttp && !isGithub && BUNDLED_TEMPLATES.includes(resolvedTemplate)) {
732
- const localTemplate = bundledTemplateDir(resolvedTemplate);
733
- if ((0, external_node_fs_namespaceObject.existsSync)(localTemplate)) {
734
- await copyDirectoryWithSymlinks(localTemplate, projectPath);
735
- await removeTemplateScaffoldingFiles(projectPath);
736
- const dropped = await removeStaleTemplateLockfiles(projectPath);
737
- if (dropped.length) logger.log(removedStaleTemplateLockfiles(dropped));
738
- return {
739
- template: resolvedTemplateName,
740
- source: 'bundled'
741
- };
742
- }
891
+ const provenance = await copyBundledTemplate(resolvedTemplate, projectPath, logger, ownerGitignore);
892
+ if (provenance) return provenance;
743
893
  }
744
894
  const tempRoot = await promises_namespaceObject.mkdtemp(external_node_path_namespaceObject.join(external_node_os_namespaceObject.tmpdir(), 'extension-js-create-'));
745
895
  const tempPath = external_node_path_namespaceObject.join(tempRoot, `${projectName}-temp`);
@@ -803,6 +953,7 @@ async function importExternalTemplate(projectPath, projectName, template, logger
803
953
  } : {}
804
954
  };
805
955
  }
956
+ await restoreOwnerGitignore(projectPath, ownerGitignore);
806
957
  await removeTemplateScaffoldingFiles(projectPath);
807
958
  const droppedLockfiles = await removeStaleTemplateLockfiles(projectPath);
808
959
  if (droppedLockfiles.length) logger.log(removedStaleTemplateLockfiles(droppedLockfiles));
@@ -812,6 +963,17 @@ async function importExternalTemplate(projectPath, projectName, template, logger
812
963
  });
813
964
  return provenance;
814
965
  } catch (error) {
966
+ if (error instanceof TemplateDownloadError && options?.allowOfflineFallback && resolvedTemplate !== OFFLINE_FALLBACK_TEMPLATE && BUNDLED_TEMPLATES.includes(OFFLINE_FALLBACK_TEMPLATE)) {
967
+ await cleanupFailedImport(projectPath, ownsProjectDir, preExistingEntries);
968
+ await promises_namespaceObject.mkdir(projectPath, {
969
+ recursive: true
970
+ });
971
+ const fallback = await copyBundledTemplate(OFFLINE_FALLBACK_TEMPLATE, projectPath, logger, ownerGitignore);
972
+ if (fallback) {
973
+ logger.log(templateOfflineFallback(resolvedTemplateName, OFFLINE_FALLBACK_TEMPLATE, error));
974
+ return fallback;
975
+ }
976
+ }
815
977
  if (error instanceof TemplateNotFoundError) logger.error(templateNotFoundInCatalog(templateName, error.cause));
816
978
  else if (error instanceof TemplateDownloadError) logger.error(templateDownloadFailed(templateName, error));
817
979
  else logger.error(installingFromTemplateError(templateName, error));
@@ -887,8 +1049,22 @@ async function runGit(args, projectPath) {
887
1049
  });
888
1050
  });
889
1051
  }
1052
+ async function hasCommittedHistory(projectPath) {
1053
+ const inside = await runGit([
1054
+ 'rev-parse',
1055
+ '--is-inside-work-tree'
1056
+ ], projectPath);
1057
+ if (!inside.ok) return false;
1058
+ const head = await runGit([
1059
+ 'rev-parse',
1060
+ '--verify',
1061
+ 'HEAD'
1062
+ ], projectPath);
1063
+ return head.ok;
1064
+ }
890
1065
  async function initializeGitRepository(projectPath, projectName, templateName, logger) {
891
1066
  if (isDebug()) logger.log(initializingGitForRepository(projectName));
1067
+ if (await hasCommittedHistory(projectPath)) return void logger.log(existingRepositoryKept(projectName));
892
1068
  const init = await runGit([
893
1069
  'init',
894
1070
  '--quiet'
@@ -1187,11 +1363,11 @@ async function hasDependenciesToInstall(projectPath) {
1187
1363
  const devDepsCount = Object.keys(packageJson?.devDependencies || {}).length;
1188
1364
  return depsCount + devDepsCount > 0;
1189
1365
  }
1190
- async function installDependencies(projectPath, projectName, logger) {
1366
+ async function installDependencies(projectPath, projectName, logger, packageManager) {
1191
1367
  const nodeModulesPath = external_node_path_namespaceObject.join(projectPath, 'node_modules');
1192
1368
  const shouldInstall = await hasDependenciesToInstall(projectPath);
1193
1369
  if (!shouldInstall) return;
1194
- const command = isDenoRuntime() ? 'deno' : await getInstallCommand();
1370
+ const command = packageManager ?? (isDenoRuntime() ? 'deno' : await getInstallCommand());
1195
1371
  const dependenciesArgs = 'deno' === command ? [
1196
1372
  'install'
1197
1373
  ] : getInstallArgs(command);
@@ -1515,7 +1691,8 @@ function resolveExtensionDevDependencyVersion(cliVersion) {
1515
1691
  if (!resolved) return 'latest';
1516
1692
  return resolved.includes('-') ? resolved : `^${resolved}`;
1517
1693
  }
1518
- async function overridePackageJson(projectPath, { template = "javascript", cliVersion }, logger) {
1694
+ async function overridePackageJson(projectPath, options, logger) {
1695
+ const { template = "javascript", cliVersion } = options;
1519
1696
  const extensionBinary = await resolveExtensionBinary();
1520
1697
  const candidatePath = external_node_path_namespaceObject.join(projectPath, 'package.json');
1521
1698
  let packageJson = {};
@@ -1537,7 +1714,8 @@ async function overridePackageJson(projectPath, { template = "javascript", cliVe
1537
1714
  ...packageJson.devDependencies || {},
1538
1715
  extension: 'development' === process.env.EXTENSION_ENV ? '*' : resolveExtensionDevDependencyVersion(cliVersion)
1539
1716
  };
1540
- const packageManagerSpec = packageJson.packageManager || (0, external_prefers_yarn_namespaceObject.getPackageManagerSpec)();
1717
+ const packageManager = options.packageManager ?? resolveProjectPackageManager(projectPath);
1718
+ const packageManagerSpec = resolvePackageManagerSpec(projectPath, packageManager);
1541
1719
  const declaredDeps = {
1542
1720
  ...packageJson.dependencies || {},
1543
1721
  ...packageJson.devDependencies || {}
@@ -1545,7 +1723,7 @@ async function overridePackageJson(projectPath, { template = "javascript", cliVe
1545
1723
  const usesMlNativeDeps = ML_DEP_TRIGGERS.some((dep)=>declaredDeps[dep]);
1546
1724
  const nativeBuildDeps = usesMlNativeDeps ? ML_NATIVE_BUILD_DEPENDENCIES : [];
1547
1725
  const existingPnpm = packageJson.pnpm && 'object' == typeof packageJson.pnpm ? packageJson.pnpm : {};
1548
- const installsWithPnpm = String(packageManagerSpec || '').startsWith('pnpm@') || 'pnpm' === resolveScaffoldPackageManager();
1726
+ const installsWithPnpm = 'pnpm' === packageManager;
1549
1727
  const ignoredBuilt = uniq([
1550
1728
  ...existingPnpm.ignoredBuiltDependencies || [],
1551
1729
  ...installsWithPnpm ? BUILD_NOOP_DEPENDENCIES : []
@@ -1700,7 +1878,7 @@ const globalMisc = [
1700
1878
  ];
1701
1879
  const localSessionState = [
1702
1880
  '',
1703
- '# extension.js local session state',
1881
+ '# Extension.js local session state',
1704
1882
  '.extension-js'
1705
1883
  ];
1706
1884
  const envFiles = [
@@ -1825,6 +2003,14 @@ async function write_readme_file_pathExists(target) {
1825
2003
  return false;
1826
2004
  }
1827
2005
  }
2006
+ function platformDocsUrl() {
2007
+ return String(process.env.EXTENSION_DEV_DOCS_URL || '').trim().replace(/\/+$/, '');
2008
+ }
2009
+ function shipItSection() {
2010
+ const docs = platformDocsUrl();
2011
+ if (!docs) return '';
2012
+ return `\n## Ship it\n\nBuilding and running your extension is local and free. When you are ready to share a build or submit it to the stores, see the [publish overview](${docs}/publish/overview?utm_source=create-readme).\n`;
2013
+ }
1828
2014
  async function writeReadmeFile(projectPath, projectName, logger) {
1829
2015
  const installCommand = await getInstallCommand();
1830
2016
  const deno = isDenoRuntime();
@@ -1838,7 +2024,7 @@ async function writeReadmeFile(projectPath, projectName, logger) {
1838
2024
  const screenshotHref = hasPublicScreenshot ? './public/screenshot.png' : hasRootScreenshot ? './screenshot.png' : null;
1839
2025
  const screenshotEmbed = screenshotHref ? `\n![screenshot](${screenshotHref})\n` : '';
1840
2026
  const blockquote = description ? `> ${description}\n\n` : '';
1841
- const readme = `<a href="https://extension.js.org" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/Powered%20by%20%7C%20Extension.js-0971fe" alt="Powered by Extension.js" align="right" /></a>\n\n# ${projectName}\n\n` + blockquote + `${screenshotEmbed}` + `## Commands\n` + `\n` + `### dev\n` + `\n` + "Run the extension in development mode. Target a browser with `--browser`:\n" + `\n` + "```bash\n" + `${runPrefix} dev\n` + `${runPrefix} dev${argSeparator} --browser=firefox\n` + `${runPrefix} dev${argSeparator} --browser=edge\n` + "```\n" + `\n` + `### build\n` + `\n` + `Build for production. Convenience scripts target each browser:\n` + `\n` + "```bash\n" + `${runPrefix} build # Chromium (default)\n` + `${runPrefix} build:firefox\n` + `${runPrefix} build:edge\n` + "```\n" + `\n` + `### preview\n` + `\n` + `Preview the production build in the browser:\n` + `\n` + "```bash\n" + `${runPrefix} preview\n` + "```\n" + `\n` + `## Learn more\n` + `\n` + `[Extension.js docs](https://extension.js.org).\n` + `\n` + `## Ship it\n` + `\n` + "Building and running your extension is local and free. When you are ready to share a build or submit it to the stores, [extension.dev](https://docs.extension.dev/publish/overview?utm_source=create-readme) " + `does that side and sponsors Extension.js.\n`;
2027
+ const readme = `<a href="https://extension.js.org" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/Powered%20by%20%7C%20Extension.js-0971fe" alt="Powered by Extension.js" align="right" /></a>\n\n# ${projectName}\n\n` + blockquote + `${screenshotEmbed}` + `## Commands\n` + `\n` + `### dev\n` + `\n` + "Run the extension in development mode. Target a browser with `--browser`:\n" + `\n` + "```bash\n" + `${runPrefix} dev\n` + `${runPrefix} dev${argSeparator} --browser=firefox\n` + `${runPrefix} dev${argSeparator} --browser=edge\n` + "```\n" + `\n` + `### build\n` + `\n` + `Build for production. Convenience scripts target each browser:\n` + `\n` + "```bash\n" + `${runPrefix} build # Chromium (default)\n` + `${runPrefix} build:firefox\n` + `${runPrefix} build:edge\n` + "```\n" + `\n` + `### preview\n` + `\n` + `Preview the production build in the browser:\n` + `\n` + "```bash\n" + `${runPrefix} preview\n` + "```\n" + `\n` + `## Learn more\n` + `\n` + `[Extension.js docs](https://extension.js.org).\n` + shipItSection();
1842
2028
  try {
1843
2029
  if (isDebug()) logger.log(writingReadmeMetaData());
1844
2030
  await promises_namespaceObject.mkdir(projectPath, {
@@ -1911,14 +2097,16 @@ async function writeTemplateProvenance(projectPath, provenance, logger) {
1911
2097
  logger.error(writingTemplateProvenanceError(error));
1912
2098
  }
1913
2099
  }
1914
- async function extensionCreate(projectNameInput, { cliVersion, template = "javascript", install = false, logger = console }) {
2100
+ async function extensionCreate(projectNameInput, { cliVersion, template, install = false, logger = console }) {
1915
2101
  if (!projectNameInput) throw new Error(noProjectName());
2102
+ const templateWasOmitted = null == template || '' === String(template).trim();
2103
+ const effectiveTemplate = templateWasOmitted ? DEFAULT_TEMPLATE_NAME : String(template);
1916
2104
  if (projectNameInput.startsWith('http')) throw new Error(noUrlAllowed());
1917
2105
  const projectPath = external_node_path_namespaceObject.isAbsolute(projectNameInput) ? projectNameInput : external_node_path_namespaceObject.join(process.cwd(), projectNameInput);
1918
2106
  const projectName = external_node_path_namespaceObject.basename(projectPath);
1919
2107
  const updateSuffix = process.env.EXTENSION_CLI_UPDATE_SUFFIX || '';
1920
2108
  if (updateSuffix) delete process.env.EXTENSION_CLI_UPDATE_SUFFIX;
1921
- const requestedTemplate = String(template);
2109
+ const requestedTemplate = effectiveTemplate;
1922
2110
  logger.log(' ');
1923
2111
  logger.log(card({
1924
2112
  version: cliVersion || process.env.EXTENSION_CLI_VERSION,
@@ -1941,28 +2129,32 @@ async function extensionCreate(projectNameInput, { cliVersion, template = "javas
1941
2129
  logger.log(' ');
1942
2130
  process.env.EXTENSION_CLI_BANNER_PRINTED = 'true';
1943
2131
  const createResult = await createDirectory(projectPath, projectName, logger);
1944
- const templateProvenance = await importExternalTemplate(projectPath, projectName, template, logger, {
1945
- ownsProjectDir: createResult?.directoryCreated ?? false
2132
+ const templateProvenance = await importExternalTemplate(projectPath, projectName, effectiveTemplate, logger, {
2133
+ ownsProjectDir: createResult?.directoryCreated ?? false,
2134
+ allowOfflineFallback: templateWasOmitted
1946
2135
  });
1947
2136
  if (templateProvenance?.template) logger.log(usingTemplate(templateProvenance.template, templateProvenance.source));
1948
- const isMonorepoTemplate = String(template).toLowerCase().includes('monorepo');
2137
+ const scaffoldedTemplate = templateProvenance?.template ?? effectiveTemplate;
2138
+ const packageManager = resolveProjectPackageManager(projectPath);
2139
+ const isMonorepoTemplate = String(scaffoldedTemplate).toLowerCase().includes('monorepo');
1949
2140
  if (isDenoRuntime() && !isMonorepoTemplate) await writeDenoJsonc(projectPath, {
1950
- template,
2141
+ template: scaffoldedTemplate,
1951
2142
  cliVersion,
1952
2143
  primary: true
1953
2144
  }, logger);
1954
2145
  else {
1955
2146
  await overridePackageJson(projectPath, {
1956
- template,
1957
- cliVersion
2147
+ template: scaffoldedTemplate,
2148
+ cliVersion,
2149
+ packageManager
1958
2150
  }, logger);
1959
2151
  await writeDenoJsonc(projectPath, {
1960
- template
2152
+ template: scaffoldedTemplate
1961
2153
  }, logger);
1962
2154
  }
1963
2155
  await writeTemplateProvenance(projectPath, templateProvenance, logger);
1964
2156
  if (install) {
1965
- await installDependencies(projectPath, projectName, logger);
2157
+ await installDependencies(projectPath, projectName, logger, packageManager);
1966
2158
  await installInternalDependencies(projectPath, logger);
1967
2159
  }
1968
2160
  await writeReadmeFile(projectPath, projectName, logger);
@@ -1970,16 +2162,16 @@ async function extensionCreate(projectNameInput, { cliVersion, template = "javas
1970
2162
  await writeStoreMetadata(projectPath, projectName, templateManifestName, logger);
1971
2163
  await writeGitignore(projectPath, logger);
1972
2164
  await setupBuiltInTests(projectPath, logger);
1973
- if (isTypeScriptTemplate(template)) await generateExtensionTypes(projectPath, projectName, logger);
2165
+ if (isTypeScriptTemplate(scaffoldedTemplate)) await generateExtensionTypes(projectPath, projectName, logger);
1974
2166
  await initializeGitRepository(projectPath, projectName, templateProvenance?.template, logger);
1975
- const readyMessage = await scaffoldReady(projectPath, projectName, Boolean(install));
2167
+ const readyMessage = await scaffoldReady(projectPath, projectName, Boolean(install), packageManager);
1976
2168
  logger.log(readyMessage);
1977
2169
  return {
1978
2170
  projectPath,
1979
2171
  projectName,
1980
- template: templateProvenance?.template ?? template,
2172
+ template: templateProvenance?.template ?? scaffoldedTemplate,
1981
2173
  depsInstalled: install,
1982
- packageManager: resolveScaffoldPackageManager(),
2174
+ packageManager,
1983
2175
  templateProvenance
1984
2176
  };
1985
2177
  }
@@ -1,5 +1,7 @@
1
1
  export declare const DEFAULT_TEMPLATES_REF = "cb6a25377bd9516a1e55447a2010537019851ab2";
2
2
  export declare const BUNDLED_TEMPLATES: readonly string[];
3
+ export declare const DEFAULT_TEMPLATE_NAME = "typescript";
4
+ export declare const OFFLINE_FALLBACK_TEMPLATE = "javascript";
3
5
  export declare function resolveCatalogUrls(ref: string, overrideUrl?: string): string[];
4
6
  export interface TemplateProvenance {
5
7
  template: string;
@@ -23,6 +25,7 @@ export declare const TEMPLATE_LOCKFILE_NAMES: string[];
23
25
  export declare function removeStaleTemplateLockfiles(projectPath: string): Promise<string[]>;
24
26
  export interface ImportExternalTemplateOptions {
25
27
  ownsProjectDir?: boolean;
28
+ allowOfflineFallback?: boolean;
26
29
  }
27
30
  export declare function cleanupFailedImport(projectPath: string, ownsProjectDir: boolean, preExistingEntries: string[]): Promise<void>;
28
31
  export declare function importExternalTemplate(projectPath: string, projectName: string, template: string, logger: {
@@ -1,4 +1,5 @@
1
+ import { type ScaffoldPackageManager } from '../lib/package-manager';
1
2
  export declare function installDependencies(projectPath: string, projectName: string, logger: {
2
3
  log(...args: unknown[]): void;
3
4
  error(...args: unknown[]): void;
4
- }): Promise<void>;
5
+ }, packageManager?: ScaffoldPackageManager): Promise<void>;
@@ -1,12 +1,15 @@
1
+ import { type ScaffoldPackageManager } from '../lib/package-manager';
1
2
  export declare function resolveExtensionBinary(): Promise<string>;
2
3
  export declare function getTemplateAwareScripts(template: string, extensionBinary: string): Record<string, string>;
3
4
  interface OverridePackageJsonOptions {
4
5
  /** Defaults to `javascript` when omitted (same as `extensionCreate`). */
5
6
  template?: string;
6
7
  cliVersion?: string;
8
+ /** The one manager the project uses; resolved from the scaffold when omitted. */
9
+ packageManager?: ScaffoldPackageManager;
7
10
  }
8
11
  export declare function resolveExtensionDevDependencyVersion(cliVersion?: string): string;
9
- export declare function overridePackageJson(projectPath: string, { template, cliVersion }: OverridePackageJsonOptions, logger: {
12
+ export declare function overridePackageJson(projectPath: string, options: OverridePackageJsonOptions, logger: {
10
13
  log(...args: unknown[]): void;
11
14
  error(...args: unknown[]): void;
12
15
  }): Promise<void>;
@@ -1,5 +1,6 @@
1
1
  {
2
- "createdWith": "extension-create@4.1.10",
3
- "template": "javascript",
4
- "source": "bundled"
2
+ "createdWith": "extension-create@4.1.12",
3
+ "template": "typescript",
4
+ "source": "https://codeload.github.com/extension-js/examples/zip/cb6a25377bd9516a1e55447a2010537019851ab2",
5
+ "ref": "cb6a25377bd9516a1e55447a2010537019851ab2"
5
6
  }
@@ -37,7 +37,3 @@ pnpm run preview
37
37
  ## Learn more
38
38
 
39
39
  [Extension.js docs](https://extension.js.org).
40
-
41
- ## Ship it
42
-
43
- Building and running your extension is local and free. When you are ready to share a build or submit it to the stores, [extension.dev](https://docs.extension.dev/publish/overview?utm_source=create-readme) does that side and sponsors Extension.js.
@@ -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-08-31
12
+ Last updated: 2026-09-05
13
13
 
14
14
  ## Listing
15
15
 
@@ -65,5 +65,5 @@ notes in most cases.
65
65
 
66
66
  ## Version history
67
67
 
68
- - 1.0.0 (unreleased): initial version from the javascript template.
68
+ - 1.0.0 (unreleased): initial version from the typescript template.
69
69
  Not yet submitted to any store.
@@ -2,18 +2,14 @@
2
2
  // Extension.js uses a fresh profile on every run.
3
3
  // Prefer that default? Remove the profile config below.
4
4
  const profile = (name) => `./dist/extension-profile-${name}`
5
- const ciFlags = process.env.CI ? ['--no-sandbox', '--disable-gpu'] : []
6
5
 
7
6
  export default {
8
7
  browser: {
9
- chrome: {profile: profile('chrome'), browserFlags: ciFlags},
10
- chromium: {profile: profile('chromium'), browserFlags: ciFlags},
11
- edge: {profile: profile('edge'), browserFlags: ciFlags},
8
+ chrome: {profile: profile('chrome')},
9
+ chromium: {profile: profile('chromium')},
10
+ edge: {profile: profile('edge')},
12
11
  firefox: {profile: profile('firefox')},
13
- 'chromium-based': {
14
- profile: profile('chromium-based'),
15
- browserFlags: ciFlags
16
- },
12
+ 'chromium-based': {profile: profile('chromium-based')},
17
13
  'gecko-based': {profile: profile('gecko-based')}
18
14
  }
19
15
  }
@@ -5,6 +5,10 @@
5
5
  "version": "1.0.0",
6
6
  "license": "MIT",
7
7
  "type": "module",
8
+ "devDependencies": {
9
+ "typescript": "7.0.2",
10
+ "extension": "^4.1.12"
11
+ },
8
12
  "scripts": {
9
13
  "dev": "extension dev",
10
14
  "start": "extension start",
@@ -15,9 +19,6 @@
15
19
  "build:edge": "extension build --browser edge"
16
20
  },
17
21
  "dependencies": {},
18
- "devDependencies": {
19
- "extension": "^4.1.10"
20
- },
21
22
  "packageManager": "pnpm@10.28.0",
22
23
  "pnpm": {
23
24
  "ignoredBuiltDependencies": [
@@ -11,7 +11,7 @@ if (isFirefoxLike) {
11
11
  browser.sidebarAction.open()
12
12
  })
13
13
 
14
- browser.runtime.onMessage.addListener((message) => {
14
+ browser.runtime.onMessage.addListener((message: any) => {
15
15
  if (!message || message.type !== 'openSidebar') return
16
16
 
17
17
  browser.sidebarAction.open()
@@ -19,7 +19,7 @@ if (isFirefoxLike) {
19
19
  }
20
20
 
21
21
  if (!isFirefoxLike) {
22
- // setPanelBehavior only affects FUTURE action clicks, registering it
22
+ // setPanelBehavior only affects FUTURE action clicks registering it
23
23
  // inside onClicked would swallow the first toolbar click.
24
24
  chrome.sidePanel.setPanelBehavior({openPanelOnActionClick: true})
25
25
  }
@@ -32,7 +32,7 @@ chrome.runtime.onMessage.addListener((message) => {
32
32
  if (!chrome.sidePanel.open) return
33
33
 
34
34
  chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
35
- const activeTabId = tabs?.[0]?.id
35
+ const activeTabId = tabs && tabs[0] && tabs[0].id
36
36
  if (!activeTabId) return
37
37
 
38
38
  try {
@@ -1,6 +1,6 @@
1
1
  import logo from '../images/icon.png'
2
2
 
3
- export default function createContentApp() {
3
+ export default function createContentApp(): HTMLDivElement {
4
4
  const container = document.createElement('div')
5
5
  container.className = 'content_script'
6
6
 
@@ -9,22 +9,17 @@ export default function createContentApp() {
9
9
  pill.className = 'content_pill'
10
10
  pill.setAttribute('aria-label', 'Open sidebar')
11
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 {
19
- chrome.runtime.sendMessage({type: 'openSidebar'})
20
- }
21
- } catch (error) {
22
- console.error(error)
12
+ if (import.meta.env.EXTENSION_PUBLIC_BROWSER === 'firefox') {
13
+ browser.runtime.sendMessage({type: 'openSidebar'})
14
+ } else {
15
+ chrome.runtime.sendMessage({type: 'openSidebar'})
23
16
  }
24
17
  })
25
18
 
26
19
  const img = document.createElement('img')
27
20
  img.className = 'content_pill_logo'
21
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
22
+ // @ts-ignore
28
23
  img.src = logo
29
24
  img.alt = ''
30
25
  img.setAttribute('aria-hidden', 'true')
@@ -1,4 +1,4 @@
1
- import createContentApp from './ContentApp.js'
1
+ import createContentApp from './ContentApp'
2
2
  import './styles.css'
3
3
 
4
4
  console.log('[From the page context] Hello from content_scripts!')
@@ -0,0 +1,15 @@
1
+ declare global {
2
+ interface ImportMeta {
3
+ webpackHot?: {
4
+ accept: (module?: string, callback?: () => void) => void
5
+ dispose: (callback: () => void) => void
6
+ }
7
+ }
8
+ }
9
+
10
+ declare module '*.svg' {
11
+ const content: string
12
+ export default content
13
+ }
14
+
15
+ export {}
@@ -53,9 +53,9 @@
53
53
  "sidePanel"
54
54
  ],
55
55
  "background": {
56
- "chromium:service_worker": "background.js",
56
+ "chromium:service_worker": "background.ts",
57
57
  "firefox:scripts": [
58
- "background.js"
58
+ "background.ts"
59
59
  ]
60
60
  },
61
61
  "content_scripts": [
@@ -64,7 +64,7 @@
64
64
  "<all_urls>"
65
65
  ],
66
66
  "js": [
67
- "content/scripts.js"
67
+ "content/scripts.ts"
68
68
  ]
69
69
  }
70
70
  ]
@@ -1,5 +1,4 @@
1
- import './styles.css'
2
- import javascriptLogo from '../images/icon.png'
1
+ import typescriptLogo from '../images/icon.png'
3
2
 
4
3
  function SidebarApp() {
5
4
  const root = document.getElementById('root')
@@ -9,8 +8,8 @@ function SidebarApp() {
9
8
  <div class="sidebar_app">
10
9
  <img
11
10
  class="sidebar_logo"
12
- src="${javascriptLogo}"
13
- alt="The JavaScript logo"
11
+ src="${typescriptLogo}"
12
+ alt="The TypeScript logo"
14
13
  />
15
14
  <h1 class="sidebar_title">Sidebar Panel</h1>
16
15
  <p class="sidebar_description">
@@ -3,11 +3,11 @@
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>JavaScript Sidebar</title>
6
+ <title>TypeScript Sidebar</title>
7
7
  </head>
8
8
  <body>
9
9
  <noscript>You need to enable JavaScript to run this extension.</noscript>
10
10
  <div id="root"></div>
11
11
  </body>
12
- <script src="./scripts.js"></script>
12
+ <script src="./scripts.ts"></script>
13
13
  </html>
@@ -1,4 +1,4 @@
1
- import './SidebarApp.js'
1
+ import './SidebarApp'
2
2
  import './styles.css'
3
3
 
4
4
  console.log('[From the sidebar page context] Hello regular page!')
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "compilerOptions": {
4
+ "allowJs": true,
5
+ "allowSyntheticDefaultImports": true,
6
+ "esModuleInterop": true,
7
+ "forceConsistentCasingInFileNames": true,
8
+ "isolatedModules": true,
9
+ "jsx": "react-jsx",
10
+ "lib": ["dom", "dom.iterable", "esnext"],
11
+ "moduleResolution": "bundler",
12
+ "module": "esnext",
13
+ "noEmit": true,
14
+ "resolveJsonModule": true,
15
+ "strict": true,
16
+ "target": "esnext",
17
+ "verbatimModuleSyntax": false,
18
+ "useDefineForClassFields": true,
19
+ "skipLibCheck": true,
20
+ "allowImportingTsExtensions": true
21
+ },
22
+ "include": ["./", "extension-env.d.ts", "browser-global.d.ts"],
23
+ "exclude": ["node_modules", "dist"]
24
+ }
package/package.json CHANGED
@@ -25,7 +25,7 @@
25
25
  "templates"
26
26
  ],
27
27
  "name": "extension-create",
28
- "version": "4.1.11",
28
+ "version": "4.1.13",
29
29
  "description": "The standalone extension creation engine for Extension.js",
30
30
  "author": {
31
31
  "name": "Cezar Augusto",