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:
parent
1ea8f2b38a
commit
b43e7ac3ec
8 changed files with 366 additions and 0 deletions
27
assets/.editorconfig
Normal file
27
assets/.editorconfig
Normal 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
23
assets/.github/workflows/ci.yml
vendored
Normal 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
48
assets/.golangci.yml
Normal 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
1
assets/webapp/.npmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
save-exact=true
|
46
assets/webapp/babel.config.js
Normal file
46
assets/webapp/babel.config.js
Normal 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;
|
115
assets/webapp/webpack.config.js
Normal file
115
assets/webapp/webpack.config.js
Normal 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;
|
|
@ -49,6 +49,8 @@ func runCommand(command string, args []string, pluginPath string) error {
|
||||||
return runDisableCommand(args, pluginPath)
|
return runDisableCommand(args, pluginPath)
|
||||||
case "reset":
|
case "reset":
|
||||||
return runResetCommand(args, pluginPath)
|
return runResetCommand(args, pluginPath)
|
||||||
|
case "updateassets":
|
||||||
|
return runUpdateAssetsCommand(args, pluginPath)
|
||||||
case "help":
|
case "help":
|
||||||
showUsage()
|
showUsage()
|
||||||
|
|
||||||
|
@ -82,6 +84,10 @@ func runResetCommand(args []string, pluginPath string) error {
|
||||||
return pluginctl.RunResetCommand(args, pluginPath)
|
return pluginctl.RunResetCommand(args, pluginPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runUpdateAssetsCommand(args []string, pluginPath string) error {
|
||||||
|
return pluginctl.RunUpdateAssetsCommand(args, pluginPath)
|
||||||
|
}
|
||||||
|
|
||||||
func showUsage() {
|
func showUsage() {
|
||||||
fmt.Printf(`pluginctl - Mattermost Plugin Development CLI
|
fmt.Printf(`pluginctl - Mattermost Plugin Development CLI
|
||||||
|
|
||||||
|
@ -96,6 +102,7 @@ Commands:
|
||||||
enable Enable plugin from current directory in Mattermost server
|
enable Enable plugin from current directory in Mattermost server
|
||||||
disable Disable 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)
|
reset Reset plugin from current directory (disable then enable)
|
||||||
|
updateassets Update plugin files from embedded assets
|
||||||
help Show this help message
|
help Show this help message
|
||||||
version Show version information
|
version Show version information
|
||||||
|
|
||||||
|
@ -105,6 +112,7 @@ Examples:
|
||||||
pluginctl enable # Enable plugin from current directory
|
pluginctl enable # Enable plugin from current directory
|
||||||
pluginctl disable # Disable plugin from current directory
|
pluginctl disable # Disable plugin from current directory
|
||||||
pluginctl reset # Reset plugin from current directory (disable then enable)
|
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
|
export PLUGINCTL_PLUGIN_PATH=/path/to/plugin
|
||||||
pluginctl info # Show info using environment variable
|
pluginctl info # Show info using environment variable
|
||||||
pluginctl version # Show version information
|
pluginctl version # Show version information
|
||||||
|
|
98
updateassets.go
Normal file
98
updateassets.go
Normal 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
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue