@yapmod/builder 0.0.0-stage → 1.0.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.
@@ -0,0 +1,17 @@
1
+ export interface BuildPluginOptions {
2
+ /** Plugin entry file (`.ts`, `.tsx`, `.js` or `.jsx`). */
3
+ entry: string;
4
+ /** Where the loadable expression is written. */
5
+ outfile: string;
6
+ /** Minify the bundle. */
7
+ minify?: boolean;
8
+ }
9
+ /**
10
+ * Bundles a plugin entry into a self-contained expression the client's
11
+ * loader can evaluate: Babel compiles TSX and rewrites shared-module
12
+ * imports, esbuild bundles everything else, and the result is wrapped in
13
+ * the `function (yapmod) { ... }` shape from `expression.ts`.
14
+ *
15
+ * Returns the expression and writes it to `options.outfile`.
16
+ */
17
+ export declare function buildPlugin(options: BuildPluginOptions): Promise<string>;
@@ -0,0 +1,10 @@
1
+ /** Name bundlers assign to the bundle; the footer reads the exports back. */
2
+ export declare const EXPRESSION_GLOBAL_NAME = "yapmodPlugin";
3
+ /** Opens the expression: runs the bundle in the loader's `yapmod` scope. */
4
+ export declare const expressionBanner = "(function () {\n";
5
+ /**
6
+ * Closes the expression and returns the default export. Bundlers expose
7
+ * `export default` as `<globalName>.default`; a function default reaches
8
+ * the loader unchanged and is called with the API, so factories work too.
9
+ */
10
+ export declare const expressionFooter = "\n;const plugin = typeof yapmodPlugin !== 'undefined'\n ? yapmodPlugin.default ?? yapmodPlugin\n : undefined;\nif (plugin === undefined) {\n throw new Error(\n 'yapmod: bundle has no default export; export the plugin with `export default`.',\n );\n}\nreturn plugin;\n})();\n";
@@ -0,0 +1,4 @@
1
+ export { EXPRESSION_GLOBAL_NAME, expressionBanner, expressionFooter } from './expression.ts';
2
+ export { buildPlugin, type BuildPluginOptions } from './build.ts';
3
+ export { transformPluginSource } from './transform.ts';
4
+ export { yapmodPlugin } from './unplugin.ts';
package/dist/index.js ADDED
@@ -0,0 +1,203 @@
1
+ // src/expression.ts
2
+ var EXPRESSION_GLOBAL_NAME = "yapmodPlugin";
3
+ var expressionBanner = "(function () {\n";
4
+ var expressionFooter = `
5
+ ;const plugin = typeof ${EXPRESSION_GLOBAL_NAME} !== 'undefined'
6
+ ? ${EXPRESSION_GLOBAL_NAME}.default ?? ${EXPRESSION_GLOBAL_NAME}
7
+ : undefined;
8
+ if (plugin === undefined) {
9
+ throw new Error(
10
+ 'yapmod: bundle has no default export; export the plugin with \`export default\`.',
11
+ );
12
+ }
13
+ return plugin;
14
+ })();
15
+ `;
16
+
17
+ // src/build.ts
18
+ import { readFileSync } from "fs";
19
+ import { mkdir, writeFile } from "fs/promises";
20
+ import { dirname } from "path";
21
+ import { build } from "esbuild";
22
+
23
+ // src/transform.ts
24
+ import { transformSync, types as t } from "@babel/core";
25
+ import presetSolid from "babel-preset-solid";
26
+ import presetTypescript from "@babel/preset-typescript";
27
+ var SHARED_MODULES = {
28
+ "solid-js": "solid",
29
+ "solid-js/web": "solidWeb",
30
+ "solid-js/store": "solidStore"
31
+ };
32
+ function sharedAccess(namespace) {
33
+ return t.memberExpression(t.identifier("yapmod"), t.identifier(namespace));
34
+ }
35
+ function reexportError(source) {
36
+ return `@yapmod/builder cannot re-export from "${source}"; import the bindings and export them locally instead.`;
37
+ }
38
+ function rewriteSharedImports() {
39
+ return {
40
+ name: "yapmod:rewrite-shared-imports",
41
+ visitor: {
42
+ ImportDeclaration(path) {
43
+ const namespace = SHARED_MODULES[path.node.source.value];
44
+ if (namespace === void 0) return;
45
+ if (path.node.importKind === "type") {
46
+ path.remove();
47
+ return;
48
+ }
49
+ const statements = [];
50
+ const named = [];
51
+ for (const specifier of path.node.specifiers) {
52
+ if (t.isImportNamespaceSpecifier(specifier)) {
53
+ statements.push(
54
+ t.variableDeclaration("const", [
55
+ t.variableDeclarator(specifier.local, sharedAccess(namespace))
56
+ ])
57
+ );
58
+ continue;
59
+ }
60
+ if (t.isImportDefaultSpecifier(specifier)) {
61
+ statements.push(
62
+ t.variableDeclaration("const", [
63
+ t.variableDeclarator(
64
+ specifier.local,
65
+ t.memberExpression(sharedAccess(namespace), t.identifier("default"))
66
+ )
67
+ ])
68
+ );
69
+ continue;
70
+ }
71
+ if (specifier.importKind === "type") continue;
72
+ named.push(specifier);
73
+ }
74
+ if (named.length > 0) {
75
+ const properties = [];
76
+ for (const specifier of named) {
77
+ const key = t.isIdentifier(specifier.imported) ? specifier.imported : t.stringLiteral(specifier.imported.value);
78
+ properties.push(t.objectProperty(key, specifier.local));
79
+ }
80
+ statements.push(
81
+ t.variableDeclaration("const", [
82
+ t.variableDeclarator(t.objectPattern(properties), sharedAccess(namespace))
83
+ ])
84
+ );
85
+ }
86
+ if (statements.length === 0) {
87
+ path.remove();
88
+ return;
89
+ }
90
+ path.replaceWithMultiple(statements);
91
+ },
92
+ ExportNamedDeclaration(path) {
93
+ if (path.node.exportKind === "type") {
94
+ path.remove();
95
+ return;
96
+ }
97
+ const source = path.node.source?.value;
98
+ if (source !== void 0 && source in SHARED_MODULES) {
99
+ throw path.buildCodeFrameError(reexportError(source));
100
+ }
101
+ },
102
+ ExportAllDeclaration(path) {
103
+ if (path.node.exportKind === "type") {
104
+ path.remove();
105
+ return;
106
+ }
107
+ const source = path.node.source.value;
108
+ if (source in SHARED_MODULES) {
109
+ throw path.buildCodeFrameError(reexportError(source));
110
+ }
111
+ }
112
+ }
113
+ };
114
+ }
115
+ function transformPluginSource(code, id) {
116
+ const first = transformSync(code, {
117
+ filename: id,
118
+ babelrc: false,
119
+ configFile: false,
120
+ // Presets run in reverse, so TypeScript strips the source before the
121
+ // Solid preset compiles the now annotation-free JSX.
122
+ presets: [presetSolid, presetTypescript],
123
+ sourceType: "module",
124
+ ast: false,
125
+ cloneInputAst: false
126
+ });
127
+ if (!first?.code) return null;
128
+ const second = transformSync(first.code, {
129
+ filename: id,
130
+ babelrc: false,
131
+ configFile: false,
132
+ plugins: [rewriteSharedImports()],
133
+ sourceType: "module",
134
+ ast: false
135
+ });
136
+ return second?.code ?? null;
137
+ }
138
+
139
+ // src/build.ts
140
+ var babelTransform = {
141
+ name: "yapmod:babel-transform",
142
+ setup(buildApi) {
143
+ buildApi.onLoad({ filter: /\.(?:[cm]?[jt]sx?)$/ }, (args) => {
144
+ if (args.path.includes("node_modules")) return void 0;
145
+ const source = readFileSync(args.path, "utf8");
146
+ const code = transformPluginSource(source, args.path);
147
+ return code === null ? void 0 : { contents: code, loader: "js" };
148
+ });
149
+ }
150
+ };
151
+ var sharedModuleGuard = {
152
+ name: "yapmod:shared-module-guard",
153
+ setup(buildApi) {
154
+ buildApi.onResolve({ filter: /^solid-js(?:\/.*)?$/ }, (args) => ({
155
+ errors: [
156
+ {
157
+ text: `Dependency "${args.path}" imports solid-js, but the page already runs the shared instance (yapmod.solid*). Plugin dependencies must not bundle their own copy of Solid.`
158
+ }
159
+ ]
160
+ }));
161
+ }
162
+ };
163
+ async function buildPlugin(options) {
164
+ const result = await build({
165
+ entryPoints: [options.entry],
166
+ bundle: true,
167
+ write: false,
168
+ format: "iife",
169
+ globalName: EXPRESSION_GLOBAL_NAME,
170
+ // The Babel pass already compiled JSX away; keep esbuild from guessing.
171
+ jsx: "preserve",
172
+ target: "es2020",
173
+ minify: options.minify ?? false,
174
+ plugins: [babelTransform, sharedModuleGuard]
175
+ });
176
+ const output = result.outputFiles[0];
177
+ if (!output) throw new Error("@yapmod/builder: esbuild produced no output.");
178
+ const expression = expressionBanner + output.text + expressionFooter;
179
+ await mkdir(dirname(options.outfile), { recursive: true });
180
+ await writeFile(options.outfile, expression, "utf8");
181
+ return expression;
182
+ }
183
+
184
+ // src/unplugin.ts
185
+ import { createUnplugin } from "unplugin";
186
+ var yapmodPlugin = createUnplugin(() => ({
187
+ name: "yapmod-plugin",
188
+ enforce: "pre",
189
+ transform(code, id) {
190
+ if (id.includes("node_modules")) return null;
191
+ if (!/\.(?:[cm]?[jt]sx?)$/.test(id)) return null;
192
+ const transformed = transformPluginSource(code, id);
193
+ return transformed === null ? null : { code: transformed, map: null };
194
+ }
195
+ }));
196
+ export {
197
+ EXPRESSION_GLOBAL_NAME,
198
+ buildPlugin,
199
+ expressionBanner,
200
+ expressionFooter,
201
+ transformPluginSource,
202
+ yapmodPlugin
203
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Compiles one plugin module: TypeScript and JSX first, then the shared-
3
+ * import rewrite. Returns `null` only when Babel produced no code at all.
4
+ */
5
+ export declare function transformPluginSource(code: string, id: string): string | null;
@@ -0,0 +1 @@
1
+ export declare const yapmodPlugin: import("unplugin").UnpluginInstance<unknown, boolean>;
package/global.d.ts ADDED
@@ -0,0 +1,72 @@
1
+ /*
2
+ * Self-contained type declaration for the `yapmod` global.
3
+ *
4
+ * Inlined from `@yapmod/types` so the builder package has no runtime
5
+ * dependency on the monorepo types package (which itself depends on
6
+ * `@yapmod/ui` and `solid-js`). Plugin authors only need these shapes
7
+ * for type-checking; the real API object is provided at runtime by the
8
+ * Yapmod client on the host page.
9
+ *
10
+ * Usage: `import type {} from '@yapmod/builder/global'` loads this
11
+ * ambient declaration into a project.
12
+ */
13
+
14
+ import type { Component, JSX } from 'solid-js';
15
+
16
+ interface YapmodPluginApi {
17
+ readonly apiVersion: 1;
18
+ readonly id: string;
19
+ readonly manifest: {
20
+ name: string;
21
+ author: string;
22
+ description: string;
23
+ version?: string;
24
+ hash?: string;
25
+ };
26
+ readonly storage: Record<string, unknown>;
27
+ flushStorage(): void;
28
+ log(...args: unknown[]): void;
29
+ observe(selector: string, onFound: (element: Element) => void): () => void;
30
+ readonly ui: {
31
+ showToast(message: string, kind?: 'success' | 'error'): void;
32
+ openModal(options: {
33
+ title: string;
34
+ description?: string;
35
+ wide?: boolean;
36
+ content: (close: () => void) => JSX.Element;
37
+ footer?: (close: () => void) => JSX.Element;
38
+ onClose?: () => void;
39
+ }): () => void;
40
+ injectCss(css: string): () => void;
41
+ Button: Component<JSX.IntrinsicElements['button'] & { variant?: string }>;
42
+ Hint: Component<JSX.IntrinsicElements['span']>;
43
+ Panel: Component<JSX.IntrinsicElements['div']>;
44
+ Icon: Component<{ name: string; class?: string; size?: string }>;
45
+ };
46
+ readonly settings: {
47
+ registerSection(section: {
48
+ id: string;
49
+ label: string;
50
+ icon?: string;
51
+ render: () => JSX.Element;
52
+ }): () => void;
53
+ };
54
+ onDispose(dispose: () => void): void;
55
+ showSettings(): void;
56
+ readonly solid: typeof import('solid-js');
57
+ readonly solidWeb: typeof import('solid-js/web');
58
+ readonly solidStore: typeof import('solid-js/store');
59
+ }
60
+
61
+ declare global {
62
+ /**
63
+ * The plugin API the host injects. Every `@yapmod/builder` bundle runs
64
+ * inside `function (yapmod) { ... }`, so top-level plugin code can use it
65
+ * without importing anything.
66
+ */
67
+ const yapmod: YapmodPluginApi;
68
+ }
69
+
70
+ // Export the interface so this file is treated as a module (allows empty
71
+ // type imports: `import type {} from '@yapmod/builder/global'`).
72
+ export type { YapmodPluginApi };
package/package.json CHANGED
@@ -1,6 +1,45 @@
1
1
  {
2
2
  "name": "@yapmod/builder",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "files": [
5
+ "dist",
6
+ "global.d.ts"
7
+ ],
8
+ "type": "module",
9
+ "main": "dist/index.js",
10
+ "module": "dist/index.js",
11
+ "types": "dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "import": "./dist/index.js",
15
+ "types": "./dist/index.d.ts"
16
+ },
17
+ "./global": {
18
+ "types": "./global.d.ts"
19
+ }
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "scripts": {
25
+ "build": "tsup && tsc --emitDeclarationOnly -p tsconfig.build.json",
26
+ "typecheck": "tsc --noEmit -p tsconfig.json",
27
+ "example": "node examples/build.mjs && vite build --config examples/vite.config.mjs && node examples/check.mjs"
28
+ },
29
+ "dependencies": {
30
+ "@babel/core": "^7.29.0",
31
+ "@babel/preset-typescript": "^7.29.0",
32
+ "babel-preset-solid": "^1.8.4",
33
+ "esbuild": "^0.28.2",
34
+ "solid-js": "^1.9.18",
35
+ "unplugin": "^3.7.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/babel__core": "^7.20.5",
39
+ "@types/node": "^26.0.0",
40
+ "@typescript/typescript6": "^6.0.0",
41
+ "tsup": "^8.0.0",
42
+ "typescript": "^7.0.2",
43
+ "vite": "^8.3.7"
44
+ }
45
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.