create-octane-xplat 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +78 -1
  2. package/compatibility.json +199 -0
  3. package/compatibility.ts +109 -0
  4. package/dist/compatibility.js +66 -0
  5. package/dist/index.js +132 -0
  6. package/dist/scaffold.js +439 -0
  7. package/index.ts +198 -0
  8. package/library-template/AGENTS.md +18 -0
  9. package/{LICENSE → library-template/LICENSE} +6 -6
  10. package/library-template/README.md +84 -0
  11. package/library-template/gitignore +5 -0
  12. package/library-template/package.json +92 -0
  13. package/library-template/pnpm-workspace.yaml +156 -0
  14. package/library-template/src/Badge.tsx +21 -0
  15. package/library-template/src/Badge.web.tsx +21 -0
  16. package/library-template/src/index.ts +3 -0
  17. package/library-template/src/index.web.ts +3 -0
  18. package/library-template/src/normalizeBadgeLabel.ts +12 -0
  19. package/library-template/src/props.ts +14 -0
  20. package/library-template/tests/normalizeBadgeLabel.test.ts +6 -0
  21. package/library-template/tests/packed-consumer.mjs +156 -0
  22. package/library-template/tsconfig.envs.json +17 -0
  23. package/library-template/tsconfig.json +27 -0
  24. package/library-template/tsconfig.types.json +11 -0
  25. package/library-template/tsrx-typegen.json +16 -0
  26. package/package.json +27 -8
  27. package/{scaffold.mjs → scaffold.ts} +149 -47
  28. package/template/.agents/skills/xplat/SKILL.md +11 -3
  29. package/template/.agents/skills/xplat/references/components.md +2 -2
  30. package/template/.agents/skills/xplat/references/navigation.md +9 -5
  31. package/template/.agents/skills/xplat/references/platform.md +2 -2
  32. package/template/AGENTS.md +47 -0
  33. package/template/App_Resources/Android/app.gradle +1 -1
  34. package/template/package.json +35 -32
  35. package/template/pnpm-workspace.yaml +73 -11
  36. package/template/src/app.spec.tsrx +80 -2
  37. package/template/src/app.web.test.tsx +20 -0
  38. package/template/src/failure-reporting.spec.ts +11 -0
  39. package/template/src/main.web.tsrx +1 -0
  40. package/template/src/native-runtime.spec.ts +18 -0
  41. package/template/src/test-setup.web.ts +17 -0
  42. package/template/tsconfig.json +32 -8
  43. package/template/vite.config.ts +2 -2
  44. package/template/vitest.web.config.mts +13 -0
  45. package/index.mjs +0 -129
  46. package/template/.oxlintrc.json +0 -25
  47. package/template/tsconfig.native.json +0 -18
@@ -0,0 +1,84 @@
1
+ # octane-xplat-library
2
+
3
+ A cross-platform component library for Octane Xplat. The starter includes a
4
+ small `Badge` component and a packed-consumer check so you can build a package
5
+ and check the artifact that will be published.
6
+
7
+ ## Start here
8
+
9
+ Install this package in an Octane Xplat app:
10
+
11
+ ```sh
12
+ pnpm add octane-xplat-library
13
+ ```
14
+
15
+ Render the example component from your app:
16
+
17
+ ```tsx
18
+ import { Badge } from 'octane-xplat-library'
19
+
20
+ export function Welcome() {
21
+ return <Badge label="Ready" />
22
+ }
23
+ ```
24
+
25
+ ## Develop and check
26
+
27
+ Run these commands from the package directory:
28
+
29
+ ```sh
30
+ pnpm typecheck
31
+ pnpm lint
32
+ pnpm lint:fix
33
+ pnpm test
34
+ pnpm typegen
35
+ pnpm test:packed
36
+ ```
37
+
38
+ Linting uses the CLI's bundled framework defaults. Apply safe lint fixes with
39
+ `lint:fix`; use `format` to format source and `fmt --check` to check formatting
40
+ without changing files:
41
+
42
+ ```sh
43
+ pnpm format
44
+ pnpm exec xplat fmt --check
45
+ ```
46
+
47
+ The package publishes authored source for the app's bundler to compile.
48
+ `pnpm typegen` generates `.d.ts` files so consumers' type checkers use
49
+ declarations. `pnpm test:packed` packs the package, places that artifact into
50
+ small web and native TypeScript consumers, and checks both export conditions.
51
+
52
+ The package sets `nodeLinker: hoisted` in `pnpm-workspace.yaml` because the
53
+ NativeScript bundler expects a flat dependency tree.
54
+
55
+ Its `catalog:xplat` build dependencies and patches follow one framework
56
+ compatibility set. Update that set together; consumer `peerDependencies`
57
+ ranges are preserved for you to maintain:
58
+
59
+ ```sh
60
+ pnpm dlx @octane-xplat/cli@latest upgrade --check
61
+ pnpm dlx @octane-xplat/cli@latest upgrade
62
+ pnpm xplat doctor
63
+ ```
64
+
65
+ ## Platform files
66
+
67
+ `src/Badge.tsx` is the shared iOS and Android implementation. The
68
+ `src/Badge.web.tsx` sibling supplies the web implementation. Keep their props
69
+ and exports aligned in `src/props.ts` and the two `src/index` barrels. Add
70
+ `.ios.tsx`, `.android.tsx`, or `.mobile.tsx` files only when a behavior needs
71
+ to differ on those targets.
72
+
73
+ The starter supports web, iOS, and Android. It does not include app shells or
74
+ desktop runtime entries. If the package uses a NativeScript plugin, declare it
75
+ as a runtime dependency and add the plugin's required platform files and
76
+ metadata to this package.
77
+
78
+ ## Prepare to publish
79
+
80
+ Before publishing, replace the generated package name and repository metadata
81
+ in `package.json`, fill in the year and copyright holder in `LICENSE`, and
82
+ confirm that every runtime import is declared. `pnpm test:packed` checks the
83
+ package tarball's web and native type exports; it does not build or launch a
84
+ consuming app on a device.
@@ -0,0 +1,5 @@
1
+ node_modules/
2
+ dist/
3
+ types/
4
+ coverage/
5
+ .turbo/
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "octane-xplat-library",
3
+ "version": "0.1.0",
4
+ "description": "A cross-platform component library for Octane Xplat",
5
+ "keywords": [
6
+ "android",
7
+ "cross-platform",
8
+ "ios",
9
+ "octane",
10
+ "octane-xplat",
11
+ "ui",
12
+ "web"
13
+ ],
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/your-org/your-library",
18
+ "directory": "."
19
+ },
20
+ "files": [
21
+ "src",
22
+ "types",
23
+ "LICENSE"
24
+ ],
25
+ "type": "module",
26
+ "exports": {
27
+ ".": {
28
+ "macos": null,
29
+ "linux": null,
30
+ "windows": null,
31
+ "web": {
32
+ "types": "./types/index.web.d.ts",
33
+ "default": "./src/index.web.ts"
34
+ },
35
+ "native": {
36
+ "types": "./types/index.d.ts",
37
+ "default": "./src/index.ts"
38
+ },
39
+ "types": "./types/index.d.ts",
40
+ "default": "./src/index.ts"
41
+ }
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "scripts": {
47
+ "typegen": "tsrx-typegen --project tsconfig.types.json",
48
+ "typecheck": "xplat typecheck",
49
+ "lint": "xplat lint",
50
+ "lint:fix": "xplat lint --fix",
51
+ "test": "vitest run",
52
+ "test:packed": "node tests/packed-consumer.mjs",
53
+ "prepack": "pnpm typegen && tsrx-typegen --pack-check",
54
+ "pack:check": "pnpm test:packed",
55
+ "format": "xplat fmt"
56
+ },
57
+ "devDependencies": {
58
+ "@nativescript-community/octane": "catalog:xplat",
59
+ "@nativescript/core": "catalog:xplat",
60
+ "@nativescript/types": "catalog:xplat",
61
+ "@octane-xplat/cli": "catalog:xplat",
62
+ "@octane-xplat/jsx": "catalog:xplat",
63
+ "@octane-xplat/ui": "catalog:xplat",
64
+ "@octanejs/vite-plugin": "catalog:xplat",
65
+ "@tsrx/oxc": "catalog:xplat",
66
+ "@tsrx/typescript-plugin": "catalog:xplat",
67
+ "octane": "catalog:xplat",
68
+ "tsrx-typegen": "catalog:xplat",
69
+ "typescript": "catalog:xplat",
70
+ "vite": "catalog:xplat",
71
+ "vitest": "catalog:xplat"
72
+ },
73
+ "peerDependencies": {
74
+ "@nativescript-community/octane": ">=0.2.1 <1",
75
+ "@nativescript/core": ">=9.1.0 <10",
76
+ "@octane-xplat/jsx": "^0.16.0",
77
+ "@octane-xplat/ui": "^0.16.0",
78
+ "octane": ">=0.6.3 <1"
79
+ },
80
+ "peerDependenciesMeta": {
81
+ "@nativescript-community/octane": {
82
+ "optional": true
83
+ },
84
+ "@nativescript/core": {
85
+ "optional": true
86
+ }
87
+ },
88
+ "packageManager": "pnpm@11.24.0",
89
+ "engines": {
90
+ "node": ">=22.18.0"
91
+ }
92
+ }
@@ -0,0 +1,156 @@
1
+ # NativeScript's bundler resolves dependencies from the flat package tree.
2
+ nodeLinker: hoisted
3
+ allowBuilds:
4
+ esbuild: true
5
+ '@parcel/watcher': true
6
+ '@nativescript/core': false
7
+ catalogs:
8
+ xplat:
9
+ "@nativescript-community/octane": 0.2.5
10
+ "@nativescript/core": 9.1.3
11
+ "@nativescript/types": 9.1.4
12
+ "@octane-xplat/cli": 0.16.0
13
+ "@octane-xplat/jsx": 0.16.0
14
+ "@octane-xplat/ui": 0.16.0
15
+ "@octanejs/vite-plugin": 0.1.61
16
+ "@tsrx/oxc": 0.20.0
17
+ "@tsrx/typescript-plugin": 0.4.11
18
+ octane: 0.11.0
19
+ tsrx-typegen: 0.2.2
20
+ typescript: 5.9.3
21
+ vite: 8.3.0
22
+ vitest: 4.1.11
23
+ overrides:
24
+ "@nativescript/vite>esbuild": 0.27.7
25
+ "@nativescript-community/vite-octane>esbuild": 0.27.7
26
+ vite>esbuild: 0.27.7
27
+ configDependencies:
28
+ "@octane-xplat/patches": 0.16.0
29
+ allowUnusedPatches: true
30
+ # Framework patches are supplied by the @octane-xplat/patches config dependency and
31
+ # applied via pnpm patchedDependencies. Managed upstream by @octane-xplat/cli (`xplat
32
+ # patches apply` refreshes, `xplat patches check` verifies); do not edit by hand.
33
+ patchedDependencies:
34
+ # Published 0.11.0 still ships none of the retained fixes: it adds the document guard
35
+ # context around installDefaultSignalOwner and reflows the compile.js hook-classification
36
+ # sites, but every patched feature remains absent upstream. Retains renderer-owned
37
+ # Children.toArray/map with explicit universal-native re-exports for older runtimes,
38
+ # universal signal-read tracking and local hooks, root event-error forwarding, the
39
+ # complete universal imperative-ref lifecycle, the Descriptor declaring-owner reader
40
+ # fallback, and the signal-owner/enclosingOwner chain so child reads reuse instance
41
+ # declarations. Verified against the registry 0.11.0 tarball; all hunks re-anchored to the
42
+ # rewritten compile.js and document-owner call sites. Disposes DOM provider descendants
43
+ # when children become null or undefined, releasing their refs and effects.
44
+ octane@0.11.0: node_modules/.pnpm-config/@octane-xplat/patches/patches/octane@0.11.0.patch
45
+ # Published 9.1.3 ships the #11446 nested-tab-frame lifecycle fix and iOS explicit-size
46
+ # flex alignment, so those hunks are removed. Retains the unshipped parent-padding
47
+ # measure-spec fix and Android explicit-size flex alignment, iOS continuous corner
48
+ # clipping, CSS @layer cascade support, custom Android variable-font weight mapping, and
49
+ # percentage translate parsing, native view, and animation support. Verified against the
50
+ # registry 9.1.3 tarball; it has no Windows implementation, so the separate Windows
51
+ # preview patch remains.
52
+ "@nativescript/core@9.1.3": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript__core@9.1.3.patch
53
+ # @nativescript/vite serializes bundled CSS and dev app.css through reworkcss `css`@3.0.0,
54
+ # which mis-parses @layer into garbage selectors — rules die before reaching
55
+ # _populateRules. Adds atlayer() covering block and statement forms. Applies to a
56
+ # transitive dep — no app declares it directly.
57
+ css@3.0.0: node_modules/.pnpm-config/@octane-xplat/patches/patches/css@3.0.0.patch
58
+ # Dev-server fixes still missing after @nativescript/vite 8.0.17 added percent decoding
59
+ # for path-style /ns/m and Vue /ns/sfc requests (NativeScript#11483): collect re-export
60
+ # specifiers in the .vite/deps rewrite, route /@fs paths outside both roots through /ns/m
61
+ # (linked checkouts bypass the device transform upstream), and probe the non-active
62
+ # platform + `native` suffix tags in module candidates. The deps-bundle esbuild passes
63
+ # ignore vite resolve.alias, so a dep importing bare `octane` bundles the DOM runtime
64
+ # beside the universal one (hooks crash on boot, octane-xplat#3) — an onResolve plugin
65
+ # redirects `octane` → `octane/universal/native`. tsconfig-paths and the generated
66
+ # build-time checker moduleSuffixes also need the `.mobile` tier for iOS/Android/visionOS
67
+ # so `.mobile` leaves resolve under mapped imports and `ns build`. Served `/node_modules/`
68
+ # tails now also scan pnpm's `.pnpm` store and `link:`/`file:`/`portal:` dep roots — a
69
+ # linked package's transitive deps (e.g. @iconify/utils under @octane-xplat/icons) resolve
70
+ # nowhere else and 404 the dev session. findMonorepoWorkspaceRoot no longer treats a lone
71
+ # turbo.json as a workspace root — Turborepo's per-package task configs marked apps/* as
72
+ # roots, masking the real pnpm root so served `/node_modules/` tails never reached the
73
+ # `.pnpm` store scan and non-hoisted transitive deps like source-map-js 404'd dev boot.
74
+ # Linked dependency fallbacks also discover each linked package's enclosing workspace and
75
+ # reuse the cached, bounded workspace node_modules scan so transitive workspace siblings
76
+ # (auth to secure-storage to a private native plugin) resolve without the consumer
77
+ # supplying that workspace root. Transform candidates now keep root-relative URLs only
78
+ # when they identify the discovered file; nested linked-dependency and pnpm-store matches
79
+ # inside the app root use their actual filesystem paths, preserving query suffixes. The
80
+ # /ns/m import rewriter now resolves `#`-prefixed package `imports` specifiers through the
81
+ # importer's own package.json — raw-read node_modules files (e.g. octane's
82
+ # dist/signals/graph.js importing `#octane/dist/signal-actions/graph`) otherwise reach the
83
+ # device verbatim and the HTTP loader throws `Cannot find module`, deadlocking dev boot
84
+ # before any boot recording can bundle the dep. NativeScript#11551: preserve user virtual
85
+ # stylesheet imports while retaining browser-helper filtering, route virtual CSS through
86
+ # the native inline compiler/registration path before the generic virtual-module shortcut,
87
+ # and relay Vite virtual stylesheet updates as tagged native CSS updates through the same
88
+ # adapted-text endpoint. Empty sheets clear previous rules; CSS compilation failures
89
+ # return HTTP 500 instead of raw CSS.
90
+ "@nativescript/vite@8.0.17": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript__vite@8.0.17.patch
91
+ # Retains the workspace's NativeScript driver fixes when upgrading to 0.2.5 for portal
92
+ # support: a prop dropped from an update's complete prop bag — or set to
93
+ # `undefined`/`null` — is a removal that restores the documented default
94
+ # (Property/CssProperty setters get `unsetValue`, plain fields the captured pre-prop
95
+ # value, `style` keys diff per key); a never-applied `undefined` still skips the write.
96
+ # Also advertises the implemented visibility capability, carries Octane callback ref
97
+ # types, and preserves authored Grid row/col/span metadata. Hosts custom-layout containers
98
+ # (e.g. MasonKit ViewBase) that own child lists via
99
+ # insertChild/removeChild/getChildrenCount without subclassing LayoutBase. Additionally,
100
+ # the masonry cutover: masonview resolves to MasonKit View and scrollview to MasonKit
101
+ # Scroll as the only layout containers, with zero layout props — every former prop name
102
+ # warns-and-drops and all CSS semantics arrive through the style bag; style keys on a
103
+ # mason child route to its leaf (stashed pre-attach, replayed at attach) via
104
+ # Style.fromView.
105
+ "@nativescript-community/octane@0.2.5": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript-community__octane@0.2.5.patch
106
+ # octaneServerStrategy's SCRIPT_FILE_RE only covers [jt]sx? — .tsrx edits never emit the
107
+ # deferred ns:hmr-delta broadcast, so component HMR silently never reaches the device.
108
+ # Also counts open /ns-hmr sockets so the update summary stops reporting recipients=0 for
109
+ # delivered updates.
110
+ "@nativescript-community/vite-octane@0.2.1": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript-community__vite-octane@0.2.1.patch
111
+ # Enables `resolveHiddenExtensions` on the language plugin's typescript facet so
112
+ # extensionless specifiers (e.g. './Text') probe `.tsrx` after moduleSuffixes — required
113
+ # for a single extensionless shared barrel to resolve per-platform leaves under tsrx-tsc
114
+ # and the IDE plugin.
115
+ "@tsrx/typescript-plugin@0.4.11": node_modules/.pnpm-config/@octane-xplat/patches/patches/@tsrx__typescript-plugin@0.4.11.patch
116
+ # linkifyjs declares /\p{L}/u and /\p{Emoji}/u as regex literals at module scope;
117
+ # NativeScript's embedded V8 builds without ICU, so the literals throw SyntaxError at
118
+ # parse time and poison the whole bundle (any bundled app importing @tiptap/extension-link
119
+ # dies on boot). Rewrites them as try/catch new RegExp builds with ASCII-range fallbacks —
120
+ # same behavior where ICU exists, graceful degradation where it doesn't.
121
+ linkifyjs@4.3.3: node_modules/.pnpm-config/@octane-xplat/patches/patches/linkifyjs@4.3.3.patch
122
+ # useEditor.ts reads bare process.env.NODE_ENV — no node global exists in the harness
123
+ # typecheck program. Adds a module-local process declaration for typecheck. (The
124
+ # './Context' self-import half of the 0.0.51 patch shipped upstream in 0.0.52 via
125
+ # octanejs/octane#1502.)
126
+ "@octanejs/tiptap@0.0.52": node_modules/.pnpm-config/@octane-xplat/patches/patches/@octanejs__tiptap@0.0.52.patch
127
+ # @lexical/link declares a /\p{L}\p{N}/u URL matcher as a regex literal at module scope;
128
+ # NativeScript's embedded V8 builds without ICU, so the literal throws SyntaxError at
129
+ # parse time and poisons the whole bundle (any bundled app importing @lexical/link dies on
130
+ # boot). Same fix as linkifyjs: try/catch new RegExp with an ASCII-range fallback — the
131
+ # matcher only feeds autolink, which native doesn't use.
132
+ "@lexical/link@0.51.0": node_modules/.pnpm-config/@octane-xplat/patches/patches/@lexical__link@0.51.0.patch
133
+ # ui-image 5.x regressed two iOS prefetch behaviors the 4.x line shipped (still present in
134
+ # 4.6.20): prefetchToCacheType ignores its cacheType argument — SDWebImagePrefetcher's
135
+ # default context stores to disk+memory either way, so a 'disk' prefetch also decodes and
136
+ # warms memory, diverging from Android's bytes-only Glide downloadOnly — and
137
+ # getContextFromOptions drops PrefetchOptions.headers because it never sets
138
+ # SDWebImageContextDownloadRequestModifier, so prefetch({headers}) silently sends no
139
+ # headers on iOS while Android's CustomGlideUrl forwards them. Ports upstream's own 4.x
140
+ # implementation — SDWebImageContextStoreCacheType on the prefetch context plus the
141
+ # request-modifier branch in getContextFromOptions — and additionally sets
142
+ # SDWebImageContextOriginalStoreCacheType so a memory prefetch does not spill original
143
+ # bytes to disk.
144
+ "@nativescript-community/ui-image@5.0.17": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript-community__ui-image@5.0.17.patch
145
+ # @nativescript/doctor's Android runtime check reads the app's raw devDependencies
146
+ # specifier and npm dist-tag resolves anything that isn't a semver range — a pnpm
147
+ # `catalog:` pin throws 'The determined Android runtime version ... is not valid' and
148
+ # kills `ns build android` before toolchain checks. Resolve catalog specifiers through the
149
+ # installed package's manifest, reusing the existing tgz-branch mechanism.
150
+ "@nativescript/doctor@2.0.18": node_modules/.pnpm-config/@octane-xplat/patches/patches/@nativescript__doctor@2.0.18.patch
151
+ # list/index.ios.js assigns `ObjCProtocols = [MasonListDelegate]` at module scope.
152
+ # MasonListDelegate is declared in Mason-Swift.h, which the app's metadata generation does
153
+ # not surface as a runtime global — importing masonkit on iOS throws `MasonListDelegate is
154
+ # not defined` during bundle eval and kills the app at boot. Guard the binding; actual
155
+ # List delegate conformance when the protocol is absent is a separate gap.
156
+ "@triniwiz/nativescript-masonkit@1.0.0-beta.110": node_modules/.pnpm-config/@octane-xplat/patches/patches/@triniwiz__nativescript-masonkit@1.0.0-beta.110.patch
@@ -0,0 +1,21 @@
1
+ /** @jsxImportSource @octane-xplat/jsx */
2
+ import { Text, View } from '@octane-xplat/ui'
3
+ import { normalizeBadgeLabel } from './normalizeBadgeLabel'
4
+ import type { BadgeProps } from './props'
5
+
6
+ /**
7
+ * Render a short label using Octane's NativeScript elements.
8
+ * @example
9
+ * ```tsx
10
+ * import { Badge } from 'octane-xplat-library'
11
+ *
12
+ * <Badge label="Ready" />
13
+ * ```
14
+ */
15
+ export function Badge(props: BadgeProps) {
16
+ return (
17
+ <View className={props.className}>
18
+ <Text>{normalizeBadgeLabel(props.label)}</Text>
19
+ </View>
20
+ )
21
+ }
@@ -0,0 +1,21 @@
1
+ /** @jsxImportSource @octane-xplat/jsx/dom */
2
+ import { Text, View } from '@octane-xplat/ui'
3
+ import { normalizeBadgeLabel } from './normalizeBadgeLabel'
4
+ import type { BadgeProps } from './props'
5
+
6
+ /**
7
+ * Render a short label using Octane's web elements.
8
+ * @example
9
+ * ```tsx
10
+ * import { Badge } from 'octane-xplat-library'
11
+ *
12
+ * <Badge label="Ready" />
13
+ * ```
14
+ */
15
+ export function Badge(props: BadgeProps) {
16
+ return (
17
+ <View className={props.className}>
18
+ <Text>{normalizeBadgeLabel(props.label)}</Text>
19
+ </View>
20
+ )
21
+ }
@@ -0,0 +1,3 @@
1
+ export { Badge } from './Badge'
2
+ export { normalizeBadgeLabel } from './normalizeBadgeLabel'
3
+ export type { BadgeProps } from './props'
@@ -0,0 +1,3 @@
1
+ export { Badge } from './Badge'
2
+ export { normalizeBadgeLabel } from './normalizeBadgeLabel'
3
+ export type { BadgeProps } from './props'
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Trim surrounding whitespace from a badge label.
3
+ * @example
4
+ * ```ts
5
+ * import { normalizeBadgeLabel } from 'octane-xplat-library'
6
+ *
7
+ * normalizeBadgeLabel(' Ready ')
8
+ * ```
9
+ */
10
+ export function normalizeBadgeLabel(label: string): string {
11
+ return label.trim()
12
+ }
@@ -0,0 +1,14 @@
1
+ /** Props for the starter badge component. */
2
+ export interface BadgeProps {
3
+ /** Text displayed inside the badge.
4
+ * @example
5
+ * ```tsx
6
+ * import { Badge } from 'octane-xplat-library'
7
+ *
8
+ * <Badge label="Ready" />
9
+ * ```
10
+ */
11
+ label: string
12
+ /** Optional class name forwarded to the outer view. */
13
+ className?: string
14
+ }
@@ -0,0 +1,6 @@
1
+ import { expect, it } from 'vitest'
2
+ import { normalizeBadgeLabel } from '../src/normalizeBadgeLabel'
3
+
4
+ it('trims surrounding whitespace from a badge label', () => {
5
+ expect(normalizeBadgeLabel(' Ready ')).toBe('Ready')
6
+ })
@@ -0,0 +1,156 @@
1
+ import assert from 'node:assert/strict'
2
+ import { spawnSync } from 'node:child_process'
3
+ import {
4
+ cpSync,
5
+ mkdirSync,
6
+ mkdtempSync,
7
+ readdirSync,
8
+ readFileSync,
9
+ rmSync,
10
+ symlinkSync,
11
+ writeFileSync,
12
+ } from 'node:fs'
13
+
14
+ import { tmpdir } from 'node:os'
15
+ import { dirname, join, resolve } from 'node:path'
16
+ import { fileURLToPath } from 'node:url'
17
+
18
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
19
+ const sourceManifest = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'))
20
+ const temporary = mkdtempSync(join(tmpdir(), 'octane-library-consumer-'))
21
+
22
+ function run(command, args, cwd) {
23
+ const result = spawnSync(command, args, { cwd, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 })
24
+ if (result.stdout) {
25
+ process.stdout.write(result.stdout)
26
+ }
27
+
28
+ if (result.stderr) {
29
+ process.stderr.write(result.stderr)
30
+ }
31
+
32
+ if (result.error) {
33
+ throw result.error
34
+ }
35
+
36
+ assert.equal(result.status, 0, `${command} ${args.join(' ')} failed`)
37
+ }
38
+
39
+ function installDirectory(root, name, destination) {
40
+ const segments = name.split('/')
41
+ const path = join(destination, 'node_modules', ...segments)
42
+ mkdirSync(dirname(path), { recursive: true })
43
+ cpSync(root, path, { recursive: true })
44
+ }
45
+
46
+ try {
47
+ const pack = join(temporary, 'pack')
48
+ const extracted = join(temporary, 'extracted')
49
+ mkdirSync(pack)
50
+ mkdirSync(extracted)
51
+ run('pnpm', ['pack', '--pack-destination', pack], packageRoot)
52
+ const tarball = readdirSync(pack).find((name) => name.endsWith('.tgz'))
53
+ assert.ok(tarball)
54
+ run('tar', ['-xzf', join(pack, tarball), '-C', extracted], temporary)
55
+ const packed = join(extracted, 'package')
56
+ const manifest = JSON.parse(readFileSync(join(packed, 'package.json'), 'utf8'))
57
+ assert.ok(manifest.exports['.'].web.default.startsWith('./src/'))
58
+ assert.ok(manifest.exports['.'].native.default.startsWith('./src/'))
59
+ assert.ok(manifest.exports['.'].web.types)
60
+ assert.ok(manifest.exports['.'].native.types)
61
+
62
+ for (const target of ['web', 'native']) {
63
+ for (const mode of ['bundler', 'nodenext']) {
64
+ const consumer = join(temporary, `${target}-${mode}`)
65
+ mkdirSync(consumer)
66
+ mkdirSync(join(consumer, 'node_modules'), { recursive: true })
67
+ installDirectory(packed, sourceManifest.name, consumer)
68
+ for (const dependency of [
69
+ 'octane',
70
+ '@octane-xplat/jsx',
71
+ '@octane-xplat/ui',
72
+ ...(target === 'native'
73
+ ? ['@nativescript-community/octane', '@nativescript/core', '@nativescript/types']
74
+ : []),
75
+ ]) {
76
+ const source = join(packageRoot, 'node_modules', ...dependency.split('/'))
77
+ const destination = join(consumer, 'node_modules', ...dependency.split('/'))
78
+ mkdirSync(dirname(destination), { recursive: true })
79
+ symlinkSync(source, destination, 'dir')
80
+ }
81
+
82
+ writeFileSync(join(consumer, 'package.json'), '{"type":"module"}')
83
+ writeFileSync(
84
+ join(consumer, 'consumer.tsx'),
85
+ `import { Badge, type BadgeProps } from '${sourceManifest.name}'
86
+ const props: BadgeProps = { label: 'Ready' }
87
+ const element = <Badge {...props} />
88
+ // @ts-expect-error label is required
89
+ const missing = <Badge />
90
+ // @ts-expect-error label must be a string
91
+ const invalid = <Badge label={1} />
92
+ void element; void missing; void invalid
93
+ `,
94
+ )
95
+
96
+ writeFileSync(
97
+ join(consumer, 'tsconfig.json'),
98
+ JSON.stringify(
99
+ {
100
+ compilerOptions: {
101
+ strict: true,
102
+ noEmit: true,
103
+ skipLibCheck: true,
104
+ target: 'esnext',
105
+ types: [],
106
+ module: mode === 'bundler' ? 'esnext' : 'nodenext',
107
+ moduleResolution: mode,
108
+ jsx: 'react-jsx',
109
+ jsxImportSource: target === 'native' ? '@octane-xplat/jsx' : '@octane-xplat/jsx/dom',
110
+ customConditions: [target],
111
+ moduleSuffixes: target === 'web' ? ['.web', ''] : ['.mobile', ''],
112
+ },
113
+ files: ['consumer.tsx'],
114
+ },
115
+ null,
116
+ '\t',
117
+ ),
118
+ )
119
+
120
+ run(
121
+ process.execPath,
122
+ [join(packageRoot, 'node_modules/typescript/bin/tsc'), '-p', 'tsconfig.json'],
123
+ consumer,
124
+ )
125
+
126
+ const listed = spawnSync(
127
+ process.execPath,
128
+ [
129
+ join(packageRoot, 'node_modules/typescript/bin/tsc'),
130
+ '-p',
131
+ 'tsconfig.json',
132
+ '--listFilesOnly',
133
+ ],
134
+ { cwd: consumer, encoding: 'utf8' },
135
+ )
136
+
137
+ assert.equal(listed.status, 0, listed.stderr)
138
+ const implementation = listed.stdout
139
+ .split('\n')
140
+ .filter(
141
+ (file) =>
142
+ file.includes(`/node_modules/${sourceManifest.name}/src/`) && !file.endsWith('.d.ts'),
143
+ )
144
+
145
+ assert.deepEqual(
146
+ implementation,
147
+ [],
148
+ 'consumer types use declarations without package implementation',
149
+ )
150
+ }
151
+ }
152
+
153
+ console.log('packed consumer: web/native declarations pass Bundler and NodeNext')
154
+ } finally {
155
+ rmSync(temporary, { recursive: true, force: true })
156
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "jsxImportSource": "@octane-xplat/jsx"
5
+ },
6
+ "environments": {
7
+ "web": {
8
+ "include": ["src/**/*.{ts,tsx,tsrx}"]
9
+ },
10
+ "native": {
11
+ "include": ["src/**/*.{ts,tsx,tsrx}"],
12
+ "compilerOptions": {
13
+ "types": ["@nativescript/types"]
14
+ }
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "strict": true,
4
+ "target": "es2022",
5
+ "module": "esnext",
6
+ "moduleResolution": "bundler",
7
+ "jsx": "react-jsx",
8
+ "jsxImportSource": "@octane-xplat/jsx",
9
+ "moduleSuffixes": [
10
+ "",
11
+ ".web",
12
+ ".ios",
13
+ ".android",
14
+ ".mobile",
15
+ ".native",
16
+ ".macos",
17
+ ".linux",
18
+ ".windows"
19
+ ],
20
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
21
+ "types": ["@nativescript/types"],
22
+ "noEmit": true,
23
+ "skipLibCheck": true
24
+ },
25
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.tsrx"],
26
+ "exclude": ["dist", "types", "node_modules"]
27
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "declaration": true,
5
+ "emitDeclarationOnly": true,
6
+ "noEmit": false,
7
+ "noEmitOnError": true,
8
+ "rootDir": "src",
9
+ "outDir": "types"
10
+ }
11
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "targets": {
3
+ "octane": {
4
+ "renderer": "@octane-xplat/jsx",
5
+ "project": "tsconfig.types.json",
6
+ "outDir": "types",
7
+ "entrypoints": {
8
+ ".": {
9
+ "source": "src/index.ts",
10
+ "runtime": ["./src/index.ts", "./src/index.web.ts"],
11
+ "types": ["./types/index.d.ts", "./types/index.web.d.ts"]
12
+ }
13
+ }
14
+ }
15
+ }
16
+ }