@ptkl/toolkit 0.7.0 → 0.8.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.
@@ -1,14 +1,14 @@
1
1
  import CompilerSDK4 from "./sdk4/vue2.js";
2
2
  import CompilerSDK5 from "./sdk5/vue2.js";
3
- import CompilerSDK5Vue3 from "./sdk5/vue3.js";
4
- import CompiltedSDK5React from "./sdk5/react.js";
5
- const latestVersion = 5;
6
- const defaultEngine = 'vue2';
3
+ import CompilerSDK6WebComponents from "./sdk6/webcomponents.js";
4
+ import CompilerSDK6Lit from "./sdk6/lit.js";
5
+ const latestVersion = 6;
6
+ const defaultEngine = 'webcomponents';
7
7
  const compilers = {
8
8
  'sdk4::vue2': new CompilerSDK4,
9
9
  'sdk5::vue2': new CompilerSDK5,
10
- 'sdk5::vue3': new CompilerSDK5Vue3,
11
- 'sdk5::react': new CompiltedSDK5React,
10
+ 'sdk6::webcomponents': new CompilerSDK6WebComponents,
11
+ 'sdk6::lit': new CompilerSDK6Lit,
12
12
  };
13
13
  export default {
14
14
  getCompiler(sdkVersion, engine) {
@@ -0,0 +1,38 @@
1
+ import * as Babel from '@babel/standalone';
2
+ import less from 'less';
3
+ export default class Compiler {
4
+ async compileBabel(expression) {
5
+ let code = Babel.transform(expression, {
6
+ sourceType: "module",
7
+ presets: ["env"]
8
+ });
9
+ return code.code;
10
+ }
11
+ async compileCSS(scope, lang, expression) {
12
+ switch (lang) {
13
+ case 'less':
14
+ if (scope) {
15
+ return await less.render(`.${scope} { ${expression} }`);
16
+ }
17
+ const { css } = await less.render(`${expression}`);
18
+ return css;
19
+ default:
20
+ return expression;
21
+ }
22
+ }
23
+ async compile(ext, content) {
24
+ switch (ext) {
25
+ case 'js':
26
+ return await this.compileBabel(content);
27
+ case 'css':
28
+ return await this.compileCSS(null, "css", content);
29
+ case 'less':
30
+ return await this.compileCSS(null, "less", content);
31
+ default:
32
+ return await Promise.resolve(content);
33
+ }
34
+ }
35
+ getSupportedExt() {
36
+ return ["js", "css", "less", "json", "svg", "html"];
37
+ }
38
+ }
@@ -0,0 +1,52 @@
1
+ import * as Babel from '@babel/standalone';
2
+ import * as sass from 'sass';
3
+ export default class Compiler {
4
+ async compileBabel(expression) {
5
+ let code = Babel.transform(expression, {
6
+ sourceType: "module",
7
+ presets: [
8
+ "env",
9
+ ["typescript", {
10
+ onlyRemoveTypeImports: true,
11
+ allowDeclareFields: true
12
+ }]
13
+ ],
14
+ plugins: [
15
+ ["proposal-decorators", { version: "2023-05" }],
16
+ "proposal-class-properties"
17
+ ]
18
+ });
19
+ return code.code;
20
+ }
21
+ async compileCSS(scope, lang, expression) {
22
+ switch (lang) {
23
+ case 'scss':
24
+ case 'sass':
25
+ if (scope) {
26
+ const result = sass.compileString(`.${scope} { ${expression} }`);
27
+ return result.css;
28
+ }
29
+ const result = sass.compileString(expression);
30
+ return result.css;
31
+ default:
32
+ return expression;
33
+ }
34
+ }
35
+ async compile(ext, content) {
36
+ switch (ext) {
37
+ case 'js':
38
+ case 'ts':
39
+ return await this.compileBabel(content);
40
+ case 'css':
41
+ return await this.compileCSS(null, "css", content);
42
+ case 'scss':
43
+ case 'sass':
44
+ return await this.compileCSS(null, ext, content);
45
+ default:
46
+ return await Promise.resolve(content);
47
+ }
48
+ }
49
+ getSupportedExt() {
50
+ return ["js", "ts", "css", "scss", "sass", "json", "svg", "html"];
51
+ }
52
+ }
@@ -0,0 +1,56 @@
1
+ import * as Babel from '@babel/standalone';
2
+ import * as sass from 'sass';
3
+ export default class Compiler {
4
+ async compileBabel(expression) {
5
+ let code = Babel.transform(expression, {
6
+ sourceType: "module",
7
+ presets: [
8
+ "env",
9
+ ["react", { runtime: "automatic" }],
10
+ ["typescript", {
11
+ onlyRemoveTypeImports: true,
12
+ isTSX: true,
13
+ allExtensions: true
14
+ }]
15
+ ],
16
+ plugins: [
17
+ '@babel/plugin-proposal-class-properties',
18
+ '@babel/plugin-transform-class-static-block'
19
+ ]
20
+ });
21
+ return code.code;
22
+ }
23
+ async compileCSS(scope, lang, expression) {
24
+ switch (lang) {
25
+ case 'scss':
26
+ case 'sass':
27
+ if (scope) {
28
+ const result = sass.compileString(`.${scope} { ${expression} }`);
29
+ return result.css;
30
+ }
31
+ const result = sass.compileString(expression);
32
+ return result.css;
33
+ default:
34
+ return expression;
35
+ }
36
+ }
37
+ async compile(ext, content) {
38
+ switch (ext) {
39
+ case 'js':
40
+ case 'jsx':
41
+ case 'ts':
42
+ case 'tsx':
43
+ return await this.compileBabel(content);
44
+ case 'css':
45
+ return await this.compileCSS(null, "css", content);
46
+ case 'scss':
47
+ case 'sass':
48
+ return await this.compileCSS(null, ext, content);
49
+ default:
50
+ return await Promise.resolve(content);
51
+ }
52
+ }
53
+ getSupportedExt() {
54
+ return ["js", "jsx", "ts", "tsx", "css", "scss", "sass", "json", "svg", "html"];
55
+ }
56
+ }
@@ -0,0 +1,52 @@
1
+ import * as Babel from '@babel/standalone';
2
+ import * as sass from 'sass';
3
+ export default class Compiler {
4
+ async compileBabel(expression) {
5
+ let code = Babel.transform(expression, {
6
+ sourceType: "module",
7
+ presets: [
8
+ "env",
9
+ ["typescript", {
10
+ onlyRemoveTypeImports: true,
11
+ allExtensions: true
12
+ }]
13
+ ],
14
+ plugins: [
15
+ '@babel/plugin-proposal-class-properties',
16
+ '@babel/plugin-transform-class-static-block'
17
+ ]
18
+ });
19
+ return code.code;
20
+ }
21
+ async compileCSS(scope, lang, expression) {
22
+ switch (lang) {
23
+ case 'scss':
24
+ case 'sass':
25
+ if (scope) {
26
+ const result = sass.compileString(`.${scope} { ${expression} }`);
27
+ return result.css;
28
+ }
29
+ const result = sass.compileString(expression);
30
+ return result.css;
31
+ default:
32
+ return expression;
33
+ }
34
+ }
35
+ async compile(ext, content) {
36
+ switch (ext) {
37
+ case 'js':
38
+ case 'ts':
39
+ return await this.compileBabel(content);
40
+ case 'css':
41
+ return await this.compileCSS(null, "css", content);
42
+ case 'scss':
43
+ case 'sass':
44
+ return await this.compileCSS(null, ext, content);
45
+ default:
46
+ return await Promise.resolve(content);
47
+ }
48
+ }
49
+ getSupportedExt() {
50
+ return ["js", "ts", "css", "scss", "sass", "json", "svg", "html", "vue"];
51
+ }
52
+ }
@@ -0,0 +1,41 @@
1
+ import * as Babel from '@babel/standalone';
2
+ import * as sass from 'sass';
3
+ export default class Compiler {
4
+ async compileBabel(expression) {
5
+ let code = Babel.transform(expression, {
6
+ sourceType: "module",
7
+ presets: ["env"]
8
+ });
9
+ return code.code;
10
+ }
11
+ async compileCSS(scope, lang, expression) {
12
+ switch (lang) {
13
+ case 'scss':
14
+ case 'sass':
15
+ if (scope) {
16
+ const result = sass.compileString(`.${scope} { ${expression} }`);
17
+ return result.css;
18
+ }
19
+ const result = sass.compileString(expression);
20
+ return result.css;
21
+ default:
22
+ return expression;
23
+ }
24
+ }
25
+ async compile(ext, content) {
26
+ switch (ext) {
27
+ case 'js':
28
+ return await this.compileBabel(content);
29
+ case 'css':
30
+ return await this.compileCSS(null, "css", content);
31
+ case 'scss':
32
+ case 'sass':
33
+ return await this.compileCSS(null, ext, content);
34
+ default:
35
+ return await Promise.resolve(content);
36
+ }
37
+ }
38
+ getSupportedExt() {
39
+ return ["js", "css", "scss", "sass", "json", "svg", "html"];
40
+ }
41
+ }
@@ -76,12 +76,12 @@ class AppsCommand {
76
76
  async upload(options) {
77
77
  const { apptype, directory, build } = options;
78
78
  const profile = util.getCurrentProfile();
79
- const client = new Api({ token: profile.token, host: profile.host }).app(apptype);
79
+ const api = new Api({ token: profile.token, host: profile.host }).app(apptype);
80
80
  try {
81
- const chunks = [];
81
+ let buffer = Buffer.alloc(0);
82
82
  const bufferStream = new Writable({
83
83
  write(chunk, encoding, callback) {
84
- chunks.push(Buffer.from(chunk));
84
+ buffer = Buffer.concat([buffer, chunk]);
85
85
  callback();
86
86
  },
87
87
  });
@@ -93,35 +93,19 @@ class AppsCommand {
93
93
  .on('error', reject);
94
94
  });
95
95
  console.log('Archive created successfully');
96
- // Convert chunks to a single ArrayBuffer to avoid Node Buffer issues
97
- const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
98
- const arrayBuffer = new ArrayBuffer(totalLength);
99
- const uint8View = new Uint8Array(arrayBuffer);
100
- let offset = 0;
101
- for (const chunk of chunks) {
102
- uint8View.set(new Uint8Array(chunk), offset);
103
- offset += chunk.length;
104
- }
105
- console.log('ArrayBuffer size:', arrayBuffer.byteLength);
106
- // Create FormData with ArrayBuffer
96
+ // Create FormData with buffer
107
97
  const formData = new FormData();
108
- const blob = new Blob([arrayBuffer], { type: 'application/gzip' });
109
- console.log('Blob size:', blob.size);
98
+ const blob = new Blob([buffer], { type: 'application/gzip' });
110
99
  formData.append('app', blob, 'app.tar.gz');
111
100
  if (build) {
112
101
  formData.append('build', 'true');
113
102
  }
114
- console.log('About to call client.upload...');
115
- try {
116
- const result = await client.upload(formData);
117
- console.log('Upload successful');
118
- return result;
119
- }
120
- catch (error) {
121
- console.log('Upload failed with error:', error.message);
122
- console.log('Error details:', error);
123
- throw error;
124
- }
103
+ return await api.client.post(`/v3/system/gateway/app-service/${apptype}/upload`, formData, {
104
+ timeout: 60000,
105
+ headers: {
106
+ 'Content-Type': 'multipart/form-data'
107
+ }
108
+ });
125
109
  }
126
110
  catch (error) {
127
111
  throw error;
@@ -145,7 +129,7 @@ class AppsCommand {
145
129
  async bundle(options) {
146
130
  const { path, upload } = options;
147
131
  const module = await import(`${path}/ptkl.config.js`);
148
- const { views, name, version, distPath, icon, label, permissions, } = module.default ?? {};
132
+ const { views, name, version, distPath, icon, label, permissions, scripts, } = module.default ?? {};
149
133
  // build manifest file
150
134
  const manifest = {
151
135
  name,
@@ -154,6 +138,7 @@ class AppsCommand {
154
138
  label,
155
139
  icon,
156
140
  permissions,
141
+ scripts,
157
142
  };
158
143
  const profile = Util.getCurrentProfile();
159
144
  const client = Util.getClientForProfile();
@@ -177,9 +162,29 @@ class AppsCommand {
177
162
  }
178
163
  });
179
164
  });
165
+ console.log("manifest", manifest);
166
+ const buildScripts = Object.keys(scripts).map((script) => {
167
+ manifest.scripts[script] = `${script}.script.js`;
168
+ return build({
169
+ root: path,
170
+ base,
171
+ build: {
172
+ rollupOptions: {
173
+ input: scripts[script],
174
+ output: {
175
+ format: 'iife',
176
+ entryFileNames: `${script}.script.js`,
177
+ }
178
+ }
179
+ }
180
+ });
181
+ });
180
182
  await Promise.allSettled(buildViews).catch(err => {
181
183
  console.error('Error building:', err);
182
184
  });
185
+ await Promise.allSettled(buildScripts).catch(err => {
186
+ console.error('Error building:', err);
187
+ });
183
188
  console.log("Packaging app...");
184
189
  // // write manifest file
185
190
  const manifestPath = `${distPath}/manifest.json`;
@@ -1,10 +1,16 @@
1
1
  import { Command } from "commander";
2
2
  import Builder from "../builder/index.js";
3
3
  import { WebSocketServer } from "ws";
4
- import { resolve } from "path";
4
+ import { resolve, join, dirname } from "path";
5
5
  import { rollup } from "rollup";
6
6
  import util from "../lib/util.js";
7
7
  import Api from "@ptkl/sdk";
8
+ import { mkdirSync, writeFileSync, rmSync, readdirSync, existsSync, readFileSync } from "fs";
9
+ import { nodeResolve } from '@rollup/plugin-node-resolve';
10
+ // @ts-ignore
11
+ import commonjsPlugin from '@rollup/plugin-commonjs';
12
+ import { babel as babelPlugin } from '@rollup/plugin-babel';
13
+ import { execSync } from 'child_process';
8
14
  class ComponentCommand {
9
15
  register() {
10
16
  return new Command("component")
@@ -13,37 +19,315 @@ class ComponentCommand {
13
19
  .addCommand(new Command("builder")
14
20
  .description("Run builder for component templates")
15
21
  .requiredOption("-v, --version <version>", "SDK version to be used for build", "6")
16
- .requiredOption("-e, --engine <engine>", "SDK engine to be used for build", "react")
17
- .action(this.builder))
22
+ .requiredOption("-e, --engine <engine>", "SDK engine to be used for build", "webcomponents")
23
+ .option("-p, --port <port>", "WSS port", "11403")
24
+ .action(this.builder.bind(this)))
18
25
  .addCommand(new Command("build")
19
26
  .description("Build templates for component")
20
27
  .requiredOption("-p, --path <path>", "Path to the source files", "./")
21
28
  .action(this.build));
22
29
  }
30
+ nodesToActualFiles(nodes, basePath) {
31
+ nodes.forEach((node) => {
32
+ const currentPath = join(basePath, node.name);
33
+ if (node.isLeaf) {
34
+ // Create directory for the file if it doesn't exist
35
+ const dir = dirname(currentPath);
36
+ mkdirSync(dir, { recursive: true });
37
+ // Write the file content
38
+ writeFileSync(currentPath, node.content || '');
39
+ }
40
+ else {
41
+ // Create directory
42
+ mkdirSync(currentPath, { recursive: true });
43
+ // Process children recursively
44
+ if (node.children && node.children.length > 0) {
45
+ this.nodesToActualFiles(node.children, basePath);
46
+ }
47
+ }
48
+ });
49
+ }
23
50
  async builder(options) {
24
- const { version, engine } = options;
25
- console.log(`Running builder for SDK version ${version} with engine ${engine}`);
26
- const ws = new WebSocketServer({ port: 11400 });
51
+ const { version, engine, port } = options;
52
+ console.log(`Running builder for SDK version ${version} with engine ${engine} on port ${port}`);
53
+ const ws = new WebSocketServer({ port: port ?? 11403 });
27
54
  ws.on('connection', (socket) => {
28
55
  socket.on('message', async (message) => {
29
- console.log("Building component templates...");
56
+ console.log("\n🔨 Building component...");
30
57
  const nodes = JSON.parse(message.toString());
31
- try {
32
- let dist = null;
33
- const builder = new Builder(version, engine);
34
- const compiledFiles = await builder.buildFromNodes(nodes);
35
- dist = Object.assign({}, compiledFiles);
36
- socket.send(JSON.stringify({ dist }));
37
- console.log('Component templates built successfully');
38
- // Do something with the parsed JSON
58
+ // SDK 6 uses new build system with Rollup and multi-view bundling
59
+ if (parseInt(version) >= 6) {
60
+ await this.buildSDK6(nodes, version, engine, socket);
39
61
  }
40
- catch (error) { // Bad Request
41
- socket.send(JSON.stringify({ error: error.message }));
42
- console.error(error.message);
62
+ else {
63
+ // SDK 5 and below use the legacy build system
64
+ await this.buildLegacy(nodes, version, engine, socket);
43
65
  }
44
66
  });
45
67
  });
46
68
  }
69
+ async buildSDK6(nodes, version, engine, socket) {
70
+ // Create temporary directory with constant name
71
+ const tempDir = '/tmp/.ptkl/components';
72
+ try {
73
+ // Parse engine to extract framework and version (e.g., "lit@3" -> { framework: "lit", version: "3" })
74
+ const engineMatch = engine.match(/^([^@]+)(?:@(.+))?$/);
75
+ const framework = engineMatch ? engineMatch[1] : engine;
76
+ const frameworkVersion = engineMatch ? engineMatch[2] : null;
77
+ // Create temp directory
78
+ mkdirSync(tempDir, { recursive: true });
79
+ // Clear all files except node_modules
80
+ const entries = readdirSync(tempDir);
81
+ for (const entry of entries) {
82
+ if (entry !== 'node_modules') {
83
+ const fullPath = join(tempDir, entry);
84
+ rmSync(fullPath, { recursive: true, force: true });
85
+ }
86
+ }
87
+ // Convert nodes to actual files
88
+ this.nodesToActualFiles(nodes, tempDir);
89
+ // Look for component.js file that defines views
90
+ const componentsConfigPath = join(tempDir, 'component.js');
91
+ let viewsConfig = {};
92
+ if (!existsSync(componentsConfigPath)) {
93
+ throw new Error('component.js not found. Please create a component.js file that exports { views: { viewName: "./path/to/view.js" } }');
94
+ }
95
+ let componentConfig;
96
+ try {
97
+ // Read the component.js file as text
98
+ const componentContent = readFileSync(componentsConfigPath, 'utf-8');
99
+ // Parse the ES module manually
100
+ // Remove 'export default' and evaluate the object
101
+ const contentWithoutExport = componentContent
102
+ .replace(/export\s+default\s+/, '')
103
+ .trim();
104
+ // Use Function constructor to safely evaluate the object
105
+ componentConfig = new Function(`return ${contentWithoutExport}`)();
106
+ if (!componentConfig || !componentConfig.views) {
107
+ throw new Error('component.js must export default object with "views" property');
108
+ }
109
+ viewsConfig = componentConfig.views;
110
+ }
111
+ catch (error) {
112
+ throw new Error(`Failed to load component.js: ${error.message}. Please ensure component.js exports { views: { viewName: './path/to/view.js' } }`);
113
+ }
114
+ // Merge user packages with internal dependencies
115
+ const userPackages = componentConfig.packages || {};
116
+ const dependencies = {
117
+ '@ptkl/sdk': '^0.9.12',
118
+ // Lit is bundled into the component, not provided by platform
119
+ ...(framework === 'lit' ? {
120
+ 'lit': frameworkVersion ? `^${frameworkVersion}` : '^3.0.0'
121
+ } : {}),
122
+ ...userPackages
123
+ };
124
+ // Create package.json with merged dependencies
125
+ const packageJson = {
126
+ name: 'component-build',
127
+ version: '1.0.0',
128
+ type: 'module',
129
+ dependencies
130
+ };
131
+ const packageJsonPath = join(tempDir, 'package.json');
132
+ writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
133
+ // Run npm install
134
+ try {
135
+ execSync('npm install', {
136
+ cwd: tempDir,
137
+ stdio: 'pipe',
138
+ env: { ...process.env, NODE_ENV: 'production' }
139
+ });
140
+ }
141
+ catch (error) {
142
+ throw new Error(`npm install failed: ${error.message}`);
143
+ }
144
+ // Build each view separately with Rollup
145
+ const dist = {};
146
+ const warnings = [];
147
+ const viewCount = Object.keys(viewsConfig).length;
148
+ console.log(`📦 Bundling ${viewCount} view${viewCount !== 1 ? 's' : ''}...`);
149
+ for (const [viewName, viewPath] of Object.entries(viewsConfig)) {
150
+ const fullPath = join(tempDir, viewPath);
151
+ // Check and strip @customElement decorator for Lit components
152
+ if (framework === 'lit') {
153
+ const sourceContent = readFileSync(fullPath, 'utf-8');
154
+ const customElementDecoratorRegex = /@customElement\s*\(\s*['"`][^'"`]*['"`]\s*\)/g;
155
+ if (customElementDecoratorRegex.test(sourceContent)) {
156
+ const warning = `${viewName}: Found @customElement decorator which has been automatically removed. The platform handles element registration dynamically.`;
157
+ warnings.push(warning);
158
+ console.warn(`⚠️ ${warning}`);
159
+ // Strip the decorator from source
160
+ const strippedContent = sourceContent.replace(/@customElement\s*\(\s*['"`][^'"`]*['"`]\s*\)/g, '');
161
+ writeFileSync(fullPath, strippedContent, 'utf-8');
162
+ }
163
+ }
164
+ // Configure Babel based on framework
165
+ let babelConfig = null;
166
+ if (framework === 'lit') {
167
+ babelConfig = {
168
+ babelHelpers: 'bundled',
169
+ extensions: ['.js', '.ts'],
170
+ presets: [
171
+ ['@babel/preset-typescript', {
172
+ onlyRemoveTypeImports: true,
173
+ allowDeclareFields: true
174
+ }]
175
+ ],
176
+ plugins: [
177
+ ['@babel/plugin-proposal-decorators', { legacy: true }],
178
+ '@babel/plugin-proposal-class-properties',
179
+ '@babel/plugin-transform-class-static-block'
180
+ ]
181
+ };
182
+ }
183
+ else if (framework === 'webcomponents') {
184
+ babelConfig = {
185
+ babelHelpers: 'bundled',
186
+ extensions: ['.js', '.ts'],
187
+ presets: [
188
+ ['@babel/preset-typescript', {
189
+ onlyRemoveTypeImports: true,
190
+ allowDeclareFields: true
191
+ }]
192
+ ],
193
+ plugins: [
194
+ '@babel/plugin-proposal-class-properties',
195
+ '@babel/plugin-transform-class-static-block'
196
+ ]
197
+ };
198
+ }
199
+ // Define external dependencies based on framework
200
+ const getExternalDependencies = (framework) => {
201
+ const externalDeps = [];
202
+ // Lit is bundled into the component, not externalized
203
+ // webcomponents framework has no external dependencies
204
+ return externalDeps;
205
+ };
206
+ const bundle = await rollup({
207
+ input: fullPath,
208
+ plugins: [
209
+ nodeResolve({
210
+ browser: true,
211
+ preferBuiltins: false,
212
+ extensions: ['.js', '.ts', '.jsx', '.tsx', '.vue']
213
+ }),
214
+ // Add Babel plugin if configured
215
+ ...(babelConfig ? [babelPlugin(babelConfig)] : []),
216
+ // @ts-ignore
217
+ commonjsPlugin(),
218
+ ],
219
+ // Externalize framework dependencies - platform provides them
220
+ external: (id) => {
221
+ const externalDeps = getExternalDependencies(framework);
222
+ return externalDeps.some(dep => {
223
+ if (typeof dep === 'string') {
224
+ return id === dep;
225
+ }
226
+ return dep.test(id);
227
+ });
228
+ },
229
+ onwarn: (warning, warn) => {
230
+ if (warning.code === 'UNRESOLVED_IMPORT') {
231
+ console.warn(`⚠️ Unresolved import in ${viewName}`);
232
+ return;
233
+ }
234
+ warn(warning);
235
+ },
236
+ });
237
+ const { output } = await bundle.generate({
238
+ format: 'esm',
239
+ sourcemap: false,
240
+ inlineDynamicImports: true,
241
+ });
242
+ // Add to dist with view name
243
+ output.forEach((chunk) => {
244
+ if (chunk.type === 'chunk') {
245
+ let code = chunk.code;
246
+ // Check for common issues and add warnings
247
+ // 1. Check if code contains customElements.define and remove it
248
+ // Handle various formats including multiline
249
+ const customElementsDefineRegex = /customElements\.define\s*\([^)]*\([\s\S]*?\)\s*\)\s*;?|customElements\.define\s*\([^)]*\)\s*;?/g;
250
+ const hasCustomElementsDefine = customElementsDefineRegex.test(code);
251
+ if (hasCustomElementsDefine) {
252
+ const warning = `${viewName}: Found customElements.define() which has been automatically removed. The platform handles element registration dynamically based on component context.`;
253
+ warnings.push(warning);
254
+ console.warn(`⚠️ ${warning}`);
255
+ // Remove customElements.define calls (reset regex after test)
256
+ code = code.replace(/customElements\.define\s*\([^)]*\([\s\S]*?\)\s*\)\s*;?/g, '');
257
+ code = code.replace(/customElements\.define\s*\([^)]*\)\s*;?/g, '');
258
+ }
259
+ // 2. Check if code has a default export
260
+ const hasDefaultExport = /export\s+default/.test(code) || /export\s*\{[^}]*default[^}]*\}/.test(code);
261
+ if (!hasDefaultExport) {
262
+ const warning = `${viewName}: Missing default export. The view must export a default component/class for the platform to use.`;
263
+ warnings.push(warning);
264
+ console.warn(`⚠️ ${warning}`);
265
+ }
266
+ // 3. Check if code is suspiciously small (might indicate build issue)
267
+ if (code.length < 50) {
268
+ const warning = `${viewName}: Bundle size is very small (${code.length} bytes). This might indicate a build issue.`;
269
+ warnings.push(warning);
270
+ console.warn(`⚠️ ${warning}`);
271
+ }
272
+ // 4. Check for missing imports that might cause runtime errors
273
+ if (framework === 'lit' && !code.includes('lit')) {
274
+ const warning = `${viewName}: Lit framework not detected in bundle. Make sure you're importing from 'lit'.`;
275
+ warnings.push(warning);
276
+ console.warn(`⚠️ ${warning}`);
277
+ }
278
+ dist[`${viewName}.js`] = code;
279
+ }
280
+ else if (chunk.type === 'asset') {
281
+ dist[`${viewName}-${chunk.fileName}`] = chunk.source.toString();
282
+ }
283
+ });
284
+ }
285
+ const totalSize = Object.values(dist).reduce((sum, code) => sum + code.length, 0);
286
+ const formattedSize = totalSize > 1024 ? `${(totalSize / 1024).toFixed(2)} KB` : `${totalSize} bytes`;
287
+ console.log(`✅ Build complete: ${Object.keys(dist).length} file${Object.keys(dist).length !== 1 ? 's' : ''} (${formattedSize})`);
288
+ Object.entries(dist).forEach(([name, code]) => {
289
+ const size = code.length > 1024 ? `${(code.length / 1024).toFixed(2)} KB` : `${code.length} bytes`;
290
+ console.log(` • ${name} - ${size}`);
291
+ });
292
+ socket.send(JSON.stringify({
293
+ dist,
294
+ views: Object.keys(viewsConfig),
295
+ warnings: warnings.length > 0 ? warnings : undefined,
296
+ sdk_version: version,
297
+ sdk_engine: engine
298
+ }));
299
+ }
300
+ catch (error) {
301
+ console.error(`❌ Build failed: ${error.message}`);
302
+ socket.send(JSON.stringify({ error: error.message }));
303
+ }
304
+ // Temporary: Keep files for inspection
305
+ // TODO: Uncomment cleanup when ready
306
+ // finally {
307
+ // try {
308
+ // rmSync(tempDir, { recursive: true, force: true });
309
+ // console.log(`Cleaned up temporary directory: ${tempDir}`);
310
+ // } catch (cleanupError) {
311
+ // console.error(`Failed to clean up temporary directory: ${cleanupError}`);
312
+ // }
313
+ // }
314
+ }
315
+ async buildLegacy(nodes, version, engine, socket) {
316
+ try {
317
+ const builder = new Builder(parseInt(version), engine);
318
+ const dist = await builder.buildFromNodes(nodes);
319
+ socket.send(JSON.stringify({
320
+ dist,
321
+ sdk_version: version,
322
+ sdk_engine: engine
323
+ }));
324
+ console.log(`✅ Build complete (SDK ${version})`);
325
+ }
326
+ catch (error) {
327
+ console.error(`❌ Build failed: ${error.message}`);
328
+ socket.send(JSON.stringify({ error: error.message }));
329
+ }
330
+ }
47
331
  async build(options) {
48
332
  try {
49
333
  const { path } = options;
@@ -36,7 +36,7 @@ class ForgeCommand {
36
36
  // Change to the app directory
37
37
  process.chdir(path);
38
38
  const module = await import(`${path}/ptkl.config.js`);
39
- const { views, name, version, distPath, icon, type, label, permissions, } = module.default ?? {};
39
+ const { views, name, version, distPath, icon, type, label, permissions, scripts, ssrRenderer, } = module.default ?? {};
40
40
  // build manifest file
41
41
  const manifest = {
42
42
  name,
@@ -45,52 +45,218 @@ class ForgeCommand {
45
45
  label,
46
46
  icon,
47
47
  permissions,
48
+ scripts: {},
48
49
  type: type || 'platform', // default to 'platform' if not specified
50
+ ssrRenderer,
49
51
  };
50
52
  const client = Util.getClientForProfile();
51
53
  // get base url of the platform client
52
54
  const baseUrl = client.getPlatformBaseURL();
53
- const base = `${baseUrl}/luma/appservice/v1/forge/static/bundle/${name}/${version}/`;
54
- manifest.icon = `${base}/${icon}`;
55
- const buildViews = Object.keys(views).map((view) => {
56
- manifest.views[view] = `${view}.bundle.js`;
57
- return build({
55
+ // Different build approach for public vs platform apps
56
+ if (type === 'public') {
57
+ // Public apps: standard SPA build with index.html
58
+ manifest.icon = icon;
59
+ console.log("Building public app...");
60
+ await build({
58
61
  root: path,
59
- base,
60
62
  build: {
61
- rollupOptions: {
62
- input: views[view],
63
- output: {
64
- format: 'esm',
65
- entryFileNames: `[name].bundle.js`,
66
- assetFileNames: (assetInfo) => {
67
- return '[name].[ext]'; // Example: Customize the output file name format
63
+ outDir: distPath,
64
+ emptyOutDir: true,
65
+ }
66
+ });
67
+ console.log("✅ Public app build completed successfully");
68
+ // Build SSR renderer if specified for public apps
69
+ if (ssrRenderer) {
70
+ try {
71
+ console.log('Building SSR renderer...');
72
+ await build({
73
+ root: path,
74
+ build: {
75
+ outDir: distPath,
76
+ emptyOutDir: false,
77
+ target: 'esnext',
78
+ ssr: ssrRenderer, // Mark this as an SSR build with the entry point
79
+ rollupOptions: {
80
+ output: {
81
+ format: 'esm',
82
+ entryFileNames: 'ssr-renderer.js',
83
+ inlineDynamicImports: true,
84
+ },
85
+ }
86
+ },
87
+ ssr: {
88
+ noExternal: true, // Bundle all dependencies
89
+ target: 'webworker', // Browser-compatible build
90
+ },
91
+ resolve: {
92
+ conditions: ['browser'], // Use browser versions of packages
93
+ },
94
+ });
95
+ manifest.ssrRenderer = 'ssr-renderer.js';
96
+ console.log('✓ SSR renderer built successfully');
97
+ }
98
+ catch (error) {
99
+ console.error('✗ Failed to build SSR renderer:', error.message || error);
100
+ throw new Error('SSR renderer build failed.');
101
+ }
102
+ }
103
+ console.log("✅ All builds completed successfully");
104
+ }
105
+ else {
106
+ // Platform apps: custom bundle format with views and scripts
107
+ const base = `${baseUrl}/luma/appservice/v1/forge/static/bundle/${name}/${version}/`;
108
+ manifest.icon = `${base}${icon}`;
109
+ const buildViews = Object.keys(views).map(async (view) => {
110
+ manifest.views[view] = `${view}.bundle.js`;
111
+ try {
112
+ const result = await build({
113
+ root: path,
114
+ base,
115
+ build: {
116
+ outDir: distPath,
117
+ emptyOutDir: false,
118
+ rollupOptions: {
119
+ input: views[view],
120
+ output: {
121
+ format: 'esm',
122
+ entryFileNames: `[name].bundle.js`,
123
+ assetFileNames: (assetInfo) => {
124
+ return '[name].[ext]'; // Example: Customize the output file name format
125
+ },
126
+ manualChunks: undefined,
127
+ inlineDynamicImports: true,
128
+ }
68
129
  },
69
- manualChunks: undefined,
70
- inlineDynamicImports: true,
71
- }
72
- },
73
- },
74
- plugins: [
75
- {
76
- name: 'transform-dynamic-imports',
77
- generateBundle(options, bundle) {
78
- // Transform after bundling is complete
79
- for (const fileName in bundle) {
80
- const chunk = bundle[fileName];
81
- if (chunk.type === 'chunk' && chunk.code) {
82
- // Transform dynamic imports in the final bundled code
83
- chunk.code = chunk.code.replace(/import\(['"`]\.\/([^'"`]+)['"`]\)/g, `dynamicImport('${base}$1')`);
84
- // Also handle relative paths without ./
85
- chunk.code = chunk.code.replace(/import\(['"`]([^'"`\/]+\.js)['"`]\)/g, `dynamicImport('${base}$1')`);
130
+ },
131
+ plugins: [
132
+ {
133
+ name: 'transform-dynamic-imports',
134
+ generateBundle(options, bundle) {
135
+ // Transform after bundling is complete
136
+ for (const fileName in bundle) {
137
+ const chunk = bundle[fileName];
138
+ if (chunk.type === 'chunk' && chunk.code) {
139
+ // Transform dynamic imports in the final bundled code
140
+ chunk.code = chunk.code.replace(/import\(['"`]\.\/([^'"`]+)['"`]\)/g, `dynamicImport('${base}$1')`);
141
+ // Also handle relative paths without ./
142
+ chunk.code = chunk.code.replace(/import\(['"`]([^'"`\/]+\.js)['"`]\)/g, `dynamicImport('${base}$1')`);
143
+ }
144
+ }
145
+ }
146
+ },
147
+ ]
148
+ });
149
+ return result;
150
+ }
151
+ catch (error) {
152
+ throw error;
153
+ }
154
+ });
155
+ const buildScripts = scripts ? Object.keys(scripts).map(async (script) => {
156
+ manifest.scripts[script] = `${script}.script.js`;
157
+ try {
158
+ const result = await build({
159
+ root: path,
160
+ base,
161
+ build: {
162
+ outDir: distPath,
163
+ emptyOutDir: false,
164
+ target: 'esnext',
165
+ rollupOptions: {
166
+ input: scripts[script],
167
+ external: ['axios'],
168
+ output: {
169
+ format: 'esm',
170
+ entryFileNames: `[name].script.js`,
171
+ assetFileNames: (assetInfo) => {
172
+ return '[name].[ext]'; // Example: Customize the output file name format
173
+ },
174
+ globals: {
175
+ axios: 'axiosAdapter'
176
+ },
177
+ manualChunks: undefined,
178
+ inlineDynamicImports: true,
179
+ }
180
+ },
181
+ },
182
+ plugins: [
183
+ {
184
+ name: 'wrap-script-in-function',
185
+ generateBundle(options, bundle) {
186
+ for (const fileName in bundle) {
187
+ const chunk = bundle[fileName];
188
+ if (chunk.type === 'chunk' && chunk.code) {
189
+ // Replace import statements with direct variable assignment from globals
190
+ // Handle both regular and minified versions
191
+ let code = chunk.code.replace(/import\s*(\w+)\s*from\s*['"]axios['"]\s*;?/g, 'const $1=axiosAdapter;');
192
+ code = code.replace(/import\s*\{([^}]+)\}\s*from\s*['"]axios['"]\s*;?/g, (match, imports) => {
193
+ // Handle named imports like { default as axios }
194
+ const parts = imports.split(',').map(i => i.trim());
195
+ return parts.map(part => {
196
+ if (part.includes(' as ')) {
197
+ const [original, alias] = part.split(' as ').map(s => s.trim());
198
+ if (original === 'default') {
199
+ return `const ${alias}=axiosAdapter;`;
200
+ }
201
+ return `const ${alias}=axiosAdapter.${original};`;
202
+ }
203
+ return `const ${part}=axiosAdapter.${part};`;
204
+ }).join('');
205
+ });
206
+ // Wrap in async function (minified)
207
+ chunk.code = `return async()=>{try{${code}}catch(err){const errorObj={_error_:true,message:err.message,name:err.name||'Error',stack:err.stack};Object.keys(err).forEach(key=>{errorObj[key]=err[key]});if(err.data)errorObj.data=err.data;if(err.statusCode)errorObj.statusCode=err.statusCode;if(err.response)errorObj.response=err.response;if(err.code)errorObj.code=err.code;return errorObj}}`;
208
+ }
209
+ }
86
210
  }
87
211
  }
212
+ ]
213
+ });
214
+ return result;
215
+ }
216
+ catch (error) {
217
+ throw error;
218
+ }
219
+ }) : [];
220
+ const viewResults = await Promise.allSettled(buildViews);
221
+ const failedViews = viewResults.filter(r => r.status === 'rejected');
222
+ if (failedViews.length > 0) {
223
+ console.error('\n❌ Failed to build views:');
224
+ failedViews.forEach((result, index) => {
225
+ const viewNames = Object.keys(views);
226
+ const failedIndices = viewResults.map((r, i) => r.status === 'rejected' ? i : -1).filter(i => i >= 0);
227
+ const viewName = viewNames[failedIndices[index]];
228
+ const reason = result.reason;
229
+ console.error(`\n View: ${viewName}`);
230
+ console.error(` Input: ${views[viewName]}`);
231
+ console.error(` Error: ${reason?.message || String(reason)}`);
232
+ if (reason?.stack) {
233
+ console.error(`\n${reason.stack}`);
234
+ }
235
+ });
236
+ throw new Error('View build failed. See errors above.');
237
+ }
238
+ if (scripts && Object.keys(scripts).length > 0) {
239
+ const scriptResults = await Promise.allSettled(buildScripts);
240
+ const failedScripts = scriptResults.filter(r => r.status === 'rejected');
241
+ if (failedScripts.length > 0) {
242
+ console.error('\n❌ Failed to build scripts:');
243
+ failedScripts.forEach((result, index) => {
244
+ const scriptNames = Object.keys(scripts);
245
+ const failedIndices = scriptResults.map((r, i) => r.status === 'rejected' ? i : -1).filter(i => i >= 0);
246
+ const scriptName = scriptNames[failedIndices[index]];
247
+ const reason = result.reason;
248
+ console.error(`\n Script: ${scriptName}`);
249
+ console.error(` Input: ${scripts[scriptName]}`);
250
+ console.error(` Error: ${reason?.message || String(reason)}`);
251
+ if (reason?.stack) {
252
+ console.error(`\n${reason.stack}`);
88
253
  }
89
- },
90
- ]
91
- });
92
- });
93
- await Promise.allSettled(buildViews);
254
+ });
255
+ throw new Error('Script build failed. See errors above.');
256
+ }
257
+ }
258
+ console.log("✅ All builds completed successfully");
259
+ }
94
260
  console.log("Packaging app...");
95
261
  // // write manifest file
96
262
  const manifestPath = `${distPath}/manifest.json`;
@@ -106,6 +272,8 @@ class ForgeCommand {
106
272
  c({ gzip: true, cwd: distPath }, ['.']).pipe(bufferStream).on('finish', () => {
107
273
  client.forge().bundleUpload(buffer).then(() => {
108
274
  console.log('Bundle uploaded successfully');
275
+ }).catch((error) => {
276
+ console.error('\x1b[31m%s\x1b[0m', `Bundle upload failed: ${error.response?.data?.message || error.message}`);
109
277
  });
110
278
  });
111
279
  }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Utility to generate import maps for component frameworks
3
+ * This should be used on the frontend to dynamically create import maps
4
+ * based on sdk_version and sdk_engine
5
+ */
6
+ /**
7
+ * Generate an import map configuration for a given framework
8
+ *
9
+ * @param sdkEngine - The engine string (e.g., "react@18", "lit@3", "vue3")
10
+ * @returns Import map configuration object
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const importMap = generateImportMap("react@18");
15
+ * // Inject into HTML:
16
+ * const script = document.createElement('script');
17
+ * script.type = 'importmap';
18
+ * script.textContent = JSON.stringify(importMap);
19
+ * document.head.appendChild(script);
20
+ * ```
21
+ */
22
+ export function generateImportMap(sdkEngine) {
23
+ // Parse engine to extract framework and version (e.g., "lit@3" -> { framework: "lit", version: "3" })
24
+ const engineMatch = sdkEngine.match(/^([^@]+)(?:@(.+))?$/);
25
+ const framework = engineMatch ? engineMatch[1] : sdkEngine;
26
+ const frameworkVersion = engineMatch ? engineMatch[2] : null;
27
+ const imports = {};
28
+ if (framework === 'react') {
29
+ const version = frameworkVersion || '18';
30
+ imports['react'] = `https://esm.sh/react@${version}`;
31
+ imports['react-dom'] = `https://esm.sh/react-dom@${version}`;
32
+ imports['react/jsx-runtime'] = `https://esm.sh/react@${version}/jsx-runtime`;
33
+ imports['react/jsx-dev-runtime'] = `https://esm.sh/react@${version}/jsx-dev-runtime`;
34
+ }
35
+ else if (framework === 'vue3' || framework === 'vue') {
36
+ const version = frameworkVersion || '3';
37
+ imports['vue'] = `https://esm.sh/vue@${version}`;
38
+ }
39
+ else if (framework === 'lit') {
40
+ const version = frameworkVersion || '3';
41
+ imports['lit'] = `https://esm.sh/lit@${version}`;
42
+ imports['lit/decorators.js'] = `https://esm.sh/lit@${version}/decorators.js`;
43
+ imports['lit/directive.js'] = `https://esm.sh/lit@${version}/directive.js`;
44
+ imports['lit/directives/class-map.js'] = `https://esm.sh/lit@${version}/directives/class-map.js`;
45
+ imports['lit/directives/style-map.js'] = `https://esm.sh/lit@${version}/directives/style-map.js`;
46
+ imports['lit/directives/if-defined.js'] = `https://esm.sh/lit@${version}/directives/if-defined.js`;
47
+ imports['lit/directives/repeat.js'] = `https://esm.sh/lit@${version}/directives/repeat.js`;
48
+ imports['lit/directives/unsafe-html.js'] = `https://esm.sh/lit@${version}/directives/unsafe-html.js`;
49
+ }
50
+ else if (framework === 'webcomponents') {
51
+ // For vanilla web components, no external framework needed
52
+ return { imports: {} };
53
+ }
54
+ return { imports };
55
+ }
56
+ /**
57
+ * Generate the import map HTML script tag as a string
58
+ *
59
+ * @param sdkEngine - The engine string (e.g., "react@18", "lit@3", "vue3")
60
+ * @returns HTML string for the import map script tag
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * const scriptTag = generateImportMapHTML("react@18");
65
+ * document.head.insertAdjacentHTML('beforeend', scriptTag);
66
+ * ```
67
+ */
68
+ export function generateImportMapHTML(sdkEngine) {
69
+ const importMap = generateImportMap(sdkEngine);
70
+ if (Object.keys(importMap.imports).length === 0) {
71
+ return ''; // No import map needed
72
+ }
73
+ return `<script type="importmap">\n${JSON.stringify(importMap, null, 2)}\n</script>`;
74
+ }
75
+ /**
76
+ * Inject import map into the document dynamically
77
+ *
78
+ * @param sdkEngine - The engine string (e.g., "react@18", "lit@3", "vue3")
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * // In your frontend code:
83
+ * injectImportMap("react@18");
84
+ * // Now you can load component modules
85
+ * ```
86
+ */
87
+ export function injectImportMap(sdkEngine) {
88
+ const importMap = generateImportMap(sdkEngine);
89
+ if (Object.keys(importMap.imports).length === 0) {
90
+ return; // No import map needed
91
+ }
92
+ const script = document.createElement('script');
93
+ script.type = 'importmap';
94
+ script.textContent = JSON.stringify(importMap, null, 2);
95
+ document.head.appendChild(script);
96
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ptkl/toolkit",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "A command-line toolkit for managing Protokol platform applications, profiles, functions, and components",
5
5
  "keywords": [
6
6
  "protokol",
@@ -28,9 +28,17 @@
28
28
  "prepublishOnly": "npm run build"
29
29
  },
30
30
  "dependencies": {
31
+ "@babel/plugin-proposal-class-properties": "^7.18.6",
32
+ "@babel/plugin-proposal-decorators": "^7.28.0",
33
+ "@babel/plugin-transform-class-static-block": "^7.28.3",
34
+ "@babel/preset-react": "^7.28.5",
35
+ "@babel/preset-typescript": "^7.28.5",
31
36
  "@babel/standalone": "^7.26.10",
32
37
  "@inquirer/password": "^4.0.21",
33
38
  "@ptkl/sdk": "^0.9.12",
39
+ "@rollup/plugin-babel": "^6.1.0",
40
+ "@rollup/plugin-commonjs": "^29.0.0",
41
+ "@rollup/plugin-node-resolve": "^16.0.3",
34
42
  "@types/axios": "^0.14.0",
35
43
  "@types/commander": "^2.12.2",
36
44
  "@types/js-yaml": "^4.0.9",
@@ -45,6 +53,7 @@
45
53
  "lodash": "^4.17.21",
46
54
  "open": "^10.1.2",
47
55
  "rollup": "^4.34.6",
56
+ "sass": "^1.95.1",
48
57
  "tar": "^7.4.3",
49
58
  "vite": "^6.0.6",
50
59
  "vue-template-compiler": "^2.7.16",