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)
|
||||
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
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