@tinycld/bootstrap 3.0.0 → 3.1.1
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.
- package/dist/assemble-workspace.js +10 -1
- package/dist/assemble-workspace.js.map +1 -1
- package/package.json +1 -1
- package/templates/full/server/go.mod +2 -2
- package/templates/full/tinycld/{{PKG_SLUG}}/collections.ts +28 -5
- package/templates/full/tinycld/{{PKG_SLUG}}/screens/index.tsx +2 -1
- package/templates/shared/.github/workflows/ci.yml +2 -2
- package/templates/workspace/.go-version +1 -1
- package/templates/workspace/scripts/link-members.ts +47 -5
- package/templates/workspace/tests/tinycld.packages.test.ts +49 -0
- package/templates/workspace/tinycld.packages.ts +36 -0
|
@@ -5,7 +5,16 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
// All feature members that exist in the ecosystem. The workspace manifest lists
|
|
6
6
|
// ALL of them (npm tolerates absent dirs), so a partial checkout still installs.
|
|
7
7
|
// But --assemble-only CLONES only app+core+requested — we do NOT force-clone all.
|
|
8
|
-
const ALL_FEATURES = [
|
|
8
|
+
const ALL_FEATURES = [
|
|
9
|
+
'contacts',
|
|
10
|
+
'mail',
|
|
11
|
+
'calendar',
|
|
12
|
+
'drive',
|
|
13
|
+
'calc',
|
|
14
|
+
'text',
|
|
15
|
+
'google-takeout-import',
|
|
16
|
+
'boards',
|
|
17
|
+
];
|
|
9
18
|
// The `tinycld` member is the one always-cloned repo (tinycld/tinycld): it is
|
|
10
19
|
// the merged app shell + core. assembleWorkspace seeds it directly, pinnable via
|
|
11
20
|
// tinycldRef. Both @tinycld/core (at tinycld/core) and @tinycld/package-scripts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assemble-workspace.js","sourceRoot":"","sources":["../src/assemble-workspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAC9E,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAExC,gFAAgF;AAChF,iFAAiF;AACjF,kFAAkF;AAClF,MAAM,YAAY,GAAG,
|
|
1
|
+
{"version":3,"file":"assemble-workspace.js","sourceRoot":"","sources":["../src/assemble-workspace.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -4,7 +4,16 @@ import type { createCollection } from 'pbtsdb/core'
|
|
|
4
4
|
import { BasicIndex } from 'pbtsdb/core'
|
|
5
5
|
import type { {{PKG_PASCAL}}Schema } from './types'
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
// Replace (not intersect) the generated entries for this package's own
|
|
8
|
+
// collections — a plain intersection would merge each overlapping entry
|
|
9
|
+
// field-wise, letting a generated `any` absorb any typed override.
|
|
10
|
+
type MergedSchema = Omit<Schema, keyof {{PKG_PASCAL}}Schema> & {{PKG_PASCAL}}Schema
|
|
11
|
+
|
|
12
|
+
// Every collection syncs on demand and subscribes per query (pbtsdb 0.10):
|
|
13
|
+
// only the rows a live query asks for enter the store, and realtime covers
|
|
14
|
+
// exactly those rows. The server emits a delete to a subscription a row
|
|
15
|
+
// leaves, so a filtered view stays correct across updates.
|
|
16
|
+
const onDemand = { syncMode: 'on-demand', realtime: 'query' } as const
|
|
8
17
|
|
|
9
18
|
// Collections contributed by this package. Core calls this during pbtsdb
|
|
10
19
|
// bootstrap; the returned object's keys become top-level keys on the app's
|
|
@@ -13,12 +22,26 @@ export function registerCollections(
|
|
|
13
22
|
newCollection: ReturnType<typeof createCollection<MergedSchema>>,
|
|
14
23
|
_core: CoreStores
|
|
15
24
|
) {
|
|
25
|
+
// Hoisted rather than inlined: an inline `collectionOptions` object literal
|
|
26
|
+
// defeats inference of `alwaysFetchRelations` against `relations`, typing every
|
|
27
|
+
// expand path as `never`.
|
|
28
|
+
const indexing = {
|
|
29
|
+
autoIndex: 'eager' as const,
|
|
30
|
+
defaultIndexType: BasicIndex,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// `relations` declares where PocketBase's expanded records are filed. Rows
|
|
34
|
+
// never carry `expand`; read a relation from its own collection, with
|
|
35
|
+
// `materialize()` in a select, a join, or `collection.get(id)`.
|
|
36
|
+
// relations: { owner: _core.users },
|
|
37
|
+
// Add `alwaysFetchRelations: ['owner']` only when this collection is the
|
|
38
|
+
// sole path by which rows enter an ON-DEMAND target — an eager target syncs
|
|
39
|
+
// itself, so fetching its relation only adds payload. For one query,
|
|
40
|
+
// use `collection.fetchRelations('owner')` instead.
|
|
16
41
|
const {{PKG_SNAKE}}_items = newCollection('{{PKG_SNAKE}}_items', {
|
|
17
42
|
omitOnInsert: ['created', 'updated'] as const,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
defaultIndexType: BasicIndex,
|
|
21
|
-
},
|
|
43
|
+
...onDemand,
|
|
44
|
+
collectionOptions: indexing,
|
|
22
45
|
})
|
|
23
46
|
|
|
24
47
|
return {
|
|
@@ -3,7 +3,8 @@ import { ScrollView, Text, View } from 'react-native'
|
|
|
3
3
|
|
|
4
4
|
// Index route for {{PKG_NAME}}, served at /a/{{PKG_SLUG}}.
|
|
5
5
|
// Replace this placeholder with your list view (cards, table, whatever you
|
|
6
|
-
// need) and wire it to your pbtsdb collections using `
|
|
6
|
+
// need) and wire it to your pbtsdb collections using `useLiveQuery`
|
|
7
|
+
// (or `useMyLiveQuery` when the query filters the current user's own rows).
|
|
7
8
|
//
|
|
8
9
|
// For navigation, use `useOrgHref()` from `@tinycld/core/lib/org-routes` —
|
|
9
10
|
// never literal paths like `router.push('/{{PKG_SLUG}}/new')`, which miss the
|
|
@@ -41,7 +41,7 @@ jobs:
|
|
|
41
41
|
node-version-file: ws/.node-version
|
|
42
42
|
- uses: actions/setup-go@v6
|
|
43
43
|
with:
|
|
44
|
-
go-version-file: ws
|
|
44
|
+
go-version-file: ws/tinycld/core/server/go.mod
|
|
45
45
|
# 5. Enable corepack so the pnpm version pinned in the workspace root's
|
|
46
46
|
# package.json "packageManager" field is used. pnpm discovers members
|
|
47
47
|
# from pnpm-workspace.yaml; node-linker=hoisted (set there) gives an
|
|
@@ -76,7 +76,7 @@ jobs:
|
|
|
76
76
|
node-version-file: ws/.node-version
|
|
77
77
|
- uses: actions/setup-go@v6
|
|
78
78
|
with:
|
|
79
|
-
go-version-file: ws
|
|
79
|
+
go-version-file: ws/tinycld/core/server/go.mod
|
|
80
80
|
- name: Enable corepack (pnpm)
|
|
81
81
|
run: corepack enable
|
|
82
82
|
- name: Install (workspace root)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.27.1
|
|
@@ -54,6 +54,36 @@ function memberDirsByName(): Map<string, string> {
|
|
|
54
54
|
// not a dir, or no/unreadable package.json — skip
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
// A member may also be nested one level down inside a dir that is not
|
|
58
|
+
// itself a member — hosting/ui, whose parent ships Go only and stays out of
|
|
59
|
+
// the workspace. Scanned after the top level so a top-level member wins a
|
|
60
|
+
// name collision.
|
|
61
|
+
for (const entry of fs.readdirSync(WS_ROOT)) {
|
|
62
|
+
if (entry === 'node_modules' || entry.startsWith('.')) continue
|
|
63
|
+
const parent = path.join(WS_ROOT, entry)
|
|
64
|
+
let nestedEntries: string[]
|
|
65
|
+
try {
|
|
66
|
+
if (!fs.statSync(parent).isDirectory()) continue
|
|
67
|
+
nestedEntries = fs.readdirSync(parent)
|
|
68
|
+
} catch {
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
for (const nested of nestedEntries) {
|
|
72
|
+
if (nested === 'node_modules' || nested.startsWith('.')) continue
|
|
73
|
+
const dir = path.join(parent, nested)
|
|
74
|
+
try {
|
|
75
|
+
if (!fs.statSync(dir).isDirectory()) continue
|
|
76
|
+
const name = JSON.parse(
|
|
77
|
+
fs.readFileSync(path.join(dir, 'package.json'), 'utf8')
|
|
78
|
+
).name
|
|
79
|
+
if (typeof name === 'string' && name.length > 0 && !index.has(name)) {
|
|
80
|
+
index.set(name, dir)
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// not a dir, or no/unreadable package.json — skip
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
57
87
|
// @tinycld/core lives nested in the app shell (<WS_ROOT>/<APP_DIR>/core/);
|
|
58
88
|
// the top-level scan won't find it. Register it explicitly so the
|
|
59
89
|
// node_modules/@tinycld/core symlink is still created.
|
|
@@ -81,13 +111,25 @@ function linkInto(nodeModulesDir: string, scopeName: string, targetDir: string):
|
|
|
81
111
|
|
|
82
112
|
// If a correct symlink already exists, leave it. Otherwise replace whatever
|
|
83
113
|
// is there (stale link, or pnpm's own link for a depended-on member).
|
|
114
|
+
//
|
|
115
|
+
// The removal must not sit behind lstat in the same try: a DANGLING symlink
|
|
116
|
+
// (one whose target moved) makes readlink/lstat throw, and the old shape
|
|
117
|
+
// then skipped rmSync and fell straight into symlinkSync -> EEXIST, failing
|
|
118
|
+
// the whole install.
|
|
119
|
+
let matches = false
|
|
84
120
|
try {
|
|
85
|
-
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
fs.rmSync(linkPath, { recursive: true, force: true })
|
|
121
|
+
matches = fs.lstatSync(linkPath).isSymbolicLink() && fs.readlinkSync(linkPath) === relTarget
|
|
89
122
|
} catch {
|
|
90
|
-
// nothing at linkPath
|
|
123
|
+
// nothing at linkPath, or a link we cannot read — treat as "replace it"
|
|
124
|
+
}
|
|
125
|
+
if (matches) return
|
|
126
|
+
// unlink first for the symlink case: rmSync follows a link to decide how to
|
|
127
|
+
// remove it, so a DANGLING one (target moved) is left in place and the
|
|
128
|
+
// symlinkSync below then fails EEXIST.
|
|
129
|
+
try {
|
|
130
|
+
fs.unlinkSync(linkPath)
|
|
131
|
+
} catch {
|
|
132
|
+
fs.rmSync(linkPath, { recursive: true, force: true })
|
|
91
133
|
}
|
|
92
134
|
fs.symlinkSync(relTarget, linkPath)
|
|
93
135
|
}
|
|
@@ -52,3 +52,52 @@ describe('getPackages (new layout)', () => {
|
|
|
52
52
|
expect(getPackages()[0]).toBe('@tinycld/core')
|
|
53
53
|
})
|
|
54
54
|
})
|
|
55
|
+
|
|
56
|
+
// A member may live one level down inside a dir that is NOT itself a member:
|
|
57
|
+
// hosting/ui, whose parent ships Go only and is deliberately kept out of the
|
|
58
|
+
// workspace, so its client surface stays in the hosting repo rather than
|
|
59
|
+
// needing one of its own.
|
|
60
|
+
describe('getPackages (nested members)', () => {
|
|
61
|
+
let root: string
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
root = makeWorkspace()
|
|
64
|
+
process.env.TINYCLD_WS_ROOT = root
|
|
65
|
+
resetPackagesCache()
|
|
66
|
+
})
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
delete process.env.TINYCLD_WS_ROOT
|
|
69
|
+
fs.rmSync(root, { recursive: true, force: true })
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('finds a member nested inside a non-member parent', () => {
|
|
73
|
+
const ui = path.join(root, 'hosting', 'ui')
|
|
74
|
+
fs.mkdirSync(ui, { recursive: true })
|
|
75
|
+
fs.writeFileSync(path.join(ui, 'manifest.ts'), 'export default {}')
|
|
76
|
+
fs.writeFileSync(path.join(ui, 'package.json'), JSON.stringify({ name: '@tinycld/host-ui' }))
|
|
77
|
+
// The parent itself is not a member: no manifest.ts of its own.
|
|
78
|
+
fs.writeFileSync(path.join(root, 'hosting', 'go.mod'), 'module example.test\n')
|
|
79
|
+
|
|
80
|
+
resetPackagesCache()
|
|
81
|
+
expect(getPackages()).toContain('@tinycld/host-ui')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('does not descend into a dir that is itself a member', () => {
|
|
85
|
+
// A feature's own subdirectory is its source tree, not another member.
|
|
86
|
+
const inner = path.join(root, 'contacts', 'vendored')
|
|
87
|
+
fs.mkdirSync(inner, { recursive: true })
|
|
88
|
+
fs.writeFileSync(path.join(inner, 'manifest.ts'), 'export default {}')
|
|
89
|
+
fs.writeFileSync(
|
|
90
|
+
path.join(inner, 'package.json'),
|
|
91
|
+
JSON.stringify({ name: '@tinycld/should-not-appear' })
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
resetPackagesCache()
|
|
95
|
+
expect(getPackages()).not.toContain('@tinycld/should-not-appear')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('lists each member once when a nested scan revisits the tree', () => {
|
|
99
|
+
resetPackagesCache()
|
|
100
|
+
const pkgs = getPackages()
|
|
101
|
+
expect(new Set(pkgs).size).toBe(pkgs.length)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
@@ -49,6 +49,25 @@ export function getPackages(): string[] {
|
|
|
49
49
|
if (hasManifest(dir)) features.push(name)
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// A member may also be nested one level down, inside a dir that is not
|
|
53
|
+
// itself a member — hosting/ui is the case: hosting/ ships Go only and
|
|
54
|
+
// stays out of the workspace, but its client surface has to be a
|
|
55
|
+
// package the generator sees, and keeping it there keeps it in the
|
|
56
|
+
// hosting repo. One level only: deep enough for this shape, shallow
|
|
57
|
+
// enough not to walk the tree.
|
|
58
|
+
for (const entry of fs.readdirSync(wsRoot)) {
|
|
59
|
+
const parent = path.join(wsRoot, entry)
|
|
60
|
+
if (entry === 'node_modules' || entry.startsWith('.')) continue
|
|
61
|
+
if (!isDir(parent) || hasManifest(parent)) continue
|
|
62
|
+
for (const nested of readdirSafe(parent)) {
|
|
63
|
+
const dir = path.join(parent, nested)
|
|
64
|
+
if (nested === 'node_modules' || nested.startsWith('.')) continue
|
|
65
|
+
if (!isDir(dir) || !hasManifest(dir)) continue
|
|
66
|
+
const name = readPackageName(dir)
|
|
67
|
+
if (name && name !== CORE_NAME) features.push(name)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
52
71
|
// @tinycld/core moved INSIDE the app shell (at <appDir>/core/); the
|
|
53
72
|
// top-level scan above won't see it. Look for it explicitly so it is
|
|
54
73
|
// still listed first. The app dir is normally <wsRoot>/tinycld, but EAS
|
|
@@ -78,6 +97,23 @@ export function resetPackagesCache(): void {
|
|
|
78
97
|
cached = null
|
|
79
98
|
}
|
|
80
99
|
|
|
100
|
+
function readdirSafe(dir: string): string[] {
|
|
101
|
+
try {
|
|
102
|
+
return fs.readdirSync(dir)
|
|
103
|
+
} catch {
|
|
104
|
+
return []
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readPackageName(dir: string): string | null {
|
|
109
|
+
try {
|
|
110
|
+
const name = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).name
|
|
111
|
+
return typeof name === 'string' && name.length > 0 ? name : null
|
|
112
|
+
} catch {
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
81
117
|
function hasManifest(dir: string): boolean {
|
|
82
118
|
return (
|
|
83
119
|
fs.existsSync(path.join(dir, 'manifest.ts')) || fs.existsSync(path.join(dir, 'manifest.js'))
|