Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ai-skills/skills/valdi-custom-view/skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class MyComponent extends Component<MyViewModel> {
}
```

`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 `<custom-view>` 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 `<custom-view>` 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

Expand Down
6 changes: 5 additions & 1 deletion bzl/valdi/app_templates/web_path_browserify_shim.js.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
10 changes: 9 additions & 1 deletion bzl/valdi/valdi_collapse_web_paths.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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/<flavor>/res directories; those are handled
# by the web/<flavor>/{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)
Expand Down Expand Up @@ -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. <module>/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))

Expand Down
52 changes: 30 additions & 22 deletions bzl/valdi/valdi_compiled.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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))
Expand Down Expand Up @@ -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):
Expand All @@ -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:
Expand All @@ -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):
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?,
Expand Down Expand Up @@ -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)")
}
}
Expand Down
3 changes: 3 additions & 0 deletions docs/docs/native-customviews.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<custom-view>` 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 `<custom-view>` 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 `<custom-view>`

Here we find a simple example on how to inject custom views inside of a Valdi rendered feature.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Void> copySourceDeclarations(const Path& outputDirectory, const Path& manifestPath) {
Expand Down
7 changes: 5 additions & 2 deletions valdi/compiler/toolbox/test/CollapseWebPaths_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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", "<svg/>\n");
auto config = directory.write("inputs/app/res/config.json", "{}\n");
Expand Down Expand Up @@ -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");
Expand Down
Loading