Subproject update
This commit is contained in:
parent
8d0d645487
commit
30d9ad05ac
11 changed files with 478 additions and 37 deletions
17
dbg.mjs
Normal file
17
dbg.mjs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { readFile } from "node:fs/promises";
|
||||
import { lex } from "./views/engine/lexer.ts";
|
||||
import { parse } from "./views/engine/parser.ts";
|
||||
import { compileTemplate } from "./views/engine/compiler.ts";
|
||||
const src = await readFile(
|
||||
"./critdotrip/muddle/modules/app/views/index.spug",
|
||||
"utf8",
|
||||
);
|
||||
const tokens = lex(src);
|
||||
const parsed = parse(tokens);
|
||||
console.log("frontmatter:", JSON.stringify(parsed.frontmatter));
|
||||
const { source } = compileTemplate(parsed.nodes, {
|
||||
path: "/x/index.spug",
|
||||
dir: "/x",
|
||||
frontmatter: "",
|
||||
});
|
||||
console.log(source.slice(0, 1200));
|
||||
18
dbg2.mjs
Normal file
18
dbg2.mjs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { readFile } from "node:fs/promises";
|
||||
import { lex } from "./views/engine/lexer.ts";
|
||||
import { parse } from "./views/engine/parser.ts";
|
||||
import { compileTemplate } from "./views/engine/compiler.ts";
|
||||
const src = await readFile(
|
||||
"./critdotrip/muddle/modules/app/views/index.spug",
|
||||
"utf8",
|
||||
);
|
||||
const tokens = lex(src);
|
||||
const parsed = parse(tokens);
|
||||
const { source } = compileTemplate(parsed.nodes, {
|
||||
path: "/x/index.spug",
|
||||
dir: "/x",
|
||||
frontmatter: parsed.frontmatter || "",
|
||||
});
|
||||
const i = source.indexOf("hellings");
|
||||
console.log("index", i);
|
||||
if (i >= 0) console.log(source.slice(Math.max(0, i - 400), i + 200));
|
||||
19
dbg3.mjs
Normal file
19
dbg3.mjs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { readFile } from "node:fs/promises";
|
||||
import { lex } from "./views/engine/lexer.ts";
|
||||
import { parse } from "./views/engine/parser.ts";
|
||||
import { compileTemplate } from "./views/engine/compiler.ts";
|
||||
const src = await readFile(
|
||||
"./critdotrip/muddle/modules/app/views/index.spug",
|
||||
"utf8",
|
||||
);
|
||||
const parsed = parse(lex(src));
|
||||
const { source } = compileTemplate(parsed.nodes, {
|
||||
path: "/x/i.spug",
|
||||
dir: "/x",
|
||||
frontmatter: parsed.frontmatter || "",
|
||||
});
|
||||
const a = source.indexOf("with (__ctx) {");
|
||||
const b = source.indexOf('return __out.join(""");', a);
|
||||
console.log("TEMPLATE BODY START", a);
|
||||
console.log(source.slice(a, b).slice(0, 400));
|
||||
console.log("...LEN", b - a);
|
||||
94
deno.lock
Normal file
94
deno.lock
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
{
|
||||
"version": "5",
|
||||
"specifiers": {
|
||||
"jsr:@std/fs@*": "1.0.24",
|
||||
"jsr:@std/internal@^1.0.14": "1.0.14",
|
||||
"jsr:@std/path@^1.1.5": "1.1.6"
|
||||
},
|
||||
"jsr": {
|
||||
"@std/fs@1.0.24": {
|
||||
"integrity": "f3061b45b81673a2bece689da041df32d174be064c89eb6397fb5718d3fb7877",
|
||||
"dependencies": [
|
||||
"jsr:@std/path"
|
||||
]
|
||||
},
|
||||
"@std/internal@1.0.14": {
|
||||
"integrity": "291516b3d4c35024d6ffbc0a9df5bf4c64116e05b50012cf846710152d2ffdf7"
|
||||
},
|
||||
"@std/path@1.1.6": {
|
||||
"integrity": "c68485c2a4dfbb5ae3cc74fae4e8c4e5d874cf8a8ed12927917235c758b46cbe",
|
||||
"dependencies": [
|
||||
"jsr:@std/internal"
|
||||
]
|
||||
}
|
||||
},
|
||||
"workspace": {
|
||||
"packageJson": {
|
||||
"dependencies": [
|
||||
"npm:@eslint/js@^10.0.1",
|
||||
"npm:@types/bun@latest",
|
||||
"npm:@types/pg@^8.15.5",
|
||||
"npm:lightningcss@^1.33.0",
|
||||
"npm:playwright@^1.64.1",
|
||||
"npm:prettier@^3.9.10",
|
||||
"npm:sass@^1.106.1",
|
||||
"npm:typescript-eslint@^8.72.0"
|
||||
]
|
||||
},
|
||||
"members": {
|
||||
"modules/astro": {
|
||||
"packageJson": {
|
||||
"dependencies": [
|
||||
"npm:@astrojs/compiler@2",
|
||||
"npm:astro@5",
|
||||
"npm:typescript@5.9.3"
|
||||
]
|
||||
}
|
||||
},
|
||||
"modules/database": {
|
||||
"packageJson": {
|
||||
"dependencies": [
|
||||
"npm:pg@^8.23.1"
|
||||
]
|
||||
}
|
||||
},
|
||||
"modules/latex": {
|
||||
"packageJson": {
|
||||
"dependencies": [
|
||||
"npm:katex@0.19"
|
||||
]
|
||||
}
|
||||
},
|
||||
"modules/mdx": {
|
||||
"packageJson": {
|
||||
"dependencies": [
|
||||
"npm:@mdx-js/mdx@^3.1.1",
|
||||
"npm:@types/react-dom@^19.3.0",
|
||||
"npm:@types/react@^19.3.0",
|
||||
"npm:react-dom@^19.3.1",
|
||||
"npm:react@^19.3.1"
|
||||
]
|
||||
}
|
||||
},
|
||||
"modules/optimize": {
|
||||
"packageJson": {
|
||||
"dependencies": [
|
||||
"npm:@types/csso@^5.0.5",
|
||||
"npm:@types/html-minifier-terser@^7.0.2",
|
||||
"npm:csso@^5.0.5",
|
||||
"npm:html-minifier-terser@^7.2.0",
|
||||
"npm:svgo@^4.1.0",
|
||||
"npm:terser@^5.51.2"
|
||||
]
|
||||
}
|
||||
},
|
||||
"modules/tailwind": {
|
||||
"packageJson": {
|
||||
"dependencies": [
|
||||
"npm:tailwindcss@^4.3.3"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,32 +10,8 @@ bun add muddle@git+https://git.crit.rip/mdl/muddle.git
|
|||
npm install muddle@git+https://git.crit.rip/mdl/muddle.git
|
||||
```
|
||||
|
||||
Integrations are isolated `@muddle/*` modules. After their Forgejo repositories are provisioned, install one independently.
|
||||
Integrations are `@muddle/*` modules.
|
||||
|
||||
```sh
|
||||
bun add @muddle/shadcn@git+https://git.crit.rip/mdl/shadcn.git
|
||||
```
|
||||
|
||||
Create a Forgejo personal access token with repository creation rights, then provision the namespace repositories. The script only reads `MUDDLE_FORGEJO_TOKEN` from the environment and never prints it.
|
||||
|
||||
```sh
|
||||
MUDDLE_FORGEJO_TOKEN=token bun scripts/forgejo-modules.ts
|
||||
```
|
||||
|
||||
Preview without network writes.
|
||||
|
||||
```sh
|
||||
bun scripts/forgejo-modules.ts --dry-run
|
||||
```
|
||||
|
||||
To commit changed integration module directories with generic `chore: update @muddle/<name> integration` messages and push their subtree history, preview the operation first. Unchanged modules are also synced so newly created Forgejo repositories receive their initial branch.
|
||||
|
||||
```sh
|
||||
bun scripts/forgejo-push.ts
|
||||
```
|
||||
|
||||
Push only after review. This requires the same token and creates local commits.
|
||||
|
||||
```sh
|
||||
MUDDLE_FORGEJO_TOKEN=token bun scripts/forgejo-push.ts --push
|
||||
```
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@
|
|||
"./contracts": "./contracts/index.ts",
|
||||
"./contracts/*": "./contracts/*",
|
||||
"./metadata": "./core/metadata.ts",
|
||||
"./decorators": "./decorators/index.ts",
|
||||
"./markdown": "./views/markdown.ts",
|
||||
"./views": "./views/api.ts",
|
||||
"./views/integrations": "./views/integrations.ts",
|
||||
|
|
|
|||
72
tests/stcss-modules.test.ts
Normal file
72
tests/stcss-modules.test.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { expect, test } from "bun:test";
|
||||
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { compileStcssModule } from "../views/css/modules.ts";
|
||||
import { createViews } from "../views/api.ts";
|
||||
|
||||
test("STCSS modules scope classes and inline relative imports", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "muddle-stcss-module-"));
|
||||
try {
|
||||
const imported = join(root, "tokens.stcss");
|
||||
await writeFile(imported, ".token { color: red; }");
|
||||
const result = await compileStcssModule(
|
||||
'@import "./tokens.stcss"; .card { color: blue; }',
|
||||
{ filename: join(root, "card.module.stcss"), root },
|
||||
);
|
||||
expect(result.classes.card).toBeDefined();
|
||||
expect(result.classes.token).toBeDefined();
|
||||
expect(result.css).toContain(`.${result.classes.card}`);
|
||||
expect(result.css).toContain(`.${result.classes.token}`);
|
||||
expect(result.css).not.toContain(".card {");
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("a component automatically receives its adjacent STCSS module", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "muddle-component-module-"));
|
||||
try {
|
||||
const components = join(root, "components");
|
||||
await mkdir(components);
|
||||
await writeFile(join(components, "badge.spug"), "span.badge badge");
|
||||
await writeFile(
|
||||
join(components, "badge.module.stcss"),
|
||||
".badge { color: rebeccapurple; }",
|
||||
);
|
||||
await writeFile(join(root, "index.spug"), "component badge");
|
||||
const html = await createViews({
|
||||
root,
|
||||
viewsDir: root,
|
||||
componentsDir: components,
|
||||
}).render("index.spug", {});
|
||||
expect(html).toContain("<style>");
|
||||
expect(html).toMatch(/class="m[^"]+"/);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("components resolve an installed package and its component entry", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "muddle-package-component-"));
|
||||
try {
|
||||
const packageRoot = join(root, "node_modules", "@muddle", "badge");
|
||||
await mkdir(packageRoot, { recursive: true });
|
||||
await writeFile(
|
||||
join(packageRoot, "package.json"),
|
||||
'{"name":"@muddle/badge"}',
|
||||
);
|
||||
await writeFile(
|
||||
join(packageRoot, "component.spug"),
|
||||
"strong.badge package badge",
|
||||
);
|
||||
await writeFile(join(root, "index.spug"), "component @muddle/badge");
|
||||
const html = await createViews({ root, viewsDir: root }).render(
|
||||
"index.spug",
|
||||
{},
|
||||
);
|
||||
expect(html).toContain("package badge");
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
|
@ -13,6 +13,12 @@ export { compileCss, type CompileOptions } from "./compiler.ts";
|
|||
export { evaluate, darken, lighten, rgba } from "./evaluate.ts";
|
||||
export { parseCss } from "./parser.ts";
|
||||
export { sassLoader, type SassLoaderOptions } from "./sass.ts";
|
||||
export {
|
||||
compileStcssModule,
|
||||
resolveStyle,
|
||||
type StcssModule,
|
||||
type StcssModuleOptions,
|
||||
} from "./modules.ts";
|
||||
|
||||
export interface StcssLoaderOptions extends CompileOptions {
|
||||
/** Directory the loader scans, relative to the project root. */
|
||||
|
|
|
|||
138
views/css/modules.ts
Normal file
138
views/css/modules.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import { existsSync } from "node:fs";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
import { transform } from "lightningcss";
|
||||
import { compileCss, type CompileOptions } from "./compiler.ts";
|
||||
|
||||
export interface StcssModule {
|
||||
/** The minified, scoped stylesheet. */
|
||||
css: string;
|
||||
/** Original local class name to emitted class name. */
|
||||
classes: Record<string, string>;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface StcssModuleOptions extends CompileOptions {
|
||||
/** Path used to resolve relative and package imports. */
|
||||
filename?: string;
|
||||
/** Project root used to find node_modules. */
|
||||
root?: string;
|
||||
/** Emits compact CSS, enabled by default. */
|
||||
minify?: boolean;
|
||||
}
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
/**
|
||||
* Compiles a component stylesheet. Local classes are deterministically scoped
|
||||
* and the resulting browser stylesheet is minified by Lightning CSS.
|
||||
*/
|
||||
export async function compileStcssModule(
|
||||
source: string,
|
||||
options: StcssModuleOptions = {},
|
||||
): Promise<StcssModule> {
|
||||
const filename = options.filename ?? "component.module.stcss";
|
||||
const imported = await inlineImports(source, filename, options, new Set());
|
||||
const names = localClasses(imported);
|
||||
const classes = Object.fromEntries(
|
||||
names.map((name, index) => [name, mangle(filename, index)]),
|
||||
);
|
||||
const scoped = imported.replace(/\.([_a-zA-Z][\w-]*)/g, (all, name) =>
|
||||
classes[name] ? `.${classes[name]}` : all,
|
||||
);
|
||||
const structured = compileCss(scoped, options);
|
||||
const css = transform({
|
||||
filename,
|
||||
code: Buffer.from(structured),
|
||||
minify: options.minify !== false,
|
||||
}).code.toString();
|
||||
return { css, classes, path: filename };
|
||||
}
|
||||
|
||||
function localClasses(source: string): string[] {
|
||||
return [
|
||||
...new Set(
|
||||
[...source.matchAll(/\.([_a-zA-Z][\w-]*)/g)].map(
|
||||
(match) => match[1] as string,
|
||||
),
|
||||
),
|
||||
]
|
||||
.filter((name) => !name.startsWith("global-"))
|
||||
.sort();
|
||||
}
|
||||
|
||||
function mangle(filename: string, index: number): string {
|
||||
let hash = 5381;
|
||||
for (const char of filename) hash = (hash * 33) ^ char.charCodeAt(0);
|
||||
return `m${(hash >>> 0).toString(36)}${index.toString(36)}`;
|
||||
}
|
||||
|
||||
async function inlineImports(
|
||||
source: string,
|
||||
from: string,
|
||||
options: StcssModuleOptions,
|
||||
seen: Set<string>,
|
||||
): Promise<string> {
|
||||
const imports = [
|
||||
...source.matchAll(/@import\s+(?:url\()?\s*["']([^"']+)["']\s*\)?\s*;?/g),
|
||||
];
|
||||
let output = source;
|
||||
for (const match of imports) {
|
||||
const specifier = match[1] as string;
|
||||
if (/^(?:https?:|data:)/.test(specifier)) continue;
|
||||
const path = resolveStyle(specifier, from, options.root);
|
||||
if (!path || seen.has(path)) {
|
||||
output = output.replace(match[0], "");
|
||||
continue;
|
||||
}
|
||||
seen.add(path);
|
||||
const nested = await inlineImports(
|
||||
await readFile(path, "utf8"),
|
||||
path,
|
||||
options,
|
||||
seen,
|
||||
);
|
||||
output = output.replace(match[0], nested);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/** Resolves relative styles and stylesheet files exposed by installed packages. */
|
||||
export function resolveStyle(
|
||||
specifier: string,
|
||||
from: string,
|
||||
root?: string,
|
||||
): string | undefined {
|
||||
const candidates: string[] = [];
|
||||
if (specifier.startsWith(".")) {
|
||||
candidates.push(resolve(dirname(from), specifier));
|
||||
} else {
|
||||
try {
|
||||
candidates.push(
|
||||
require.resolve(specifier, {
|
||||
paths: [dirname(from), root].filter(Boolean) as string[],
|
||||
}),
|
||||
);
|
||||
} catch {}
|
||||
const packageName = specifier.startsWith("@")
|
||||
? specifier.split("/").slice(0, 2).join("/")
|
||||
: (specifier.split("/")[0] as string);
|
||||
const rest = specifier.slice(packageName.length).replace(/^\//, "");
|
||||
for (const base of [dirname(from), root].filter(Boolean) as string[]) {
|
||||
candidates.push(join(base, "node_modules", packageName, rest));
|
||||
}
|
||||
}
|
||||
for (const candidate of candidates) {
|
||||
for (const path of [
|
||||
candidate,
|
||||
`${candidate}.stcss`,
|
||||
`${candidate}.css`,
|
||||
join(candidate, "index.stcss"),
|
||||
join(candidate, "index.css"),
|
||||
]) {
|
||||
if (existsSync(path)) return path;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { existsSync, statSync } from "node:fs";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { dirname, isAbsolute, join, resolve } from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
import { compileTemplate } from "./compiler.ts";
|
||||
import { lex } from "./lexer.ts";
|
||||
import { collectProps, parse } from "./parser.ts";
|
||||
|
|
@ -12,6 +13,7 @@ import {
|
|||
type SpugComponentRender,
|
||||
} from "../components.ts";
|
||||
import { compileCss, styleTag } from "../css/index.ts";
|
||||
import { compileStcssModule } from "../css/modules.ts";
|
||||
import { isRaw, raw, styleValue } from "../raw.ts";
|
||||
import type { DeferredChunk } from "../../contracts/typegen/http/response.ts";
|
||||
import type { MuddleHooks } from "../hooks.ts";
|
||||
|
|
@ -411,8 +413,9 @@ export class TemplateEngine {
|
|||
name: string,
|
||||
props: Record<string, unknown>,
|
||||
context?: Partial<RenderContext>,
|
||||
from?: string,
|
||||
): Promise<string> {
|
||||
const path = this.resolveComponent(name);
|
||||
const path = this.resolveComponent(name, from);
|
||||
const compiled = await this.load(path);
|
||||
props = { ...props };
|
||||
this.fillLocals(compiled, props);
|
||||
|
|
@ -423,7 +426,8 @@ export class TemplateEngine {
|
|||
context,
|
||||
);
|
||||
await this.runFrontmatter(compiled, props, runtime);
|
||||
return compiled.template(runtime, props);
|
||||
const html = await compiled.template(runtime, props);
|
||||
return this.attachComponentModule(path, html);
|
||||
}
|
||||
|
||||
/** Renders a component and marks it for client side hydration. */
|
||||
|
|
@ -437,9 +441,12 @@ export class TemplateEngine {
|
|||
return `${html}<script type="application/json" data-muddle-island="${name}">${payload}</script>`;
|
||||
}
|
||||
|
||||
private resolveComponent(name: string): string {
|
||||
private resolveComponent(name: string, from?: string): string {
|
||||
if (isAbsolute(name) && existsSync(name)) return name;
|
||||
const candidates = [
|
||||
...(from && name.startsWith(".")
|
||||
? [resolve(dirname(from), name), resolve(dirname(from), `${name}.spug`)]
|
||||
: []),
|
||||
join(this.componentsDir, `${name}.spug`),
|
||||
join(this.componentsDir, name),
|
||||
join(this.viewsDir, `${name}.spug`),
|
||||
|
|
@ -450,9 +457,68 @@ export class TemplateEngine {
|
|||
return candidate;
|
||||
}
|
||||
}
|
||||
const packagePath = this.resolvePackageComponent(name, from);
|
||||
if (packagePath) return packagePath;
|
||||
throw new Error(`Component not found: ${name}`);
|
||||
}
|
||||
|
||||
private resolvePackageComponent(
|
||||
name: string,
|
||||
from?: string,
|
||||
): string | undefined {
|
||||
if (name.startsWith(".")) return undefined;
|
||||
const packageName = name.startsWith("@")
|
||||
? name.split("/").slice(0, 2).join("/")
|
||||
: (name.split("/")[0] as string);
|
||||
const subpath = name.slice(packageName.length).replace(/^\//, "");
|
||||
const require = createRequire(
|
||||
from ?? join(this.options.root ?? this.viewsDir, "package.json"),
|
||||
);
|
||||
let packageJson: string;
|
||||
try {
|
||||
packageJson = require.resolve(`${packageName}/package.json`);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
const root = dirname(packageJson);
|
||||
const candidates = [
|
||||
subpath && join(root, subpath),
|
||||
subpath && join(root, `${subpath}.spug`),
|
||||
join(root, "component.spug"),
|
||||
join(root, "index.spug"),
|
||||
].filter(Boolean) as string[];
|
||||
return candidates.find(
|
||||
(candidate) => existsSync(candidate) && statSync(candidate).isFile(),
|
||||
);
|
||||
}
|
||||
|
||||
private async attachComponentModule(
|
||||
path: string,
|
||||
html: string,
|
||||
): Promise<string> {
|
||||
const modulePath = path.replace(/\.spug$/, ".module.stcss");
|
||||
if (!existsSync(modulePath)) return html;
|
||||
const module = await compileStcssModule(
|
||||
await readFile(modulePath, "utf8"),
|
||||
{
|
||||
filename: modulePath,
|
||||
root: this.options.root,
|
||||
hooks: this.options.hooks,
|
||||
},
|
||||
);
|
||||
const scopedHtml = html.replace(
|
||||
/\bclass=(['"])([^'"]*)\1/g,
|
||||
(_all, quote: string, value: string) => {
|
||||
const classes = value
|
||||
.split(/\s+/)
|
||||
.map((name) => module.classes[name] ?? name)
|
||||
.join(" ");
|
||||
return `class=${quote}${classes}${quote}`;
|
||||
},
|
||||
);
|
||||
return `${styleTag(module.css)}${scopedHtml}`;
|
||||
}
|
||||
|
||||
private createRuntime(
|
||||
compiled: CompiledTemplate,
|
||||
locals: Record<string, unknown>,
|
||||
|
|
@ -490,7 +556,12 @@ export class TemplateEngine {
|
|||
renderComponent: async (name, props) => {
|
||||
const definition = this.components.get(name);
|
||||
if (!definition) {
|
||||
return this.renderComponent(name, props, renderContext);
|
||||
return this.renderComponent(
|
||||
name,
|
||||
props,
|
||||
renderContext,
|
||||
compiled.path,
|
||||
);
|
||||
}
|
||||
if (definition.deferred) {
|
||||
deferredCount += 1;
|
||||
|
|
@ -552,9 +623,17 @@ export class TemplateEngine {
|
|||
const css = await Promise.all(
|
||||
styles.map(async (style) => {
|
||||
const path = resolve(this.componentsDir, style);
|
||||
return existsSync(path)
|
||||
? compileCss(await readFile(path, "utf8"))
|
||||
: compileCss(style);
|
||||
if (!existsSync(path)) return compileCss(style);
|
||||
const source = await readFile(path, "utf8");
|
||||
return path.endsWith(".module.stcss")
|
||||
? (
|
||||
await compileStcssModule(source, {
|
||||
filename: path,
|
||||
root: this.options.root,
|
||||
hooks: this.options.hooks,
|
||||
})
|
||||
).css
|
||||
: compileCss(source, { hooks: this.options.hooks });
|
||||
}),
|
||||
);
|
||||
return `${css.filter(Boolean).map(styleTag).join("")}${html}`;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type {
|
|||
IntegrationContext,
|
||||
} from "../contracts/index.ts";
|
||||
import { compileCss, type CompileOptions } from "./css/index.ts";
|
||||
import { compileStcssModule } from "./css/modules.ts";
|
||||
import { markdownLoader } from "./markdown.ts";
|
||||
import { sassLoader, type SassLoaderOptions } from "./css/sass.ts";
|
||||
|
||||
|
|
@ -85,13 +86,22 @@ export function stcssIntegration(
|
|||
const dir = resolve(context.config.root, options.dir ?? "styles");
|
||||
if (!existsSync(dir)) return;
|
||||
|
||||
const files = (await readdir(dir)).filter((file) =>
|
||||
file.endsWith(".stcss"),
|
||||
);
|
||||
const files = await stcssFiles(dir);
|
||||
const parts: string[] = [];
|
||||
for (const file of files.sort()) {
|
||||
const source = await readFile(join(dir, file), "utf8");
|
||||
parts.push(compileCss(source, options));
|
||||
const path = join(dir, file);
|
||||
const source = await readFile(path, "utf8");
|
||||
parts.push(
|
||||
file.endsWith(".module.stcss")
|
||||
? (
|
||||
await compileStcssModule(source, {
|
||||
...options,
|
||||
filename: path,
|
||||
root: context.config.root,
|
||||
})
|
||||
).css
|
||||
: compileCss(source, options),
|
||||
);
|
||||
}
|
||||
const css = parts.join("\n");
|
||||
|
||||
|
|
@ -110,6 +120,17 @@ export function stcssIntegration(
|
|||
};
|
||||
}
|
||||
|
||||
async function stcssFiles(dir: string, prefix = ""): Promise<string[]> {
|
||||
const files: string[] = [];
|
||||
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
||||
const relative = `${prefix}${entry.name}`;
|
||||
if (entry.isDirectory())
|
||||
files.push(...(await stcssFiles(join(dir, entry.name), `${relative}/`)));
|
||||
else if (entry.name.endsWith(".stcss")) files.push(relative);
|
||||
}
|
||||
return files.sort();
|
||||
}
|
||||
|
||||
/** Registers Sass files as loaders and serves their compiled CSS alongside STCSS. */
|
||||
export function sassIntegration(options: SassLoaderOptions = {}): Integration {
|
||||
const route = "/_muddle/sass.css";
|
||||
|
|
|
|||
Loading…
Reference in a new issue