diff --git a/ai-skills/skills/valdi-custom-view/skill.md b/ai-skills/skills/valdi-custom-view/skill.md index f6821c3a0..60c791119 100644 --- a/ai-skills/skills/valdi-custom-view/skill.md +++ b/ai-skills/skills/valdi-custom-view/skill.md @@ -53,7 +53,7 @@ class MyComponent extends Component { } ``` -`viewFactory` and `*Class` attributes are mutually exclusive — use one or the other. `viewFactory` takes precedence when both are provided. +`viewFactory` and `*Class` attributes are mutually exclusive — use one or the other. Specifying both on the same `` is a compile error (the JSXProcessor rejects it). If a view needs a `viewFactory` on one platform and a registered class on another, branch into two sibling `` elements, each with a single mechanism. Prefer gating on whether a `viewFactory` is provided (`if (viewFactory) { … } else { …class… }`) over `Device.isWeb()`: the factory is often supplied by native too, so an `isWeb` branch that routes native to classes can break it. Only keep a platform class you've confirmed actually exists. ## macOS Attribute Binding diff --git a/bzl/valdi/app_templates/web_path_browserify_shim.js.tpl b/bzl/valdi/app_templates/web_path_browserify_shim.js.tpl index 87f9e3667..02020f4b0 100644 --- a/bzl/valdi/app_templates/web_path_browserify_shim.js.tpl +++ b/bzl/valdi/app_templates/web_path_browserify_shim.js.tpl @@ -10,7 +10,11 @@ function normalize(value) { continue; } if (part === '..') { - out.pop(); + if (out.length === 0 || out[out.length - 1] === '..') { + if (!absolute) out.push('..'); + } else { + out.pop(); + } } else { out.push(part); } diff --git a/bzl/valdi/valdi_collapse_web_paths.bzl b/bzl/valdi/valdi_collapse_web_paths.bzl index 1940ff7a4..adf6c49cd 100644 --- a/bzl/valdi/valdi_collapse_web_paths.bzl +++ b/bzl/valdi/valdi_collapse_web_paths.bzl @@ -107,7 +107,9 @@ def _dest(rel): parts = rel.split("/") for i in range(1, len(parts)): - if parts[i] == "res": + # Skip the compiler-generated web//res directories; those are handled + # by the web//{assets,res} logic below so the real module name is kept. + if parts[i] == "res" and parts[i - 1] not in ["debug", "release"]: module_name = parts[i - 1] tail = "/".join(parts[i:]) return "src/{}/{}".format(module_name, tail) @@ -297,6 +299,12 @@ def _impl_native(ctx): lines = [] for f in ctx.files.srcs: rel = _repository_relative_short_path(f) + + # Only files under a module's web/ dir belong in the native tree. Source files + # (e.g. /src/*.d.ts) have no web/ segment; they are placed by the main + # collapse_web_paths pass instead, so skip them here. + if "web" not in rel.split("/"): + continue lines.append("{}\t{}".format(f.path, _dest_native(rel))) ctx.actions.write(manifest, "\n".join(lines)) diff --git a/bzl/valdi/valdi_compiled.bzl b/bzl/valdi/valdi_compiled.bzl index 7ea95022d..10c668a56 100644 --- a/bzl/valdi/valdi_compiled.bzl +++ b/bzl/valdi/valdi_compiled.bzl @@ -893,15 +893,24 @@ def _get_web_output_target(attr, force_debug): def _supports_web_release(ctx, code_coverage): return _get_web_output_target(ctx.attr, code_coverage or ctx.attr.output_flavor[BuildSettingInfo].value == "debug") == "release" +def _web_output_flavor(ctx, code_coverage): + # Must equal the compiler's web_output_target (see _get_web_output_target, passed to + # the compiler as output_target): a module emits/consumes web/release only when both + # android and ios output_target are release and the build isn't forced to debug; + # otherwise web/debug. Keying off output_flavor alone mis-declares web/release for + # debug-output_target modules (e.g. jasmine), whose compiler only writes web/debug. + return _get_web_output_target(ctx.attr, code_coverage or ctx.attr.output_flavor[BuildSettingInfo].value == "debug") + def _get_files_output_paths(ctx, module_name, module_directory, localization_mode, enable_web, code_coverage, enable_android = True, enable_ios = True, emit_debug = True, emit_release = True): filtered_srcs = _get_compiled_srcs(ctx) outputs = _get_srcs_dts_paths(filtered_srcs, module_name, module_directory) - web_release_enabled = enable_web and _supports_web_release(ctx, code_coverage) - if web_release_enabled: - outputs += _get_srcs_js_paths(filtered_srcs + ctx.files.protodecl_srcs, module_name, module_directory, bool(ctx.files.res), bool(ctx.file.ids_yaml), bool(ctx.attr.strings_dir)) - outputs += _get_web_native_module_package_file_paths(ctx.attr.web_register_native_module_id_overrides) - outputs += _get_srcs_vue_paths(ctx.files.legacy_vue_srcs, module_name, module_directory) + web_flavor = _web_output_flavor(ctx, code_coverage) + web_res_index = 0 if web_flavor == "debug" else 1 + if enable_web: + outputs += _get_srcs_js_paths(filtered_srcs + ctx.files.protodecl_srcs, module_name, module_directory, bool(ctx.files.res), bool(ctx.file.ids_yaml), bool(ctx.attr.strings_dir), web_flavor) + outputs += _get_web_native_module_package_file_paths(ctx.attr.web_register_native_module_id_overrides, web_flavor) + outputs += _get_srcs_vue_paths(ctx.files.legacy_vue_srcs, module_name, module_directory, web_flavor) outputs += get_sql_js_paths(ctx.attr.sql_db_names, ctx.files.sql_srcs, module_name, module_directory) outputs += get_legacy_vue_srcs_dts_paths(ctx.files.legacy_vue_srcs, module_name, module_directory) @@ -945,8 +954,8 @@ def _get_files_output_paths(ctx, module_name, module_directory, localization_mod strings_json_srcs = ctx.files.strings_json_srcs outputs += get_strings_dts_path(TYPESCRIPT_GENERATED_TS_DIR, module_name, strings_json_srcs) - if web_release_enabled: - outputs += _get_web_string_resource_paths(module_name, strings_json_srcs, ctx.attr.strings_dir)[1] + if enable_web: + outputs += _get_web_string_resource_paths(module_name, strings_json_srcs, ctx.attr.strings_dir)[web_res_index] if localization_mode == "external": # Android strings-xx.xml @@ -967,9 +976,9 @@ def _get_files_output_paths(ctx, module_name, module_directory, localization_mod outputs = _append_debug_and_maybe_release(outputs, android_output_target, _get_android_image_resources_paths(module_name, basenames), emit_debug, emit_release) if enable_ios: outputs = _append_debug_and_maybe_release(outputs, ios_output_target, _get_ios_image_resources_paths(module_name, basenames), emit_debug, emit_release) - if web_release_enabled: + if enable_web: renamed_resources = _extract_renamed_resources(ctx.files.res) - outputs += _get_web_resource_paths(module_name, renamed_resources)[1] + outputs += _get_web_resource_paths(module_name, renamed_resources)[web_res_index] outputs += _get_web_generated_resource_paths(module_name, outputs) outputs.append(_get_dumped_compilation_metadata(module_name)) @@ -1140,9 +1149,8 @@ def _get_srcs_dts_paths(srcs, module_name, module_directory): if f.extension in ["tsx", "ts"] and not f.basename.endswith(".d.ts") ] -def _get_srcs_js_paths(srcs, module_name, module_directory, has_resources, has_ids, has_strings): +def _get_srcs_js_paths(srcs, module_name, module_directory, has_resources, has_ids, has_strings, output_target): out = [] - output_target = "release" for f in srcs: if _is_test_file(f): @@ -1159,8 +1167,8 @@ def _get_srcs_js_paths(srcs, module_name, module_directory, has_resources, has_i return out -def _get_web_native_module_package_file_paths(web_register_native_module_id_overrides): - output_dir = base_relative_dir("web", "release", "assets") +def _get_web_native_module_package_file_paths(web_register_native_module_id_overrides, output_target): + output_dir = base_relative_dir("web", output_target, "assets") out = [] for implementation_path in web_register_native_module_id_overrides: @@ -1172,9 +1180,8 @@ def _get_web_native_module_package_file_paths(web_register_native_module_id_over return out -def _get_srcs_vue_paths(srcs, module_name, module_directory): +def _get_srcs_vue_paths(srcs, module_name, module_directory, output_target): out = [] - output_target = "release" for f in srcs: if _is_test_file(f): @@ -1911,11 +1918,12 @@ def _create_valdi_module_info(ctx, module_name, module_yaml, module_definition, base_path = paths.join(ctx.label.workspace_root, ctx.label.package) single_file_codegen = ctx.attr.single_file_codegen - web_release_enabled = _supports_web_release(ctx, code_coverage) - web_input_dts_files = _extract_web_dts_files(in_declarations) if web_release_enabled else [] - web_output_dts_files = _extract_web_dts_files(out_declarations) if web_release_enabled else [] - web_resource_files = _extract_web_resources("release", outputs) if web_release_enabled else [] - if web_release_enabled and ctx.attr.inline_assets: + enable_web = ctx.var.get("enable_web") + web_flavor = _web_output_flavor(ctx, code_coverage) + web_input_dts_files = _extract_web_dts_files(in_declarations) if enable_web else [] + web_output_dts_files = _extract_web_dts_files(out_declarations) if enable_web else [] + web_resource_files = _extract_web_resources(web_flavor, outputs) if enable_web else [] + if enable_web and ctx.attr.inline_assets: web_resource_files += ctx.files.res return ValdiModuleInfo( @@ -1979,11 +1987,11 @@ def _create_valdi_module_info(ctx, module_name, module_yaml, module_definition, # web outputs protodecl_srcs = ctx.files.protodecl_srcs, - web_sources = _extract_js_files(module_name, "release", outputs) if web_release_enabled else [], + web_sources = _extract_js_files(module_name, web_flavor, outputs) if enable_web else [], web_resource_files = web_resource_files, web_no_inline_images = ctx.attr.web_no_inline_images, web_module_file_entries = _collect_web_module_file_entries(ctx, module_name), - web_strings = _extract_web_strings("release", outputs) if web_release_enabled else [], + web_strings = _extract_web_strings(web_flavor, outputs) if enable_web else [], web_deps = _extract_npm_package_files(ctx.attr.web_deps), web_input_dts_files = web_input_dts_files, web_output_dts_files = web_output_dts_files, diff --git a/compiler/compiler/Compiler/Sources/Reloader/HotReloadLifecycleReporter.swift b/compiler/compiler/Compiler/Sources/Reloader/HotReloadLifecycleReporter.swift index f24a6a3b0..19d2c2efe 100644 --- a/compiler/compiler/Compiler/Sources/Reloader/HotReloadLifecycleReporter.swift +++ b/compiler/compiler/Compiler/Sources/Reloader/HotReloadLifecycleReporter.swift @@ -25,6 +25,9 @@ final class HotReloadLifecycleReporter { private let port: Int? private let output: Output private let errorOutput: Output + // Emitted from multiple queues (DaemonService connection queue, AutoRecompiler main + // queue); serialize writes so concurrent lines are not interleaved. + private let writeLock = NSLock() init(target: String, port: Int?, @@ -102,8 +105,13 @@ final class HotReloadLifecycleReporter { do { let encoder = JSONEncoder() encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] - output(String(decoding: try encoder.encode(payload), as: UTF8.self)) + let line = String(decoding: try encoder.encode(payload), as: UTF8.self) + writeLock.lock() + defer { writeLock.unlock() } + output(line) } catch { + writeLock.lock() + defer { writeLock.unlock() } errorOutput("Failed to encode Valdi hot reload lifecycle event '\(event)': \(error)") } } diff --git a/docs/docs/native-customviews.md b/docs/docs/native-customviews.md index 1a501480b..0f75dfd17 100644 --- a/docs/docs/native-customviews.md +++ b/docs/docs/native-customviews.md @@ -6,6 +6,9 @@ Valdi supports injecting native views inside of an existing valdi feature throug This type of integration is useful when a very complex view (such as a system view) is already implemented natively and we want to re-use platform-specific code instead of writing a new cross-platform component. +> [!IMPORTANT] +> A `` binds its native view **one** of two ways: a `viewFactory` (a `ViewFactory` supplied at runtime, shown below) **or** platform class names (`iosClass`/`androidClass`/`macosClass`/`webClass`, covered further down). The two are mutually exclusive — specifying both on the same element is a compile error. If a view needs a `viewFactory` on one platform and a registered class on another, branch into two sibling `` elements, each with a single mechanism. Prefer gating on whether a `viewFactory` is provided (`if (viewFactory) { … } else { …class… }`) over `Device.isWeb()`, since the factory is often supplied by native too. Keep a platform class only when it actually exists. + ## Using a `` Here we find a simple example on how to inject custom views inside of a Valdi rendered feature. diff --git a/valdi/compiler/toolbox/src/valdi/compiler_toolbox/CollapseWebPaths.cpp b/valdi/compiler/toolbox/src/valdi/compiler_toolbox/CollapseWebPaths.cpp index 2ce5f10ad..a3f4d100e 100644 --- a/valdi/compiler/toolbox/src/valdi/compiler_toolbox/CollapseWebPaths.cpp +++ b/valdi/compiler/toolbox/src/valdi/compiler_toolbox/CollapseWebPaths.cpp @@ -142,6 +142,8 @@ static void rewriteDeclarationImports(std::string& content, const std::string& p rewriteDeclarationImportsForPrefix(content, "from \"", '"', packageName); rewriteDeclarationImportsForPrefix(content, "import '", '\'', packageName); rewriteDeclarationImportsForPrefix(content, "import \"", '"', packageName); + rewriteDeclarationImportsForPrefix(content, "import('", '\'', packageName); + rewriteDeclarationImportsForPrefix(content, "import(\"", '"', packageName); } static Result copySourceDeclarations(const Path& outputDirectory, const Path& manifestPath) { diff --git a/valdi/compiler/toolbox/test/CollapseWebPaths_tests.cpp b/valdi/compiler/toolbox/test/CollapseWebPaths_tests.cpp index 207dd0d4d..d0b3d0baa 100644 --- a/valdi/compiler/toolbox/test/CollapseWebPaths_tests.cpp +++ b/valdi/compiler/toolbox/test/CollapseWebPaths_tests.cpp @@ -73,7 +73,9 @@ TEST(CollapseWebPaths, buildsTheCompleteWebPackage) { "module.exports = require('core/src/Core');\n"); auto worker = directory.write("inputs/app/src/Worker.js", "workerService(module);\n"); auto strings = directory.write("inputs/app/src/Strings.js", "\"use strict\";\n"); - auto declaration = directory.write("inputs/app/src/Types.d.ts", "import { Core } from 'core/src/Core';\n"); + auto declaration = directory.write("inputs/app/src/Types.d.ts", + "import { Core } from 'core/src/Core';\n" + "export type Lazy = typeof import('core/src/Core');\n"); auto locale = directory.write("inputs/app/strings/en.json", "{}\n"); auto image = directory.write("inputs/app/res/music_icon.svg", "\n"); auto config = directory.write("inputs/app/res/config.json", "{}\n"); @@ -115,7 +117,8 @@ TEST(CollapseWebPaths, buildsTheCompleteWebPackage) { "NavigationPage)(module);\n" "module.exports = require('../../core/src/Core.js');\n"); EXPECT_EQ(directory.read("output/src/app/src/Types.d.ts"), - "import { Core } from '@scope/package/src/core/src/Core';\n"); + "import { Core } from '@scope/package/src/core/src/Core';\n" + "export type Lazy = typeof import('@scope/package/src/core/src/Core');\n"); EXPECT_EQ(directory.read("output/src/_navigation_registry.js"), "var __r = (globalThis.__valdiNavigationPages = globalThis.__valdiNavigationPages || {});\n" "__r['app/src/Main'] = function() { return require('./app/src/Main'); };\n");