From 2c5da82c4db29b815cf2e75b652f19706d00aa83 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Tue, 30 Dec 2025 20:01:31 +0900 Subject: [PATCH 1/6] Rm console --- Cargo.lock | 63 ++++++ devup.schema.json | 306 ++++++++++++++++++++++++++ libs/sheet/Cargo.toml | 5 + libs/sheet/src/bin/generate_schema.rs | 8 + libs/sheet/src/theme.rs | 142 +++++++++++- packages/next-plugin/src/preload.ts | 129 ++++++----- 6 files changed, 586 insertions(+), 67 deletions(-) create mode 100644 devup.schema.json create mode 100644 libs/sheet/src/bin/generate_schema.rs diff --git a/Cargo.lock b/Cargo.lock index 78f24431..0c742a44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -319,6 +319,12 @@ version = "0.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d742b56656e8b14d63e7ea9806597b1849ae25412584c8adf78c0f67bd985e66" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "either" version = "1.15.0" @@ -1136,6 +1142,26 @@ dependencies = [ "bitflags", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "regex" version = "1.12.2" @@ -1258,6 +1284,31 @@ dependencies = [ "sdd", ] +[[package]] +name = "schemars" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" +dependencies = [ + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4908ad288c5035a8eb12cfdf0d49270def0a268ee162b75eeee0f85d155a7c45" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1329,6 +1380,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_json" version = "1.0.146" @@ -1378,6 +1440,7 @@ dependencies = [ "once_cell", "regex", "rstest", + "schemars", "serde", "serde_json", ] diff --git a/devup.schema.json b/devup.schema.json new file mode 100644 index 00000000..d56a36f4 --- /dev/null +++ b/devup.schema.json @@ -0,0 +1,306 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "DevupJson", + "description": "Root devup.json configuration", + "type": "object", + "properties": { + "theme": { + "description": "Theme configuration including colors, typography, and breakpoints", + "anyOf": [ + { + "$ref": "#/$defs/Theme" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "$defs": { + "ColorTheme": { + "description": "Color theme with color name to value mappings. Supports nested objects for color scales.", + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "description": "Color value (e.g., '#000', 'rgb(0,0,0)')", + "type": "string" + }, + { + "description": "Nested color object", + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/ColorValue" + } + } + ] + }, + "$defs": { + "ColorValue": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/ColorValue" + } + } + ] + } + } + }, + "Theme": { + "type": "object", + "properties": { + "breakpoints": { + "description": "Breakpoints in pixels for responsive typography (default: [0, 480, 768, 992, 1280, 1600])", + "type": "array", + "default": [0, 480, 768, 992, 1280, 1600], + "items": { + "type": "integer", + "format": "uint16", + "maximum": 65535, + "minimum": 0 + } + }, + "colors": { + "description": "Color themes by mode name (e.g., \"light\", \"dark\")", + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/ColorTheme" + }, + "default": {} + }, + "typography": { + "description": "Typography definitions by name (e.g., \"h1\", \"body\", \"caption\")", + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/Typographies" + }, + "default": {} + } + } + }, + "Typographies": { + "description": "Typography definition supporting both traditional array format and compact object format", + "oneOf": [ + { + "description": "Traditional array format: array of Typography objects or null for each breakpoint", + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/$defs/Typography" + }, + { + "type": "null" + } + ] + } + }, + { + "description": "Compact object format: each property can be a single value or array of values per breakpoint", + "type": "object", + "properties": { + "fontFamily": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + { + "type": "null" + } + ] + }, + "fontSize": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + { + "type": "null" + } + ] + }, + "fontStyle": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + { + "type": "null" + } + ] + }, + "fontWeight": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "array", + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + { + "type": "null" + } + ] + }, + "letterSpacing": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + { + "type": "null" + } + ] + }, + "lineHeight": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "array", + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + ] + }, + "Typography": { + "type": "object", + "properties": { + "fontFamily": { + "type": ["string", "null"] + }, + "fontSize": { + "type": ["string", "null"] + }, + "fontWeight": { + "default": null, + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "letterSpacing": { + "type": ["string", "null"] + }, + "lineHeight": { + "default": null, + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "null" + } + ] + } + } + } + } +} diff --git a/libs/sheet/Cargo.toml b/libs/sheet/Cargo.toml index 8e3ecfc1..17d3def1 100644 --- a/libs/sheet/Cargo.toml +++ b/libs/sheet/Cargo.toml @@ -10,6 +10,7 @@ serde_json = "1.0.146" regex = "1.12.2" once_cell = "1.21.3" extractor = { path = "../extractor" } +schemars = "1.2.0" [dev-dependencies] insta = "1.45.0" @@ -19,3 +20,7 @@ rstest = "0.26.1" [[bench]] name = "my_benchmark" harness = false + +[[bin]] +name = "generate-schema" +path = "src/bin/generate_schema.rs" diff --git a/libs/sheet/src/bin/generate_schema.rs b/libs/sheet/src/bin/generate_schema.rs new file mode 100644 index 00000000..8c0878a8 --- /dev/null +++ b/libs/sheet/src/bin/generate_schema.rs @@ -0,0 +1,8 @@ +use schemars::schema_for; +use sheet::theme::DevupJson; + +fn main() { + let schema = schema_for!(DevupJson); + let json = serde_json::to_string_pretty(&schema).unwrap(); + println!("{}", json); +} diff --git a/libs/sheet/src/theme.rs b/libs/sheet/src/theme.rs index 37e34bb8..dcac190f 100644 --- a/libs/sheet/src/theme.rs +++ b/libs/sheet/src/theme.rs @@ -1,8 +1,21 @@ use css::optimize_value::optimize_value; +use schemars::{JsonSchema, SchemaGenerator, json_schema}; +use schemars::Schema; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; use std::collections::{BTreeMap, HashMap}; +/// Schema helper for typography values that accept string or number +fn typography_value_schema(_generator: &mut SchemaGenerator) -> Schema { + json_schema!({ + "oneOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "null" } + ] + }) +} + /// ColorEntry stores both the original key (for TypeScript interface) and CSS key (for CSS variables) #[derive(Debug, Clone, Serialize)] pub struct ColorEntry { @@ -26,6 +39,42 @@ pub struct ColorTheme { entries: HashMap, } +impl JsonSchema for ColorTheme { + fn schema_name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("ColorTheme") + } + + fn json_schema(_generator: &mut SchemaGenerator) -> Schema { + json_schema!({ + "type": "object", + "additionalProperties": { + "oneOf": [ + { "type": "string", "description": "Color value (e.g., '#000', 'rgb(0,0,0)')" }, + { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/ColorValue" + }, + "description": "Nested color object" + } + ] + }, + "description": "Color theme with color name to value mappings. Supports nested objects for color scales.", + "$defs": { + "ColorValue": { + "oneOf": [ + { "type": "string" }, + { + "type": "object", + "additionalProperties": { "$ref": "#/$defs/ColorValue" } + } + ] + } + } + }) + } +} + /// Recursively flatten a JSON value into ColorEntry list /// interface_prefix uses dots, css_prefix uses dashes fn flatten_color_value( @@ -145,15 +194,17 @@ where StringOrNumber::Float(n) => Ok(Some(n.to_string())), } } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct Typography { pub font_family: Option, pub font_size: Option, #[serde(deserialize_with = "deserialize_string_from_number", default)] + #[schemars(schema_with = "typography_value_schema")] pub font_weight: Option, #[serde(deserialize_with = "deserialize_string_from_number", default)] + #[schemars(schema_with = "typography_value_schema")] pub line_height: Option, pub letter_spacing: Option, } @@ -184,6 +235,82 @@ impl From>> for Typographies { } } +impl JsonSchema for Typographies { + fn schema_name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("Typographies") + } + + fn json_schema(generator: &mut SchemaGenerator) -> Schema { + let typography_schema = generator.subschema_for::(); + json_schema!({ + "oneOf": [ + { + "type": "array", + "items": { + "oneOf": [ + typography_schema, + { "type": "null" } + ] + }, + "description": "Traditional array format: array of Typography objects or null for each breakpoint" + }, + { + "type": "object", + "properties": { + "fontFamily": { + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "oneOf": [{ "type": "string" }, { "type": "null" }] } }, + { "type": "null" } + ] + }, + "fontStyle": { + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "oneOf": [{ "type": "string" }, { "type": "null" }] } }, + { "type": "null" } + ] + }, + "fontWeight": { + "oneOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "array", "items": { "oneOf": [{ "type": "string" }, { "type": "number" }, { "type": "null" }] } }, + { "type": "null" } + ] + }, + "fontSize": { + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "oneOf": [{ "type": "string" }, { "type": "null" }] } }, + { "type": "null" } + ] + }, + "lineHeight": { + "oneOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "array", "items": { "oneOf": [{ "type": "string" }, { "type": "number" }, { "type": "null" }] } }, + { "type": "null" } + ] + }, + "letterSpacing": { + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "oneOf": [{ "type": "string" }, { "type": "null" }] } }, + { "type": "null" } + ] + } + }, + "additionalProperties": false, + "description": "Compact object format: each property can be a single value or array of values per breakpoint" + } + ], + "description": "Typography definition supporting both traditional array format and compact object format" + }) + } +} + /// Helper to deserialize a typography property that can be either a single value or an array fn deserialize_typo_prop(value: &Value) -> Result>, String> { match value { @@ -333,17 +460,28 @@ impl<'de> Deserialize<'de> for Typographies { } } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct Theme { + /// Color themes by mode name (e.g., "light", "dark") #[serde(default)] pub colors: BTreeMap, + /// Breakpoints in pixels for responsive typography (default: [0, 480, 768, 992, 1280, 1600]) #[serde(default = "default_breakpoints")] pub breakpoints: Vec, + /// Typography definitions by name (e.g., "h1", "body", "caption") #[serde(default)] pub typography: BTreeMap, } +/// Root devup.json configuration +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +pub struct DevupJson { + /// Theme configuration including colors, typography, and breakpoints + #[serde(default)] + pub theme: Option, +} + fn default_breakpoints() -> Vec { vec![0, 480, 768, 992, 1280, 1600] } diff --git a/packages/next-plugin/src/preload.ts b/packages/next-plugin/src/preload.ts index d3839eab..b4ada71a 100644 --- a/packages/next-plugin/src/preload.ts +++ b/packages/next-plugin/src/preload.ts @@ -1,65 +1,64 @@ -import { readFileSync, realpathSync, writeFileSync } from 'node:fs' -import { basename, dirname, join, relative } from 'node:path' - -import { codeExtract, getCss } from '@devup-ui/wasm' -import { globSync } from 'glob' - -import { findTopPackageRoot } from './find-top-package-root' -import { getPackageName } from './get-package-name' -import { hasLocalPackage } from './has-localpackage' - -export function preload( - excludeRegex: RegExp, - libPackage: string, - singleCss: boolean, - cssDir: string, - include: string[], - pwd = process.cwd(), -) { - if (include.length > 0 && hasLocalPackage()) { - const packageRoot = findTopPackageRoot() - const collected = globSync(['package.json', '!**/node_modules/**'], { - follow: true, - absolute: true, - cwd: packageRoot, - }) - .filter((file) => include.includes(getPackageName(file))) - .map((file) => dirname(file)) - - for (const file of collected) { - preload(excludeRegex, libPackage, singleCss, cssDir, include, file) - } - return - } - const collected = globSync(['**/*.tsx', '**/*.ts', '**/*.js', '**/*.mjs'], { - follow: true, - absolute: true, - cwd: pwd, - }) - // fix multi core build issue - collected.sort() - // console.log('collected', collected) - for (const file of collected) { - const filePath = relative(process.cwd(), realpathSync(file)) - if ( - /\.(test(-d)?|d|spec)\.(tsx|ts|js|mjs)$/.test(filePath) || - /^(out|.next)[/\\]/.test(filePath) || - excludeRegex.test(filePath) - ) - continue - const { cssFile, css } = codeExtract( - filePath, - readFileSync(filePath, 'utf-8'), - libPackage, - cssDir, - singleCss, - false, - true, - ) - - if (cssFile) { - writeFileSync(join(cssDir, basename(cssFile!)), css ?? '', 'utf-8') - } - } - writeFileSync(join(cssDir, 'devup-ui.css'), getCss(null, false), 'utf-8') -} +import { readFileSync, realpathSync, writeFileSync } from 'node:fs' +import { basename, dirname, join, relative } from 'node:path' + +import { codeExtract, getCss } from '@devup-ui/wasm' +import { globSync } from 'glob' + +import { findTopPackageRoot } from './find-top-package-root' +import { getPackageName } from './get-package-name' +import { hasLocalPackage } from './has-localpackage' + +export function preload( + excludeRegex: RegExp, + libPackage: string, + singleCss: boolean, + cssDir: string, + include: string[], + pwd = process.cwd(), +) { + if (include.length > 0 && hasLocalPackage()) { + const packageRoot = findTopPackageRoot() + const collected = globSync(['package.json', '!**/node_modules/**'], { + follow: true, + absolute: true, + cwd: packageRoot, + }) + .filter((file) => include.includes(getPackageName(file))) + .map((file) => dirname(file)) + + for (const file of collected) { + preload(excludeRegex, libPackage, singleCss, cssDir, include, file) + } + return + } + const collected = globSync(['**/*.tsx', '**/*.ts', '**/*.js', '**/*.mjs'], { + follow: true, + absolute: true, + cwd: pwd, + }) + // fix multi core build issue + collected.sort() + for (const file of collected) { + const filePath = relative(process.cwd(), realpathSync(file)) + if ( + /\.(test(-d)?|d|spec)\.(tsx|ts|js|mjs)$/.test(filePath) || + /^(out|.next)[/\\]/.test(filePath) || + excludeRegex.test(filePath) + ) + continue + const { cssFile, css } = codeExtract( + filePath, + readFileSync(filePath, 'utf-8'), + libPackage, + cssDir, + singleCss, + false, + true, + ) + + if (cssFile) { + writeFileSync(join(cssDir, basename(cssFile!)), css ?? '', 'utf-8') + } + } + writeFileSync(join(cssDir, 'devup-ui.css'), getCss(null, false), 'utf-8') +} From c23f215f09d6e230bdad4c4cab2ee5ec41f802c1 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Tue, 30 Dec 2025 20:25:53 +0900 Subject: [PATCH 2/6] Fix workspace issue --- .../changepack_log_gfbYKEAW0SjV8Y_4ve5rm.json | 5 + .github/workflows/publish.yml | 241 ++++--- devup.schema.json | 306 --------- packages/next-plugin/package.json | 2 +- .../next-plugin/src/__tests__/preload.test.ts | 620 +++++++++--------- packages/next-plugin/src/preload.ts | 11 +- pnpm-lock.yaml | 6 +- 7 files changed, 441 insertions(+), 750 deletions(-) create mode 100644 .changepacks/changepack_log_gfbYKEAW0SjV8Y_4ve5rm.json delete mode 100644 devup.schema.json diff --git a/.changepacks/changepack_log_gfbYKEAW0SjV8Y_4ve5rm.json b/.changepacks/changepack_log_gfbYKEAW0SjV8Y_4ve5rm.json new file mode 100644 index 00000000..d210648a --- /dev/null +++ b/.changepacks/changepack_log_gfbYKEAW0SjV8Y_4ve5rm.json @@ -0,0 +1,5 @@ +{ + "changes": { "packages/next-plugin/package.json": "Patch" }, + "note": "Fix workspace issue", + "date": "2025-12-30T11:25:19.439193400Z" +} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9a13d512..3f261898 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,125 +1,116 @@ -name: Publish Package to npm - -on: - push: - branches: - - main - pull_request: - branches: - - main -permissions: write-all - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: false - -jobs: - benchmark: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v5 - - - uses: actions-rust-lang/setup-rust-toolchain@v1 - - name: Cargo tarpaulin and fmt - run: | - cargo install cargo-tarpaulin - rustup component add rustfmt clippy - - uses: pnpm/action-setup@v4 - name: Install pnpm - with: - run_install: false - - - uses: jetli/wasm-pack-action@v0.4.0 - with: - version: 'latest' - - name: Install Node.js - uses: actions/setup-node@v4 - with: - registry-url: "https://registry.npmjs.org" - node-version: 22 - cache: 'pnpm' - - run: pnpm i - - run: pnpm build - - name: Benchmark - run: pnpm benchmark - - publish: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v5 - - - uses: actions-rust-lang/setup-rust-toolchain@v1 - - name: Cargo tarpaulin and fmt - run: | - cargo install cargo-tarpaulin - rustup component add rustfmt clippy - - uses: pnpm/action-setup@v4 - name: Install pnpm - with: - run_install: false - - - uses: jetli/wasm-pack-action@v0.4.0 - with: - version: 'latest' - - name: Install Node.js - uses: actions/setup-node@v4 - with: - registry-url: "https://registry.npmjs.org" - node-version: 22 - cache: 'pnpm' - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - run: pnpm i - - run: pnpm build - - run: | - pnpm lint - # rust coverage issue - echo 'max_width = 100000' > .rustfmt.toml - echo 'tab_spaces = 4' >> .rustfmt.toml - echo 'newline_style = "Unix"' >> .rustfmt.toml - echo 'fn_call_width = 100000' >> .rustfmt.toml - echo 'fn_params_layout = "Compressed"' >> .rustfmt.toml - echo 'chain_width = 100000' >> .rustfmt.toml - echo 'merge_derives = true' >> .rustfmt.toml - echo 'use_small_heuristics = "Default"' >> .rustfmt.toml - cargo fmt - - run: pnpm test - - name: Format Rollback - run: | - rm -rf .rustfmt.toml - cargo fmt - - name: Build Landing - run: | - pnpm -F components build-storybook - mv ./packages/components/storybook-static ./apps/landing/public/storybook - pnpm -F landing build - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: ./apps/landing/out - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - - uses: actions/deploy-pages@v4 - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - - name: Upload to codecov.io - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: true - - - uses: changepacks/action@main - id: changepacks - - - name: Publish to npm - run: | - echo '${{ steps.changepacks.outputs.changepacks }}' | jq -r '.[]' | while read package; do - package_dir=$(dirname "$package") - echo "Publishing package: $package_dir" - cd "$package_dir" - pnpm publish --access public --no-git-checks - cd "${{ github.workspace }}" - done - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - if: steps.changepacks.outputs.changepacks != '' +name: Publish Package to npm + +on: + push: + branches: + - main + pull_request: + branches: + - main +permissions: write-all + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + benchmark: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + - name: Cargo tarpaulin and fmt + run: | + cargo install cargo-tarpaulin + rustup component add rustfmt clippy + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + run_install: false + + - uses: jetli/wasm-pack-action@v0.4.0 + with: + version: 'latest' + - name: Install Node.js + uses: actions/setup-node@v4 + with: + registry-url: "https://registry.npmjs.org" + node-version: 22 + cache: 'pnpm' + - run: pnpm i + - run: pnpm build + - name: Benchmark + run: pnpm benchmark + + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + - name: Cargo tarpaulin and fmt + run: | + cargo install cargo-tarpaulin + rustup component add rustfmt clippy + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + run_install: false + + - uses: jetli/wasm-pack-action@v0.4.0 + with: + version: 'latest' + - name: Install Node.js + uses: actions/setup-node@v4 + with: + registry-url: "https://registry.npmjs.org" + node-version: 22 + cache: 'pnpm' + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - run: pnpm i + - run: pnpm build + - run: | + pnpm lint + # rust coverage issue + echo 'max_width = 100000' > .rustfmt.toml + echo 'tab_spaces = 4' >> .rustfmt.toml + echo 'newline_style = "Unix"' >> .rustfmt.toml + echo 'fn_call_width = 100000' >> .rustfmt.toml + echo 'fn_params_layout = "Compressed"' >> .rustfmt.toml + echo 'chain_width = 100000' >> .rustfmt.toml + echo 'merge_derives = true' >> .rustfmt.toml + echo 'use_small_heuristics = "Default"' >> .rustfmt.toml + cargo fmt + - run: pnpm test + - name: Format Rollback + run: | + rm -rf .rustfmt.toml + cargo fmt + - name: Build Landing + run: | + pnpm -F components build-storybook + mv ./packages/components/storybook-static ./apps/landing/public/storybook + pnpm -F landing build + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./apps/landing/out + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + - uses: actions/deploy-pages@v4 + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + - name: Upload to codecov.io + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true + + - uses: changepacks/action@main + id: changepacks + with: + publish: true + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/devup.schema.json b/devup.schema.json deleted file mode 100644 index d56a36f4..00000000 --- a/devup.schema.json +++ /dev/null @@ -1,306 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "title": "DevupJson", - "description": "Root devup.json configuration", - "type": "object", - "properties": { - "theme": { - "description": "Theme configuration including colors, typography, and breakpoints", - "anyOf": [ - { - "$ref": "#/$defs/Theme" - }, - { - "type": "null" - } - ], - "default": null - } - }, - "$defs": { - "ColorTheme": { - "description": "Color theme with color name to value mappings. Supports nested objects for color scales.", - "type": "object", - "additionalProperties": { - "oneOf": [ - { - "description": "Color value (e.g., '#000', 'rgb(0,0,0)')", - "type": "string" - }, - { - "description": "Nested color object", - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/ColorValue" - } - } - ] - }, - "$defs": { - "ColorValue": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/ColorValue" - } - } - ] - } - } - }, - "Theme": { - "type": "object", - "properties": { - "breakpoints": { - "description": "Breakpoints in pixels for responsive typography (default: [0, 480, 768, 992, 1280, 1600])", - "type": "array", - "default": [0, 480, 768, 992, 1280, 1600], - "items": { - "type": "integer", - "format": "uint16", - "maximum": 65535, - "minimum": 0 - } - }, - "colors": { - "description": "Color themes by mode name (e.g., \"light\", \"dark\")", - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/ColorTheme" - }, - "default": {} - }, - "typography": { - "description": "Typography definitions by name (e.g., \"h1\", \"body\", \"caption\")", - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/Typographies" - }, - "default": {} - } - } - }, - "Typographies": { - "description": "Typography definition supporting both traditional array format and compact object format", - "oneOf": [ - { - "description": "Traditional array format: array of Typography objects or null for each breakpoint", - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/$defs/Typography" - }, - { - "type": "null" - } - ] - } - }, - { - "description": "Compact object format: each property can be a single value or array of values per breakpoint", - "type": "object", - "properties": { - "fontFamily": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - } - }, - { - "type": "null" - } - ] - }, - "fontSize": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - } - }, - { - "type": "null" - } - ] - }, - "fontStyle": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - } - }, - { - "type": "null" - } - ] - }, - "fontWeight": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "number" - }, - { - "type": "array", - "items": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "number" - }, - { - "type": "null" - } - ] - } - }, - { - "type": "null" - } - ] - }, - "letterSpacing": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - } - }, - { - "type": "null" - } - ] - }, - "lineHeight": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "number" - }, - { - "type": "array", - "items": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "number" - }, - { - "type": "null" - } - ] - } - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - } - ] - }, - "Typography": { - "type": "object", - "properties": { - "fontFamily": { - "type": ["string", "null"] - }, - "fontSize": { - "type": ["string", "null"] - }, - "fontWeight": { - "default": null, - "oneOf": [ - { - "type": "string" - }, - { - "type": "number" - }, - { - "type": "null" - } - ] - }, - "letterSpacing": { - "type": ["string", "null"] - }, - "lineHeight": { - "default": null, - "oneOf": [ - { - "type": "string" - }, - { - "type": "number" - }, - { - "type": "null" - } - ] - } - } - } - } -} diff --git a/packages/next-plugin/package.json b/packages/next-plugin/package.json index bd135feb..f547d438 100644 --- a/packages/next-plugin/package.json +++ b/packages/next-plugin/package.json @@ -52,7 +52,7 @@ "@devup-ui/webpack-plugin": "workspace:^", "next": "^16.1", "@devup-ui/wasm": "workspace:^", - "glob": "^13.0" + "tinyglobby": "^0.2" }, "devDependencies": { "vite": "^7.3", diff --git a/packages/next-plugin/src/__tests__/preload.test.ts b/packages/next-plugin/src/__tests__/preload.test.ts index d46ec6fd..8fd59de7 100644 --- a/packages/next-plugin/src/__tests__/preload.test.ts +++ b/packages/next-plugin/src/__tests__/preload.test.ts @@ -1,310 +1,310 @@ -import { realpathSync, writeFileSync } from 'node:fs' -import { readFileSync } from 'node:fs' -import { existsSync } from 'node:fs' -import { join } from 'node:path' - -import { codeExtract, getCss } from '@devup-ui/wasm' -import { globSync } from 'glob' - -import { findTopPackageRoot } from '../find-top-package-root' -import { getPackageName } from '../get-package-name' -import { hasLocalPackage } from '../has-localpackage' -import { preload } from '../preload' - -// Mock dependencies -vi.mock('node:fs') -vi.mock('@devup-ui/wasm') -vi.mock('glob') - -// Mock globSync -vi.mock('node:fs', () => ({ - readFileSync: vi.fn(), - writeFileSync: vi.fn(), - mkdirSync: vi.fn(), - existsSync: vi.fn(), - realpathSync: vi.fn().mockReturnValue('src/App.tsx'), -})) - -vi.mock('glob', () => ({ - globSync: vi.fn(), -})) - -// Mock @devup-ui/wasm -vi.mock('@devup-ui/wasm', () => ({ - codeExtract: vi.fn(), - registerTheme: vi.fn(), - getCss: vi.fn(), -})) - -vi.mock('../find-top-package-root', () => ({ - findTopPackageRoot: vi.fn(), -})) - -vi.mock('../get-package-name', () => ({ - getPackageName: vi.fn(), -})) - -vi.mock('../has-localpackage', () => ({ - hasLocalPackage: vi.fn(), -})) - -describe('preload', () => { - beforeEach(() => { - vi.clearAllMocks() - - // Default mock implementations - vi.mocked(globSync).mockReturnValue([ - 'src/App.tsx', - 'src/components/Button.tsx', - ]) - vi.mocked(readFileSync).mockReturnValue( - 'const Button = () =>
Hello
', - ) - vi.mocked(codeExtract).mockReturnValue({ - free: vi.fn(), - cssFile: 'styles.css', - css: '.button { color: red; }', - code: '', - map: '', - updatedBaseStyle: false, - [Symbol.dispose]: vi.fn(), - }) - vi.mocked(existsSync).mockReturnValue(true) - }) - - it('should find project root and collect files', () => { - const excludeRegex = /node_modules/ - const libPackage = '@devup-ui/react' - const singleCss = false - const cssDir = '/output/css' - - preload(excludeRegex, libPackage, singleCss, cssDir, []) - - expect(globSync).toHaveBeenCalledWith( - ['**/*.tsx', '**/*.ts', '**/*.js', '**/*.mjs'], - { - follow: true, - absolute: true, - cwd: expect.any(String), - }, - ) - }) - - it('should process each collected file', () => { - const files = ['src/App.tsx', 'src/components/Button.tsx', '.next/page.tsx'] - vi.mocked(globSync).mockReturnValue(files) - vi.mocked(realpathSync) - .mockReturnValueOnce('src/App.tsx') - .mockReturnValueOnce('src/components/Button.tsx') - .mockReturnValueOnce('.next/page.tsx') - preload(/node_modules/, '@devup-ui/react', false, '/output/css', []) - - expect(codeExtract).toHaveBeenCalledTimes(2) - expect(codeExtract).toHaveBeenCalledWith( - expect.stringMatching(/App\.tsx$/), - 'const Button = () =>
Hello
', - '@devup-ui/react', - '/output/css', - false, - false, - true, - ) - }) - - it('should write CSS file when cssFile is returned', () => { - vi.mocked(codeExtract).mockReturnValue({ - cssFile: 'styles.css', - css: '.button { color: red; }', - free: vi.fn(), - code: '', - map: '', - updatedBaseStyle: false, - [Symbol.dispose]: vi.fn(), - }) - - preload(/node_modules/, '@devup-ui/react', false, '/output/css', []) - - expect(writeFileSync).toHaveBeenCalledWith( - join('/output/css', 'styles.css'), - '.button { color: red; }', - 'utf-8', - ) - }) - - it('should not write CSS file when cssFile is null', () => { - vi.mocked(codeExtract).mockReturnValue({ - cssFile: undefined, - css: '.button { color: red; }', - free: vi.fn(), - code: '', - map: '', - updatedBaseStyle: false, - [Symbol.dispose]: vi.fn(), - }) - vi.mocked(getCss).mockReturnValue('') - - preload(/node_modules/, '@devup-ui/react', false, '/output/css', []) - - expect(writeFileSync).toHaveBeenCalledWith( - join('/output/css', 'devup-ui.css'), - '', - 'utf-8', - ) - }) - - it('should handle empty CSS content', () => { - vi.mocked(codeExtract).mockReturnValue({ - cssFile: 'styles.css', - css: '', - free: vi.fn(), - code: '', - map: '', - updatedBaseStyle: false, - [Symbol.dispose]: vi.fn(), - }) - - preload(/node_modules/, '@devup-ui/react', false, '/output/css', []) - - expect(writeFileSync).toHaveBeenCalledWith( - join('/output/css', 'styles.css'), - '', - 'utf-8', - ) - }) - - it('should handle undefined CSS content', () => { - vi.mocked(codeExtract).mockReturnValue({ - cssFile: 'styles.css', - css: undefined, - free: vi.fn(), - code: '', - map: '', - updatedBaseStyle: false, - [Symbol.dispose]: vi.fn(), - }) - - preload(/node_modules/, '@devup-ui/react', false, '/output/css', []) - - expect(writeFileSync).toHaveBeenCalledWith( - join('/output/css', 'styles.css'), - '', - 'utf-8', - ) - }) - - it('should pass correct parameters to codeExtract', () => { - const libPackage = '@devup-ui/react' - const singleCss = true - const cssDir = '/custom/css/dir' - - preload(/node_modules/, libPackage, singleCss, cssDir, []) - - expect(codeExtract).toHaveBeenCalledWith( - expect.stringMatching(/App\.tsx$/), - 'const Button = () =>
Hello
', - libPackage, - cssDir, - singleCss, - false, - true, - ) - }) - - it('should handle multiple files with different CSS outputs', () => { - const files = ['src/App.tsx', 'src/components/Button.tsx'] - vi.mocked(globSync).mockReturnValue(files) - - vi.mocked(codeExtract) - .mockReturnValueOnce({ - cssFile: 'app.css', - css: '.app { margin: 0; }', - free: vi.fn(), - code: '', - map: '', - updatedBaseStyle: false, - [Symbol.dispose]: vi.fn(), - }) - .mockReturnValueOnce({ - free: vi.fn(), - cssFile: 'button.css', - css: '.button { color: blue; }', - code: '', - map: '', - updatedBaseStyle: false, - [Symbol.dispose]: vi.fn(), - }) - - preload(/node_modules/, '@devup-ui/react', false, '/output/css', []) - - expect(writeFileSync).toHaveBeenCalledTimes(3) - expect(writeFileSync).toHaveBeenCalledWith( - join('/output/css', 'app.css'), - '.app { margin: 0; }', - 'utf-8', - ) - expect(writeFileSync).toHaveBeenCalledWith( - join('/output/css', 'button.css'), - '.button { color: blue; }', - 'utf-8', - ) - }) - - it('should recurse into local workspaces when include is provided', () => { - const files = ['src/App.tsx'] - vi.mocked(findTopPackageRoot).mockReturnValue('/repo') - vi.mocked(hasLocalPackage) - .mockReturnValueOnce(true) - .mockReturnValueOnce(false) - vi.mocked(globSync) - .mockReturnValueOnce([ - '/repo/packages/pkg-a/package.json', - '/repo/packages/pkg-b/package.json', - ]) - .mockReturnValueOnce(files) - vi.mocked(getPackageName) - .mockReturnValueOnce('pkg-a') - .mockReturnValueOnce('pkg-b') - vi.mocked(realpathSync).mockReturnValueOnce('src/App.tsx') - - preload(/node_modules/, '@devup-ui/react', false, '/output/css', ['pkg-a']) - - expect(findTopPackageRoot).toHaveBeenCalled() - expect(globSync).toHaveBeenCalledWith( - ['package.json', '!**/node_modules/**'], - { - follow: true, - absolute: true, - cwd: '/repo', - }, - ) - expect(codeExtract).toHaveBeenCalledTimes(1) - expect(realpathSync).toHaveBeenCalledWith('src/App.tsx') - }) - - it('should skip test and build outputs based on filters', () => { - vi.mocked(globSync).mockReturnValue([ - 'src/App.test.tsx', - '.next/page.tsx', - 'out/index.js', - 'src/keep.ts', - ]) - vi.mocked(realpathSync) - .mockReturnValueOnce('src/App.test.tsx') - .mockReturnValueOnce('.next/page.tsx') - .mockReturnValueOnce('out/index.js') - .mockReturnValueOnce('src/keep.ts') - - preload(/exclude/, '@devup-ui/react', false, '/output/css', []) - - expect(codeExtract).toHaveBeenCalledTimes(1) - expect(codeExtract).toHaveBeenCalledWith( - expect.stringMatching(/keep\.ts$/), - 'const Button = () =>
Hello
', - '@devup-ui/react', - '/output/css', - false, - false, - true, - ) - }) -}) +import { realpathSync, writeFileSync } from 'node:fs' +import { readFileSync } from 'node:fs' +import { existsSync } from 'node:fs' +import { join } from 'node:path' + +import { codeExtract, getCss } from '@devup-ui/wasm' +import { globSync } from 'tinyglobby' + +import { findTopPackageRoot } from '../find-top-package-root' +import { getPackageName } from '../get-package-name' +import { hasLocalPackage } from '../has-localpackage' +import { preload } from '../preload' + +// Mock dependencies +vi.mock('node:fs') +vi.mock('@devup-ui/wasm') +vi.mock('tinyglobby') + +// Mock globSync +vi.mock('node:fs', () => ({ + readFileSync: vi.fn(), + writeFileSync: vi.fn(), + mkdirSync: vi.fn(), + existsSync: vi.fn(), + realpathSync: vi.fn().mockReturnValue('src/App.tsx'), +})) + +vi.mock('tinyglobby', () => ({ + globSync: vi.fn(), +})) + +// Mock @devup-ui/wasm +vi.mock('@devup-ui/wasm', () => ({ + codeExtract: vi.fn(), + registerTheme: vi.fn(), + getCss: vi.fn(), +})) + +vi.mock('../find-top-package-root', () => ({ + findTopPackageRoot: vi.fn(), +})) + +vi.mock('../get-package-name', () => ({ + getPackageName: vi.fn(), +})) + +vi.mock('../has-localpackage', () => ({ + hasLocalPackage: vi.fn(), +})) + +describe('preload', () => { + beforeEach(() => { + vi.clearAllMocks() + + // Default mock implementations + vi.mocked(globSync).mockReturnValue([ + 'src/App.tsx', + 'src/components/Button.tsx', + ]) + vi.mocked(readFileSync).mockReturnValue( + 'const Button = () =>
Hello
', + ) + vi.mocked(codeExtract).mockReturnValue({ + free: vi.fn(), + cssFile: 'styles.css', + css: '.button { color: red; }', + code: '', + map: '', + updatedBaseStyle: false, + [Symbol.dispose]: vi.fn(), + }) + vi.mocked(existsSync).mockReturnValue(true) + }) + + it('should find project root and collect files', () => { + const excludeRegex = /node_modules/ + const libPackage = '@devup-ui/react' + const singleCss = false + const cssDir = '/output/css' + + preload(excludeRegex, libPackage, singleCss, cssDir, []) + + expect(globSync).toHaveBeenCalledWith( + ['**/*.tsx', '**/*.ts', '**/*.js', '**/*.mjs'], + { + followSymbolicLinks: true, + absolute: true, + cwd: expect.any(String), + }, + ) + }) + + it('should process each collected file', () => { + const files = ['src/App.tsx', 'src/components/Button.tsx', '.next/page.tsx'] + vi.mocked(globSync).mockReturnValue(files) + vi.mocked(realpathSync) + .mockReturnValueOnce('src/App.tsx') + .mockReturnValueOnce('src/components/Button.tsx') + .mockReturnValueOnce('.next/page.tsx') + preload(/node_modules/, '@devup-ui/react', false, '/output/css', []) + + expect(codeExtract).toHaveBeenCalledTimes(2) + expect(codeExtract).toHaveBeenCalledWith( + expect.stringMatching(/App\.tsx$/), + 'const Button = () =>
Hello
', + '@devup-ui/react', + '/output/css', + false, + false, + true, + ) + }) + + it('should write CSS file when cssFile is returned', () => { + vi.mocked(codeExtract).mockReturnValue({ + cssFile: 'styles.css', + css: '.button { color: red; }', + free: vi.fn(), + code: '', + map: '', + updatedBaseStyle: false, + [Symbol.dispose]: vi.fn(), + }) + + preload(/node_modules/, '@devup-ui/react', false, '/output/css', []) + + expect(writeFileSync).toHaveBeenCalledWith( + join('/output/css', 'styles.css'), + '.button { color: red; }', + 'utf-8', + ) + }) + + it('should not write CSS file when cssFile is null', () => { + vi.mocked(codeExtract).mockReturnValue({ + cssFile: undefined, + css: '.button { color: red; }', + free: vi.fn(), + code: '', + map: '', + updatedBaseStyle: false, + [Symbol.dispose]: vi.fn(), + }) + vi.mocked(getCss).mockReturnValue('') + + preload(/node_modules/, '@devup-ui/react', false, '/output/css', []) + + expect(writeFileSync).toHaveBeenCalledWith( + join('/output/css', 'devup-ui.css'), + '', + 'utf-8', + ) + }) + + it('should handle empty CSS content', () => { + vi.mocked(codeExtract).mockReturnValue({ + cssFile: 'styles.css', + css: '', + free: vi.fn(), + code: '', + map: '', + updatedBaseStyle: false, + [Symbol.dispose]: vi.fn(), + }) + + preload(/node_modules/, '@devup-ui/react', false, '/output/css', []) + + expect(writeFileSync).toHaveBeenCalledWith( + join('/output/css', 'styles.css'), + '', + 'utf-8', + ) + }) + + it('should handle undefined CSS content', () => { + vi.mocked(codeExtract).mockReturnValue({ + cssFile: 'styles.css', + css: undefined, + free: vi.fn(), + code: '', + map: '', + updatedBaseStyle: false, + [Symbol.dispose]: vi.fn(), + }) + + preload(/node_modules/, '@devup-ui/react', false, '/output/css', []) + + expect(writeFileSync).toHaveBeenCalledWith( + join('/output/css', 'styles.css'), + '', + 'utf-8', + ) + }) + + it('should pass correct parameters to codeExtract', () => { + const libPackage = '@devup-ui/react' + const singleCss = true + const cssDir = '/custom/css/dir' + + preload(/node_modules/, libPackage, singleCss, cssDir, []) + + expect(codeExtract).toHaveBeenCalledWith( + expect.stringMatching(/App\.tsx$/), + 'const Button = () =>
Hello
', + libPackage, + cssDir, + singleCss, + false, + true, + ) + }) + + it('should handle multiple files with different CSS outputs', () => { + const files = ['src/App.tsx', 'src/components/Button.tsx'] + vi.mocked(globSync).mockReturnValue(files) + + vi.mocked(codeExtract) + .mockReturnValueOnce({ + cssFile: 'app.css', + css: '.app { margin: 0; }', + free: vi.fn(), + code: '', + map: '', + updatedBaseStyle: false, + [Symbol.dispose]: vi.fn(), + }) + .mockReturnValueOnce({ + free: vi.fn(), + cssFile: 'button.css', + css: '.button { color: blue; }', + code: '', + map: '', + updatedBaseStyle: false, + [Symbol.dispose]: vi.fn(), + }) + + preload(/node_modules/, '@devup-ui/react', false, '/output/css', []) + + expect(writeFileSync).toHaveBeenCalledTimes(3) + expect(writeFileSync).toHaveBeenCalledWith( + join('/output/css', 'app.css'), + '.app { margin: 0; }', + 'utf-8', + ) + expect(writeFileSync).toHaveBeenCalledWith( + join('/output/css', 'button.css'), + '.button { color: blue; }', + 'utf-8', + ) + }) + + it('should recurse into local workspaces when include is provided', () => { + const files = ['src/App.tsx'] + vi.mocked(findTopPackageRoot).mockReturnValue('/repo') + vi.mocked(hasLocalPackage) + .mockReturnValueOnce(true) + .mockReturnValueOnce(false) + vi.mocked(globSync) + .mockReturnValueOnce([ + '/repo/packages/pkg-a/package.json', + '/repo/packages/pkg-b/package.json', + ]) + .mockReturnValueOnce(files) + vi.mocked(getPackageName) + .mockReturnValueOnce('pkg-a') + .mockReturnValueOnce('pkg-b') + vi.mocked(realpathSync).mockReturnValueOnce('src/App.tsx') + + preload(/node_modules/, '@devup-ui/react', false, '/output/css', ['pkg-a']) + + expect(findTopPackageRoot).toHaveBeenCalled() + expect(globSync).toHaveBeenCalledWith( + ['package.json', '!**/node_modules/**'], + { + followSymbolicLinks: true, + absolute: true, + cwd: '/repo', + }, + ) + expect(codeExtract).toHaveBeenCalledTimes(3) + expect(realpathSync).toHaveBeenCalledWith('src/App.tsx') + }) + + it('should skip test and build outputs based on filters', () => { + vi.mocked(globSync).mockReturnValue([ + 'src/App.test.tsx', + '.next/page.tsx', + 'out/index.js', + 'src/keep.ts', + ]) + vi.mocked(realpathSync) + .mockReturnValueOnce('src/App.test.tsx') + .mockReturnValueOnce('.next/page.tsx') + .mockReturnValueOnce('out/index.js') + .mockReturnValueOnce('src/keep.ts') + + preload(/exclude/, '@devup-ui/react', false, '/output/css', []) + + expect(codeExtract).toHaveBeenCalledTimes(1) + expect(codeExtract).toHaveBeenCalledWith( + expect.stringMatching(/keep\.ts$/), + 'const Button = () =>
Hello
', + '@devup-ui/react', + '/output/css', + false, + false, + true, + ) + }) +}) diff --git a/packages/next-plugin/src/preload.ts b/packages/next-plugin/src/preload.ts index b4ada71a..6ffbe240 100644 --- a/packages/next-plugin/src/preload.ts +++ b/packages/next-plugin/src/preload.ts @@ -2,7 +2,7 @@ import { readFileSync, realpathSync, writeFileSync } from 'node:fs' import { basename, dirname, join, relative } from 'node:path' import { codeExtract, getCss } from '@devup-ui/wasm' -import { globSync } from 'glob' +import { globSync } from 'tinyglobby' import { findTopPackageRoot } from './find-top-package-root' import { getPackageName } from './get-package-name' @@ -15,11 +15,12 @@ export function preload( cssDir: string, include: string[], pwd = process.cwd(), + nested = false, ) { if (include.length > 0 && hasLocalPackage()) { const packageRoot = findTopPackageRoot() const collected = globSync(['package.json', '!**/node_modules/**'], { - follow: true, + followSymbolicLinks: true, absolute: true, cwd: packageRoot, }) @@ -27,12 +28,12 @@ export function preload( .map((file) => dirname(file)) for (const file of collected) { - preload(excludeRegex, libPackage, singleCss, cssDir, include, file) + preload(excludeRegex, libPackage, singleCss, cssDir, include, file, true) } - return + if (nested) return } const collected = globSync(['**/*.tsx', '**/*.ts', '**/*.js', '**/*.mjs'], { - follow: true, + followSymbolicLinks: true, absolute: true, cwd: pwd, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61886968..34024f63 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -757,12 +757,12 @@ importers: '@devup-ui/webpack-plugin': specifier: workspace:^ version: link:../webpack-plugin - glob: - specifier: ^13.0 - version: 13.0.0 next: specifier: ^16.1 version: 16.1.1(@babel/core@7.28.5)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + tinyglobby: + specifier: ^0.2 + version: 0.2.15 devDependencies: '@types/webpack': specifier: ^5.28 From 543117c6216f96b7aa53c91f46a81de5a2745769 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Tue, 30 Dec 2025 20:57:36 +0900 Subject: [PATCH 3/6] Add preload --- .../next-plugin/src/__tests__/preload.test.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/next-plugin/src/__tests__/preload.test.ts b/packages/next-plugin/src/__tests__/preload.test.ts index 8fd59de7..66a42008 100644 --- a/packages/next-plugin/src/__tests__/preload.test.ts +++ b/packages/next-plugin/src/__tests__/preload.test.ts @@ -307,4 +307,31 @@ describe('preload', () => { true, ) }) + + it('should return early when nested is true and include packages exist', () => { + vi.mocked(findTopPackageRoot).mockReturnValue('/repo') + vi.mocked(hasLocalPackage).mockReturnValue(true) + // Return empty array so no recursive calls happen, but include.length > 0 check passes + vi.mocked(globSync).mockReturnValue([]) + vi.mocked(getPackageName).mockReturnValue('pkg-a') + + // Call with nested = true (7th parameter) + preload( + /node_modules/, + '@devup-ui/react', + false, + '/output/css', + ['pkg-a'], + '/some/path', + true, + ) + + // When nested is true, it should return early after processing includes + // and not write the final devup-ui.css file + expect(writeFileSync).not.toHaveBeenCalledWith( + expect.stringContaining('devup-ui.css'), + expect.any(String), + 'utf-8', + ) + }) }) From f519302d6538b566b1422af0a34e4d415cf46d8f Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Tue, 30 Dec 2025 21:02:13 +0900 Subject: [PATCH 4/6] Add preload --- libs/sheet/Cargo.toml | 5 -- libs/sheet/src/theme.rs | 142 +--------------------------------------- 2 files changed, 2 insertions(+), 145 deletions(-) diff --git a/libs/sheet/Cargo.toml b/libs/sheet/Cargo.toml index 17d3def1..8e3ecfc1 100644 --- a/libs/sheet/Cargo.toml +++ b/libs/sheet/Cargo.toml @@ -10,7 +10,6 @@ serde_json = "1.0.146" regex = "1.12.2" once_cell = "1.21.3" extractor = { path = "../extractor" } -schemars = "1.2.0" [dev-dependencies] insta = "1.45.0" @@ -20,7 +19,3 @@ rstest = "0.26.1" [[bench]] name = "my_benchmark" harness = false - -[[bin]] -name = "generate-schema" -path = "src/bin/generate_schema.rs" diff --git a/libs/sheet/src/theme.rs b/libs/sheet/src/theme.rs index dcac190f..37e34bb8 100644 --- a/libs/sheet/src/theme.rs +++ b/libs/sheet/src/theme.rs @@ -1,21 +1,8 @@ use css::optimize_value::optimize_value; -use schemars::{JsonSchema, SchemaGenerator, json_schema}; -use schemars::Schema; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; use std::collections::{BTreeMap, HashMap}; -/// Schema helper for typography values that accept string or number -fn typography_value_schema(_generator: &mut SchemaGenerator) -> Schema { - json_schema!({ - "oneOf": [ - { "type": "string" }, - { "type": "number" }, - { "type": "null" } - ] - }) -} - /// ColorEntry stores both the original key (for TypeScript interface) and CSS key (for CSS variables) #[derive(Debug, Clone, Serialize)] pub struct ColorEntry { @@ -39,42 +26,6 @@ pub struct ColorTheme { entries: HashMap, } -impl JsonSchema for ColorTheme { - fn schema_name() -> std::borrow::Cow<'static, str> { - std::borrow::Cow::Borrowed("ColorTheme") - } - - fn json_schema(_generator: &mut SchemaGenerator) -> Schema { - json_schema!({ - "type": "object", - "additionalProperties": { - "oneOf": [ - { "type": "string", "description": "Color value (e.g., '#000', 'rgb(0,0,0)')" }, - { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/ColorValue" - }, - "description": "Nested color object" - } - ] - }, - "description": "Color theme with color name to value mappings. Supports nested objects for color scales.", - "$defs": { - "ColorValue": { - "oneOf": [ - { "type": "string" }, - { - "type": "object", - "additionalProperties": { "$ref": "#/$defs/ColorValue" } - } - ] - } - } - }) - } -} - /// Recursively flatten a JSON value into ColorEntry list /// interface_prefix uses dots, css_prefix uses dashes fn flatten_color_value( @@ -194,17 +145,15 @@ where StringOrNumber::Float(n) => Ok(Some(n.to_string())), } } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Typography { pub font_family: Option, pub font_size: Option, #[serde(deserialize_with = "deserialize_string_from_number", default)] - #[schemars(schema_with = "typography_value_schema")] pub font_weight: Option, #[serde(deserialize_with = "deserialize_string_from_number", default)] - #[schemars(schema_with = "typography_value_schema")] pub line_height: Option, pub letter_spacing: Option, } @@ -235,82 +184,6 @@ impl From>> for Typographies { } } -impl JsonSchema for Typographies { - fn schema_name() -> std::borrow::Cow<'static, str> { - std::borrow::Cow::Borrowed("Typographies") - } - - fn json_schema(generator: &mut SchemaGenerator) -> Schema { - let typography_schema = generator.subschema_for::(); - json_schema!({ - "oneOf": [ - { - "type": "array", - "items": { - "oneOf": [ - typography_schema, - { "type": "null" } - ] - }, - "description": "Traditional array format: array of Typography objects or null for each breakpoint" - }, - { - "type": "object", - "properties": { - "fontFamily": { - "oneOf": [ - { "type": "string" }, - { "type": "array", "items": { "oneOf": [{ "type": "string" }, { "type": "null" }] } }, - { "type": "null" } - ] - }, - "fontStyle": { - "oneOf": [ - { "type": "string" }, - { "type": "array", "items": { "oneOf": [{ "type": "string" }, { "type": "null" }] } }, - { "type": "null" } - ] - }, - "fontWeight": { - "oneOf": [ - { "type": "string" }, - { "type": "number" }, - { "type": "array", "items": { "oneOf": [{ "type": "string" }, { "type": "number" }, { "type": "null" }] } }, - { "type": "null" } - ] - }, - "fontSize": { - "oneOf": [ - { "type": "string" }, - { "type": "array", "items": { "oneOf": [{ "type": "string" }, { "type": "null" }] } }, - { "type": "null" } - ] - }, - "lineHeight": { - "oneOf": [ - { "type": "string" }, - { "type": "number" }, - { "type": "array", "items": { "oneOf": [{ "type": "string" }, { "type": "number" }, { "type": "null" }] } }, - { "type": "null" } - ] - }, - "letterSpacing": { - "oneOf": [ - { "type": "string" }, - { "type": "array", "items": { "oneOf": [{ "type": "string" }, { "type": "null" }] } }, - { "type": "null" } - ] - } - }, - "additionalProperties": false, - "description": "Compact object format: each property can be a single value or array of values per breakpoint" - } - ], - "description": "Typography definition supporting both traditional array format and compact object format" - }) - } -} - /// Helper to deserialize a typography property that can be either a single value or an array fn deserialize_typo_prop(value: &Value) -> Result>, String> { match value { @@ -460,28 +333,17 @@ impl<'de> Deserialize<'de> for Typographies { } } -#[derive(Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Theme { - /// Color themes by mode name (e.g., "light", "dark") #[serde(default)] pub colors: BTreeMap, - /// Breakpoints in pixels for responsive typography (default: [0, 480, 768, 992, 1280, 1600]) #[serde(default = "default_breakpoints")] pub breakpoints: Vec, - /// Typography definitions by name (e.g., "h1", "body", "caption") #[serde(default)] pub typography: BTreeMap, } -/// Root devup.json configuration -#[derive(Serialize, Deserialize, Debug, JsonSchema)] -pub struct DevupJson { - /// Theme configuration including colors, typography, and breakpoints - #[serde(default)] - pub theme: Option, -} - fn default_breakpoints() -> Vec { vec![0, 480, 768, 992, 1280, 1600] } From 0cb776515c97eaa77f755c8b8c12cf67b4a3c5a9 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Tue, 30 Dec 2025 21:02:27 +0900 Subject: [PATCH 5/6] Add preload --- Cargo.lock | 63 ------------------------------------------------------ 1 file changed, 63 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0c742a44..78f24431 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -319,12 +319,6 @@ version = "0.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d742b56656e8b14d63e7ea9806597b1849ae25412584c8adf78c0f67bd985e66" -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - [[package]] name = "either" version = "1.15.0" @@ -1142,26 +1136,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "regex" version = "1.12.2" @@ -1284,31 +1258,6 @@ dependencies = [ "sdd", ] -[[package]] -name = "schemars" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" -dependencies = [ - "dyn-clone", - "ref-cast", - "schemars_derive", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4908ad288c5035a8eb12cfdf0d49270def0a268ee162b75eeee0f85d155a7c45" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -1380,17 +1329,6 @@ dependencies = [ "syn", ] -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "serde_json" version = "1.0.146" @@ -1440,7 +1378,6 @@ dependencies = [ "once_cell", "regex", "rstest", - "schemars", "serde", "serde_json", ] From dc3539f1fc2bdc40a9148ba2354c447bcd703458 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Tue, 30 Dec 2025 21:08:47 +0900 Subject: [PATCH 6/6] Rm schema --- libs/sheet/src/bin/generate_schema.rs | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 libs/sheet/src/bin/generate_schema.rs diff --git a/libs/sheet/src/bin/generate_schema.rs b/libs/sheet/src/bin/generate_schema.rs deleted file mode 100644 index 8c0878a8..00000000 --- a/libs/sheet/src/bin/generate_schema.rs +++ /dev/null @@ -1,8 +0,0 @@ -use schemars::schema_for; -use sheet::theme::DevupJson; - -fn main() { - let schema = schema_for!(DevupJson); - let json = serde_json::to_string_pretty(&schema).unwrap(); - println!("{}", json); -}