webim-plugin 1.0.1 → 1.1.1

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
@@ -15,6 +15,18 @@ npm create webim-plugin my-widget # or the create flow
15
15
 
16
16
  Requires Node 18+.
17
17
 
18
+ > **Note:** on npm ≥ 11.17 the install may print an `allow-scripts` warning
19
+ > about esbuild's postinstall script. It's safe to ignore — esbuild's platform
20
+ > binary is installed via `optionalDependencies`, so the CLI works without the
21
+ > script. To silence it, allow the script explicitly:
22
+ >
23
+ > ```bash
24
+ > npm install -g webim-plugin --allow-scripts=esbuild
25
+ > ```
26
+ >
27
+ > In a project-scoped install (e.g. a scaffolded plugin), add
28
+ > `"allowScripts": { "esbuild": true }` to the project's `package.json` instead.
29
+
18
30
  ## Usage
19
31
 
20
32
  ```bash
@@ -25,6 +37,31 @@ webim-plugin push # build, sign in, merge into builder_plugins
25
37
  webim-plugin dev # watch src/, rebuild + push on every change
26
38
  ```
27
39
 
40
+ ### Converting an existing project
41
+
42
+ ```bash
43
+ cd my-existing-app
44
+ npx webim-plugin wrap # adds webim-plugin.json + an adapter entry + WEBIM_PLUGIN.md
45
+ ```
46
+
47
+ `wrap` never touches existing files. It detects the project (TypeScript?
48
+ React?) and generates a matching adapter entry (`webim.ts/.tsx/.js/.jsx`)
49
+ that builds and renders a placeholder immediately — you replace the
50
+ placeholder with your app. The adapter pattern is Shadow-DOM based, so the
51
+ app's CSS can't restyle the host page and the block can be mounted several
52
+ times on one page.
53
+
54
+ It also writes **`WEBIM_PLUGIN.md`** — the plugin contract and constraints
55
+ written as directives for AI assistants (module shape, build-pipeline facts,
56
+ rendering MUSTs, a conversion checklist). Reference it from your project's
57
+ `AGENTS.md`/`CLAUDE.md` and any AI coding tool can do the conversion work
58
+ correctly. `init` scaffolds include the same file.
59
+
60
+ The bundler inlines everything into the single plugin module: `.css` and
61
+ `.html` imports arrive as strings, images and fonts as `data:` URLs, and
62
+ `.jsx`/`.tsx` compile with the automatic JSX runtime (no `import React`
63
+ needed).
64
+
28
65
  ## Config — `webim-plugin.json`
29
66
 
30
67
  ```json
@@ -21,7 +21,6 @@
21
21
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
22
22
  import { existsSync } from 'node:fs';
23
23
  import path from 'node:path';
24
- import { pathToFileURL } from 'node:url';
25
24
 
26
25
  const CONFIG_FILE = 'webim-plugin.json';
27
26
 
@@ -29,13 +28,16 @@ const HELP = `webim-plugin — Web-IM builder plugin CLI
29
28
 
30
29
  Commands:
31
30
  init [dir] scaffold a plugin project (webim-plugin.json, src/, README)
31
+ wrap [dir] convert an EXISTING project: add config, an adapter entry
32
+ (Shadow-DOM boilerplate, React-aware) and WEBIM_PLUGIN.md —
33
+ the plugin contract written as rules for AI assistants
32
34
  build bundle the entry into dist/plugin.js and validate it
33
35
  push build, then upload to the tenant's builder_plugins setting
34
36
  dev watch the source; rebuild and push on every change
35
37
 
36
38
  Config (webim-plugin.json):
37
39
  { "entry": "src/index.js", "out": "dist/plugin.js",
38
- "api": "http://localhost:4100/api", "tenant": "root" }
40
+ "api": "https://webim.app/api", "tenant": "root" }
39
41
 
40
42
  Auth (push/dev): WEBIM_TOKEN, or WEBIM_EMAIL + WEBIM_PASSWORD.`;
41
43
 
@@ -43,7 +45,7 @@ Auth (push/dev): WEBIM_TOKEN, or WEBIM_EMAIL + WEBIM_PASSWORD.`;
43
45
  // scaffolding
44
46
 
45
47
  const TEMPLATE_CONFIG = (name) => `${JSON.stringify(
46
- { entry: 'src/index.js', out: 'dist/plugin.js', api: 'http://localhost:4100/api', tenant: 'root' },
48
+ { entry: 'src/index.js', out: 'dist/plugin.js', api: 'https://webim.app/api', tenant: 'root' },
47
49
  null,
48
50
  2
49
51
  )}\n`;
@@ -119,7 +121,10 @@ const TEMPLATE_PACKAGE = (id) => `${JSON.stringify(
119
121
  version: '1.0.0',
120
122
  type: 'module',
121
123
  scripts: { build: 'webim-plugin build', push: 'webim-plugin push', dev: 'webim-plugin dev' },
122
- devDependencies: { 'webim-plugin': '^0.1.0' }
124
+ devDependencies: { 'webim-plugin': '^1.1.0' },
125
+ // npm >= 11.17 blocks dependency install scripts unless allowlisted;
126
+ // esbuild works without its postinstall, this just silences the warning.
127
+ allowScripts: { esbuild: true }
123
128
  },
124
129
  null,
125
130
  2
@@ -145,6 +150,7 @@ async function init(dirArg) {
145
150
  await writeFile(path.join(dir, CONFIG_FILE), TEMPLATE_CONFIG(name));
146
151
  await writeFile(path.join(dir, 'src', 'index.js'), TEMPLATE_PLUGIN(id, name));
147
152
  await writeFile(path.join(dir, 'README.md'), TEMPLATE_README(id, name));
153
+ await writeFile(path.join(dir, 'WEBIM_PLUGIN.md'), TEMPLATE_GUIDE(id, name, 'src/index.js'));
148
154
 
149
155
  if (!existsSync(path.join(dir, 'package.json'))) {
150
156
  await writeFile(path.join(dir, 'package.json'), TEMPLATE_PACKAGE(id));
@@ -154,6 +160,324 @@ async function init(dirArg) {
154
160
  console.log('Next: edit webim-plugin.json (api/tenant), then `webim-plugin build`.');
155
161
  }
156
162
 
163
+ // ---------------------------------------------------------------------------
164
+ // wrap — convert an existing project into a plugin
165
+
166
+ /**
167
+ * The plugin contract written as rules for AI assistants. Dropped into the
168
+ * project by `wrap` (and referenced from AGENTS.md/CLAUDE.md by the user) so
169
+ * any model editing the project builds a valid plugin on the first try.
170
+ */
171
+ const TEMPLATE_GUIDE = (id, name, entry) => `# Web-IM plugin rules — ${name}
172
+
173
+ Directives for AI assistants (and humans) working on the Web-IM builder
174
+ plugin in this project. Follow every MUST. The plugin entry is \`${entry}\`;
175
+ \`npx webim-plugin build\` bundles it to \`dist/plugin.js\` and validates it.
176
+
177
+ ## The contract
178
+
179
+ The entry module's **default export** is the whole plugin:
180
+
181
+ \`\`\`js
182
+ export default {
183
+ id: '${id}', // kebab-case slug, /^[a-z0-9-]+$/
184
+ name: '${name}',
185
+ version: '1.0.0',
186
+ blocks: [{
187
+ id: 'main', // kebab-case slug
188
+ label: '${name}', // shown in the builder Toolbox/Inspector
189
+ props: { /* default prop values for a freshly dropped block */ },
190
+ settings: [ /* typed Inspector fields, see below */ ],
191
+ mount(el, props, ctx) {}, // REQUIRED — render into el
192
+ update(el, props, ctx) {}, // optional — props changed; omit to get unmount+mount
193
+ unmount(el) {} // optional — cleanup
194
+ }]
195
+ };
196
+ \`\`\`
197
+
198
+ \`ctx\` is \`{ tenant, apiUrl, locale, settings }\`.
199
+
200
+ \`settings\` fields are \`{ name, label, type }\` with type one of
201
+ \`text | number | switch | select | color | image\`; \`select\` also takes
202
+ \`options: [{ value, label }]\` (or plain strings). Every setting's \`name\`
203
+ must have a default in \`props\`.
204
+
205
+ ## Build pipeline facts
206
+
207
+ - Everything must land in ONE ES module — \`push\` uploads only the built JS.
208
+ - \`import\` of \`.css\`/\`.html\` files yields the file content as a STRING
209
+ (inject a \`<style>\` tag / parse markup yourself at mount time). In \`.css\`
210
+ imports the CLI rewrites \`:root\` to \`:host\` — inside a shadow root
211
+ \`:root\` matches nothing, \`:host\` is its exact analogue.
212
+ - Images and fonts (\`.svg .png .jpg .jpeg .gif .webp .woff .woff2\`) import
213
+ as \`data:\` URLs.
214
+ - After bundling, the CLI IMPORTS the bundle in Node.js to validate it.
215
+ Module scope therefore MUST NOT touch \`document\`, \`window\`,
216
+ \`localStorage\`, or run any DOM code. All DOM work goes inside \`mount()\`.
217
+ Guard any standalone auto-init with \`typeof document !== 'undefined'\`.
218
+
219
+ ## Rendering rules (MUST)
220
+
221
+ 1. Render inside a Shadow DOM:
222
+ \`const shadow = el.shadowRoot ?? el.attachShadow({ mode: 'open' })\` —
223
+ the host clears \`el.innerHTML\` between remounts but an attached shadow
224
+ root SURVIVES, so always reuse it and reset with \`shadow.innerHTML = ''\`.
225
+ 2. Never style or mutate \`document\`, \`document.body\`, or
226
+ \`document.documentElement\` (no classes, no attributes, no global CSS).
227
+ The single exception: webfont \`<link>\` tags may be appended to
228
+ \`document.head\` (deduplicate first) — \`@font-face\` is inert inside a
229
+ shadow root.
230
+ 3. If reusing an app's page-level stylesheet, rescope it to a wrapper div
231
+ inside the shadow: \`body\`/\`html\` selectors → the wrapper class
232
+ (\`:root\` is rewritten to \`:host\` by the build automatically),
233
+ \`position: fixed\` → \`absolute\`, \`100vw/100vh\` → \`100%\`. Give the
234
+ wrapper \`position: relative; overflow: hidden\`.
235
+ 4. The same block can be mounted MULTIPLE times on one page: no module-level
236
+ singletons or mutable shared state — key per-instance state by \`el\`
237
+ (e.g. a \`WeakMap\`). Element IDs inside the shadow root are fine.
238
+ 5. \`unmount()\` must undo everything attached outside the shadow root:
239
+ \`document\`/\`window\` listeners, timers, observers, animation-frame
240
+ loops, open sockets.
241
+ 6. \`update()\` runs on every Inspector prop edit. When in-place diffing is
242
+ not trivial, destroy + remount — correctness beats cleverness.
243
+ 7. Network calls: use plain \`fetch\`; never assume same-origin cookies.
244
+ Degrade gracefully — a failed request must not blank the block.
245
+
246
+ ## Converting an existing app (checklist)
247
+
248
+ - [ ] Write the adapter entry (\`${entry}\`) — it maps builder props to the
249
+ app's options and boots the app inside the shadow root.
250
+ - [ ] Remove the app's assumption that it owns the page: inject its markup
251
+ into the wrapper, query elements from the shadow root (note:
252
+ \`getElementById\` exists on ShadowRoot), move body-level classes and
253
+ theme attributes onto the wrapper.
254
+ - [ ] For outside-click handlers on \`document\`, use
255
+ \`e.composedPath()[0]\` — \`e.target\` retargets to the host element.
256
+ - [ ] Expose the app's config as \`settings\` + \`props\` defaults; pass them
257
+ explicitly so the host page's URL params never leak in.
258
+ - [ ] \`npx webim-plugin build\` passes, then test mount/update/unmount and
259
+ two instances on one page.
260
+
261
+ ## Ship
262
+
263
+ \`\`\`bash
264
+ npx webim-plugin build # bundle + validate -> dist/plugin.js
265
+ npx webim-plugin push # upload to the tenant in webim-plugin.json
266
+ npx webim-plugin dev # watch + rebuild + push
267
+ \`\`\`
268
+
269
+ Auth for push: \`WEBIM_TOKEN\`, or \`WEBIM_EMAIL\` + \`WEBIM_PASSWORD\`.
270
+ `;
271
+
272
+ /** Vanilla adapter skeleton — builds and renders a placeholder as generated. */
273
+ const TEMPLATE_WRAP_VANILLA = (id, name, ts) => {
274
+ const el = ts ? 'el: HTMLElement' : 'el';
275
+ const props = ts ? 'props: Record<string, any>' : 'props';
276
+ const ctx = ts ? 'ctx: { tenant?: string; apiUrl?: string; locale?: string }' : 'ctx';
277
+
278
+ return `// ${name} — Web-IM builder plugin adapter (entry for \`webim-plugin build\`).
279
+ // Read WEBIM_PLUGIN.md before editing: it is the contract and the rules.
280
+ //
281
+ // CSS imports arrive as strings under the webim-plugin CLI, e.g.:
282
+ // import cssText from './styles.css';
283
+
284
+ function mountBlock(${el}, ${props}, ${ctx}) {
285
+ // The host clears el.innerHTML between remounts but a shadow root survives.
286
+ const shadow = el.shadowRoot ?? el.attachShadow({ mode: 'open' });
287
+ shadow.innerHTML = '';
288
+
289
+ const style = document.createElement('style');
290
+ style.textContent = \`
291
+ :host { display: block; }
292
+ .root { position: relative; overflow: hidden; font-family: system-ui, sans-serif; }
293
+ \`; // TODO: append your (rescoped) stylesheet string here
294
+
295
+ const root = document.createElement('div');
296
+ root.className = 'root';
297
+ // TODO: render your app into \`root\` (markup, components, listeners).
298
+ root.textContent = \`${name} — replace mountBlock() with your app\`;
299
+
300
+ shadow.append(style, root);
301
+ }
302
+
303
+ export default {
304
+ id: '${id}',
305
+ name: '${name}',
306
+ version: '1.0.0',
307
+ blocks: [
308
+ {
309
+ id: 'main',
310
+ label: '${name}',
311
+ // TODO: defaults for every settings field below.
312
+ props: { title: '${name}' },
313
+ // Inspector fields: text | number | switch | select | color | image.
314
+ settings: [{ name: 'title', label: 'Title', type: 'text' }],
315
+ mount(${el}, ${props}, ${ctx}) {
316
+ mountBlock(el, props, ctx);
317
+ },
318
+ update(${el}, ${props}, ${ctx}) {
319
+ // Props changed. Remount is the simple correct default.
320
+ mountBlock(el, props, ctx);
321
+ },
322
+ unmount(${el}) {
323
+ // TODO: remove document/window listeners, timers, observers.
324
+ if (el.shadowRoot) el.shadowRoot.innerHTML = '';
325
+ }
326
+ }
327
+ ]
328
+ };
329
+ `;
330
+ };
331
+
332
+ /** React adapter skeleton — builds as generated; wire the real component in. */
333
+ const TEMPLATE_WRAP_REACT = (id, name, ts) => {
334
+ const el = ts ? 'el: HTMLElement' : 'el';
335
+ const props = ts ? 'props: Record<string, any>' : 'props';
336
+ const ctx = ts ? 'ctx: { tenant?: string; apiUrl?: string; locale?: string }' : 'ctx';
337
+ const rootsType = ts ? '<HTMLElement, Root>' : '';
338
+
339
+ return `// ${name} — Web-IM builder plugin adapter (entry for \`webim-plugin build\`).
340
+ // Read WEBIM_PLUGIN.md before editing: it is the contract and the rules.
341
+ import { createRoot${ts ? ', type Root' : ''} } from 'react-dom/client';
342
+ // TODO: import your component and render it in mountBlock, e.g.:
343
+ // import App from './App';
344
+ //
345
+ // CSS imports arrive as strings under the webim-plugin CLI, e.g.:
346
+ // import cssText from './styles.css';
347
+
348
+ const roots = new WeakMap${rootsType}();
349
+
350
+ function mountBlock(${el}, ${props}, ${ctx}) {
351
+ // The host clears el.innerHTML between remounts but a shadow root survives.
352
+ const shadow = el.shadowRoot ?? el.attachShadow({ mode: 'open' });
353
+ shadow.innerHTML = '';
354
+
355
+ const style = document.createElement('style');
356
+ style.textContent = ':host { display: block; }'; // TODO: + your rescoped stylesheet string
357
+
358
+ const container = document.createElement('div');
359
+ shadow.append(style, container);
360
+
361
+ const root = createRoot(container);
362
+ roots.set(el, root);
363
+ // TODO: replace the placeholder with your component, e.g. <App {...props} />
364
+ root.render(<div style={{ fontFamily: 'system-ui' }}>${name} — wire your component into mountBlock()</div>);
365
+ }
366
+
367
+ export default {
368
+ id: '${id}',
369
+ name: '${name}',
370
+ version: '1.0.0',
371
+ blocks: [
372
+ {
373
+ id: 'main',
374
+ label: '${name}',
375
+ // TODO: defaults for every settings field below.
376
+ props: { title: '${name}' },
377
+ // Inspector fields: text | number | switch | select | color | image.
378
+ settings: [{ name: 'title', label: 'Title', type: 'text' }],
379
+ mount(${el}, ${props}, ${ctx}) {
380
+ mountBlock(el, props, ctx);
381
+ },
382
+ update(${el}, ${props}, ${ctx}) {
383
+ mountBlock(el, props, ctx);
384
+ },
385
+ unmount(${el}) {
386
+ roots.get(el)?.unmount();
387
+ roots.delete(el);
388
+ if (el.shadowRoot) el.shadowRoot.innerHTML = '';
389
+ }
390
+ }
391
+ ]
392
+ };
393
+ `;
394
+ };
395
+
396
+ const TEMPLATE_MODULE_DECLS = `// The webim-plugin CLI bundles .css and .html imports as plain strings
397
+ // (esbuild \`text\` loader); images and fonts import as data: URLs.
398
+ declare module '*.css' {
399
+ const text: string;
400
+ export default text;
401
+ }
402
+ declare module '*.html' {
403
+ const text: string;
404
+ export default text;
405
+ }
406
+ `;
407
+
408
+ /**
409
+ * Converts an existing project in place: writes webim-plugin.json, an adapter
410
+ * entry matched to the project (TS? React?) and WEBIM_PLUGIN.md. Only ever
411
+ * CREATES files — an existing file with a target name aborts or is skipped.
412
+ */
413
+ async function wrap(dirArg) {
414
+ const dir = path.resolve(dirArg ?? '.');
415
+
416
+ if (existsSync(path.join(dir, CONFIG_FILE))) {
417
+ throw new Error(`${CONFIG_FILE} already exists in ${dir} — this project is already wired up`);
418
+ }
419
+
420
+ let pkg = {};
421
+
422
+ try {
423
+ pkg = JSON.parse(await readFile(path.join(dir, 'package.json'), 'utf8'));
424
+ } catch {
425
+ // no package.json is fine — fall back to the directory name
426
+ }
427
+
428
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
429
+ const isReact = 'react' in deps;
430
+ const isTS = existsSync(path.join(dir, 'tsconfig.json')) || 'typescript' in deps;
431
+ const ext = isReact ? (isTS ? 'tsx' : 'jsx') : isTS ? 'ts' : 'js';
432
+
433
+ const id = String(pkg.name ?? path.basename(dir))
434
+ .toLowerCase()
435
+ .replace(/^@[^/]+\//, '')
436
+ .replace(/[^a-z0-9]+/g, '-')
437
+ .replace(/(^-|-$)/g, '') || 'my-plugin';
438
+ const name = id
439
+ .split('-')
440
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
441
+ .join(' ');
442
+
443
+ const srcDir = existsSync(path.join(dir, 'src')) ? 'src' : '.';
444
+ const entry = `${srcDir === '.' ? '' : 'src/'}webim.${ext}`;
445
+ const entryFile = path.join(dir, entry);
446
+
447
+ if (existsSync(entryFile)) throw new Error(`${entry} already exists in ${dir}`);
448
+
449
+ await mkdir(path.dirname(entryFile), { recursive: true });
450
+ await writeFile(path.join(dir, CONFIG_FILE), `${JSON.stringify(
451
+ { entry, out: 'dist/plugin.js', api: 'https://webim.app/api', tenant: 'root' },
452
+ null,
453
+ 2
454
+ )}\n`);
455
+ await writeFile(entryFile, (isReact ? TEMPLATE_WRAP_REACT : TEMPLATE_WRAP_VANILLA)(id, name, isTS));
456
+
457
+ const created = [CONFIG_FILE, entry];
458
+
459
+ if (isTS) {
460
+ const decls = path.join(dir, srcDir, 'webim-modules.d.ts');
461
+
462
+ if (!existsSync(decls)) {
463
+ await writeFile(decls, TEMPLATE_MODULE_DECLS);
464
+ created.push(path.relative(dir, decls));
465
+ }
466
+ }
467
+
468
+ if (!existsSync(path.join(dir, 'WEBIM_PLUGIN.md'))) {
469
+ await writeFile(path.join(dir, 'WEBIM_PLUGIN.md'), TEMPLATE_GUIDE(id, name, entry));
470
+ created.push('WEBIM_PLUGIN.md');
471
+ }
472
+
473
+ console.log(`Wrapped "${id}" (${isReact ? 'React' : 'vanilla'}${isTS ? ' + TypeScript' : ''}) — created ${created.join(', ')}`);
474
+ console.log(`Next:
475
+ 1. Wire your app into ${entry} — WEBIM_PLUGIN.md has the contract and rules.
476
+ Working with an AI assistant? Point it at WEBIM_PLUGIN.md (reference it
477
+ from AGENTS.md / CLAUDE.md so it is picked up automatically).
478
+ 2. Edit ${CONFIG_FILE} (api/tenant), then \`npx webim-plugin build\`.`);
479
+ }
480
+
157
481
  // ---------------------------------------------------------------------------
158
482
  // build + validate
159
483
 
@@ -167,11 +491,51 @@ async function loadConfig() {
167
491
  return {
168
492
  entry: config.entry ?? 'src/index.js',
169
493
  out: config.out ?? 'dist/plugin.js',
170
- api: config.api ?? 'http://localhost:4100/api',
494
+ api: config.api ?? 'https://webim.app/api',
171
495
  tenant: config.tenant ?? ''
172
496
  };
173
497
  }
174
498
 
499
+ // Everything must land in ONE JS module — the only thing push uploads. CSS
500
+ // and HTML come in as strings (inject a <style> / parse markup at mount),
501
+ // images and fonts as data: URLs.
502
+ const LOADERS = {
503
+ '.html': 'text',
504
+ '.svg': 'dataurl',
505
+ '.png': 'dataurl',
506
+ '.jpg': 'dataurl',
507
+ '.jpeg': 'dataurl',
508
+ '.gif': 'dataurl',
509
+ '.webp': 'dataurl',
510
+ '.woff': 'dataurl',
511
+ '.woff2': 'dataurl'
512
+ };
513
+
514
+ // .css imports are strings too, but rescoped first: plugins render inside a
515
+ // Shadow DOM, where `:root` matches nothing — its analogue is `:host`, so the
516
+ // rewrite is always safe (same specificity, same "tree root" meaning).
517
+ // `html`/`body` selectors CAN'T be rewritten automatically — the wrapper
518
+ // class they should target is the plugin's own choice — so they only warn.
519
+ const warnedCssFiles = new Set();
520
+ const cssRescopePlugin = {
521
+ name: 'css-rescope',
522
+ setup(build) {
523
+ build.onLoad({ filter: /\.css$/ }, async (args) => {
524
+ const css = await readFile(args.path, 'utf8');
525
+
526
+ if (!warnedCssFiles.has(args.path) && /(^|[\s,}])(html|body)\s*[,{[.:>#]/m.test(css)) {
527
+ warnedCssFiles.add(args.path);
528
+ console.warn(
529
+ `Warning: ${path.relative(process.cwd(), args.path)} styles html/body — those selectors match nothing ` +
530
+ 'inside the shadow root; rescope them to a wrapper class (see WEBIM_PLUGIN.md, Rendering rules).'
531
+ );
532
+ }
533
+
534
+ return { contents: css.replace(/:root\b/g, ':host'), loader: 'text' };
535
+ });
536
+ }
537
+ };
538
+
175
539
  async function bundle(config) {
176
540
  const esbuild = await import('esbuild');
177
541
  const result = await esbuild.build({
@@ -182,7 +546,12 @@ async function bundle(config) {
182
546
  platform: 'browser',
183
547
  target: 'es2020',
184
548
  minify: false,
185
- logLevel: 'silent'
549
+ logLevel: 'silent',
550
+ // Modern JSX runtime (React 17+/Vite default): .jsx/.tsx files work
551
+ // without a manual `import React` in every file.
552
+ jsx: 'automatic',
553
+ loader: LOADERS,
554
+ plugins: [cssRescopePlugin]
186
555
  });
187
556
 
188
557
  if (result.errors.length) throw new Error(result.errors.map((e) => e.text).join('\n'));
@@ -230,9 +599,13 @@ function validatePlugin(plugin) {
230
599
  async function build() {
231
600
  const config = await loadConfig();
232
601
  const out = await bundle(config);
233
- // Import the BUILT file: what ships is what gets validated. Module-level
234
- // DOM access would fail right here — keep DOM work inside mount().
235
- const mod = await import(`${pathToFileURL(path.resolve(out)).href}?v=${Date.now()}`);
602
+ // Import the BUILT code: what ships is what gets validated. Module-level
603
+ // DOM access would fail right here — keep DOM work inside mount(). A data:
604
+ // URL instead of the file path so Node always parses it as ESM — a file
605
+ // import would inherit the project's package.json "type", and a project
606
+ // without "type": "module" breaks on the bundle's export statement.
607
+ const code = await readFile(path.resolve(out), 'utf8');
608
+ const mod = await import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`);
236
609
  const manifest = validatePlugin(mod?.default);
237
610
 
238
611
  console.log(`Built ${out}`);
@@ -336,7 +709,10 @@ async function dev() {
336
709
  platform: 'browser',
337
710
  target: 'es2020',
338
711
  logLevel: 'silent',
712
+ jsx: 'automatic',
713
+ loader: LOADERS,
339
714
  plugins: [
715
+ cssRescopePlugin,
340
716
  {
341
717
  name: 'webim-push',
342
718
  setup(buildHook) {
@@ -372,6 +748,7 @@ try {
372
748
 
373
749
  console.log(pkg.version);
374
750
  } else if (command === 'init') await init(argument);
751
+ else if (command === 'wrap') await wrap(argument);
375
752
  else if (command === 'build') await build();
376
753
  else if (command === 'push') await push();
377
754
  else if (command === 'dev') await dev();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webim-plugin",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "description": "CLI for Web-IM builder plugins — scaffold, bundle TS/JSX into the single-module standard, validate, and push to a tenant",
5
5
  "license": "MIT",
6
6
  "author": "Orhan POLAT",