@vcmap/plugin-cli 2.0.1 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,11 @@
1
1
  # @vcmap/plugin-cli
2
2
  > Part of the [VC Map Project](https://github.com/virtualcitySYSTEMS/map-ui)
3
3
 
4
- The `vcmplugin` cli helps develop and build plugins for the **VC Map**.
4
+ > **Note: This documentation is for version 2, compatible with the [VC Map](https://github.com/virtualcitySYSTEMS/map-ui) v5.
5
+ > For documentation on version 1 compatible with VC Map v4, see [this tag](https://github.com/virtualcitySYSTEMS/map-plugin-cli/tree/v1.1.1)
6
+ > and be sure to install using `npm i -g @vcmap/plugin-cli@^1.1.0`**
5
7
 
6
- For more information on plugin development refer to [map plugin examples](https://github.com/virtualcitySYSTEMS/map-plugin-examples),
7
- which provides documentations and a tutorial on plugin development.
8
+ The `vcmplugin` cli helps develop and build plugins for the **VC Map**.
8
9
 
9
10
  ## Features
10
11
 
@@ -29,6 +30,15 @@ npm i -g @vcmap/plugin-cli
29
30
  ```
30
31
 
31
32
  ## Usage
33
+ You can use the following workflow to quickly develop plugins. Note, that
34
+ the `@vcmap/plugin-cli` does _not_ directly depend on `@vcmap/ui` to avoid version
35
+ conflicts in the used API within a plugin. This means, that _all_ commands
36
+ (except for the `create` command) must be executed from within an installed
37
+ plugin cli _within the plugin itself_ using npx. When using the `create`
38
+ command, the `@vcmap/plugin-cli` will automatically be installed as a devDependency in
39
+ its current major version. You can then use either the scripts defined
40
+ by the template in your package.json `npm start`, `npm run pack` etc. or `npx`
41
+ to execute CLI commands.
32
42
 
33
43
  ### 1. Creating a new plugin
34
44
 
@@ -43,7 +53,7 @@ Be sure to check out the [peer dependecy section](#about_peer_dependencies) as w
43
53
 
44
54
  To serve your plugin in dev mode, run the following within your projects root:
45
55
  ```
46
- vcmplugin serve
56
+ npx vcmplugin serve
47
57
  ```
48
58
  The dev mode gives you complete debug information on all integrated libraries (@vcmap/core, ol etc.)
49
59
  By default this command will launch a dev server at localhost:8008 using
@@ -58,7 +68,7 @@ your plugin integrates with others, use the `preview` command.
58
68
 
59
69
  To serve your plugin in preview mode, run the following within your projects root:
60
70
  ```
61
- vcmplugin preview
71
+ npx vcmplugin preview
62
72
  ```
63
73
 
64
74
  The preview mode allows you to view your plugin _in its destined environment_.
@@ -73,7 +83,7 @@ and use said application as its base instead.
73
83
 
74
84
  To build your project, run the following from within your projects root:
75
85
  ```bash
76
- vcmplugin build
86
+ npx vcmplugin build
77
87
  ```
78
88
  This will build your application and place it in the `dist` directory.
79
89
 
@@ -81,7 +91,7 @@ This will build your application and place it in the `dist` directory.
81
91
 
82
92
  To pack your project for productive use, run the following from within your projects root:
83
93
  ```bash
84
- vcmplugin pack
94
+ npx vcmplugin pack
85
95
  ```
86
96
 
87
97
  This will create a folder `dist` with a zip file containing your bundled code and assets.
@@ -137,19 +147,82 @@ or
137
147
  import { Feature } from 'ol';
138
148
  ```
139
149
 
140
- ## Non-Global CLI & npm run
141
- If you only use the `vcmplugin-cli` as a package dependency, you must add the above scripts to
142
- the `package.json` and use `npm run` to execute:
150
+ ## VC Map Plugins
151
+ The following defines a plugin in its rough structure. If you use the `@vcmap/plugin-cli`
152
+ to create your project, a template already adhering to these specs will be created for you.
153
+ - All plugins must provide the following:
154
+ - `package.json` with name, description, version, author and dependencies.
155
+ - `config.json` with default parameters for the plugins' configuration.
156
+ - `README.md` describing the plugins' capabilities and usage.
157
+ - `src/index.js` JS entry point.
158
+ - Plugin names are defined by the plugins' package name and therefore must obey npm [package name guidelines](https://docs.npmjs.com/package-name-guidelines):
159
+ - choose a name that
160
+ - is unique
161
+ - is descriptive
162
+ - is lowercase
163
+ - is uri encode-able
164
+ - doesn't start with `.`, `_` or a digit
165
+ - doesn't contain white spaces or any special characters like `~\'!()*"`
166
+ - do not use scope `@vcmap`, since it is only to be used by official plugins provided
167
+ by virtual city systems. But you are encouraged to use your own scope.
168
+ - Plugin dependencies have to be defined in the `package.json`.
169
+ - `dependency`: all plugin specific dependencies NOT provided by the `@vcmap/ui`.
170
+ - `peerDependency`: dependencies provided by the `@vcmap/ui`,
171
+ - e.g. `@vcmap/core` or `@vcmap/ui` (see [About Peer Dependencies](#About_Peer_Dependencies) for more details)
172
+ - `devDependency`: all dependencies only required for development, e.g. `eslint`.
173
+ - Plugins can be published to NPM, but should contain both source and minified code
174
+ to allow seamless integration into the [VC Map UI](https://github.com/virtualcitySYSTEMS/map-ui) environment.
175
+ For this reason the package.json of a plugin defines two exports:
143
176
  ```json
144
177
  {
145
- "name": "plugin-name",
146
- "main": "src/index.js",
147
- "scripts": {
148
- "build": "vcmplugin build",
149
- "serve": "vcmplugin serve --vcm ./vcm"
150
- },
151
- "devDependencies": {
152
- "@vcmap/plugin-cli": "^0.1.1"
178
+ ".": "./src/index.js",
179
+ "./dist": "./dist/index.js"
180
+ }
181
+ ```
182
+
183
+
184
+ ### Plugin Interface:
185
+ Plugins must provide a function default export which returns an Object complying
186
+ with the VC Map Plugin Interface describe below:
187
+
188
+ ```typescript
189
+ declare interface VcsPlugin<T extends Object> {
190
+ readonly name: string;
191
+ readonly version: string;
192
+ initialize(app: VcsUiApp):void;
193
+ onVcsAppMounted(app: VcsUiApp):void;
194
+ toJSON():T;
195
+ destroy():void;
196
+ }
197
+
198
+ declare function defaultExport<T extends Object>(config: T):VcsPlugin<T>;
199
+ ```
200
+
201
+ A Simple JavaScript implementation of this interface can be seen below::
202
+ ```javascript
203
+ // index.js
204
+ /**
205
+ * @param {PluginExampleConfig} config
206
+ * @returns {VcsPlugin}
207
+ */
208
+ export default function defaultExport(config) {
209
+ return {
210
+ get name() {
211
+ return packageJSON.name;
212
+ },
213
+ get version() {
214
+ return packageJSON.version;
215
+ },
216
+ initialize(app) {},
217
+ onVcsAppMounted(app) {},
218
+ toJSON() {},
219
+ destroy() {},
153
220
  }
154
221
  }
155
222
  ```
223
+
224
+
225
+ ## Notes on Developing
226
+ To develop the plugin-cli, be sure to not `npm link` into plugins, since this will
227
+ throw the resolver in resolving the @vcmap/ui peer dependency from the current plugin.
228
+ Instead run `npm pack` in the plugin cli and install the tarball in the plugin directly.
@@ -0,0 +1,2 @@
1
+ node_modules/
2
+ dist/
@@ -0,0 +1,93 @@
1
+ default:
2
+ image: gitlab.virtualcitysystems.de:5050/vcsuite/devops/gitlabrunner/node:16-bullseye
3
+
4
+ variables:
5
+ GIT_CLONE_PATH: $CI_BUILDS_DIR/$CI_PROJECT_PATH_SLUG/$CI_COMMIT_REF_SLUG
6
+
7
+ stages:
8
+ - build
9
+ - test
10
+ - bundle
11
+ - version
12
+ - publish
13
+
14
+ .template: &job_definition
15
+ only:
16
+ - /^(feature-.*|hotfix-.*|main|release-.*)$/
17
+ tags:
18
+ - linux-2.0
19
+
20
+ build:
21
+ <<: *job_definition
22
+ script:
23
+ - npm set registry 'http://npmregistry:4873'
24
+ - npm ci
25
+ before_script:
26
+ - mkdir -p ~/.ssh
27
+ - chmod 700 ~/.ssh
28
+ - echo "$SSH_RUNNER_KEY" | tr -d '\r' > ~/.ssh/id_rsa
29
+ - chmod 600 ~/.ssh/id_rsa
30
+ - ssh-keyscan gitlab.virtualcitysystems.de >> ~/.ssh/known_hosts
31
+ - chmod 644 ~/.ssh/known_hosts
32
+ - git config user.name "Gitlab Runner"
33
+ - git config user.email "gitlab-runner@vc.systems"
34
+ stage: build
35
+
36
+ .after_build_template: &after_build_definition
37
+ <<: *job_definition
38
+ variables:
39
+ GIT_STRATEGY: none
40
+
41
+ lint:
42
+ <<: *after_build_definition
43
+ stage: test
44
+ script:
45
+ - npm run lint
46
+
47
+ audit:
48
+ <<: *after_build_definition
49
+ stage: test
50
+ script:
51
+ - npm audit --production --audit-level=low
52
+
53
+ bundle:
54
+ <<: *after_build_definition
55
+ stage: bundle
56
+ script:
57
+ - npm run build
58
+
59
+ version:
60
+ <<: *after_build_definition
61
+ stage: version
62
+ only:
63
+ variables:
64
+ - $PUBLISH
65
+ refs:
66
+ - /^(main|release-v.*)$/
67
+ script:
68
+ - npm version patch -m "%s [skip-ci]"
69
+ - TAG=`git describe --abbrev=0`
70
+ - echo git push git@gitlab:vcsuite/"$CI_PROJECT_PATH".git
71
+ - git push git@gitlab:vcsuite/"$CI_PROJECT_PATH".git $TAG
72
+ - git push git@gitlab:vcsuite/"$CI_PROJECT_PATH".git HEAD:$CI_COMMIT_REF_NAME
73
+ before_script:
74
+ - mkdir -p ~/.ssh
75
+ - chmod 700 ~/.ssh
76
+ - echo "$SSH_RUNNER_KEY" | tr -d '\r' > ~/.ssh/id_rsa
77
+ - chmod 600 ~/.ssh/id_rsa
78
+ - ssh-keyscan gitlab >> ~/.ssh/known_hosts
79
+ - chmod 644 ~/.ssh/known_hosts
80
+ - git config user.name "Gitlab Runner"
81
+ - git config user.email "gitlab-runner@vc.systems"
82
+
83
+ publish:
84
+ <<: *after_build_definition
85
+ stage: publish
86
+ only:
87
+ refs:
88
+ - /^(main|release-v.*)$/
89
+ variables:
90
+ - $PUBLISH
91
+ script:
92
+ - npm config set '//npmregistry:4873/:_authToken' "${VERDACCIO_TOKEN}"
93
+ - npm publish --registry http://npmregistry:4873
@@ -0,0 +1,40 @@
1
+ <template>
2
+ <v-sheet>
3
+ <v-card class="pa-2 ma-2">
4
+ <v-container>
5
+ <v-row class="justify-center mb-4">
6
+ <h1>Hello World</h1>
7
+ </v-row>
8
+ <v-row class="justify-center">
9
+ <VcsButton
10
+ icon="mdi-times"
11
+ @click="closeSelf"
12
+ >
13
+ Close Window
14
+ </VcsButton>
15
+ </v-row>
16
+ </v-container>
17
+ </v-card>
18
+ </v-sheet>
19
+ </template>
20
+
21
+ <script>
22
+ import { inject } from '@vue/composition-api';
23
+ import { VcsButton } from '@vcsuite/ui-components';
24
+
25
+ export const windowId = 'hello_world_window_id';
26
+
27
+ export default {
28
+ name: 'HelloWorld',
29
+ components: { VcsButton },
30
+ setup() {
31
+ const app = inject('vcsApp');
32
+
33
+ return {
34
+ closeSelf() {
35
+ app.windowManager.remove(windowId);
36
+ },
37
+ };
38
+ },
39
+ };
40
+ </script>
@@ -0,0 +1,35 @@
1
+ import { WindowSlot } from '@vcmap/ui';
2
+ import { version, name } from '../package.json';
3
+ import HelloWorld, { windowId } from './helloWorld.vue';
4
+
5
+ /**
6
+ * @param {VcsApp} app - the app from which this plugin is loaded.
7
+ * @param {Object} config - the configuration of this plugin instance, passed in from the app.
8
+ * @returns {VcsPlugin}
9
+ */
10
+ export default function helloWorld(app, config) {
11
+ return {
12
+ get name() { return name; },
13
+ get version() { return version; },
14
+ initialize: async (vcsUiApp) => {
15
+ console.log('Called before loading the rest of the current context. Passed in the containing Vcs UI App ');
16
+ console.log(app, config);
17
+ console.log(vcsUiApp);
18
+ },
19
+ onVcsAppMounted: async (vcsUiApp) => {
20
+ console.log('Called when the root UI component is mounted and managers are ready to accept components');
21
+ vcsUiApp.windowManager.add({
22
+ id: windowId,
23
+ component: HelloWorld,
24
+ WindowSlot: WindowSlot.DETACHED,
25
+ position: {
26
+ left: '40%',
27
+ right: '40%',
28
+ },
29
+ }, name);
30
+ },
31
+ toJSON: async () => {
32
+ console.log('Called when serializing this plugin instance');
33
+ },
34
+ };
35
+ }
package/cli.js CHANGED
@@ -3,6 +3,7 @@ import program from 'commander';
3
3
  import './src/defaultCommand.js';
4
4
  import { create, serve, build, pack, preview } from './index.js';
5
5
  import { version } from './src/create.js';
6
+ import setupMapUi from './src/setupMapUi.js';
6
7
 
7
8
  program.version(version);
8
9
 
@@ -44,4 +45,8 @@ program
44
45
  .option('--watch', 'watch file changes')
45
46
  .safeAction(build);
46
47
 
48
+ program
49
+ .command('setup-map-ui')
50
+ .safeAction(setupMapUi);
51
+
47
52
  program.parse(process.argv);
package/index.js CHANGED
@@ -3,3 +3,5 @@ export { default as serve } from './src/serve.js';
3
3
  export { default as build } from './src/build.js';
4
4
  export { default as pack } from './src/pack.js';
5
5
  export { default as preview } from './src/preview.js';
6
+ export { default as setupMapUi } from './src/setupMapUi.js';
7
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vcmap/plugin-cli",
3
- "version": "2.0.1",
3
+ "version": "2.0.4",
4
4
  "description": "A CLI to help develop and build plugins for the VC Map",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -11,6 +11,10 @@
11
11
  "lint": "eslint . --env node",
12
12
  "test": "echo \"Error: no test specified\" && exit 1"
13
13
  },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/virtualcitySYSTEMS/map-plugin-cli.git"
17
+ },
14
18
  "author": "Ben Kuster <bkuster@virtualcitysystems.de>",
15
19
  "license": "MIT",
16
20
  "files": [
@@ -32,21 +36,44 @@
32
36
  "semver": "^7.3.5",
33
37
  "unplugin-vue-components": "^0.17.21",
34
38
  "vinyl-fs": "^3.0.3",
35
- "vite": "^2.4.4",
39
+ "vite": "^2.9.1",
36
40
  "vite-plugin-vue2": "^1.7.3",
37
41
  "vue-template-compiler": "^2.6.14"
38
42
  },
43
+ "peerDependencies": {
44
+ "@vcmap/ui": "^5.0.0-rc.7",
45
+ "vue": "^2.6.14"
46
+ },
47
+ "peerDependenciesMeta": {
48
+ "@vcmap/ui": {
49
+ "optional": true
50
+ },
51
+ "vue": {
52
+ "optional": true
53
+ }
54
+ },
39
55
  "devDependencies": {
40
56
  "@vcsuite/eslint-config": "^2.1.1",
41
57
  "eslint": "^8.9.0"
42
58
  },
43
59
  "eslintIgnore": [
44
- "node_modules"
60
+ "node_modules",
61
+ "assets/helloWorld"
45
62
  ],
46
63
  "eslintConfig": {
47
64
  "extends": "@vcsuite/eslint-config/node",
48
65
  "parserOptions": {
49
66
  "ecmaVersion": 2020
67
+ },
68
+ "rules": {
69
+ "import/no-unresolved": [
70
+ 2,
71
+ {
72
+ "ignore": [
73
+ "^@vcmap/ui"
74
+ ]
75
+ }
76
+ ]
50
77
  }
51
78
  },
52
79
  "engines": {
package/src/build.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import path from 'path';
2
- import { build } from 'vite';
2
+ import fs from 'fs/promises';
3
3
  import { createVuePlugin } from 'vite-plugin-vue2';
4
4
  import vcsOl from '@vcmap/rollup-plugin-vcs-ol';
5
5
  import { logger } from '@vcsuite/cli-logger';
6
6
  import { VuetifyResolver } from 'unplugin-vue-components/dist/resolvers.js';
7
7
  import Components from 'unplugin-vue-components/dist/vite.js';
8
8
  import { getPluginEntry, getPluginName } from './packageJsonHelpers.js';
9
+ import { getContext } from './context.js';
9
10
 
10
11
  /**
11
12
  * @typedef {Object} BuildOptions
@@ -13,38 +14,45 @@ import { getPluginEntry, getPluginName } from './packageJsonHelpers.js';
13
14
  * @property {boolean} [watch]
14
15
  */
15
16
 
17
+ export function getDefaultConfig() {
18
+ return {
19
+ publicDir: false,
20
+ plugins: [
21
+ createVuePlugin(),
22
+ Components({
23
+ resolvers: [
24
+ VuetifyResolver(),
25
+ ],
26
+ dirs: ['./src'],
27
+ include: [],
28
+ exclude: [],
29
+ }),
30
+ vcsOl(),
31
+ ],
32
+ };
33
+ }
34
+
16
35
  /**
17
36
  * @param {string} pluginName
18
37
  * @returns {Object<string, string>}
19
38
  */
20
- export function getLibraryPaths(pluginName) {
39
+ export async function getLibraryPaths(pluginName) {
40
+ const { libraries } = await import('@vcmap/ui/build/buildHelpers.js');
21
41
  const pluginPath = path.join('plugins', ...pluginName.split('/'));
22
-
23
- const libraries = {
24
- vue: 'vue',
25
- '@vue/composition-api': 'vue-composition-api',
26
- '@vcmap/cesium': 'cesium',
27
- ol: 'ol',
28
- '@vcmap/core': 'core',
29
- 'vuetify/lib': 'vuetify',
30
- '@vcsuite/ui-components': 'uicomponents',
31
- '@vcmap/ui': 'ui',
32
- };
33
-
42
+ const libraryPaths = {};
34
43
  Object.entries(libraries).forEach(([library, assetName]) => {
35
44
  const assetPath = path.join('assets', `${assetName}.js`);
36
45
 
37
- libraries[library] = path.relative(pluginPath, assetPath);
46
+ libraryPaths[library] = path.relative(pluginPath, assetPath);
38
47
  });
39
- return libraries;
48
+ return libraryPaths;
40
49
  }
41
50
 
42
51
  /**
43
52
  * @param {BuildOptions} options
44
- * @param {boolean} [preview]
45
53
  * @returns {Promise<void>}
46
54
  */
47
- export default async function buildModule(options, preview) {
55
+ export default async function buildModule(options) {
48
56
  const entry = await getPluginEntry();
49
57
  if (path.relative('src', entry).startsWith('.')) {
50
58
  logger.warning(`detected irregular entry ${entry}`);
@@ -52,67 +60,35 @@ export default async function buildModule(options, preview) {
52
60
  }
53
61
 
54
62
  const pluginName = await getPluginName();
55
- const libraries = getLibraryPaths(pluginName);
56
-
57
- await build({
58
- publicDir: false,
59
- plugins: [
60
- createVuePlugin(),
61
- Components({
62
- resolvers: [
63
- VuetifyResolver(),
64
- ],
65
- dirs: ['./src'],
66
- include: [],
67
- exclude: [],
68
- }),
69
- vcsOl(),
70
- {
71
- name: 'pluginCssLoaderPrefix',
72
- generateBundle(opts, bundle) {
73
- const indexJs = bundle['index.js'];
74
- if (indexJs && indexJs.code) {
75
- const resource = preview ?
76
- './dist/style.css' :
77
- `./plugins/${pluginName}/style.css`;
78
-
79
- indexJs.code = `
80
- function loadCss(href) {
81
- return new Promise((resolve, reject) => {
82
- const elem = document.createElement('link');
83
- elem.rel = 'stylesheet';
84
- elem.href = href;
85
- elem.defer = false;
86
- elem.async = false;
87
- elem.onload = resolve;
88
- elem.onerror = reject;
89
- document.head.appendChild(elem);
90
- });
91
- }
92
- await loadCss('${resource}');
93
- ${indexJs.code}
94
- `;
95
- }
96
- },
97
- },
98
- ],
63
+ const libraryPaths = await getLibraryPaths(pluginName);
64
+ const distPath = path.join(getContext(), 'dist');
65
+ await fs.rm(distPath, { recursive: true, force: true });
66
+ await fs.mkdir(distPath);
67
+ const external = Object.keys(libraryPaths);
68
+ const config = {
69
+ ...getDefaultConfig(),
99
70
  esbuild: {
100
71
  minify: !options.development,
101
72
  },
102
73
  build: {
103
- emptyOutDir: true,
74
+ write: false,
75
+ emptyOutDir: false,
104
76
  lib: {
105
77
  entry,
106
78
  formats: ['es'],
107
79
  fileName: () => 'index.js',
108
80
  },
109
81
  rollupOptions: {
110
- external: Object.keys(libraries),
82
+ external,
111
83
  output: {
112
- paths: libraries,
84
+ paths: libraryPaths,
113
85
  },
114
86
  },
115
- watch: options.watch ? {} : null,
87
+ watch: options.watch ? {
88
+ skipWrite: true,
89
+ } : null,
116
90
  },
117
- });
91
+ };
92
+ const { buildLibrary } = await import('@vcmap/ui/build/buildHelpers.js');
93
+ await buildLibrary(config, '', 'index', '', true);
118
94
  }
package/src/create.js CHANGED
@@ -17,9 +17,10 @@ export const { version, name } = JSON.parse(fs.readFileSync(path.join(getDirname
17
17
  * @property {string} description
18
18
  * @property {Array<Object>} scripts
19
19
  * @property {string} author
20
+ * @property {string} repository
20
21
  * @property {string} license
21
- * @property {string} mapVersion
22
- * @property {boolean} addDevDep
22
+ * @property {string} registry
23
+ * @property {boolean} addCiCd
23
24
  * @property {Array<string>} peerDeps
24
25
  */
25
26
 
@@ -47,16 +48,42 @@ async function createPluginTemplate(options) {
47
48
  version: options.version,
48
49
  description: options.description,
49
50
  main: 'src/index.js',
50
- scripts: Object.assign({}, ...options.scripts),
51
+ scripts: Object.assign({ prepublishOnly: 'vcmplugin build' }, ...options.scripts),
51
52
  author: options.author,
52
53
  license: options.license,
53
- engines: {
54
- vcm: options.mapVersion,
55
- },
56
54
  dependencies: {},
57
- devDependencies: options.addDevDep ? { [name]: `^${version}` } : {}, // XXX should we add all our deps here? cesium, ol, vue etc.
55
+ keywords: [
56
+ 'vcmap',
57
+ 'plugin',
58
+ ],
59
+ files: [
60
+ 'src/',
61
+ 'dist/',
62
+ 'plugin-assets/',
63
+ 'LICENSE.md',
64
+ 'README.md',
65
+ ],
66
+ exports: {
67
+ '.': './src/index.js',
68
+ './dist': './dist/index.js',
69
+ },
58
70
  };
59
71
 
72
+ if (options.repository) {
73
+ packageJson.repository = {
74
+ url: options.repository,
75
+ };
76
+ }
77
+
78
+ const installEsLint = options.scripts.find(script => script.lint);
79
+ if (installEsLint) {
80
+ packageJson.eslintIgnore = ['node_modules'];
81
+ packageJson.eslintConfig = {
82
+ root: true,
83
+ extends: '@vcsuite/eslint-config/vue',
84
+ };
85
+ }
86
+
60
87
  const writePackagePromise = fs.promises.writeFile(
61
88
  path.join(pluginPath, 'package.json'),
62
89
  JSON.stringify(packageJson, null, 2),
@@ -71,6 +98,11 @@ async function createPluginTemplate(options) {
71
98
  JSON.stringify(configJson, null, 2),
72
99
  );
73
100
 
101
+ const writeNpmrcPromise = fs.promises.writeFile(
102
+ path.join(pluginPath, '.npmrc'),
103
+ `registry=${options.registry}\n`,
104
+ );
105
+
74
106
  const writeReadmePromise = fs.promises.writeFile(
75
107
  path.join(pluginPath, 'README.md'),
76
108
  [
@@ -79,36 +111,27 @@ async function createPluginTemplate(options) {
79
111
  ].join('\n'),
80
112
  );
81
113
 
114
+ const writeChangesPromise = fs.promises.writeFile(
115
+ path.join(pluginPath, 'CHANGES.md'),
116
+ `# v${options.version}\nDocument features and fixes`,
117
+ );
118
+
82
119
  await fs.promises.mkdir(path.join(pluginPath, 'src'));
83
120
  logger.debug('created src directory');
84
121
 
85
- const writeIndexPromise = fs.promises.writeFile(
86
- path.join(pluginPath, 'src', 'index.js'),
87
- [
88
- 'import { version, name } from \'../package.json\';',
89
- '',
90
- '/**',
91
- ' * @param {VcsApp} app - the app from which this plugin is loaded.',
92
- ' * @param {Object} config - the configuration of this plugin instance, passed in from the app.',
93
- ' */',
94
- 'export default function(app, config) {',
95
- ' return {',
96
- ' get name() { return name; },',
97
- ' get version() { return version; },',
98
- ' initialize: async (vcsUiApp) => { console.log(\'Called before loading the rest of the current context. Passed in the containing Vcs UI App \'); },',
99
- ' onVcsAppMounted: async (vcsUiApp) => { console.log(\'Called when the root UI component is mounted and managers are ready to accept components\'); },',
100
- ' toJSON: async () => { console.log(\'Called when serializing this plugin instance\'); },',
101
- ' };',
102
- '};',
103
- '',
104
- ].join('\n'),
122
+ const copyTemplatePromise = fs.promises.cp(
123
+ path.join(getDirname(), '..', 'assets', 'helloWorld'),
124
+ pluginPath,
125
+ { recursive: true },
105
126
  );
106
127
 
107
128
  await Promise.all([
108
129
  writePackagePromise,
109
130
  writeConfigPromise,
131
+ writeNpmrcPromise,
110
132
  writeReadmePromise,
111
- writeIndexPromise,
133
+ writeChangesPromise,
134
+ copyTemplatePromise,
112
135
  writeLicense(options.author, options.license, pluginPath),
113
136
  ]);
114
137
 
@@ -116,13 +139,19 @@ async function createPluginTemplate(options) {
116
139
  logger.spin('installing dependencies... (this may take a while)');
117
140
  const exec = util.promisify(childProcess.exec);
118
141
  try {
119
- let installCmd = 'npm i';
120
- if (options.peerDeps.length > 0) {
121
- installCmd = `${installCmd} --save-peer ${options.peerDeps.join(' ')}`;
122
- }
142
+ options.peerDeps.push('@vcmap/ui');
143
+ const installCmd = `npm i --save-peer ${options.peerDeps.join(' ')}`;
123
144
  const { stdout, stderr } = await exec(installCmd, { cwd: pluginPath });
124
145
  logger.log(stdout);
125
146
  logger.error(stderr);
147
+ const devDeps = [`${name}@${version}`];
148
+ if (installEsLint) {
149
+ devDeps.push('@vcsuite/eslint-config');
150
+ }
151
+ const installDevCmd = `npm i --save-dev ${devDeps.join(' ')}`;
152
+ const { stdout: stdoutDev, stderr: stderrDev } = await exec(installDevCmd, { cwd: pluginPath });
153
+ logger.log(stdoutDev);
154
+ logger.error(stderrDev);
126
155
  logger.success('installed dependencies');
127
156
  } catch (e) {
128
157
  logger.error(e);
@@ -137,15 +166,14 @@ async function createPluginTemplate(options) {
137
166
  */
138
167
  export default async function create() {
139
168
  const scriptChoices = [
140
- { title: 'build', value: { build: 'vcmplugin build' } },
141
- { title: 'pack', value: { pack: 'vcmplugin pack' } },
142
- { title: 'start', value: { start: 'vcmplugin serve' } },
143
- { title: 'preview', value: { preview: 'vcmplugin preview' } },
144
- { title: 'test', value: { test: 'echo "Error: no test specified" && exit 1' } },
169
+ { title: 'build', value: { build: 'vcmplugin build' }, selected: true },
170
+ { title: 'pack', value: { pack: 'vcmplugin pack' }, selected: true },
171
+ { title: 'start', value: { start: 'vcmplugin serve' }, selected: true },
172
+ { title: 'preview', value: { preview: 'vcmplugin preview' }, selected: true },
173
+ { title: 'lint', value: { lint: 'eslint "{src,tests}/**/*.{js,vue}"' }, selected: true },
145
174
  ];
146
175
 
147
176
  const peerDependencyChoices = [
148
- { title: '@vcmap/ui', value: '@vcmap/ui', selected: true },
149
177
  { title: '@vcsuite/ui-components', value: '@vcsuite/ui-components', selected: true },
150
178
  { title: '@vcmap/core', value: '@vcmap/core' },
151
179
  { title: '@vcmap/cesium', value: '@vcmap/cesium' },
@@ -195,6 +223,12 @@ export default async function create() {
195
223
  message: 'Author',
196
224
  initial: 'author <email>',
197
225
  },
226
+ {
227
+ type: 'text',
228
+ name: 'repository',
229
+ message: 'Repository url',
230
+ // initial: (prev, values) => `https://github.com/virtualcitySYSTEMS/${values.name}.git`,
231
+ },
198
232
  {
199
233
  type: 'select',
200
234
  name: 'license',
@@ -208,9 +242,9 @@ export default async function create() {
208
242
  },
209
243
  {
210
244
  type: 'text',
211
- name: 'mapVersion',
212
- message: 'Map version',
213
- initial: '>=5.0',
245
+ name: 'registry',
246
+ message: 'Set default npm registry',
247
+ initial: 'https://registry.npmjs.org',
214
248
  },
215
249
  {
216
250
  name: 'peerDeps',
@@ -221,9 +255,9 @@ export default async function create() {
221
255
  },
222
256
  {
223
257
  type: 'toggle',
224
- name: 'addDevDep',
225
- message: 'Add vcmplugin-cli as dev dependency?',
226
- initial: true,
258
+ name: 'addCiCd',
259
+ message: 'Add default VCS gitlab ci/cd?',
260
+ initial: false,
227
261
  active: 'yes',
228
262
  inactive: 'no',
229
263
  },
@@ -1,4 +1,6 @@
1
1
  import { fileURLToPath, URL } from 'url';
2
+ import { promisify } from 'util';
3
+ import { exec } from 'child_process';
2
4
  import https from 'https';
3
5
  import http from 'http';
4
6
  import fs from 'fs';
@@ -15,8 +17,21 @@ import { getPluginEntry, getPluginName } from './packageJsonHelpers.js';
15
17
  * @property {boolean} [https]
16
18
  */
17
19
 
20
+ /**
21
+ * @type {(arg1: string, opt?: Object) => Promise<string>}
22
+ */
23
+ const promiseExec = promisify(exec);
24
+
25
+ /**
26
+ * @param {...string} pathSegments
27
+ * @returns {string}
28
+ */
29
+ export function resolveMapUi(...pathSegments) {
30
+ return path.join(getContext(), 'node_modules', '@vcmap', 'ui', ...pathSegments);
31
+ }
32
+
18
33
  export function checkReservedDirectories() {
19
- ['assets', 'plugins']
34
+ ['assets', 'plugins', 'config']
20
35
  .forEach((dir) => {
21
36
  if (fs.existsSync(path.join(getContext(), dir))) {
22
37
  logger.warning(`found reserved directory ${dir}. serving my not work as exptected`);
@@ -69,13 +84,13 @@ export async function readConfigJson(fileName) {
69
84
  return config;
70
85
  }
71
86
 
72
- let configJson = null;
87
+ const configMap = new Map();
73
88
 
74
89
  /**
75
90
  * @returns {Promise<string>}
76
91
  */
77
92
  export async function printVcmapUiVersion() {
78
- const packageJsonPath = path.join(getContext(), 'node_modules', '@vcmap', 'ui', 'package.json');
93
+ const packageJsonPath = resolveMapUi('package.json');
79
94
  if (!fs.existsSync(packageJsonPath)) {
80
95
  throw new Error(`Cannot find the @vcmap/ui package in ${getContext()}. Are you sure you installed it?`);
81
96
  }
@@ -85,6 +100,24 @@ export async function printVcmapUiVersion() {
85
100
  logger.info(`Using @vcmap/ui version: ${version} found in current project.`);
86
101
  }
87
102
 
103
+ /**
104
+ * @param {Object} config
105
+ * @param {Object} pluginConfig
106
+ * @param {boolean} production
107
+ * @returns {Promise<void>}
108
+ */
109
+ export async function reWriteConfig(config, pluginConfig, production) {
110
+ config.plugins = config.plugins ?? []; // XXX check if we have plugins in this repos dependencies?
111
+ pluginConfig.entry = production ? 'dist/index.js' : await getPluginEntry();
112
+ pluginConfig.name = await getPluginName();
113
+ const idx = config.plugins.findIndex(p => p.name === pluginConfig.name);
114
+ if (idx > -1) {
115
+ config.plugins.splice(idx, 1, pluginConfig);
116
+ } else {
117
+ config.plugins.push(pluginConfig);
118
+ }
119
+ }
120
+
88
121
  /**
89
122
  * @param {string} [mapConfig] - fs or https to config. defaults to @vcmap/ui/map.config.json
90
123
  * @param {string} [auth]
@@ -93,9 +126,9 @@ export async function printVcmapUiVersion() {
93
126
  * @returns {Promise<unknown>}
94
127
  */
95
128
  export function getConfigJson(mapConfig, auth, production, configFile) {
96
- const usedConfig = mapConfig || path.join(getContext(), 'node_modules', '@vcmap', 'ui', 'dist', 'config', 'base.config.json');
97
- if (configJson) {
98
- return Promise.resolve(configJson);
129
+ const usedConfig = mapConfig || resolveMapUi('map.config.json');
130
+ if (configMap.has('map.config.json')) {
131
+ return Promise.resolve(configMap.get('map.config.json'));
99
132
  }
100
133
  const isWebVcm = /^https?:\/\//.test(usedConfig);
101
134
  return new Promise((resolve, reject) => {
@@ -107,17 +140,10 @@ export function getConfigJson(mapConfig, auth, production, configFile) {
107
140
 
108
141
  stream.on('close', async () => {
109
142
  try {
110
- configJson = JSON.parse(data);
111
- configJson.plugins = production && configJson.plugins ? configJson.plugins : [];
143
+ const configJson = JSON.parse(data);
144
+ configMap.set('map.config.json', configJson);
112
145
  const pluginConfig = await readConfigJson(configFile);
113
- pluginConfig.entry = production ? 'dist/index.js' : await getPluginEntry();
114
- pluginConfig.name = await getPluginName();
115
- const idx = configJson.plugins.findIndex(p => p.name === pluginConfig.name);
116
- if (idx > -1) {
117
- configJson.plugins.splice(idx, 1, pluginConfig);
118
- } else {
119
- configJson.plugins.push(pluginConfig);
120
- }
146
+ await reWriteConfig(configJson, pluginConfig, production);
121
147
  resolve(configJson);
122
148
  } catch (e) {
123
149
  reject(e);
@@ -167,7 +193,7 @@ export function createConfigJsonReloadPlugin() {
167
193
  name: 'ConfigJsonReload',
168
194
  handleHotUpdate({ file }) {
169
195
  if (file === path.join(getContext(), 'config.json')) {
170
- configJson = null;
196
+ configMap.clear();
171
197
  }
172
198
  },
173
199
  };
@@ -192,13 +218,52 @@ export function addMapConfigRoute(app, mapConfig, auth, configFile, production)
192
218
  });
193
219
  }
194
220
 
221
+ /**
222
+ * @param {Express} app
223
+ * @param {string} [auth]
224
+ * @param {string} [configFileName]
225
+ * @param {boolean} [production]
226
+ */
227
+ export async function addConfigRoute(app, auth, configFileName, production) { // IDEA pass in available plugins and strip unavailable ones?
228
+ const mapUiDir = resolveMapUi();
229
+ const pluginConfig = await readConfigJson(configFileName);
230
+
231
+ app.get('/config*', async (req, res) => {
232
+ const { url } = req;
233
+ const fileName = path.join(mapUiDir, ...url.substring(1).split('/'));
234
+ let response;
235
+ if (configMap.has(url)) {
236
+ response = JSON.stringify(configMap.get(url));
237
+ } else if (fs.existsSync(fileName)) {
238
+ try {
239
+ const configContent = await fs.promises.readFile(fileName);
240
+ const config = JSON.parse(configContent);
241
+ configMap.set(url, config);
242
+ await reWriteConfig(config, pluginConfig, production);
243
+ response = JSON.stringify(config);
244
+ } catch (e) {
245
+ configMap.delete(url);
246
+ logger.warning(`Failed to parse config ${url}`);
247
+ }
248
+ }
249
+
250
+ if (!response) {
251
+ res.statusCode = 404;
252
+ } else {
253
+ res.setHeader('Content-Type', 'application/json');
254
+ res.write(response);
255
+ }
256
+ res.end();
257
+ });
258
+ }
259
+
195
260
  /**
196
261
  * @param {boolean} [production]
197
262
  * @returns {Promise<string>}
198
263
  */
199
264
  export async function getMapUiIndexHtml(production) {
200
265
  const indexHtmlFileName = production ?
201
- path.join(getContext(), 'node_modules', '@vcmap', 'ui', 'dist', 'index.html') :
266
+ resolveMapUi('dist', 'index.html') :
202
267
  path.join(getDirname(), '..', 'assets', 'index.html');
203
268
  const buffer = await fs.promises.readFile(indexHtmlFileName);
204
269
  return buffer.toString();
@@ -217,10 +282,19 @@ export function addIndexRoute(app, server, production, hostedVcm, auth) {
217
282
  await getIndexHtml(`${hostedVcm}/`, auth) :
218
283
  await getMapUiIndexHtml(production); // TODO change hosted vcm index via option?
219
284
 
220
- originalIndex = await server.transformIndexHtml('index.html', originalIndex);
285
+ originalIndex = await server.transformIndexHtml('/index.html', originalIndex);
286
+
221
287
  res.status(200)
222
288
  .set({ 'Content-Type': 'text/html' })
223
289
  .end(originalIndex);
224
290
  });
225
291
  }
226
292
 
293
+ /**
294
+ * @param {string} command
295
+ * @returns {Promise<string>}
296
+ */
297
+ export function executeUiNpm(command) {
298
+ const mapUiDir = resolveMapUi();
299
+ return promiseExec(`npm run ${command}`, { cwd: mapUiDir });
300
+ }
package/src/preview.js CHANGED
@@ -1,16 +1,19 @@
1
1
  import path from 'path';
2
+ import fs from 'fs';
2
3
  import { createServer } from 'vite';
3
4
  import express from 'express';
4
5
  import { logger } from '@vcsuite/cli-logger';
5
6
  import {
7
+ addConfigRoute,
6
8
  addIndexRoute,
7
9
  addMapConfigRoute,
8
10
  checkReservedDirectories,
9
11
  createConfigJsonReloadPlugin,
10
- printVcmapUiVersion,
12
+ printVcmapUiVersion, resolveMapUi,
11
13
  } from './hostingHelpers.js';
12
- import build, { getLibraryPaths } from './build.js';
14
+ import build, { getDefaultConfig, getLibraryPaths } from './build.js';
13
15
  import { getContext } from './context.js';
16
+ import setupMapUi from './setupMapUi.js';
14
17
 
15
18
  /**
16
19
  * @typedef {HostingOptions} PreviewOptions
@@ -34,8 +37,8 @@ function setAliases(alias, libraryPaths) {
34
37
  */
35
38
  async function getServerOptions(hostedVcm, https) {
36
39
  let proxy;
37
- const normalLibraries = getLibraryPaths('normal');
38
- const scopedLibraries = getLibraryPaths('@scoped/plugin');
40
+ const normalLibraries = await getLibraryPaths('normal');
41
+ const scopedLibraries = await getLibraryPaths('@scoped/plugin');
39
42
  const alias = {};
40
43
  setAliases(alias, normalLibraries);
41
44
  setAliases(alias, scopedLibraries);
@@ -73,7 +76,7 @@ export default async function preview(options) {
73
76
  await printVcmapUiVersion();
74
77
  }
75
78
  checkReservedDirectories();
76
- build({ development: false, watch: true }, true);
79
+ await build({ development: false, watch: true });
77
80
  const app = express();
78
81
  logger.info('Starting preview server...');
79
82
  const server = await createServer(await getServerOptions(options.vcm, options.https));
@@ -82,8 +85,18 @@ export default async function preview(options) {
82
85
  addIndexRoute(app, server, true, options.vcm, options.auth);
83
86
 
84
87
  if (!options.vcm) {
88
+ logger.spin('compiling preview');
89
+ if (!fs.existsSync(resolveMapUi('plugins', 'node_modules'))) {
90
+ logger.info('Could not detect node_modules in map ui plugins. Assuming map UI not setup');
91
+ await setupMapUi();
92
+ }
93
+ const { buildPluginsForPreview } = await import('@vcmap/ui/build/buildHelpers.js');
94
+ await buildPluginsForPreview(getDefaultConfig(), true);
95
+ logger.stopSpinner();
96
+ logger.info('@vcmap/ui built for preview');
85
97
  app.use('/assets', express.static(path.join(getContext(), 'node_modules', '@vcmap', 'ui', 'dist', 'assets')));
86
- app.use('/plugins', express.static(path.join(getContext(), 'node_modules', '@vcmap', 'ui', 'dist', 'plugins')));
98
+ app.use('/plugins', express.static(path.join(getContext(), 'dist', 'plugins')));
99
+ await addConfigRoute(app, options.auth, options.config, true);
87
100
  }
88
101
 
89
102
  app.use(server.middlewares);
package/src/serve.js CHANGED
@@ -8,18 +8,51 @@ import { VuetifyResolver } from 'unplugin-vue-components/dist/resolvers.js';
8
8
  import Components from 'unplugin-vue-components/dist/vite.js';
9
9
  import { getContext } from './context.js';
10
10
  import {
11
+ addConfigRoute,
11
12
  addIndexRoute,
12
13
  addMapConfigRoute,
13
14
  checkReservedDirectories,
14
15
  createConfigJsonReloadPlugin,
15
16
  printVcmapUiVersion,
17
+ resolveMapUi,
16
18
  } from './hostingHelpers.js';
19
+ import { getPluginName } from './packageJsonHelpers.js';
17
20
 
18
21
  /**
19
22
  * @typedef {HostingOptions} ServeOptions
20
23
  * @property {string} [mapConfig] - an filename or URL to a map config
21
24
  */
22
25
 
26
+ async function getProxy(protocol, port) {
27
+ const { default: getPluginProxies } = await import('@vcmap/ui/build/getPluginProxies.js');
28
+ const { determineHostIpFromInterfaces } = await import('@vcmap/ui/build/determineHost.js');
29
+ const { getInlinePlugins } = await import('@vcmap/ui/build/buildHelpers.js');
30
+
31
+ const target = `${protocol}://${determineHostIpFromInterfaces()}:${port}`;
32
+ const proxy = await getPluginProxies(target);
33
+ const mapUiPlugins = resolveMapUi('plugins');
34
+ const inlinePlugins = await getInlinePlugins();
35
+ inlinePlugins.forEach((inlinePlugin) => {
36
+ proxy[`^/plugins/${inlinePlugin}/.*`] = {
37
+ target,
38
+ rewrite: (route) => {
39
+ const rest = route.replace(new RegExp(`^/plugins/${inlinePlugin}/`), '');
40
+ const file = rest || 'index.js';
41
+ return path.posix.join(path.relative(getContext(), mapUiPlugins), inlinePlugin, file);
42
+ },
43
+ };
44
+ });
45
+
46
+ const pluginRoutes = Object.keys(proxy);
47
+ const name = await getPluginName();
48
+ const hasThisPlugin = pluginRoutes.find(p => p.startsWith(`^/plugins/${name}`));
49
+
50
+ if (hasThisPlugin) {
51
+ delete proxy[hasThisPlugin];
52
+ }
53
+ return proxy;
54
+ }
55
+
23
56
  /**
24
57
  * @param {ServeOptions} options
25
58
  * @returns {Promise<void>}
@@ -32,7 +65,10 @@ export default async function serve(options) {
32
65
  await printVcmapUiVersion();
33
66
  checkReservedDirectories();
34
67
  const app = express();
68
+ const port = options.port || 8008;
69
+
35
70
  logger.info('Starting development server...');
71
+ const proxy = await getProxy(options.https ? 'https' : 'http', port);
36
72
 
37
73
  const server = await createServer({
38
74
  root: getContext(),
@@ -66,15 +102,16 @@ export default async function serve(options) {
66
102
  server: {
67
103
  middlewareMode: 'html',
68
104
  https: options.https,
105
+ proxy,
69
106
  },
70
107
  });
71
108
 
72
109
  addMapConfigRoute(app, options.mapConfig, options.auth, options.config);
73
110
  addIndexRoute(app, server);
111
+ await addConfigRoute(app, options.auth, options.config);
74
112
 
75
113
  app.use(server.middlewares);
76
114
 
77
- const port = options.port || 8008;
78
115
  await app.listen(port);
79
116
  logger.info(`Server running on port ${port}`);
80
117
  }
@@ -0,0 +1,9 @@
1
+ import { logger } from '@vcsuite/cli-logger';
2
+ import { executeUiNpm } from './hostingHelpers.js';
3
+
4
+ export default async function setupMapUi() {
5
+ logger.spin('installing dev plugins in @vcmap/ui');
6
+ await executeUiNpm('install-plugins');
7
+ logger.stopSpinner();
8
+ logger.info('dev plugins installed');
9
+ }