Commit 59e72c8c authored by Mickaël Desfrênes's avatar Mickaël Desfrênes
Browse files

better management of vocabulary bundles

parent e4a4d2ca
Loading
Loading
Loading
Loading
+23 −0
Original line number Diff line number Diff line
@@ -40,6 +40,29 @@ Maintain `climax` as a stable CLI app, while preserving behavior and output expe
- On cache miss, show network activity feedback (spinner/progress) on interactive terminals.
- Keep non-interactive output clean (no noisy progress output when not on a TTY).

## Vocabulary Bundle Behavior

- A vocabulary bundle is detected from the installed bundle contents, not only
  from `available-bundles.xml`.
- Detection requires:
  - `expath-pkg.xml`
  - an EXPath `<xquery>` namespace
  - an EXPath-listed `<file>`
  - a function named `xml-to-html` declared in that namespace.
- XQuery namespace declarations can use either:
  - `declare namespace prefix = "...";`
  - `module namespace prefix = "...";`
- If an EXPath `<file>` value is only a basename, the detector falls back to a
  recursive basename search inside the bundle. Keep path traversal protections
  intact when changing this logic.
- When `bundles-add` adds a vocabulary bundle, it becomes the configured
  `vocabulary-bundle`; the previous vocabulary bundle is removed from active
  bundles in `config.xml` and sync is run again so the old bundle is ignored
  like a removed bundle.
- Only one vocabulary bundle should be active. Do not reintroduce behavior where
  every bundle containing `expath-pkg.xml` is treated as a vocabulary bundle.
- `bundles-list` should keep showing whether each row is a vocabulary bundle.

## Storage and Project Registry

- Use JSON storage in `~/.climax/projects.json`.
+3 −0
Original line number Diff line number Diff line
@@ -229,6 +229,9 @@ func TestBundlesAddHelpDocumentsInputKinds(t *testing.T) {
		"MaX-provided bundle",
		"local zip bundle archive",
		"HTTPS URL to a remote zip bundle archive",
		"project's vocabulary bundle",
		"previous vocabulary bundle is removed from active bundles",
		"sync runs again",
		"climax bundles-add max-dev",
		"climax bundles-add ./local-bundle.zip",
		"climax bundles-add https://example.org/local-bundle.zip --yes",
+12 −0
Original line number Diff line number Diff line
@@ -190,6 +190,7 @@ Options :
Liste les bundles disponibles et configurés.
Un bundle est un paquet d'extension MaX qui peut ajouter des templates, des
fichiers statiques, des scripts ou d'autres ressources au projet.
Le tableau indique aussi si chaque bundle est un bundle de vocabulaire.

- Liste les bundles du projet courant :

@@ -211,6 +212,17 @@ Options :

Ajoute un bundle à la configuration du projet, puis synchronise le projet.

Si le bundle ajouté est détecté comme bundle de vocabulaire, il devient le
bundle de vocabulaire configuré du projet. Le précédent bundle de vocabulaire
est retiré des bundles actifs dans `config.xml`, puis la synchronisation est
relancée pour l'ignorer comme un bundle supprimé.

La détection d'un bundle de vocabulaire s'appuie sur le contenu du bundle
installé : `expath-pkg.xml` doit lister un namespace XQuery et un fichier, et
ce module doit déclarer une fonction `xml-to-html` dans le namespace du paquet.
Les fichiers listés peuvent se trouver dans des sous-dossiers ; `bundles-add`
les résout après extraction.

- Ajoute le bundle `max-dev` au projet courant :

```sh
+65 −28
Original line number Diff line number Diff line
@@ -160,7 +160,7 @@ func runInfo(directory string, jsonOut bool) error {
}

func runBundlesList(directory string) error {
	_, config, err := prepareProject(directory)
	abs, config, err := prepareProject(directory)
	if err != nil {
		return err
	}
@@ -170,13 +170,18 @@ func runBundlesList(directory string) error {
	}

	t := newStyledTable()
	t.AppendHeader(table.Row{T("table.bundles.name"), T("table.bundles.installed"), T("table.bundles.description")})
	t.AppendHeader(table.Row{T("table.bundles.name"), T("table.bundles.installed"), T("table.bundles.vocabulary"), T("table.bundles.description")})
	done := map[string]struct{}{}
	for _, name := range sortedMapKeys(available) {
		b := available[name]
		isVocabulary, err := bundleVocabularyStatus(abs, b.Name, b.Vocabulary)
		if err != nil {
			return err
		}
		t.AppendRow(table.Row{
			b.Name,
			coloredYesNo(b.Active),
			coloredYesNo(isVocabulary),
			b.Description,
		})
		done[name] = struct{}{}
@@ -188,12 +193,28 @@ func runBundlesList(directory string) error {
		}
	}
	for _, name := range localNames {
		t.AppendRow(table.Row{name, coloredYesNo(true), T("msg.bundle.local_no_desc")})
		isVocabulary, err := bundleVocabularyStatus(abs, name, false)
		if err != nil {
			return err
		}
		t.AppendRow(table.Row{name, coloredYesNo(true), coloredYesNo(isVocabulary), T("msg.bundle.local_no_desc")})
	}
	t.Render()
	return nil
}

func bundleVocabularyStatus(rootDirectory, bundleName string, fallback bool) (bool, error) {
	bundleDir := filepath.Join(rootDirectory, ".max", "basex", "webapp", "max", "bundles", bundleName)
	if !dirExists(bundleDir) {
		return fallback, nil
	}
	isVocabulary, err := bundleIsVocabularyBundle(bundleDir)
	if err != nil {
		return false, err
	}
	return isVocabulary, nil
}

func runBundlesAdd(bundleName, directory string, yes bool) error {
	abs, config, err := prepareProject(directory)
	if err != nil {
@@ -221,14 +242,7 @@ func runBundlesAdd(bundleName, directory string, yes bool) error {
			}
		}
		current[remoteName] = bundleConfig{Name: remoteName, URL: remoteURL}
		config.setBundlesMap(current)
		if err := config.write(); err != nil {
			return err
		}
		if err := syncMaxInstance(abs, false); err != nil {
			return err
		}
		return runBundlesList(abs)
		return writeSyncAndListAfterBundleAdd(abs, config, current, remoteName)
	}

	if fromArchive != "" {
@@ -246,39 +260,62 @@ func runBundlesAdd(bundleName, directory string, yes bool) error {
			return err
		}
		current[bundleName] = bundleConfig{Name: bundleName, URL: "local://" + filepath.Base(localDest)}
		config.setBundlesMap(current)
		if fileExists(filepath.Join(localDest, "expath-pkg.xml")) {
			config.Cfg.VocabularyBundle = bundleName
		return writeSyncAndListAfterBundleAdd(abs, config, current, bundleName)
	}

	available, err := config.availableBundles()
	if err != nil {
		return err
	}
	selected, ok := available[bundleName]
	if !ok {
		return fmt.Errorf(T("err.bundle_not_available"), bundleName)
	}
	current[bundleName] = bundleConfig{Name: bundleName, URL: selected.URL}
	return writeSyncAndListAfterBundleAdd(abs, config, current, bundleName)
}

func writeSyncAndListAfterBundleAdd(rootDirectory string, config *projectConfig, bundles map[string]bundleConfig, bundleName string) error {
	config.setBundlesMap(bundles)
	if err := config.write(); err != nil {
		return err
	}
		if err := syncMaxInstance(abs, false); err != nil {
	if err := syncMaxInstance(rootDirectory, false); err != nil {
		return err
	}
		return runBundlesList(abs)
	if err := updateVocabularyBundleAfterAdd(rootDirectory, bundleName); err != nil {
		return err
	}
	return runBundlesList(rootDirectory)
}

	available, err := config.availableBundles()
func updateVocabularyBundleAfterAdd(rootDirectory, bundleName string) error {
	bundleDir := filepath.Join(rootDirectory, ".max", "basex", "webapp", "max", "bundles", bundleName)
	isVocabulary, err := bundleIsVocabularyBundle(bundleDir)
	if err != nil {
		return err
	}
	selected, ok := available[bundleName]
	if !ok {
		return fmt.Errorf(T("err.bundle_not_available"), bundleName)
	if !isVocabulary {
		return nil
	}
	if selected.Vocabulary {
		config.Cfg.VocabularyBundle = bundleName
	config, err := loadProjectConfig(filepath.Join(rootDirectory, maxConfigFile))
	if err != nil {
		return err
	}
	current[bundleName] = bundleConfig{Name: bundleName, URL: selected.URL}
	current := config.bundlesMap()
	previous := strings.TrimSpace(config.Cfg.VocabularyBundle)
	if previous != "" && previous != bundleName {
		delete(current, previous)
	}
	if _, ok := current[bundleName]; !ok {
		current[bundleName] = bundleConfig{Name: bundleName}
	}
	config.Cfg.VocabularyBundle = bundleName
	config.setBundlesMap(current)
	if err := config.write(); err != nil {
		return err
	}
	if err := syncMaxInstance(abs, false); err != nil {
		return err
	}
	return runBundlesList(abs)
	return syncMaxInstance(rootDirectory, false)
}

func remoteBundleArchive(input string) (string, string, bool, error) {
+11 −0
Original line number Diff line number Diff line
@@ -186,6 +186,7 @@ Options:
List available and configured bundles. Output keeps the existing table style.
A bundle is a MaX extension package that can add templates, static assets,
scripts, or other project resources.
The table also indicates whether each bundle is a vocabulary bundle.

- List bundles for the current project:

@@ -207,6 +208,16 @@ Options:

Add a bundle to the project configuration, then sync the project.

If the added bundle is detected as a vocabulary bundle, it becomes the
project's configured vocabulary bundle. The previously configured vocabulary
bundle is removed from active bundles in `config.xml`, then sync runs again so
the previous bundle is ignored in the same way as a removed bundle.

Vocabulary bundle detection is based on the installed bundle contents:
`expath-pkg.xml` must list an XQuery namespace and file, and that listed module
must declare an `xml-to-html` function in the package namespace. Listed files
may be stored in subdirectories; `bundles-add` resolves them after extraction.

- Add the `max-dev` bundle to the current project:

```sh
Loading