@moloodev/cli 0.0.0-stage → 0.3.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,149 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { createRequire } from "node:module";
3
+ import { join } from "node:path";
4
+ import type { Plugin } from "esbuild";
5
+ import { rewriteThreeLoaders } from "./three-local-loaders";
6
+
7
+ const staticIcons = createRequire(import.meta.url).resolve("@moloodev/ui/kind-icon-static");
8
+
9
+ export function publishBundlePlugin(): Plugin {
10
+ return {
11
+ name: "publish-bundle",
12
+ setup(build) {
13
+ build.onResolve({ filter: /\/kind-icon-load$/ }, (args) => {
14
+ if (!args.importer.endsWith(`${join("components", "ui", "kind-icon.tsx")}`)) return undefined;
15
+ return { path: staticIcons };
16
+ });
17
+ build.onLoad({ filter: /\.(?:js|mjs|cjs)$/ }, async (args) => {
18
+ const rewrite = rewriterFor(args.path);
19
+ if (rewrite === undefined) return undefined;
20
+ return { contents: rewrite(await readFile(args.path, "utf8")), loader: "js" };
21
+ });
22
+ },
23
+ };
24
+ }
25
+
26
+ function rewriterFor(pathname: string): ((source: string) => string) | undefined {
27
+ if (pathname.endsWith("/three/build/three.core.js")) return rewriteThreeLoaders;
28
+ if (pathname.endsWith("/zod/v4/core/util.js")) return rewriteZodUtil;
29
+ if (pathname.endsWith("/zod/v4/core/doc.js")) return rewriteZodDoc;
30
+ if (pathname.endsWith("/zod/v4/core/compile.js")) return rewriteZodCompile;
31
+ if (pathname.endsWith("/prismjs/prism.js")) return rewritePrism;
32
+ if (pathname.endsWith("/react-dom-server.browser.production.js")) return rewriteReactServerBind;
33
+ if (pathname.endsWith("/react-dom-server-legacy.browser.production.js")) return rewriteReactServerBind;
34
+ if (pathname.endsWith("/@e965/xlsx/xlsx.mjs")) return rewriteXlsx;
35
+ if (pathname.endsWith("/modern-json-react/dist/index.mjs")) return rewriteJsonSearch;
36
+ if (pathname.endsWith("/@xyflow/react/dist/esm/index.js")) return rewriteXyflow;
37
+ return undefined;
38
+ }
39
+
40
+ function rewriteZodUtil(source: string): string {
41
+ const from = [
42
+ " try {",
43
+ " const F = Function;",
44
+ ' new F("");',
45
+ " return true;",
46
+ " }",
47
+ " catch (_) {",
48
+ " return false;",
49
+ " }",
50
+ ].join("\n");
51
+ const next = replaceOnce(source, from, " return false;", "zod allowsEval");
52
+ if (next.includes("const F = Function")) throw new Error("publish bundle: zod allowsEval still probes Function");
53
+ return next;
54
+ }
55
+
56
+ function rewriteZodDoc(source: string): string {
57
+ const start = source.indexOf(" compile() {");
58
+ const close = source.indexOf("\n }", start);
59
+ if (start < 0 || close < 0) throw new Error("publish bundle: zod doc compile() is missing");
60
+ const next = `${source.slice(0, start)} compile() {\n throw new Error("zod jit is disabled");\n }${source.slice(close + "\n }".length)}`;
61
+ if (next.includes("const F = Function")) throw new Error("publish bundle: zod doc still builds a function");
62
+ return next;
63
+ }
64
+
65
+ function rewriteZodCompile(source: string): string {
66
+ const start = source.indexOf(" const F = Function;");
67
+ const marker = "fn = factory(...constantValues);";
68
+ const end = source.indexOf(marker, start);
69
+ if (start < 0 || end < 0) throw new Error("publish bundle: zod compile() is missing");
70
+ const replacement = [
71
+ " let fn;",
72
+ " try {",
73
+ ' throw new Error("zod jit is disabled");',
74
+ ].join("\n");
75
+ const next = source.slice(0, start) + replacement + source.slice(end + marker.length);
76
+ if (next.includes("const F = Function") || next.includes("new F(")) {
77
+ throw new Error("publish bundle: zod compile() still builds a function");
78
+ }
79
+ return next;
80
+ }
81
+
82
+ function rewriteReactServerBind(source: string): string {
83
+ const next = replaceOnce(
84
+ source,
85
+ "var bind = Function.prototype.bind,",
86
+ "var bind = function (thisArg) { return this.bind.apply(this, arguments); },",
87
+ "react-dom server bind",
88
+ );
89
+ if (/\bFunction\b/.test(next)) throw new Error("publish bundle: react-dom server still names Function");
90
+ return next;
91
+ }
92
+
93
+ function rewritePrism(source: string): string {
94
+ const next = replaceOnce(source, "var worker = new Worker(_.filename);", "var worker = null;", "prism worker")
95
+ .replace("xhr.open('GET', src, true);", "error(FAILURE_EMPTY_MESSAGE); return;");
96
+ if (next.includes("xhr.open(")) throw new Error("publish bundle: prism file load remains");
97
+ if (next.includes("new Worker")) throw new Error("publish bundle: prism worker remains");
98
+ return next;
99
+ }
100
+
101
+ function rewriteXlsx(source: string): string {
102
+ const next = replaceOnce(
103
+ source,
104
+ 'var out = File(fname); out.open("w"); out.encoding = "binary";',
105
+ 'throw new Error("extendscript save is unavailable");',
106
+ "xlsx save",
107
+ );
108
+ const both = replaceOnce(
109
+ next,
110
+ 'var infile = File(path); infile.open("r"); infile.encoding = "binary";',
111
+ 'throw new Error("extendscript read is unavailable");',
112
+ "xlsx read",
113
+ );
114
+ if (both.includes(".open(")) throw new Error("publish bundle: xlsx open() remains");
115
+ return both;
116
+ }
117
+
118
+ function rewriteJsonSearch(source: string): string {
119
+ const shortcut = replaceOnce(source, "y&&g.open()", "false", "json search shortcut");
120
+ const next = replaceOnce(
121
+ shortcut,
122
+ "g.isActive?g.close():g.open()",
123
+ "g.isActive?g.close():void 0",
124
+ "json search toggle",
125
+ );
126
+ if (next.includes(".open(")) throw new Error("publish bundle: json search open() remains");
127
+ return next;
128
+ }
129
+
130
+ function rewriteXyflow(source: string): string {
131
+ const memoCalls = [
132
+ "const MiniMapNode = memo(",
133
+ "const NodeComponentWrapper = memo(",
134
+ "var MiniMapNodes$1 = memo(",
135
+ "const MiniMap = memo(",
136
+ ];
137
+ let next = source;
138
+ for (const call of memoCalls) {
139
+ next = replaceOnce(next, call, call.replace("= memo(", "= /* @__PURE__ */ memo("), call);
140
+ }
141
+ return replaceOnce(next, "MiniMapComponent.displayName = 'MiniMap';\n", "", "minimap displayName");
142
+ }
143
+
144
+ function replaceOnce(source: string, from: string, to: string, label: string): string {
145
+ const index = source.indexOf(from);
146
+ if (index < 0) throw new Error(`publish bundle: missing ${label}`);
147
+ if (source.indexOf(from, index + from.length) !== -1) throw new Error(`publish bundle: ${label} is not unique`);
148
+ return source.slice(0, index) + to + source.slice(index + from.length);
149
+ }
package/src/publish.ts ADDED
@@ -0,0 +1,124 @@
1
+ import { createHash } from "node:crypto";
2
+ import { putScriptUpload } from "@moloodev/editor-store/upload";
3
+ import { getApiOrigin } from "@moloodev/platform";
4
+ import { builtinEditors } from "@moloodev/platform/builtin-editors";
5
+ import { buildEditorPackage } from "./editor-views";
6
+
7
+ const DEFAULT_SERVICE = "editor-origin-build";
8
+
9
+ export function publishEndpoint(apiUrl?: string): string {
10
+ return `${apiBase(apiUrl)}/v1/revisions/publish`;
11
+ }
12
+
13
+ export function uploadEndpoint(apiUrl?: string): string {
14
+ return `${apiBase(apiUrl)}/v1/revisions/uploads`;
15
+ }
16
+
17
+ export async function publishEditor(input: {
18
+ manifest: Record<string, unknown>;
19
+ script: string;
20
+ token: string;
21
+ apiUrl?: string;
22
+ service?: string;
23
+ }): Promise<{ status: number; body: string }> {
24
+ const uploadUrl = uploadEndpoint(input.apiUrl);
25
+ const publishUrl = publishEndpoint(input.apiUrl);
26
+ const service = input.service ?? DEFAULT_SERVICE;
27
+ const issued = await fetch(uploadUrl, {
28
+ method: "POST",
29
+ headers: serviceHeaders(input.token, service),
30
+ });
31
+ const issuedBody = await issued.text();
32
+ if (issued.status !== 201) return { status: issued.status, body: issuedBody };
33
+ const upload = readUpload(issuedBody);
34
+ if (upload === undefined) return { status: issued.status, body: issuedBody };
35
+
36
+ const bytes = Buffer.from(input.script, "utf8");
37
+ try {
38
+ await putScriptUpload(upload.token, upload.pathname, bytes);
39
+ } catch {
40
+ return { status: 0, body: "upload_failed" };
41
+ }
42
+
43
+ const response = await fetch(publishUrl, {
44
+ method: "POST",
45
+ headers: {
46
+ ...serviceHeaders(input.token, service),
47
+ "content-type": "application/json",
48
+ },
49
+ body: JSON.stringify({
50
+ manifest: input.manifest,
51
+ editorId: reservedEditorId(input.manifest),
52
+ upload: {
53
+ pathname: upload.pathname,
54
+ sha256: createHash("sha256").update(bytes).digest("hex"),
55
+ },
56
+ }),
57
+ });
58
+ return { status: response.status, body: await response.text() };
59
+ }
60
+
61
+ export async function publishEditorPackage(
62
+ packageRoot: string,
63
+ options: { token?: string; apiUrl?: string; service?: string } = {},
64
+ ): Promise<{ id: string; status: number; body: string }> {
65
+ const token = (options.token ?? process.env.MOLOO_SERVICE_TOKEN ?? "").trim();
66
+ if (token.length === 0) throw new Error("MOLOO_SERVICE_TOKEN is empty");
67
+ const built = await buildEditorPackage(packageRoot);
68
+ const result = await publishEditor({
69
+ manifest: built.manifest,
70
+ script: built.script,
71
+ token,
72
+ apiUrl: options.apiUrl,
73
+ service: options.service,
74
+ });
75
+ return { id: built.id, ...result };
76
+ }
77
+
78
+ function serviceHeaders(token: string, service: string): Record<string, string> {
79
+ return {
80
+ "x-moloo-service-token": token,
81
+ "x-moloo-service": service,
82
+ };
83
+ }
84
+
85
+ function readUpload(text: string): { pathname: string; token: string } | undefined {
86
+ let value: unknown;
87
+ try {
88
+ value = JSON.parse(text);
89
+ } catch {
90
+ return undefined;
91
+ }
92
+ if (!isRecord(value)) return undefined;
93
+ const data = value.data;
94
+ if (!isRecord(data)) return undefined;
95
+ const pathname = data.pathname;
96
+ const token = data.token;
97
+ if (typeof pathname !== "string" || typeof token !== "string") return undefined;
98
+ return { pathname, token };
99
+ }
100
+
101
+ function apiBase(apiUrl: string | undefined): string {
102
+ const raw = apiUrl?.trim() ?? "";
103
+ const base = raw.length === 0 ? getApiOrigin("production") : raw;
104
+ let url: URL;
105
+ try {
106
+ url = new URL(base);
107
+ } catch {
108
+ throw new Error("publish API origin is invalid");
109
+ }
110
+ if (url.protocol !== "https:") throw new Error("publish API origin must be https");
111
+ return `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
112
+ }
113
+
114
+ export function reservedEditorId(manifest: Record<string, unknown>): string {
115
+ const id = manifest.id;
116
+ if (typeof id !== "string") throw new Error("builtin manifest id is missing");
117
+ const row = builtinEditors.find((item) => `${item.publisherHandle}.${item.slug}` === id);
118
+ if (row === undefined) throw new Error(`builtin editor ${id} is missing`);
119
+ return row.id;
120
+ }
121
+
122
+ function isRecord(value: unknown): value is Record<string, unknown> {
123
+ return typeof value === "object" && value !== null && !Array.isArray(value);
124
+ }
@@ -0,0 +1,113 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { parse } from "@babel/parser";
3
+
4
+ function isRecord(value: unknown): value is Record<string, unknown> {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6
+ }
7
+
8
+ function literal(node: unknown): unknown {
9
+ if (!isRecord(node) || typeof node.type !== "string") {
10
+ throw new Error("Manifest values must be static literals");
11
+ }
12
+ switch (node.type) {
13
+ case "StringLiteral":
14
+ case "NumericLiteral":
15
+ case "BooleanLiteral":
16
+ return node.value;
17
+ case "NullLiteral":
18
+ return null;
19
+ case "UnaryExpression":
20
+ if (
21
+ node.operator === "-" &&
22
+ isRecord(node.argument) &&
23
+ node.argument.type === "NumericLiteral" &&
24
+ typeof node.argument.value === "number"
25
+ ) {
26
+ return -node.argument.value;
27
+ }
28
+ throw new Error("Manifest values must be static literals");
29
+ case "ArrayExpression":
30
+ if (!Array.isArray(node.elements)) {
31
+ throw new Error("Manifest values must be static literals");
32
+ }
33
+ return node.elements.map(literal);
34
+ case "ObjectExpression": {
35
+ if (!Array.isArray(node.properties)) {
36
+ throw new Error("Manifest properties must be static literals");
37
+ }
38
+ const entries = node.properties.map((property: unknown): [string, unknown] => {
39
+ if (
40
+ !isRecord(property) ||
41
+ property.type !== "ObjectProperty" ||
42
+ property.computed === true ||
43
+ property.shorthand === true ||
44
+ !isRecord(property.key) ||
45
+ (property.key.type !== "Identifier" && property.key.type !== "StringLiteral")
46
+ ) {
47
+ throw new Error("Manifest properties must be static literals");
48
+ }
49
+ const key = property.key.type === "Identifier" ? property.key.name : property.key.value;
50
+ if (typeof key !== "string") {
51
+ throw new Error("Manifest properties must be static literals");
52
+ }
53
+ return [key, literal(property.value)];
54
+ });
55
+ if (new Set(entries.map(([key]) => key)).size !== entries.length) {
56
+ throw new Error("Duplicate manifest property");
57
+ }
58
+ return Object.fromEntries(entries);
59
+ }
60
+ default:
61
+ throw new Error(`Manifest values must be static literals: ${node.type}`);
62
+ }
63
+ }
64
+
65
+ /** Read literal declarations without importing an editor or executing its code. */
66
+ export function readEditorManifest(file: string): Record<string, unknown> {
67
+ const ast = parse(readFileSync(file, "utf8"), { sourceType: "module", plugins: ["typescript"] });
68
+ let declaration;
69
+ for (const statement of ast.program.body) {
70
+ if (statement.type === "ImportDeclaration") {
71
+ if (
72
+ !["@moloodev/editor-sdk/view", "@moloodev/editor-sdk/commands", "@moloodev/editor-sdk/commands/editor-document"].includes(statement.source.value) ||
73
+ statement.specifiers.some((item) => item.type !== "ImportSpecifier" || item.imported.type !== "Identifier" || item.imported.name !== "defineEditorManifest" || item.local.name !== "defineEditorManifest")
74
+ ) {
75
+ throw new Error(`Invalid static manifest import: ${file}`);
76
+ }
77
+ continue;
78
+ }
79
+ if (statement.type !== "ExportDefaultDeclaration" || declaration) {
80
+ throw new Error(`Expected one static manifest declaration: ${file}`);
81
+ }
82
+ declaration = statement.declaration;
83
+ }
84
+ if (
85
+ declaration?.type !== "CallExpression" ||
86
+ declaration.callee.type !== "Identifier" ||
87
+ declaration.callee.name !== "defineEditorManifest" ||
88
+ declaration.arguments.length !== 1
89
+ ) {
90
+ throw new Error(`Expected export default defineEditorManifest({...}): ${file}`);
91
+ }
92
+ const input = literal(declaration.arguments[0]);
93
+ if (!isRecord(input)) throw new Error(`Manifest must be an object: ${file}`);
94
+ if ("icon" in input && input.icon !== undefined && (typeof input.icon !== "string" || !/^tabler:[a-z0-9]+(-[a-z0-9]+)*$/.test(input.icon))) {
95
+ throw new Error("Invalid editor manifest: icon must be tabler:{name}");
96
+ }
97
+ const contributes = input.contributes ?? {};
98
+ if (!isRecord(contributes)) throw new Error(`Manifest contributes must be an object: ${file}`);
99
+ if (input.layoutVersion != null && (typeof input.layoutVersion !== "number" || !Number.isInteger(input.layoutVersion) || input.layoutVersion < 1)) {
100
+ throw new Error("Invalid editor manifest: layoutVersion must be a positive integer");
101
+ }
102
+ return {
103
+ ...input,
104
+ categories: input.categories ?? [],
105
+ capabilities: input.capabilities ?? [],
106
+ contributes: {
107
+ commands: contributes.commands ?? [],
108
+ menus: contributes.menus ?? [],
109
+ keybindings: contributes.keybindings ?? [],
110
+ ...(contributes.configuration ? { configuration: contributes.configuration } : {}),
111
+ },
112
+ };
113
+ }
@@ -0,0 +1,102 @@
1
+ /** Published views receive authorized asset bytes from the host, not network URLs.
2
+ * Replace only Three's I/O methods; parsers, textures and renderers stay upstream.
3
+ * Exact method markers fail the build if an upstream update changes this boundary.
4
+ */
5
+ export function rewriteThreeLoaders(source: string): string {
6
+ let next = replaceLoad(
7
+ source,
8
+ "FileLoader",
9
+ `
10
+ url = this.manager.resolveURL(url ?? '');
11
+ this.manager.itemStart(url);
12
+ queueMicrotask(() => {
13
+ try {
14
+ const match = /^data:([^,]*),(.*)$/s.exec(url);
15
+ if (!match) throw new Error('Only host-provided data resources are supported');
16
+ const encoded = match[1].endsWith(';base64');
17
+ const bytes = encoded
18
+ ? Uint8Array.from(atob(match[2]), character => character.charCodeAt(0))
19
+ : new TextEncoder().encode(decodeURIComponent(match[2]));
20
+ const text = () => new TextDecoder().decode(bytes);
21
+ let value;
22
+ switch (this.responseType) {
23
+ case 'arraybuffer': value = bytes.buffer; break;
24
+ case 'blob': value = new Blob([bytes], { type: this.mimeType || match[1].split(';')[0] }); break;
25
+ case 'json': value = JSON.parse(text()); break;
26
+ case 'document': throw new Error('Document resources are unsupported');
27
+ default: value = text();
28
+ }
29
+ if (onLoad) onLoad(value);
30
+ } catch (error) {
31
+ if (onError) onError(error);
32
+ this.manager.itemError(url);
33
+ } finally {
34
+ this.manager.itemEnd(url);
35
+ }
36
+ });
37
+ `,
38
+ );
39
+ next = replaceLoad(
40
+ next,
41
+ "ImageBitmapLoader",
42
+ `
43
+ url = this.manager.resolveURL(url ?? '');
44
+ this.manager.itemStart(url);
45
+ const finishError = (error) => {
46
+ if (onError) onError(error);
47
+ this.manager.itemError(url);
48
+ this.manager.itemEnd(url);
49
+ };
50
+ if (!url.startsWith('data:') && !url.startsWith('blob:')) {
51
+ queueMicrotask(() => finishError(new Error('Only local image resources are supported')));
52
+ return;
53
+ }
54
+ const image = document.createElementNS('http://www.w3.org/1999/xhtml', 'img');
55
+ image.onload = () => {
56
+ createImageBitmap(image, this.options).then(bitmap => {
57
+ try { if (onLoad) onLoad(bitmap); }
58
+ finally { this.manager.itemEnd(url); }
59
+ }, finishError);
60
+ };
61
+ image.onerror = finishError;
62
+ image.src = url;
63
+ `,
64
+ );
65
+ // TextureLoader and older browsers use ImageLoader instead of ImageBitmapLoader.
66
+ const start = next.indexOf("class ImageLoader extends Loader {");
67
+ const marker = "\t\turl = this.manager.resolveURL( url );";
68
+ const index = next.indexOf(marker, start);
69
+ if (start < 0 || index < 0)
70
+ throw new Error("publish bundle: Three ImageLoader is missing");
71
+ const end = index + marker.length;
72
+ return (
73
+ next.slice(0, end) +
74
+ `
75
+ if (!url.startsWith('data:') && !url.startsWith('blob:')) {
76
+ this.manager.itemStart(url);
77
+ queueMicrotask(() => {
78
+ try { if (onError) onError(new Error('Only local image resources are supported')); }
79
+ finally { this.manager.itemError(url); this.manager.itemEnd(url); }
80
+ });
81
+ return;
82
+ }
83
+ ` +
84
+ next.slice(end)
85
+ );
86
+ }
87
+
88
+ function replaceLoad(source: string, name: string, body: string): string {
89
+ const start = source.indexOf(`class ${name} extends Loader {`);
90
+ const method = source.indexOf(
91
+ "\n\tload( url, onLoad, onProgress, onError ) {",
92
+ start,
93
+ );
94
+ const end = source.indexOf("\n\t}", method);
95
+ if (start < 0 || method < 0 || end < 0)
96
+ throw new Error(`publish bundle: Three ${name}.load is missing`);
97
+ return (
98
+ source.slice(0, method) +
99
+ `\n\tload( url, onLoad, onProgress, onError ) {${body}\n\t}` +
100
+ source.slice(end + 3)
101
+ );
102
+ }
@@ -0,0 +1,57 @@
1
+ import { connectEditorView, type EditorView } from "@moloodev/editor-sdk/view";
2
+
3
+ export type HandshakePageDeps = {
4
+ readonly search: string;
5
+ readonly editorId: string;
6
+ postMessage(
7
+ message: unknown,
8
+ targetOrigin: string,
9
+ transfer: readonly MessagePort[],
10
+ ): void;
11
+ createChannel(): MessageChannel;
12
+ setHandshake(value: string): void;
13
+ };
14
+
15
+ /** Handshake only. The dataset records `ok` or the failure code from the existing channel. */
16
+ export function startHandshakePage(deps: HandshakePageDeps): EditorView {
17
+ const view = connectEditorView({
18
+ parent: singleParent(deps.search),
19
+ editorId: deps.editorId,
20
+ postMessage: deps.postMessage,
21
+ createChannel: deps.createChannel,
22
+ });
23
+ void view.handshake.then((result) => {
24
+ deps.setHandshake(result.ok ? "ok" : result.code);
25
+ });
26
+ return view;
27
+ }
28
+
29
+ function singleParent(search: string): string | null {
30
+ const values = new URLSearchParams(search).getAll("parent");
31
+ if (values.length !== 1) return null;
32
+ const parent = values[0];
33
+ if (parent === undefined || parent.length === 0) return null;
34
+ return parent;
35
+ }
36
+
37
+ function wireWindow(): void {
38
+ if (typeof document === "undefined") return;
39
+ const root = document.documentElement;
40
+ startHandshakePage({
41
+ search: window.location.search,
42
+ editorId: root.dataset.editorId ?? "",
43
+ postMessage(message, targetOrigin, transfer) {
44
+ const ports: Transferable[] = [];
45
+ for (const port of transfer) ports.push(port);
46
+ window.parent.postMessage(message, targetOrigin, ports);
47
+ },
48
+ createChannel() {
49
+ return new MessageChannel();
50
+ },
51
+ setHandshake(value) {
52
+ root.dataset.handshake = value;
53
+ },
54
+ });
55
+ }
56
+
57
+ wireWindow();
package/src/yjs-log.ts ADDED
@@ -0,0 +1,16 @@
1
+ export const BOLD = Symbol("bold");
2
+ export const UNBOLD = Symbol("unbold");
3
+ export const RED = Symbol("red");
4
+ export const ORANGE = Symbol("orange");
5
+
6
+ function words(args: readonly unknown[]): unknown[] {
7
+ return args.filter((arg) => typeof arg !== "symbol");
8
+ }
9
+
10
+ export function print(...args: unknown[]): void {
11
+ console.log(...words(args));
12
+ }
13
+
14
+ export function warn(...args: unknown[]): void {
15
+ console.warn(...words(args));
16
+ }
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.