Add updateassets command with webapp code detection

- Add updateassets command to update plugin files from embedded assets
- Only include webapp assets if plugin manifest indicates webapp code presence
- Compare file contents before updating to avoid unnecessary writes
- Display count of updated files in completion message

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Felipe M 2025-07-09 16:38:26 +02:00
parent 1ea8f2b38a
commit b43e7ac3ec
No known key found for this signature in database
GPG key ID: 52E5D65FCF99808A
8 changed files with 366 additions and 0 deletions

27
assets/.editorconfig Normal file
View file

@ -0,0 +1,27 @@
# http://editorconfig.org/
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
[*.go]
indent_style = tab
[*.{js,jsx,ts,tsx,json,html}]
indent_style = space
indent_size = 4
[webapp/package.json]
indent_size = 2
[{Makefile,*.mk}]
indent_style = tab
[*.md]
indent_style = space
indent_size = 4
trim_trailing_whitespace = false

23
assets/.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,23 @@
name: ci
on:
schedule:
- cron: "0 0 * * *"
push:
branches:
- main
tags:
- "v*"
pull_request:
permissions:
contents: read
env:
IS_CI: true
jobs:
plugin-ci:
uses: mattermost/actions-workflows/.github/workflows/plugin-ci.yml
secrets: inherit
with:
mage-version: v1.15.0

48
assets/.golangci.yml Normal file
View file

@ -0,0 +1,48 @@
run:
timeout: 5m
modules-download-mode: readonly
linters-settings:
gofmt:
simplify: true
goimports:
local-prefixes: github.com/mattermost/mattermost-starter-template
govet:
check-shadowing: true
enable-all: true
disable:
- fieldalignment
misspell:
locale: US
linters:
disable-all: true
enable:
- bodyclose
- errcheck
- gocritic
- gofmt
- goimports
- gosec
- gosimple
- govet
- ineffassign
- misspell
- nakedret
- revive
- staticcheck
- stylecheck
- typecheck
- unconvert
- unused
- whitespace
issues:
exclude-rules:
- path: server/configuration.go
linters:
- unused
- path: _test\.go
linters:
- bodyclose
- scopelint # https://github.com/kyoh86/scopelint/issues/4

1
assets/webapp/.npmrc Normal file
View file

@ -0,0 +1 @@
save-exact=true

View file

@ -0,0 +1,46 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
const config = {
presets: [
['@babel/preset-env', {
targets: {
chrome: 66,
firefox: 60,
edge: 42,
safari: 12,
},
modules: false,
corejs: 3,
debug: false,
useBuiltIns: 'usage',
shippedProposals: true,
}],
['@babel/preset-react', {
useBuiltIns: true,
}],
['@babel/typescript', {
allExtensions: true,
isTSX: true,
}],
['@emotion/babel-preset-css-prop'],
],
plugins: [
'@babel/plugin-proposal-class-properties',
'@babel/plugin-syntax-dynamic-import',
'@babel/proposal-object-rest-spread',
'@babel/plugin-proposal-optional-chaining',
'babel-plugin-typescript-to-proptypes',
],
};
// Jest needs module transformation
config.env = {
test: {
presets: config.presets,
plugins: config.plugins,
},
};
config.env.test.presets[0][1].modules = 'auto';
module.exports = config;

View file

@ -0,0 +1,115 @@
const exec = require('child_process').exec;
const path = require('path');
const webpack = require('webpack');
const PLUGIN_ID = require('../plugin.json').id;
const NPM_TARGET = process.env.npm_lifecycle_event; //eslint-disable-line no-process-env
const isDev = NPM_TARGET === 'debug' || NPM_TARGET === 'debug:watch';
const plugins = [
new webpack.ProvidePlugin({
process: 'process/browser',
}),
];
if (NPM_TARGET === 'build:watch' || NPM_TARGET === 'debug:watch') {
plugins.push({
apply: (compiler) => {
compiler.hooks.watchRun.tap('WatchStartPlugin', () => {
// eslint-disable-next-line no-console
console.log('Change detected. Rebuilding webapp.');
});
compiler.hooks.afterEmit.tap('AfterEmitPlugin', () => {
exec('cd .. && make deploy-from-watch', (err, stdout, stderr) => {
if (stdout) {
process.stdout.write(stdout);
}
if (stderr) {
process.stderr.write(stderr);
}
});
});
},
});
}
const config = {
entry: [
'./src/index.tsx',
],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
modules: [
'src',
'node_modules',
path.resolve(__dirname),
],
extensions: ['*', '.js', '.jsx', '.ts', '.tsx'],
},
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
//exclude: /node_modules\/(?!(mattermost-webapp|@mattermost)\/).*/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
// Babel configuration is in babel.config.js because jest requires it to be there.
},
},
},
{
test: /\.(scss|css)$/,
use: [
'style-loader',
{
loader: 'css-loader',
},
{
loader: 'sass-loader',
options: {
sassOptions: {
includePaths: ['node_modules/compass-mixins/lib', 'sass'],
},
},
},
],
},
{
test: /\.svg$/,
use: ['@svgr/webpack'],
},
],
},
externals: {
react: 'React',
'react-dom': 'ReactDOM',
redux: 'Redux',
'react-redux': 'ReactRedux',
'prop-types': 'PropTypes',
'react-bootstrap': 'ReactBootstrap',
'react-router-dom': 'ReactRouterDom',
},
output: {
devtoolNamespace: PLUGIN_ID,
path: path.join(__dirname, '/dist'),
publicPath: '/',
filename: 'main.js',
},
mode: (isDev) ? 'eval-source-map' : 'production',
plugins,
};
if (isDev) {
Object.assign(config, {devtool: 'eval-source-map'});
}
module.exports = config;

View file

@ -49,6 +49,8 @@ func runCommand(command string, args []string, pluginPath string) error {
return runDisableCommand(args, pluginPath)
case "reset":
return runResetCommand(args, pluginPath)
case "updateassets":
return runUpdateAssetsCommand(args, pluginPath)
case "help":
showUsage()
@ -82,6 +84,10 @@ func runResetCommand(args []string, pluginPath string) error {
return pluginctl.RunResetCommand(args, pluginPath)
}
func runUpdateAssetsCommand(args []string, pluginPath string) error {
return pluginctl.RunUpdateAssetsCommand(args, pluginPath)
}
func showUsage() {
fmt.Printf(`pluginctl - Mattermost Plugin Development CLI
@ -96,6 +102,7 @@ Commands:
enable Enable plugin from current directory in Mattermost server
disable Disable plugin from current directory in Mattermost server
reset Reset plugin from current directory (disable then enable)
updateassets Update plugin files from embedded assets
help Show this help message
version Show version information
@ -105,6 +112,7 @@ Examples:
pluginctl enable # Enable plugin from current directory
pluginctl disable # Disable plugin from current directory
pluginctl reset # Reset plugin from current directory (disable then enable)
pluginctl updateassets # Update plugin files from embedded assets
export PLUGINCTL_PLUGIN_PATH=/path/to/plugin
pluginctl info # Show info using environment variable
pluginctl version # Show version information

98
updateassets.go Normal file
View file

@ -0,0 +1,98 @@
package pluginctl
import (
"bytes"
"embed"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
)
//go:embed assets/**/*
var assetsFS embed.FS
func RunUpdateAssetsCommand(args []string, pluginPath string) error {
if len(args) > 0 {
return fmt.Errorf("updateassets command does not accept arguments")
}
fmt.Printf("Updating assets in plugin directory: %s\n", pluginPath)
// Load plugin manifest to check for webapp code
manifest, err := LoadPluginManifestFromPath(pluginPath)
if err != nil {
return fmt.Errorf("failed to load plugin manifest: %w", err)
}
// Check if the plugin has webapp code according to manifest
hasWebapp := HasWebappCode(manifest)
// Counter for updated files
var updatedCount int
// Walk through the embedded assets
err = fs.WalkDir(assetsFS, "assets", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
// Skip the root assets directory
if path == "assets" {
return nil
}
// Remove the "assets/" prefix to get the relative path
relativePath := path[7:] // len("assets/") = 7
// Skip webapp assets if plugin doesn't have webapp code
if !hasWebapp && strings.HasPrefix(relativePath, "webapp") {
return nil
}
targetPath := filepath.Join(pluginPath, relativePath)
if d.IsDir() {
// Create directory if it doesn't exist
if err := os.MkdirAll(targetPath, 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %w", targetPath, err)
}
} else {
// Read file content from embedded FS
content, err := assetsFS.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read embedded file %s: %w", path, err)
}
// Check if target file exists and compare content
existingContent, err := os.ReadFile(targetPath)
if err == nil && bytes.Equal(existingContent, content) {
// File exists and content is identical, skip update
return nil
}
// Create parent directory if it doesn't exist
parentDir := filepath.Dir(targetPath)
if err := os.MkdirAll(parentDir, 0755); err != nil {
return fmt.Errorf("failed to create parent directory %s: %w", parentDir, err)
}
// Write file to target location
if err := os.WriteFile(targetPath, content, 0644); err != nil {
return fmt.Errorf("failed to write file %s: %w", targetPath, err)
}
fmt.Printf("Updated file: %s\n", relativePath)
updatedCount++
}
return nil
})
if err != nil {
return fmt.Errorf("failed to update assets: %w", err)
}
fmt.Printf("Assets updated successfully! (%d files updated)\n", updatedCount)
return nil
}