@citolab/tspci 2.1.3 → 2.3.0-alpha.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@citolab/tspci",
3
- "version": "2.1.3",
3
+ "version": "2.3.0-alpha.0",
4
4
  "description": "an opiniated typescript ims-pci builder with tao integration",
5
5
  "author": "patrick <patrick.deklein@cito.nl>",
6
6
  "repository": "@citolab/tspci",
@@ -9,51 +9,30 @@
9
9
  },
10
10
  "type": "module",
11
11
  "types": "types/index.d.ts",
12
- "devDependencies": {
13
- "@rollup/plugin-alias": "^4.0.2",
14
- "@rollup/plugin-babel": "^6.0.3",
15
- "@rollup/plugin-commonjs": "^23.0.4",
16
- "@rollup/plugin-image": "^3.0.1",
17
- "@rollup/plugin-json": "^5.0.2",
18
- "@rollup/plugin-node-resolve": "^15.0.1",
19
- "@rollup/plugin-replace": "^5.0.1",
20
- "@rollup/plugin-terser": "^0.2.0",
21
- "@rollup/plugin-typescript": "^10.0.1",
22
- "@testing-library/dom": "^8.19.0",
23
- "@types/node": "^18.11.12",
24
- "@typescript-eslint/eslint-plugin": "^5.46.0",
25
- "@typescript-eslint/parser": "^5.46.0",
26
- "live-server": "^1.2.2",
27
- "postcss": "^8.4.19",
28
- "postcss-import": "^15.1.0",
29
- "rollup": "^3.7.0",
30
- "rollup-plugin-copy": "^3.4.0",
31
- "rollup-plugin-live-server": "^2.0.0",
32
- "rollup-plugin-node-builtins": "^2.1.2",
33
- "rollup-plugin-node-globals": "^1.4.0",
34
- "rollup-plugin-postcss": "^4.0.2",
35
- "rollup-plugin-string": "^3.0.0",
36
- "rollup-plugin-watch-assets": "^1.0.1",
37
- "tailwindcss": "^3.2.4"
38
- },
39
12
  "dependencies": {
40
- "@rollup/plugin-alias": "^4.0.2",
13
+ "@rollup/plugin-alias": "^5.0.0",
41
14
  "@rollup/plugin-babel": "^6.0.3",
42
- "@rollup/plugin-commonjs": "^23.0.4",
43
- "@rollup/plugin-image": "^3.0.1",
44
- "@rollup/plugin-json": "^5.0.2",
45
- "@rollup/plugin-node-resolve": "^15.0.1",
46
- "@rollup/plugin-replace": "^5.0.1",
47
- "@rollup/plugin-terser": "^0.2.0",
48
- "@rollup/plugin-typescript": "^10.0.1",
49
- "@testing-library/dom": "^8.19.0",
50
- "@types/node": "^18.11.12",
51
- "@typescript-eslint/eslint-plugin": "^5.46.0",
52
- "@typescript-eslint/parser": "^5.46.0",
15
+ "@rollup/plugin-commonjs": "^24.1.0",
16
+ "@rollup/plugin-image": "^3.0.2",
17
+ "@rollup/plugin-json": "^6.0.0",
18
+ "@rollup/plugin-node-resolve": "^15.0.2",
19
+ "@rollup/plugin-replace": "^5.0.2",
20
+ "@rollup/plugin-terser": "^0.4.1",
21
+ "@rollup/plugin-typescript": "^11.1.0",
22
+ "@tailwindcss/nesting": "0.0.0-insiders.565cd3e",
23
+ "@testing-library/dom": "^9.2.0",
24
+ "@types/node": "^18.16.3",
25
+ "@typescript-eslint/eslint-plugin": "^5.59.2",
26
+ "@typescript-eslint/parser": "^5.59.2",
27
+ "autoprefixer": "10.4.14",
28
+ "chalk": "5.2.0",
29
+ "clear": "0.1.0",
30
+ "figlet": "1.6.0",
31
+ "inquirer": "9.2.0",
53
32
  "live-server": "^1.2.2",
54
- "postcss": "^8.4.19",
33
+ "postcss": "^8.4.23",
55
34
  "postcss-import": "^15.1.0",
56
- "rollup": "^3.7.0",
35
+ "rollup": "^3.21.4",
57
36
  "rollup-plugin-copy": "^3.4.0",
58
37
  "rollup-plugin-live-server": "^2.0.0",
59
38
  "rollup-plugin-node-builtins": "^2.1.2",
@@ -61,8 +40,11 @@
61
40
  "rollup-plugin-postcss": "^4.0.2",
62
41
  "rollup-plugin-string": "^3.0.0",
63
42
  "rollup-plugin-watch-assets": "^1.0.1",
64
- "tailwindcss": "^3.2.4"
43
+ "tailwindcss": "^3.3.2"
44
+ },
45
+ "scripts": {
46
+ "help": "tspci --help",
47
+ "init": "tspci init --force --path=../../pcis"
65
48
  },
66
- "scripts": {},
67
- "gitHead": "7cdbb1e4a3b177e3d8fc103679ecee6e65fdf79d"
49
+ "gitHead": "872d3b8628f1e7e9b2568563b8aae5ffa1a818d6"
68
50
  }
@@ -0,0 +1,13 @@
1
+ declare module "*.css" {
2
+ const content: string;
3
+ export = content;
4
+ }
5
+
6
+ declare module "*.png" {
7
+ const content: string;
8
+ export = content;
9
+ }
10
+ declare module "qtiCustomInteractionContext" {
11
+ const register: {register: (PortableInteraction) => void};
12
+ export = register;
13
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "my-pci",
3
+ "description": "Here your PCI description",
4
+ "version": "1.0.0",
5
+ "main": "dist/index.js",
6
+ "types": "dist/types.d.ts",
7
+ "private": true,
8
+ "dependencies": {},
9
+ "devDependencies": {
10
+ "@citolab/tspci": "^2.2.0"
11
+ },
12
+ "scripts": {
13
+ "dev": "tspci --dev",
14
+ "prod": "tspci"
15
+ },
16
+ "config": {
17
+ "tspci": {
18
+ "typeIdentifier": "myPci"
19
+ }
20
+ },
21
+ "source": "src/index.ts"
22
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "title" : "Percentages",
3
+ "prompt" : "What percentage is colored green?",
4
+ "width": "200",
5
+ "height": "200"
6
+ }
@@ -0,0 +1,60 @@
1
+ import { Configuration, IMSpci, QtiVariableJSON } from "@citolab/tspci";
2
+ import * as ctx from "qtiCustomInteractionContext";
3
+ import configProps from "./config.json";
4
+ import style from "./styles.css"; // import and bundle this style file ( you can use tailwind and nested css )
5
+ import procenten from "./assets/procenten.png"; // image types are bundled inside the js
6
+ // Configuration
7
+ type PropTypes = typeof configProps;
8
+
9
+ class Pci implements IMSpci<PropTypes> {
10
+ typeIdentifier = "###___PCI_NAME___###"; // typeIdentifier is mandatory for all PCI's
11
+ config: Configuration<PropTypes>; // reference to the interface of the config object which you get when getInstance is called by the player
12
+ state: string; // keep a reference to the state
13
+ shadowdom: ShadowRoot | Element; // Not mandatory, but its wise to create a shadowroot
14
+
15
+ constructor() {
16
+ ctx && ctx.register(this); // we assume the qtiCustomInteractionContext is avaible due to the import above
17
+ }
18
+
19
+ // First in the lifecycle of a PCI, this method is called with the domElement ( usually qti-interaction-markup ) where we can add our dom tree.
20
+ // config is the configuration object which has an onready
21
+ getInstance = (dom: HTMLElement, config: Configuration<any>, state: string) => {
22
+ config.properties = { ...configProps, ...config.properties }; // merge current props with incoming
23
+ this.config = config;
24
+ this.state = state ? state : "";
25
+
26
+ this.shadowdom = dom.attachShadow({ mode: "closed" });
27
+ this.render();
28
+
29
+ config.onready(this);
30
+ };
31
+
32
+ private render = () => {
33
+ this.shadowdom.addEventListener("change", (e: Event) => {
34
+ const value = (e.target as HTMLInputElement).value;
35
+ this.state = value;
36
+ });
37
+ this.shadowdom.innerHTML = `<div class="pci-container">
38
+ <h1>${this.config.properties.title}</h1>
39
+ <div class="body">
40
+ <img width="${+this.config.properties.width}" height="${+this.config.properties.height}" src="${procenten}" />
41
+ </div>
42
+ <div class="interaction">
43
+ <label for="tentacles">${this.config.properties.prompt}</label>
44
+ <input type="number" value="${this.state}" min="0" max="100">%
45
+ </div>
46
+ </div>`;
47
+ const css = document.createElement("style");
48
+ css.innerHTML = style;
49
+ this.shadowdom.appendChild(css);
50
+ };
51
+
52
+ getResponse = () => {
53
+ const response = (this.state === null || this.state === undefined) ? undefined : { base: { integer: this.state }} as QtiVariableJSON;
54
+ return response;
55
+ };
56
+
57
+ getState = () => this.state.toString();
58
+ }
59
+
60
+ export default new Pci();
@@ -0,0 +1,3 @@
1
+ h1 {
2
+ color: red;
3
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "declaration": true,
4
+ "lib": ["es6", "dom", "dom.iterable"],
5
+ "module": "es6",
6
+ "moduleResolution": "node",
7
+ "removeComments": true,
8
+ "sourceMap": true,
9
+ "strict": false,
10
+ "target": "es6",
11
+ "outDir": "./dist",
12
+ "jsx": "react-jsx",
13
+ "jsxImportSource": "preact",
14
+ "allowSyntheticDefaultImports": true,
15
+ "resolveJsonModule": true,
16
+ "esModuleInterop": true,
17
+ },
18
+ "include": ["**/*"],
19
+ }
@@ -0,0 +1,66 @@
1
+ import "preact/debug";
2
+ import { render } from "preact";
3
+ import { initStore, Store } from "@citolab/preact-store";
4
+
5
+ import * as ctx from "qtiCustomInteractionContext";
6
+ import Interaction from "./interaction";
7
+ import style from "./styles.css";
8
+ import { Configuration, IMSpci, QtiVariableJSON } from "@citolab/tspci";
9
+ import configProps from "./config.json";
10
+ import { actions, StateModel } from "./store";
11
+
12
+ type PropTypes = typeof configProps;
13
+
14
+ class App implements IMSpci<PropTypes> {
15
+ typeIdentifier = "myPci"; // typeIdentifier is mandatory for all PCI's
16
+ config: Configuration<PropTypes>; // reference to the interface of the config object which you get when getInstance is called by the player
17
+ state: string; // keep a reference to the state
18
+ shadowdom: ShadowRoot; // Not mandatory, but its wise to create a shadowroot
19
+ store: Store<StateModel>;
20
+
21
+ private logActions: { type: string; payload: unknown }[] = []; // optional logActions
22
+
23
+ constructor() {
24
+ ctx && ctx.register(this);
25
+ }
26
+
27
+ getInstance = (dom: HTMLElement, config: Configuration<PropTypes>, stateString: string) => {
28
+ config.properties = { ...configProps, ...config.properties }; // merge current props with incoming
29
+ this.config = config;
30
+
31
+ const initState: StateModel = stateString ? JSON.parse(stateString).state : { input: 0 };
32
+ this.logActions = stateString ? JSON.parse(stateString).log : [];
33
+ this.store = initStore<StateModel>(actions as any[], initState, (action) => this.logActions.push(action));
34
+
35
+ this.shadowdom = dom.attachShadow({ mode: "closed" });
36
+ this.render();
37
+
38
+ this.config.onready && this.config.onready(this);
39
+ };
40
+
41
+ render = () => {
42
+ render(null, this.shadowdom);
43
+ const css = document.createElement("style");
44
+ css.innerHTML = style;
45
+ this.shadowdom.appendChild(css);
46
+ render(<Interaction config={this.config.properties} dom={this.shadowdom} />, this.shadowdom);
47
+ };
48
+
49
+ getState = () =>
50
+ JSON.stringify({
51
+ state: this.store.getState(),
52
+ log: this.logActions,
53
+ });
54
+
55
+ getResponse = () => {
56
+ const state = this.store?.getState()?.input || undefined;
57
+ if (state === undefined) return undefined;
58
+ return {
59
+ base: {
60
+ integer: this.store.getState().input,
61
+ },
62
+ } as QtiVariableJSON;
63
+ }
64
+ }
65
+
66
+ export default new App();
@@ -0,0 +1,36 @@
1
+ import { useStore } from "@citolab/preact-store";
2
+ import { ActionType, StateModel } from "./store";
3
+ import configProps from "./config.json";
4
+ import procenten from "./assets/procenten.png"; // image types are bundled inside the js
5
+
6
+ type PropTypes = typeof configProps;
7
+
8
+ const Interaction = ({ config, dom }: { config: PropTypes; dom: Document | ShadowRoot }) => {
9
+ const [state, dispatch] = useStore<StateModel, ActionType>(
10
+ (type, payload) => {
11
+ console.log(`type: ${type}, payload: ${JSON.stringify(payload)}`);
12
+ }
13
+ );
14
+ return <div className="pci-container">
15
+ <h1>{config.title}</h1>
16
+ <div className="body">
17
+ <img width={config.width} height={+config.height} src={procenten} />
18
+ </div>
19
+ <div className="interaction">
20
+ <label htmlFor="tentacles">{config.prompt}</label>
21
+ <input type="number"
22
+ onKeyUp={(e) => {
23
+ const input = e.target as HTMLInputElement;
24
+ dispatch<{ input: number }>("SET_INPUT", { input: +input.value });
25
+ }
26
+ }
27
+ onChange={(e) => {
28
+ const input = e.target as HTMLInputElement;
29
+ dispatch<{ input: number }>("SET_INPUT", { input: +input.value });
30
+ }} min="0" max="100" />%
31
+ </div>
32
+ </div>;
33
+ };
34
+
35
+
36
+ export default Interaction;
@@ -0,0 +1,12 @@
1
+ export type ActionType = "SET_INPUT";
2
+
3
+ export type StateModel = { input: number };
4
+
5
+ export const actions = [
6
+ {
7
+ type: "SET_INPUT" as ActionType,
8
+ action: (state: StateModel, { input }: { input: number }) => {
9
+ return {...state, input };
10
+ },
11
+ },
12
+ ];
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ plugins: [
3
+ require('postcss-import'),
4
+ require('@tailwindcss/nesting'),
5
+ require('tailwindcss'),
6
+ require('autoprefixer'),
7
+ ]
8
+ }
@@ -0,0 +1,7 @@
1
+ @tailwind components;
2
+ @tailwind utilities;
3
+ @tailwind base;
4
+
5
+ .pci-container { @apply container flex flex-col justify-center items-center; }
6
+ h1 { @apply text-2xl text-red-500 m-4; }
7
+ input { @apply border border-gray-400 rounded p-2 m-1; }
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ content: ["./src/**/*.{html,js,ts,jsx,tsx}", "./*.xml"], // html/javascript and typescript, and everything in pci markup
3
+ theme: {
4
+ extend: {},
5
+ },
6
+ plugins: [
7
+ ]
8
+ }
package/public/index.html CHANGED
@@ -19,6 +19,7 @@
19
19
  #pciContainer {
20
20
  width: 100%;
21
21
  /* min-width: 800px; */
22
+
22
23
  max-width: 1024px;
23
24
  height: 600px;
24
25
  min-height: 600px;
@@ -47,7 +48,6 @@
47
48
  user has not yet interacted yet, getResponse should return undefined.
48
49
  </div>
49
50
 
50
-
51
51
  <div id="pciContainer" class="shadow-sm">
52
52
  <div id="loading" class="text-muted mx-auto p-4">Loading...</div>
53
53
  </div>
@@ -124,6 +124,11 @@
124
124
  location.reload();
125
125
  }
126
126
 
127
+ function playState() {
128
+ const state = item.getState();
129
+ item.store.replay(state.log); // state.log
130
+ }
131
+
127
132
  function changeConfig(key, value) {
128
133
  item.config.properties[key] = value;
129
134
  item.trigger(`change${key}`, value);
@@ -210,20 +215,44 @@
210
215
  <button class="btn btn-outline-secondary" onClick="${() => saveResponse()}">store</button>
211
216
  <button class="btn btn-outline-secondary" onClick="${() => compareResponse()}">compare</button>
212
217
  <button class="btn btn-outline-secondary" onClick="${() => setResponse()}">set</button>
213
- <button class="btn btn-outline-secondary" disabled="${!item.resetResponse}" onClick="${() => resetResponse()}">reset</button>
218
+ <button
219
+ class="btn btn-outline-secondary"
220
+ disabled="${!item.resetResponse}"
221
+ onClick="${() => resetResponse()}"
222
+ >
223
+ reset
224
+ </button>
214
225
  </div>
215
226
  </div>
216
227
 
217
228
  <div>
218
229
  <small class="text-muted">ㅤ</small>
219
230
  <div class="d-flex">
220
- <button class="btn bg-light shadow-sm btn-outline-danger me-2" disabled="${!(item.destroy || item.oncompleted)}" onClick="${() => unload()}">
221
- unload
222
- </button>
223
- <button class="btn bg-light shadow-sm btn-outline-danger" onClick="${() => reset()}">reset</button>
231
+ <button
232
+ class="btn bg-light shadow-sm btn-outline-danger me-2"
233
+ disabled="${!(item.destroy || item.oncompleted)}"
234
+ onClick="${() => unload()}"
235
+ >
236
+ unload
237
+ </button>
238
+ <button class="btn bg-light shadow-sm btn-outline-danger" onClick="${() => reset()}">reset</button>
224
239
  </div>
225
240
  </div>
226
241
  </div>
242
+
243
+ <div class="mt-2 d-flex gap-2">
244
+ <button class="btn btn-outline-secondary" onClick="${() => playState()}">play</button>
245
+ <input
246
+ class="form-control-range flex-grow-1"
247
+ type="range"
248
+ min="0"
249
+ max="${JSON.parse(item.getState()).log.length}"
250
+ value=${range}
251
+ onChange=${(e) => setRange(e.target.value)}
252
+ step="1"
253
+ />
254
+ </div>
255
+
227
256
  </div>
228
257
  `;
229
258
  }
@@ -267,8 +296,8 @@
267
296
  window.onload = () => loadItem();
268
297
  </script>
269
298
 
270
- <h3 style="margin-top: 12rem;">checklist</h3>
271
- <ul style="margin-bottom:10rem;">
299
+ <h3 style="margin-top: 12rem">checklist</h3>
300
+ <ul style="margin-bottom: 10rem">
272
301
  <li>Is the css scoped to the item?</li>
273
302
  <li>Is the css/item somewhat responsive on smaller screens</li>
274
303
  <li>Can you reset the item to its initial state</li>
package/src/cli.js CHANGED
@@ -1,13 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { loadConfigFile } from "rollup/dist/loadConfigFile.js";
3
+ import { spawn } from "child_process";
3
4
  import { rollup, watch } from "rollup";
5
+ import readline from "readline";
6
+ import chalk from "chalk";
4
7
  import path from "path";
5
8
  import fs from "fs";
9
+ import os from "os";
10
+ import clear from "clear";
11
+ import figlet from "figlet";
12
+ import inquirer from "inquirer";
6
13
 
7
14
  const args = process.argv.slice(2);
8
15
 
9
-
10
-
11
16
  // PK: retreive dirname, most of the time automaticly inserted in es modules
12
17
  import { fileURLToPath } from "url";
13
18
  const __filename = fileURLToPath(import.meta.url);
@@ -18,12 +23,12 @@ const PACKAGE_DIST_PATH = path.join(PACKAGE_ROOT_PATH, "dist");
18
23
  const NODE_MODULES = path.join(process.cwd(), "node_modules");
19
24
 
20
25
  // https://rollupjs.org/guide/en/#programmatically-loading-a-config-file
21
- const watchBuild = (devServer = false, target = 'IMS') => {
26
+ const watchBuild = (devServer = false, target = "IMS") => {
22
27
  const rollupFile = devServer ? "rollup.config.dev.js" : "rollup.config.js";
23
28
 
24
29
  // PK: this ensures tailwind watches changes
25
30
  // PK: See link here: https://github.com/tailwindlabs/tailwindcss/issues/7063
26
- process.env.ROLLUP_WATCH = "true"
31
+ process.env.ROLLUP_WATCH = "true";
27
32
 
28
33
  fs.rmSync(PACKAGE_DIST_PATH, { recursive: true, force: true });
29
34
  !fs.existsSync(PACKAGE_DIST_PATH) && fs.mkdirSync(PACKAGE_DIST_PATH);
@@ -41,8 +46,8 @@ const watchBuild = (devServer = false, target = 'IMS') => {
41
46
  );
42
47
  } else {
43
48
  let fileToCopy;
44
- if (target == 'IMS') {
45
- fileToCopy = path.join(__dirname, `../public/index.html`)
49
+ if (target == "IMS") {
50
+ fileToCopy = path.join(__dirname, `../public/index.html`);
46
51
  } else {
47
52
  fileToCopy = path.join(NODE_MODULES, `@citolab/tspci-${target}/public/index.html`);
48
53
  }
@@ -100,17 +105,309 @@ const prodBuild = async () => {
100
105
  });
101
106
  };
102
107
 
108
+ function promptYesNo(prompt) {
109
+ const rl = readline.createInterface({
110
+ input: process.stdin,
111
+ output: process.stdout,
112
+ });
113
+ return new Promise((resolve) => {
114
+ rl.question(`${prompt} (y/n): `, (answer) => {
115
+ const normalizedAnswer = answer.trim().toLowerCase();
116
+ if (normalizedAnswer === "y" || normalizedAnswer === "yes") {
117
+ console.log(chalk.green("Yes"));
118
+ resolve(true);
119
+ } else if (normalizedAnswer === "n" || normalizedAnswer === "no") {
120
+ console.log(chalk.red("No"));
121
+ resolve(false);
122
+ } else {
123
+ console.log("Invalid input. Please enter 'y' or 'n'.");
124
+ resolve(promptYesNo(prompt));
125
+ }
126
+ });
127
+ }).finally(() => {
128
+ rl.close();
129
+ });
130
+ }
131
+
132
+ function selectOptions(prompt, options) {
133
+ return inquirer.prompt([
134
+ {
135
+ type: "checkbox",
136
+ message: prompt,
137
+ name: "values",
138
+ choices: options.map((o) => ({ name: o, checked: true })),
139
+ },
140
+ ]);
141
+ }
142
+
143
+ function askOption(prompt) {
144
+ const rl = readline.createInterface({
145
+ input: process.stdin,
146
+ output: process.stdout,
147
+ });
148
+ return new Promise((resolve) => {
149
+ rl.question(prompt, (option) => {
150
+ rl.close();
151
+ resolve(option);
152
+ });
153
+ });
154
+ }
155
+
156
+ function createDirectoryIfNotExists(file) {
157
+ const dir = path.dirname(file);
158
+ if (!fs.existsSync(dir)) {
159
+ fs.mkdirSync(dir, { recursive: true });
160
+ }
161
+ }
162
+
163
+ function copyDir(presetFolder, destinationFolder) {
164
+ for (const fileOfDir of fs.readdirSync(presetFolder)) {
165
+ // check if object is a file or a folder
166
+ if (fs.statSync(path.join(presetFolder, fileOfDir)).isDirectory()) {
167
+ copyDir(path.join(presetFolder, fileOfDir), path.join(destinationFolder, fileOfDir));
168
+ } else {
169
+ const filename = fileOfDir;
170
+ const file = path.join(presetFolder, filename);
171
+ const newFileLocation = path.join(destinationFolder, filename);
172
+ createDirectoryIfNotExists(newFileLocation);
173
+ fs.copyFileSync(file, newFileLocation);
174
+ }
175
+ }
176
+ }
177
+
178
+ function copyDirAndReplaceInFiles(presetFolder, destinationFolder, pciName) {
179
+ for (const fileOfDir of fs.readdirSync(presetFolder)) {
180
+ // check if object is a file or a folder
181
+ if (fs.statSync(path.join(presetFolder, fileOfDir)).isDirectory()) {
182
+ copyDirAndReplaceInFiles(path.join(presetFolder, fileOfDir), path.join(destinationFolder, fileOfDir), pciName);
183
+ } else {
184
+ const fileExtensionsToCheck = [".js", ".ts", ".tsx"];
185
+ const filename = fileOfDir;
186
+ const file = path.join(presetFolder, filename);
187
+ const fileExtension = path.extname(file);
188
+ const newFileLocation = path.join(destinationFolder, filename);
189
+ createDirectoryIfNotExists(newFileLocation);
190
+ if (fileExtensionsToCheck.includes(fileExtension)) {
191
+ // remove ###___name___### in the file content to the name
192
+ let content = fs.readFileSync(file, "utf8");
193
+ content = content.replace(/###___PCI_NAME___###/g, pciName);
194
+ // write newContent to newFileLocation
195
+ fs.writeFileSync(newFileLocation, content, "utf8");
196
+ } else {
197
+ fs.copyFileSync(file, path.join(destinationFolder, filename));
198
+ }
199
+ }
200
+ }
201
+ }
202
+
103
203
  const run = async () => {
104
204
  const help = args.indexOf("--help") >= 0 || args.indexOf("-h") >= 0;
105
205
  const watch = args.indexOf("--watch") >= 0 || args.indexOf("-w") >= 0;
106
206
  const dev = args.indexOf("--dev") >= 0 || args.indexOf("-d") >= 0;
107
207
  const target = args.indexOf("--target") >= 0 || args.indexOf("-t") >= 0;
208
+ const init = args.indexOf("init") >= 0;
108
209
 
109
210
  if (help) {
110
- console.log("--help -h list these commands");
111
- console.log("--build a one of build of your item");
112
- console.log("--watch watch the files");
113
- console.log("--target build a prod item and bundle for platform");
211
+ console.log(`•\t${chalk.blue("--help -h")} list these commands`);
212
+ console.log(`•\t${chalk.blue("--build")} build the project`);
213
+ console.log(`•\t${chalk.blue("--watch")} watch the files`);
214
+ console.log(`•\t${chalk.blue("--target")} build a prod item and bundle for platform`);
215
+ console.log("\n");
216
+ console.log(
217
+ `${chalk.blueBright("tspci init")} creates a new project \n•\t${chalk.blue(
218
+ "--force"
219
+ )} deletes the directory if it already exists \n•\t${chalk.blue(
220
+ "--no-install"
221
+ )} don't install dependencies \n•\t${chalk.blue("--path")} path to create the project in`
222
+ );
223
+ console.log(
224
+ `${chalk.blueBright("tspci add --target")} adds a new target to the project. E.g. tspci add --target=tao`
225
+ );
226
+ console.log("\n\n");
227
+ return;
228
+ }
229
+
230
+ if (init) {
231
+ clear();
232
+ console.log(chalk.green(figlet.textSync("@citolab/tspci", { horizontalLayout: "full" })));
233
+
234
+ // prompt for name
235
+ const pciName = (await askOption("What your pci identifier (typeIdentifier) (default: myPci) ")) || "myPci";
236
+ let destinationFolder = path.join(process.cwd(), pciName);
237
+ if (args.find((arg) => arg.startsWith("--path"))) {
238
+ const pathIndex = args.findIndex((arg) => arg.startsWith("--path"));
239
+ destinationFolder = path.join(process.cwd(), path.join(args[pathIndex].split("=")[1], pciName));
240
+ }
241
+ // if args contains --force then delete the directory if already exists
242
+ if (args.indexOf("--force") >= 0 || args.indexOf("-f") >= 0) {
243
+ if (fs.existsSync(destinationFolder)) {
244
+ console.log(chalk.yellow("Force deleting directory"));
245
+ fs.rmSync(destinationFolder, { recursive: true, force: true });
246
+ }
247
+ }
248
+ // check if directory exists
249
+ while (pciName && fs.existsSync(destinationFolder)) {
250
+ console.log(
251
+ chalk.red(
252
+ "The directory already exists, please choose another name. Running tscpi init --force will delete the directory"
253
+ )
254
+ );
255
+ return;
256
+ }
257
+ // check if contains other characters than letters and numbers
258
+ if (!/^[a-zA-Z0-9]+$/.test(pciName)) {
259
+ console.log(
260
+ chalk.yellow("The identifier of your pci can only contain letters and numbers, stripping special characters")
261
+ );
262
+ pciName = pciName.replace(/[^a-zA-Z0-9]/g, "");
263
+ }
264
+ console.log(chalk.green(`pci: ${pciName}`));
265
+ const description = await askOption("What is the description of your pci? (default: pci created with tspci)") || "pci created with tspci";
266
+
267
+ const options = await selectOptions("Would you like to add other features?", ["tailwind css", "preact"]);
268
+ console.log(chalk.green(`Options: ${options.values.length === 0 ? "none" : options.values}`));
269
+ // get relative path to executable
270
+ const cliPath = __dirname;
271
+
272
+ // create the project folder
273
+ fs.mkdirSync(destinationFolder);
274
+
275
+ // copy the content of preset folder to the current folder
276
+ const presetFolder = path.resolve(cliPath, "./../preset/base");
277
+ if (!fs.existsSync(presetFolder)) {
278
+ console.log(chalk.red("Preset folder not found"));
279
+ }
280
+
281
+ console.log(chalk.green("Copying files to PCI project folder"));
282
+ copyDirAndReplaceInFiles(presetFolder, destinationFolder, pciName);
283
+ const packageJson = path.join(destinationFolder, "package.json");
284
+ const packageJsonContent = fs.readFileSync(packageJson, "utf8");
285
+ const packageJsonContentJson = JSON.parse(packageJsonContent);
286
+
287
+ const kebabCasedProjectName = pciName.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
288
+ packageJsonContentJson.name = kebabCasedProjectName;
289
+ packageJsonContentJson.description = description;
290
+ packageJsonContentJson.author = os?.userInfo()?.username;
291
+ if (!packageJsonContentJson.config) {
292
+ packageJsonContentJson.config = {};
293
+ }
294
+ if (!packageJsonContentJson.config.tspci) {
295
+ packageJsonContentJson.config.tspci = {};
296
+ }
297
+ packageJsonContentJson.config.tspci.typeIdentifier = pciName;
298
+
299
+ if (options.values.includes("tailwind css")) {
300
+ console.log(chalk.green("Adding tailwind css"));
301
+
302
+ // read postcss.config.js
303
+ const postcssConfig = path.resolve(cliPath, "./../preset/tailwind/postcss.config.js");
304
+ const postcssConfigContent = fs.readFileSync(postcssConfig, "utf8");
305
+ // get all plugin names
306
+ const pluginNames = postcssConfigContent
307
+ .match(/require\('(.*)'\)/g)
308
+ .map((plugin) => plugin.replace(/require\('(.*)'\)/, "$1"));
309
+ const packageJsonCli = path.resolve(cliPath, "./../package.json");
310
+ const packageJsonCliContent = fs.readFileSync(packageJsonCli, "utf8");
311
+ const packageJsonCliContentJson = JSON.parse(packageJsonCliContent);
312
+ // add all plugins to package.json and map the version to the version in tspci package.json
313
+ for (const pluginName of pluginNames) {
314
+ const pluginVersion = packageJsonCliContentJson.dependencies[pluginName];
315
+ packageJsonContentJson.devDependencies = {
316
+ ...packageJsonContentJson.devDependencies,
317
+ [pluginName]: pluginVersion,
318
+ };
319
+ }
320
+ const newPackageJsonContent = JSON.stringify(packageJsonContentJson, null, 2);
321
+ fs.writeFileSync(packageJson, newPackageJsonContent);
322
+ // copy tailwind config file
323
+ copyDir(path.resolve(cliPath, "./../preset/tailwind"), destinationFolder);
324
+ }
325
+
326
+ if (options.values.includes("preact")) {
327
+ console.log(chalk.green("Adding preact"));
328
+ const packageJson = path.join(destinationFolder, "package.json");
329
+ const packageJsonContent = fs.readFileSync(packageJson, "utf8");
330
+ let packageJsonContentJson = JSON.parse(packageJsonContent);
331
+ // delete index.ts
332
+ fs.rmSync(path.join(destinationFolder, "src/index.ts"), { force: true });
333
+ const preactPresetFolder = path.resolve(cliPath, "./../preset/preact");
334
+ copyDirAndReplaceInFiles(preactPresetFolder, destinationFolder, pciName);
335
+ // add preact and @citolab/preact-store to the package json
336
+ packageJsonContentJson.dependencies = {
337
+ ...packageJsonContentJson.dependencies,
338
+ preact: "^10.13.2",
339
+ "@citolab/preact-store": "^2.0.9",
340
+ };
341
+ packageJsonContentJson = { ...packageJsonContentJson, source: "src/index.tsx" };
342
+ const newPackageJsonContent = JSON.stringify(packageJsonContentJson, null, 2);
343
+ fs.writeFileSync(packageJson, newPackageJsonContent);
344
+ }
345
+
346
+ // run npm instaal in destination folder
347
+ if (args.indexOf("--no-install") === -1) {
348
+ console.log(chalk.green("Installing dependencies"));
349
+ const npm = spawn("npm", ["install"], { cwd: destinationFolder });
350
+ npm.stdout.on("data", (data) => {
351
+ console.log(chalk.green(data.toString()));
352
+ console.log(chalk.green("Done! You can now start the dev server with npm run dev"));
353
+
354
+ // console.log(chalk.blue("Starting dev server..."));
355
+ // // run npm run dev in destination folder
356
+ // const npm = spawn("npm", ["run", "dev"], { cwd: destinationFolder });
357
+ // npm.stdout.on("data", (data) => {
358
+ // console.log(chalk.blue(data.toString()));
359
+ // });
360
+ });
361
+ return;
362
+ } else {
363
+ console.log(chalk.yellow("Skipping npm install, run npm install in the project folder to install dependencies"));
364
+ }
365
+ }
366
+
367
+ if (args.indexOf("add") !== -1) {
368
+ const targetIndex = args.indexOf("--target");
369
+ if (args.length <= targetIndex + 1) {
370
+ console.error("Missing target name. Use tspci add --target target");
371
+ return;
372
+ }
373
+ const targetname = args[targetIndex + 1];
374
+ const packageFileContent = fs.readFileSync(path.join(PACKAGE_ROOT_PATH, "package.json"), "utf8");
375
+ const packageJson = JSON.parse(packageFileContent);
376
+
377
+ const modifyPci = () => {
378
+ const scriptFile = path.join(path.join(path.join(path.join(NODE_MODULES, `@citolab`), `tspci-${targetname}`), 'src'), `init.mjs`);
379
+ let init = false;
380
+ if (fs.existsSync(scriptFile)) {
381
+ // run init() script
382
+ import(`file:///${scriptFile}`).then((script) => {
383
+ if (script.init) {
384
+ init = true;
385
+ console.log(chalk.green(`Running init script for @citolab/tspci-${targetname}`));
386
+ script.init();
387
+ }
388
+ });
389
+ }
390
+ if (!init) {
391
+ console.log(chalk.green(`No init script found for @citolab/tspci-${targetname}`));
392
+ }
393
+ }
394
+
395
+ // check if `@citolab/tspci-${targetname}]}` is already in devDependencies
396
+ if (!packageJson.devDependencies || !packageJson.devDependencies[`@citolab/tspci-${targetname}`]) {
397
+ console.log(chalk.white(`Adding @citolab/tspci-${targetname}`));
398
+ // install @citolab/tspci-${targetname} to devDepencies
399
+ const npm = spawn("npm", ["install", `@citolab/tspci-${targetname}`, "--save-dev"], { cwd: process.cwd() });
400
+ // wait till npm install is done
401
+ npm.stdout.on("data", (data) => {
402
+ console.log(chalk.green(`successfull installed @citolab/tspci-${targetname}`));
403
+ // run init script if available
404
+ modifyPci();
405
+ });
406
+ } else {
407
+ modifyPci();
408
+ }
409
+
410
+
114
411
  return;
115
412
  }
116
413
 
@@ -138,7 +435,7 @@ const run = async () => {
138
435
  console.error("Missing target name. Use tspci --target target");
139
436
  }
140
437
  const targetname = args[targetIndex + 1];
141
- const scriptFile = path.join(NODE_MODULES, `@citolab\\tspci-${targetname}\\src\\createPackage.mjs`);
438
+ const scriptFile = path.join(path.join(path.join(path.join(NODE_MODULES, `@citolab`), `tspci-${targetname}`), 'src'), `createPackage.mjs`);
142
439
  import(`file:///${scriptFile}`).then((script) => {
143
440
  script.bundle();
144
441
  });