@@ -3,27 +3,30 @@ import { outputFile, pathExists, readFile } from "fs-extra";
33import { dump as dumpYaml , load as loadYaml } from "js-yaml" ;
44import { minimatch } from "minimatch" ;
55import { CancellationToken , window } from "vscode" ;
6- import { CodeQLCliServer } from "../codeql-cli/cli" ;
7- import {
8- getOnDiskWorkspaceFolders ,
9- getOnDiskWorkspaceFoldersObjects ,
10- } from "../common/vscode/workspace-folders" ;
6+ import { CodeQLCliServer , QlpacksInfo } from "../codeql-cli/cli" ;
7+ import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders" ;
118import { ProgressCallback } from "../common/vscode/progress" ;
129import { DatabaseItem } from "../databases/local-databases" ;
1310import { getQlPackPath , QLPACK_FILENAMES } from "../common/ql" ;
1411import { getErrorMessage } from "../common/helpers-pure" ;
1512import { ExtensionPack , ExtensionPackModelFile } from "./shared/extension-pack" ;
1613import { NotificationLogger , showAndLogErrorMessage } from "../common/logging" ;
1714import { containsPath } from "../common/files" ;
15+ import { disableAutoNameExtensionPack } from "../config" ;
16+ import {
17+ autoNameExtensionPack ,
18+ ExtensionPackName ,
19+ formatPackName ,
20+ parsePackName ,
21+ validatePackName ,
22+ } from "./extension-pack-name" ;
23+ import {
24+ askForWorkspaceFolder ,
25+ autoPickExtensionsDirectory ,
26+ } from "./extensions-workspace-folder" ;
1827
1928const maxStep = 3 ;
2029
21- const packNamePartRegex = / [ a - z 0 - 9 ] (?: [ a - z 0 - 9 - ] * [ a - z 0 - 9 ] ) ? / ;
22- const packNameRegex = new RegExp (
23- `^(?<scope>${ packNamePartRegex . source } )/(?<name>${ packNamePartRegex . source } )$` ,
24- ) ;
25- const packNameLength = 128 ;
26-
2730export async function pickExtensionPackModelFile (
2831 cliServer : Pick < CodeQLCliServer , "resolveQlpacks" | "resolveExtensions" > ,
2932 databaseItem : Pick < DatabaseItem , "name" | "language" > ,
@@ -79,6 +82,21 @@ async function pickExtensionPack(
7982 true ,
8083 ) ;
8184
85+ if ( ! disableAutoNameExtensionPack ( ) ) {
86+ progress ( {
87+ message : "Creating extension pack..." ,
88+ step : 2 ,
89+ maxStep,
90+ } ) ;
91+
92+ return autoCreateExtensionPack (
93+ databaseItem . name ,
94+ databaseItem . language ,
95+ extensionPacksInfo ,
96+ logger ,
97+ ) ;
98+ }
99+
82100 if ( Object . keys ( extensionPacksInfo ) . length === 0 ) {
83101 return pickNewExtensionPack ( databaseItem , token ) ;
84102 }
@@ -239,51 +257,35 @@ async function pickNewExtensionPack(
239257 databaseItem : Pick < DatabaseItem , "name" | "language" > ,
240258 token : CancellationToken ,
241259) : Promise < ExtensionPack | undefined > {
242- const workspaceFolders = getOnDiskWorkspaceFoldersObjects ( ) ;
243- const workspaceFolderOptions = workspaceFolders . map ( ( folder ) => ( {
244- label : folder . name ,
245- detail : folder . uri . fsPath ,
246- path : folder . uri . fsPath ,
247- } ) ) ;
248-
249- // We're not using window.showWorkspaceFolderPick because that also includes the database source folders while
250- // we only want to include on-disk workspace folders.
251- const workspaceFolder = await window . showQuickPick ( workspaceFolderOptions , {
252- title : "Select workspace folder to create extension pack in" ,
253- } ) ;
260+ const workspaceFolder = await askForWorkspaceFolder ( ) ;
254261 if ( ! workspaceFolder ) {
255262 return undefined ;
256263 }
257264
258- let examplePackName = ` ${ databaseItem . name } -extensions` ;
259- if ( ! examplePackName . includes ( "/" ) ) {
260- examplePackName = `pack/ ${ examplePackName } ` ;
261- }
265+ const examplePackName = autoNameExtensionPack (
266+ databaseItem . name ,
267+ databaseItem . language ,
268+ ) ;
262269
263- const packName = await window . showInputBox (
270+ const name = await window . showInputBox (
264271 {
265272 title : "Create new extension pack" ,
266273 prompt : "Enter name of extension pack" ,
267- placeHolder : `e.g. ${ examplePackName } ` ,
274+ placeHolder : examplePackName
275+ ? `e.g. ${ formatPackName ( examplePackName ) } `
276+ : "" ,
268277 validateInput : async ( value : string ) : Promise < string | undefined > => {
269- if ( ! value ) {
270- return "Pack name must not be empty" ;
271- }
272-
273- if ( value . length > packNameLength ) {
274- return `Pack name must be no longer than ${ packNameLength } characters` ;
278+ const message = validatePackName ( value ) ;
279+ if ( message ) {
280+ return message ;
275281 }
276282
277- const matches = packNameRegex . exec ( value ) ;
278- if ( ! matches ?. groups ) {
279- if ( ! value . includes ( "/" ) ) {
280- return "Invalid package name: a pack name must contain a slash to separate the scope from the pack name" ;
281- }
282-
283- return "Invalid package name: a pack name must contain only lowercase ASCII letters, ASCII digits, and hyphens" ;
283+ const packName = parsePackName ( value ) ;
284+ if ( ! packName ) {
285+ return "Invalid pack name" ;
284286 }
285287
286- const packPath = join ( workspaceFolder . path , matches . groups . name ) ;
288+ const packPath = join ( workspaceFolder . uri . fsPath , packName . name ) ;
287289 if ( await pathExists ( packPath ) ) {
288290 return `A pack already exists at ${ packPath } ` ;
289291 }
@@ -293,31 +295,121 @@ async function pickNewExtensionPack(
293295 } ,
294296 token ,
295297 ) ;
298+ if ( ! name ) {
299+ return undefined ;
300+ }
301+
302+ const packName = parsePackName ( name ) ;
296303 if ( ! packName ) {
297304 return undefined ;
298305 }
299306
300- const matches = packNameRegex . exec ( packName ) ;
301- if ( ! matches ?. groups ) {
302- return ;
307+ const packPath = join ( workspaceFolder . uri . fsPath , packName . name ) ;
308+
309+ if ( await pathExists ( packPath ) ) {
310+ return undefined ;
303311 }
304312
305- const name = matches . groups . name ;
306- const packPath = join ( workspaceFolder . path , name ) ;
313+ return writeExtensionPack ( packPath , packName , databaseItem . language ) ;
314+ }
315+
316+ async function autoCreateExtensionPack (
317+ name : string ,
318+ language : string ,
319+ extensionPacksInfo : QlpacksInfo ,
320+ logger : NotificationLogger ,
321+ ) : Promise < ExtensionPack | undefined > {
322+ // Get the extensions directory to create the extension pack in
323+ const extensionsDirectory = await autoPickExtensionsDirectory ( ) ;
324+ if ( ! extensionsDirectory ) {
325+ return undefined ;
326+ }
327+
328+ // Generate the name of the extension pack
329+ const packName = autoNameExtensionPack ( name , language ) ;
330+ if ( ! packName ) {
331+ void showAndLogErrorMessage (
332+ logger ,
333+ `Could not automatically name extension pack for database ${ name } ` ,
334+ ) ;
335+
336+ return undefined ;
337+ }
338+
339+ // Find any existing locations of this extension pack
340+ const existingExtensionPackPaths =
341+ extensionPacksInfo [ formatPackName ( packName ) ] ;
342+
343+ // If there is already an extension pack with this name, use it if it is valid
344+ if ( existingExtensionPackPaths ?. length === 1 ) {
345+ let extensionPack : ExtensionPack ;
346+ try {
347+ extensionPack = await readExtensionPack ( existingExtensionPackPaths [ 0 ] ) ;
348+ } catch ( e : unknown ) {
349+ void showAndLogErrorMessage (
350+ logger ,
351+ `Could not read extension pack ${ formatPackName ( packName ) } ` ,
352+ {
353+ fullMessage : `Could not read extension pack ${ formatPackName (
354+ packName ,
355+ ) } at ${ existingExtensionPackPaths [ 0 ] } : ${ getErrorMessage ( e ) } `,
356+ } ,
357+ ) ;
358+
359+ return undefined ;
360+ }
361+
362+ return extensionPack ;
363+ }
364+
365+ // If there is already an existing extension pack with this name, but it resolves
366+ // to multiple paths, then we can't use it
367+ if ( existingExtensionPackPaths ?. length > 1 ) {
368+ void showAndLogErrorMessage (
369+ logger ,
370+ `Extension pack ${ formatPackName ( packName ) } resolves to multiple paths` ,
371+ {
372+ fullMessage : `Extension pack ${ formatPackName (
373+ packName ,
374+ ) } resolves to multiple paths: ${ existingExtensionPackPaths . join (
375+ ", " ,
376+ ) } `,
377+ } ,
378+ ) ;
379+
380+ return undefined ;
381+ }
382+
383+ const packPath = join ( extensionsDirectory . fsPath , packName . name ) ;
307384
308385 if ( await pathExists ( packPath ) ) {
386+ void showAndLogErrorMessage (
387+ logger ,
388+ `Directory ${ packPath } already exists for extension pack ${ formatPackName (
389+ packName ,
390+ ) } `,
391+ ) ;
392+
309393 return undefined ;
310394 }
311395
396+ return writeExtensionPack ( packPath , packName , language ) ;
397+ }
398+
399+ async function writeExtensionPack (
400+ packPath : string ,
401+ packName : ExtensionPackName ,
402+ language : string ,
403+ ) : Promise < ExtensionPack > {
312404 const packYamlPath = join ( packPath , "codeql-pack.yml" ) ;
313405
314406 const extensionPack : ExtensionPack = {
315407 path : packPath ,
316408 yamlPath : packYamlPath ,
317- name : packName ,
409+ name : formatPackName ( packName ) ,
318410 version : "0.0.0" ,
319411 extensionTargets : {
320- [ `codeql/${ databaseItem . language } -all` ] : "*" ,
412+ [ `codeql/${ language } -all` ] : "*" ,
321413 } ,
322414 dataExtensions : [ "models/**/*.yml" ] ,
323415 } ;
0 commit comments