@examplary/cli 1.7.0 → 1.9.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.
Files changed (45) hide show
  1. package/dist/commands/preview.d.ts +5 -0
  2. package/dist/commands/preview.js +174 -0
  3. package/dist/commands/upload.d.ts +5 -0
  4. package/dist/commands/upload.js +138 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +37 -0
  7. package/dist/lib/api.d.ts +5 -0
  8. package/dist/lib/api.js +47 -0
  9. package/dist/lib/bundle.d.ts +4 -0
  10. package/dist/lib/bundle.js +27 -0
  11. package/dist/lib/jsonata.d.ts +1 -0
  12. package/dist/lib/jsonata.js +26 -0
  13. package/dist/lib/read-meta.d.ts +16 -0
  14. package/dist/lib/read-meta.js +36 -0
  15. package/dist/lib/styles.d.ts +1 -0
  16. package/dist/lib/styles.js +24 -0
  17. package/dist/preview-ui/assets/icon-nodes-Ds7bJID4.js +2 -0
  18. package/dist/preview-ui/assets/icon-nodes-Ds7bJID4.js.map +1 -0
  19. package/dist/preview-ui/assets/index-DcEcr1Zm.css +1 -0
  20. package/dist/preview-ui/assets/index-NlxMWEJQ.js +3629 -0
  21. package/dist/preview-ui/assets/index-NlxMWEJQ.js.map +1 -0
  22. package/dist/preview-ui/assets/tags-DZBm0mOh.js +2 -0
  23. package/dist/preview-ui/assets/tags-DZBm0mOh.js.map +1 -0
  24. package/{src → dist}/preview-ui/index.html +2 -1
  25. package/package.json +23 -22
  26. package/bin/index.js +0 -37
  27. package/src/commands/preview.js +0 -220
  28. package/src/commands/upload.js +0 -179
  29. package/src/lib/api.js +0 -53
  30. package/src/lib/bundle.js +0 -35
  31. package/src/lib/jsonata.js +0 -31
  32. package/src/lib/middleware.js +0 -16
  33. package/src/lib/read-meta.js +0 -40
  34. package/src/lib/styles.js +0 -32
  35. package/src/preview-ui/App.tsx +0 -148
  36. package/src/preview-ui/components/json-editor.tsx +0 -27
  37. package/src/preview-ui/components/preview-assessment.tsx +0 -43
  38. package/src/preview-ui/components/preview-print.tsx +0 -38
  39. package/src/preview-ui/components/preview-results.tsx +0 -38
  40. package/src/preview-ui/components/preview-settings-area.tsx +0 -97
  41. package/src/preview-ui/components/question-component.tsx +0 -75
  42. package/src/preview-ui/index.css +0 -11
  43. package/src/preview-ui/lib/demo-api.ts +0 -29
  44. package/src/preview-ui/lib/i18n.ts +0 -15
  45. package/src/preview-ui/main.tsx +0 -8
@@ -0,0 +1,5 @@
1
+ #! /usr/bin/env node
2
+ export declare const previewCommand: (argv: {
3
+ host: string;
4
+ key: string;
5
+ }) => Promise<void>;
@@ -0,0 +1,174 @@
1
+ #! /usr/bin/env node
2
+ import { watch } from "fs";
3
+ import { readFile } from "fs/promises";
4
+ import { createServer as createHttpServer } from "http";
5
+ import { resolve } from "path";
6
+ import { cwd, exit } from "process";
7
+ import { serve } from "@hono/node-server";
8
+ import { serveStatic } from "@hono/node-server/serve-static";
9
+ import { Hono } from "hono";
10
+ import open from "open";
11
+ import { WebSocketServer } from "ws";
12
+ import { setApiHost, setApiKey } from "../lib/api.js";
13
+ import { buildComponent } from "../lib/bundle.js";
14
+ import { readMetaFile } from "../lib/read-meta.js";
15
+ import { buildStyles } from "../lib/styles.js";
16
+ export const previewCommand = async (argv) => {
17
+ setApiHost(argv.host);
18
+ setApiKey(argv.key);
19
+ const dir = cwd();
20
+ // Verify a question-type file exists (json or yml)
21
+ try {
22
+ await readMetaFile();
23
+ }
24
+ catch (e) {
25
+ console.error(`🚫 ${e.message}`);
26
+ exit(1);
27
+ }
28
+ // Let's start a Vite server to preview the question type
29
+ const root = resolve(import.meta.dirname, "..", "preview-ui");
30
+ const app = new Hono();
31
+ app.get("/api/bundle", async (c) => {
32
+ const path = c.req.query("path");
33
+ const sourcemap = c.req.query("sourcemap") === "1";
34
+ const componentName = c.req.query("componentName");
35
+ try {
36
+ const componentOutput = await buildComponent(path);
37
+ if (sourcemap) {
38
+ return c.body(componentOutput.sourcemap, 200, {
39
+ "Content-Type": "application/json",
40
+ });
41
+ }
42
+ else {
43
+ const sizeInKb = componentOutput.js.length / 1000;
44
+ if (sizeInKb > 100) {
45
+ console.warn(`⚠️ Component ${componentName} is larger than 100 kB! Consider optimizing it.`);
46
+ }
47
+ let output = componentOutput.js;
48
+ if (componentOutput.sourcemap) {
49
+ output += `\n//# sourceMappingURL=/api/bundle?componentName=${componentName}&path=${encodeURIComponent(path || "")}&sourcemap=1`;
50
+ }
51
+ return c.body(output, 200, {
52
+ "Content-Type": "application/javascript",
53
+ });
54
+ }
55
+ }
56
+ catch (error) {
57
+ console.error(`🚫 Error bundling component ${componentName} from ${path}: ${error.message}`);
58
+ return c.text(`Error bundling component ${componentName}: ${error.message}`, 500);
59
+ }
60
+ });
61
+ app.get("/api/icon", async (c) => {
62
+ const path = c.req.query("path") || "";
63
+ const icon = await readFile(resolve(dir, path), "utf-8");
64
+ return c.body(icon, 200, {
65
+ "Content-Type": "image/svg+xml",
66
+ });
67
+ });
68
+ app.get("/api/stylesheet", async (c) => {
69
+ const componentName = c.req.query("componentName");
70
+ try {
71
+ const componentOutput = await buildStyles();
72
+ return c.body(componentOutput || "", 200, {
73
+ "Content-Type": "text/css",
74
+ });
75
+ }
76
+ catch (error) {
77
+ console.error(`🚫 Error building CSS for component ${componentName}: ${error.message}`);
78
+ return c.text(`Error building CSS for component ${componentName}: ${error.message}`, 500);
79
+ }
80
+ });
81
+ app.get("/__reload", async (c) => {
82
+ if (wss) {
83
+ wss.clients.forEach((client) => {
84
+ if (client.readyState === 1) {
85
+ // WebSocket.OPEN
86
+ client.send(JSON.stringify({ type: "reload" }));
87
+ }
88
+ });
89
+ }
90
+ return c.body(null, 201);
91
+ });
92
+ let wsPort; // WebSocket port will be set when server starts
93
+ app.get("/", async (c) => {
94
+ let questionType;
95
+ try {
96
+ ({ definition: questionType } = await readMetaFile());
97
+ questionType.components = questionType.components
98
+ ? Object.entries(questionType.components).reduce((acc, [componentName, componentPath]) => ({
99
+ ...acc,
100
+ [componentName]: `/api/bundle?componentName=${componentName}&path=${encodeURIComponent(componentPath)}`,
101
+ }), {})
102
+ : questionType.components;
103
+ }
104
+ catch (e) {
105
+ return c.text(`Error reading question type definition: ${e.message}`, 500);
106
+ }
107
+ let template = await readFile(resolve(root, "index.html"), "utf-8");
108
+ // Inject hot reload script
109
+ const hotReloadScript = `
110
+ <script>
111
+ (function() {
112
+ const wsPort = ${wsPort || "null"};
113
+ if (wsPort) {
114
+ const ws = new WebSocket('ws://localhost:' + wsPort);
115
+ ws.onmessage = function(event) {
116
+ const data = JSON.parse(event.data);
117
+ if (data.type === 'reload') {
118
+ console.log('🔄 Reloading page due to file change...');
119
+ window.location.reload();
120
+ }
121
+ };
122
+ ws.onopen = function() {
123
+ console.log('🔗 Hot reload connected');
124
+ };
125
+ ws.onclose = function() {
126
+ console.log('🔌 Hot reload disconnected');
127
+ };
128
+ ws.onerror = function(error) {
129
+ console.log('❌ Hot reload error:', error);
130
+ };
131
+ }
132
+ })();
133
+ </script>
134
+ `;
135
+ template =
136
+ "<script>window._questionType = " +
137
+ JSON.stringify(questionType) +
138
+ `;</script>${template}${hotReloadScript}`;
139
+ return c.html(template);
140
+ });
141
+ app.use("/assets/*", serveStatic({ root }));
142
+ // Set up file watcher
143
+ const watchDir = cwd();
144
+ console.log(`📁 Watching ${watchDir} for changes...`);
145
+ let wss; // Will be initialized after server starts
146
+ const watcher = watch(watchDir, { recursive: true }, (filename) => {
147
+ if (filename && !filename.startsWith(".")) {
148
+ // Broadcast reload message to all connected clients
149
+ if (wss) {
150
+ wss.clients.forEach((client) => {
151
+ if (client.readyState === 1) {
152
+ // WebSocket.OPEN
153
+ client.send(JSON.stringify({ type: "reload" }));
154
+ }
155
+ });
156
+ }
157
+ }
158
+ });
159
+ // Clean up watcher on process exit
160
+ process.on("SIGINT", () => {
161
+ watcher.close();
162
+ process.exit(0);
163
+ });
164
+ serve({ fetch: app.fetch, port: 5777 }, (address) => {
165
+ const port = address.port;
166
+ wsPort = port + 1234;
167
+ const wsServer = createHttpServer();
168
+ wss = new WebSocketServer({ server: wsServer });
169
+ wsServer.listen(wsPort);
170
+ const url = `http://localhost:${port}`;
171
+ console.log(`🚀 Preview server running at ${url}`);
172
+ open(url);
173
+ });
174
+ };
@@ -0,0 +1,5 @@
1
+ #! /usr/bin/env node
2
+ export declare const uploadCommand: (argv: {
3
+ host: string;
4
+ key: string;
5
+ }) => Promise<void>;
@@ -0,0 +1,138 @@
1
+ #! /usr/bin/env node
2
+ import { readFile } from "fs/promises";
3
+ import { resolve } from "path";
4
+ import { cwd, exit } from "process";
5
+ import { setApiHost, setApiKey, uploadFile } from "../lib/api.js";
6
+ import { buildComponent } from "../lib/bundle.js";
7
+ import { deepCompileJsonata } from "../lib/jsonata.js";
8
+ import { readMetaFile } from "../lib/read-meta.js";
9
+ import { buildStyles } from "../lib/styles.js";
10
+ export const uploadCommand = async (argv) => {
11
+ setApiHost(argv.host);
12
+ setApiKey(argv.key);
13
+ const dir = cwd();
14
+ let definition = {};
15
+ try {
16
+ ({ definition } = await readMetaFile());
17
+ }
18
+ catch (e) {
19
+ console.error(`🚫 ${e.message}`);
20
+ exit(1);
21
+ }
22
+ console.log(`Uploading question type: ${definition.id}...`);
23
+ if (!definition.id) {
24
+ console.error(`🚫 Missing required field: id`);
25
+ exit(1);
26
+ }
27
+ if (!definition.name) {
28
+ console.error(`🚫 Missing required field: name`);
29
+ exit(1);
30
+ }
31
+ // Validate JSONata expressions in the definition
32
+ if (definition.export?.qti?.interaction?.options) {
33
+ try {
34
+ await deepCompileJsonata(definition.export.qti.interaction.options);
35
+ }
36
+ catch (e) {
37
+ console.error(`🚫 Invalid JSONata expression in export.qti.interaction.options: ${e.message}`);
38
+ exit(1);
39
+ }
40
+ }
41
+ // Bundle and upload components
42
+ const components = {};
43
+ const validComponentTypes = [
44
+ "settings-area",
45
+ "assessment",
46
+ "print",
47
+ "results",
48
+ ];
49
+ for (const [componentType, componentPath] of Object.entries(definition.components || {})) {
50
+ // Validation
51
+ if (!validComponentTypes.includes(componentType)) {
52
+ console.error(`🚫 Invalid component type specified: ${componentType}`);
53
+ exit(1);
54
+ }
55
+ const realPath = resolve(dir, componentPath);
56
+ if (!realPath) {
57
+ console.error(`🚫 Missing required file: ${componentPath}`);
58
+ exit(1);
59
+ }
60
+ // Bundle component
61
+ let componentOutput = { js: "" };
62
+ try {
63
+ componentOutput = await buildComponent(realPath);
64
+ const sizeInKb = componentOutput.js.length / 1000;
65
+ const sizeHint = `(${sizeInKb.toFixed(2)} kB)`;
66
+ console.log(`• Bundled component ${componentType} from ${componentPath} ${sizeHint}`);
67
+ if (sizeInKb > 100) {
68
+ console.warn(` ⚠️ Component ${componentType} is larger than 100 kB! Consider optimizing it.`);
69
+ if (process.env.GITHUB_ACTIONS) {
70
+ console.log(`::warning file=${realPath},title=Large component size::Component ${componentType} is larger than 100 kB ${sizeHint}! Consider optimizing it.`);
71
+ }
72
+ }
73
+ }
74
+ catch (error) {
75
+ console.error(`🚫 Error bundling component ${componentType} from ${componentPath}: ${error.message}`);
76
+ exit(1);
77
+ }
78
+ // Upload source map first if it exists
79
+ let sourceMapStatement = "";
80
+ if (componentOutput.sourcemap) {
81
+ const url = await uploadFile(`component-${componentType}.js.map`, componentOutput.sourcemap, "application/json");
82
+ sourceMapStatement = `\n//# sourceMappingURL=${url}`;
83
+ }
84
+ // Upload component
85
+ components[componentType] = await uploadFile(`component-${componentType}.js`, componentOutput.js + sourceMapStatement, "application/javascript");
86
+ console.log(`• Uploaded component ${componentType}`);
87
+ }
88
+ definition.components = components;
89
+ // Upload styles if present
90
+ if (Object.keys(definition.components).length) {
91
+ const styles = await buildStyles();
92
+ if (styles) {
93
+ definition.stylesheet = await uploadFile("styles.css", styles, "text/css");
94
+ const sizeInKb = styles.length / 1000;
95
+ const sizeHint = `(${sizeInKb.toFixed(2)} kB)`;
96
+ console.log(`• Uploaded styles ${sizeHint}`);
97
+ }
98
+ }
99
+ // Upload icon if specified
100
+ if (definition.icon) {
101
+ const iconPath = resolve(dir, definition.icon);
102
+ let iconContents;
103
+ try {
104
+ iconContents = await readFile(iconPath, "utf-8");
105
+ }
106
+ catch (error) {
107
+ console.error(`🚫 Error reading icon file: ${error.message}`);
108
+ exit(1);
109
+ }
110
+ if (!iconContents) {
111
+ console.error(`🚫 Icon file is empty: ${iconPath}`);
112
+ exit(1);
113
+ }
114
+ definition.icon = await uploadFile("icon.svg", iconContents, "image/svg+xml");
115
+ console.log(`• Uploaded icon`);
116
+ }
117
+ // Upload the main question type definition
118
+ const res = await fetch(`${argv.host}/question-types`, {
119
+ headers: {
120
+ Authorization: `Bearer ${argv.key}`,
121
+ "Content-Type": "application/json",
122
+ },
123
+ method: "POST",
124
+ body: JSON.stringify(definition),
125
+ });
126
+ if (!res.ok) {
127
+ console.error(`🚫 Uploading question type failed`);
128
+ try {
129
+ const errorResponse = await res.json();
130
+ console.log(errorResponse);
131
+ }
132
+ catch (e) {
133
+ console.log(e);
134
+ }
135
+ exit(1);
136
+ }
137
+ console.log(`• Finished uploading definition\n`);
138
+ };
@@ -0,0 +1,2 @@
1
+ #! /usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,37 @@
1
+ #! /usr/bin/env node
2
+ import yargs from "yargs";
3
+ import { hideBin } from "yargs/helpers";
4
+ import { previewCommand } from "./commands/preview.js";
5
+ import { uploadCommand } from "./commands/upload.js";
6
+ yargs()
7
+ .usage("examplary <cmd> [args]")
8
+ .command("upload", "Bundle and upload a question type definition", (yargs) => {
9
+ return yargs
10
+ .option("key", {
11
+ type: "string",
12
+ description: "API key for authentication (uses EXAMPLARY_API_KEY env var if not provided)",
13
+ default: process.env.EXAMPLARY_API_KEY || "",
14
+ })
15
+ .option("host", {
16
+ type: "string",
17
+ description: "API host to upload to",
18
+ default: "https://api.examplary.ai",
19
+ });
20
+ }, (args) => uploadCommand({ host: args.host, key: args.key }))
21
+ .command("preview", "Run local development server", (yargs) => {
22
+ return yargs
23
+ .option("key", {
24
+ type: "string",
25
+ description: "API key for authentication (uses EXAMPLARY_API_KEY env var if not provided)",
26
+ default: process.env.EXAMPLARY_API_KEY || "",
27
+ })
28
+ .option("host", {
29
+ type: "string",
30
+ description: "API host to upload to",
31
+ default: "https://api.examplary.ai",
32
+ });
33
+ }, (args) => previewCommand({ host: args.host, key: args.key }))
34
+ .help()
35
+ .recommendCommands()
36
+ .demandCommand()
37
+ .parse(hideBin(process.argv));
@@ -0,0 +1,5 @@
1
+ export declare let API_HOST: string;
2
+ export declare let API_KEY: string;
3
+ export declare const setApiKey: (key: string) => void;
4
+ export declare const setApiHost: (host: string) => void;
5
+ export declare const uploadFile: (fileName: string, fileContents: any, contentType: string) => Promise<any>;
@@ -0,0 +1,47 @@
1
+ import { basename } from "path";
2
+ import { exit } from "process";
3
+ // These get overwritten by the CLI options
4
+ export let API_HOST = "https://api.examplary.ai";
5
+ export let API_KEY = process.env.EXAMPLARY_API_KEY || "";
6
+ export const setApiKey = (key) => {
7
+ if (!key)
8
+ return;
9
+ API_KEY = key;
10
+ };
11
+ export const setApiHost = (host) => {
12
+ if (!host)
13
+ return;
14
+ API_HOST = host;
15
+ };
16
+ export const uploadFile = async (fileName, fileContents, contentType) => {
17
+ const params = new URLSearchParams();
18
+ params.append("filename", basename(fileName));
19
+ params.append("contentType", contentType);
20
+ params.append("type", "question-type");
21
+ // Request the upload URL
22
+ const res = await fetch(`${API_HOST}/media/upload?${params.toString()}`, {
23
+ headers: {
24
+ Authorization: `Bearer ${API_KEY}`,
25
+ },
26
+ });
27
+ if (!res.ok) {
28
+ const errorText = await res.text();
29
+ console.error(`🚫 Upload failed: ${errorText}`);
30
+ exit(1);
31
+ }
32
+ // Actually upload the file
33
+ const { uploadUrl, publicUrl } = await res.json();
34
+ const uploadRes = await fetch(uploadUrl, {
35
+ method: "PUT",
36
+ headers: {
37
+ "Content-Type": contentType,
38
+ },
39
+ body: fileContents,
40
+ });
41
+ if (!uploadRes.ok) {
42
+ const errorText = await uploadRes.text();
43
+ console.error(`🚫 Upload failed: ${errorText}`);
44
+ exit(1);
45
+ }
46
+ return publicUrl;
47
+ };
@@ -0,0 +1,4 @@
1
+ export declare const buildComponent: (file: string) => Promise<{
2
+ js: string;
3
+ sourcemap: string;
4
+ }>;
@@ -0,0 +1,27 @@
1
+ import { build } from "esbuild";
2
+ export const buildComponent = async (file) => {
3
+ const res = await build({
4
+ entryPoints: [file],
5
+ bundle: true,
6
+ write: false,
7
+ minify: true,
8
+ sourcemap: "inline",
9
+ platform: "browser",
10
+ format: "cjs",
11
+ external: [
12
+ "@examplary/ui",
13
+ "react",
14
+ "react-dom",
15
+ "react/jsx-runtime",
16
+ "react-dom/client",
17
+ ],
18
+ });
19
+ let js = res.outputFiles[0].text;
20
+ let sourcemap = "";
21
+ const sourceMapStatement = "\n//# sourceMappingURL=data:application/json;base64,";
22
+ if (js.includes(sourceMapStatement)) {
23
+ sourcemap = Buffer.from(js.split(sourceMapStatement)[1].trim(), "base64").toString("utf-8");
24
+ js = js.split(sourceMapStatement)[0].trim();
25
+ }
26
+ return { js, sourcemap };
27
+ };
@@ -0,0 +1 @@
1
+ export declare const deepCompileJsonata: (expression: any) => Promise<any>;
@@ -0,0 +1,26 @@
1
+ import jsonata from "jsonata";
2
+ export const deepCompileJsonata = async (expression) => {
3
+ if (typeof expression === "string" && expression.startsWith("=")) {
4
+ try {
5
+ jsonata(expression.substring(1));
6
+ }
7
+ catch (error) {
8
+ throw new Error(`JSONata evaluation failed for "${expression}": ${error.message}`);
9
+ }
10
+ }
11
+ if (Array.isArray(expression)) {
12
+ const output = [];
13
+ for (const o of expression) {
14
+ output.push(await deepCompileJsonata(o));
15
+ }
16
+ return output;
17
+ }
18
+ if (typeof expression === "object" && expression !== null) {
19
+ const result = {};
20
+ for (const [key, value] of Object.entries(expression)) {
21
+ result[key] = await deepCompileJsonata(value);
22
+ }
23
+ return result;
24
+ }
25
+ return expression;
26
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Reads and parses the question-type definition file.
3
+ * Prefers question-type.json over question-type.yml.
4
+ */
5
+ export declare const readMetaFile: () => Promise<{
6
+ definition: {
7
+ $schema?: string;
8
+ id?: string;
9
+ name?: string;
10
+ components?: Record<string, string>;
11
+ export?: any;
12
+ stylesheet?: string;
13
+ icon?: string;
14
+ };
15
+ path: string;
16
+ }>;
@@ -0,0 +1,36 @@
1
+ import { readFile } from "fs/promises";
2
+ import { join } from "path";
3
+ import { cwd } from "process";
4
+ import yaml from "yaml";
5
+ /**
6
+ * Reads and parses the question-type definition file.
7
+ * Prefers question-type.json over question-type.yml.
8
+ */
9
+ export const readMetaFile = async () => {
10
+ const dir = cwd();
11
+ const jsonPath = join(dir, "question-type.json");
12
+ const ymlPath = join(dir, "question-type.yml");
13
+ // Try JSON first, then YAML
14
+ for (const { path, parse, format } of [
15
+ { path: jsonPath, parse: (v) => JSON.parse(v), format: "JSON" },
16
+ { path: ymlPath, parse: (v) => yaml.parse(v), format: "YAML" },
17
+ ]) {
18
+ let contents;
19
+ try {
20
+ contents = await readFile(path, "utf-8");
21
+ }
22
+ catch {
23
+ continue;
24
+ }
25
+ try {
26
+ const definition = parse(contents);
27
+ if (definition.$schema)
28
+ delete definition.$schema;
29
+ return { definition, path };
30
+ }
31
+ catch (error) {
32
+ throw new Error(`Invalid ${format} in ${path}: ${error.message}`);
33
+ }
34
+ }
35
+ throw new Error("No question-type.json or question-type.yml found in the current directory");
36
+ };
@@ -0,0 +1 @@
1
+ export declare const buildStyles: () => Promise<string | null>;
@@ -0,0 +1,24 @@
1
+ import { createRequire } from "module";
2
+ import path from "path";
3
+ import tailwindcss from "@tailwindcss/postcss";
4
+ import postcss from "postcss";
5
+ const require = createRequire(import.meta.url);
6
+ const tailwindDir = path.dirname(require.resolve("tailwindcss/package.json"));
7
+ export const buildStyles = async () => {
8
+ const absolutePath = path.resolve(tailwindDir);
9
+ const inputCSS = `
10
+ @import "${absolutePath}/theme.css" layer(theme);
11
+ @import "${absolutePath}/utilities.css" layer(utilities);
12
+ `;
13
+ const result = await postcss([
14
+ tailwindcss({
15
+ optimize: { minify: true },
16
+ }),
17
+ ]).process(inputCSS, { from: "module-css.css" });
18
+ const utilitiesStyles = result.css.split("@layer utilities")[1].trim();
19
+ if (utilitiesStyles.length < 5) {
20
+ return null;
21
+ }
22
+ const css = result.css.replace(/@layer /g, "@layer modules_");
23
+ return css;
24
+ };