@synopackageland/templates 0.9.9 → 0.10.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.
- package/README.md +27 -27
- package/package.json +2 -2
- package/templates/_shared/AGENTS.md +53 -15
- package/templates/native-go/backend/THIRD_PARTY_NOTICES.txt +1 -1
- package/templates/native-go/backend/cmd/app/main.go +15 -1
- package/templates/native-go/backend/cmd/app/main_test.go +33 -0
- package/templates/native-go/backend/go.mod +1 -1
- package/templates/native-go/backend/go.sum +2 -2
- package/templates/native-go/i18n/messages/de.json +4 -0
- package/templates/native-go/i18n/messages/en.json +4 -0
- package/templates/native-go/i18n/messages/es.json +4 -0
- package/templates/native-go/i18n/messages/fr.json +4 -0
- package/templates/native-go/i18n/messages/it.json +4 -0
- package/templates/native-go/i18n/messages/ja.json +4 -0
- package/templates/native-go/i18n/messages/ko.json +4 -0
- package/templates/native-go/i18n/messages/zh-CN.json +4 -0
- package/templates/native-go/i18n/messages/zh-TW.json +4 -0
- package/templates/native-go/ui/app.js +2 -1
- package/templates/native-go-delegate/frontend/test/capability-panels.test.ts +3 -2
- package/templates/native-go-jobs/frontend/test/background-page.test.ts +4 -4
- package/templates/native-go-vue/README.md +19 -7
- package/templates/native-go-vue/frontend/package-lock.json +9 -9
- package/templates/native-go-vue/frontend/package.json +2 -2
- package/templates/native-go-vue/frontend/src/App.vue +31 -36
- package/templates/native-go-vue/frontend/test/apps.test.ts +65 -12
- package/templates/native-go-vue/frontend/test/context.ts +8 -0
- package/templates/native-go-vue/frontend/test/native-controls.test.ts +3 -4
- package/templates/native-go-vue/frontend/test/shell-i18n.test.ts +2 -2
- package/templates/native-node/README.md +153 -0
- package/templates/native-node/backend/app/package.json +4 -0
- package/templates/native-node/backend/app/permissions.json +5 -0
- package/templates/native-node/backend/package-lock.json +59 -0
- package/templates/native-node/backend/package.json +17 -0
- package/templates/native-node/backend/src/delegate.ts +5 -0
- package/templates/native-node/backend/src/jobs.ts +5 -0
- package/templates/native-node/backend/src/main.ts +4 -0
- package/templates/native-node/backend/src/params.ts +11 -0
- package/templates/native-node/backend/src/root-ops.ts +5 -0
- package/templates/native-node/backend/src/routes.ts +97 -0
- package/templates/native-node/backend/src/service.ts +22 -0
- package/templates/native-node/backend/test/routes.test.ts +75 -0
- package/templates/native-node/backend/test/support.ts +67 -0
- package/templates/native-node/backend/tsconfig.build.json +8 -0
- package/templates/native-node/backend/tsconfig.json +18 -0
- package/templates/native-node/dsm-package.yaml +80 -0
- package/templates/native-node/gitignore +4 -0
- package/templates/native-node-delegate/backend/src/delegate.ts +35 -0
- package/templates/native-node-delegate/backend/test/delegate.test.ts +51 -0
- package/templates/native-node-jobs/backend/src/jobs.ts +38 -0
- package/templates/native-node-jobs/backend/test/jobs.test.ts +73 -0
- package/templates/native-node-root-ops/backend/src/root-ops.ts +16 -0
- package/templates/native-node-root-ops/backend/test/root-ops.test.ts +23 -0
- package/templates/native-node-vue/README.md +56 -0
- package/templates/native-node-vue/gitignore +4 -0
- package/templates/native-python/README.md +150 -0
- package/templates/native-python/backend/app/delegate.py +5 -0
- package/templates/native-python/backend/app/jobs.py +5 -0
- package/templates/native-python/backend/app/main.py +5 -0
- package/templates/native-python/backend/app/params.py +10 -0
- package/templates/native-python/backend/app/permissions.json +5 -0
- package/templates/native-python/backend/app/root_ops.py +5 -0
- package/templates/native-python/backend/app/routes.py +107 -0
- package/templates/native-python/backend/tests/support.py +72 -0
- package/templates/native-python/backend/tests/test_routes.py +73 -0
- package/templates/native-python/dsm-package.yaml +77 -0
- package/templates/native-python/gitignore +2 -0
- package/templates/native-python-delegate/backend/app/delegate.py +42 -0
- package/templates/native-python-delegate/backend/tests/test_delegate.py +58 -0
- package/templates/native-python-jobs/backend/app/jobs.py +40 -0
- package/templates/native-python-jobs/backend/tests/test_jobs.py +72 -0
- package/templates/native-python-root-ops/backend/app/root_ops.py +18 -0
- package/templates/native-python-root-ops/backend/tests/test_root_ops.py +34 -0
- package/templates/native-python-vue/README.md +56 -0
- package/templates/native-python-vue/gitignore +3 -0
- package/templates/native-rust/README.md +74 -296
- package/templates/native-rust/backend/Cargo.lock +6 -6
- package/templates/native-rust/backend/Cargo.toml +2 -10
- package/templates/native-rust/backend/src/delegate.rs +6 -0
- package/templates/native-rust/backend/src/jobs.rs +6 -0
- package/templates/native-rust/backend/src/main.rs +12 -0
- package/templates/native-rust/backend/src/root_ops.rs +6 -0
- package/templates/native-rust/backend/src/routes.rs +352 -0
- package/templates/native-rust/backend/src/test_support.rs +91 -0
- package/templates/native-rust/dsm-package.yaml +10 -21
- package/templates/native-rust-delegate/backend/src/delegate.rs +171 -0
- package/templates/native-rust-jobs/backend/src/jobs.rs +240 -0
- package/templates/native-rust-root-ops/backend/src/root_ops.rs +83 -0
- package/templates/native-rust-vue/README.md +35 -34
- package/templates/native-rust/backend/THIRD_PARTY_NOTICES.txt +0 -207
- package/templates/native-rust/backend/src/app/background/mod.rs +0 -424
- package/templates/native-rust/backend/src/app/background/owner.rs +0 -77
- package/templates/native-rust/backend/src/app/background/owner_tests.rs +0 -174
- package/templates/native-rust/backend/src/app/background/schedule.rs +0 -126
- package/templates/native-rust/backend/src/app/background/schedule_tests.rs +0 -108
- package/templates/native-rust/backend/src/app/background/tests.rs +0 -505
- package/templates/native-rust/backend/src/app/background/work.rs +0 -78
- package/templates/native-rust/backend/src/app/background/work_tests.rs +0 -123
- package/templates/native-rust/backend/src/app/background_routes.rs +0 -22
- package/templates/native-rust/backend/src/app/delegate_config.rs +0 -188
- package/templates/native-rust/backend/src/app/delegate_routes.rs +0 -22
- package/templates/native-rust/backend/src/app/identity.rs +0 -56
- package/templates/native-rust/backend/src/app/migrations.rs +0 -79
- package/templates/native-rust/backend/src/app/mod.rs +0 -30
- package/templates/native-rust/backend/src/app/portal.rs +0 -317
- package/templates/native-rust/backend/src/app/root_routes.rs +0 -23
- package/templates/native-rust/backend/src/app/routes.rs +0 -315
- package/templates/native-rust/backend/src/app/session_files.rs +0 -19
- package/templates/native-rust/backend/src/app/shared_folder.rs +0 -383
- package/templates/native-rust/backend/src/app/test_support.rs +0 -202
- package/templates/native-rust/backend/src/app/uninstall.rs +0 -38
- package/templates/native-rust/backend/src/bin/api.rs +0 -72
- package/templates/native-rust/backend/src/bin/background-worker.rs +0 -37
- package/templates/native-rust/backend/src/build_env.rs +0 -155
- package/templates/native-rust/backend/src/lib.rs +0 -6
- package/templates/native-rust/backend/src/permissions.rs +0 -54
- package/templates/native-rust/backend/src/process.rs +0 -91
- package/templates/native-rust/i18n/glossary.json +0 -665
- package/templates/native-rust/i18n/messages/de.json +0 -32
- package/templates/native-rust/i18n/messages/en.json +0 -32
- package/templates/native-rust/i18n/messages/es.json +0 -32
- package/templates/native-rust/i18n/messages/fr.json +0 -32
- package/templates/native-rust/i18n/messages/it.json +0 -32
- package/templates/native-rust/i18n/messages/ja.json +0 -32
- package/templates/native-rust/i18n/messages/ko.json +0 -32
- package/templates/native-rust/i18n/messages/zh-CN.json +0 -32
- package/templates/native-rust/i18n/messages/zh-TW.json +0 -32
- package/templates/native-rust/ui/app.css +0 -45
- package/templates/native-rust/ui/app.js +0 -111
- package/templates/native-rust-delegate/backend/src/app/delegate_routes.rs +0 -92
- package/templates/native-rust-delegate/backend/src/bin/background-worker.rs +0 -52
- package/templates/native-rust-jobs/backend/src/app/background_routes.rs +0 -109
- package/templates/native-rust-root-ops/backend/src/app/root_routes.rs +0 -475
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import {computed, onMounted, onUnmounted, ref, watch} from 'vue'
|
|
3
|
-
import {
|
|
4
|
-
import type
|
|
5
|
-
import {AppShell, BackgroundPanel, FilesPage, LoginPage, PortalSettings, createSession, shellPages
|
|
3
|
+
import {Button, Callout, Form, FormActions, FormItem, SettingsSection, Textarea} from '@synopackageland/ui'
|
|
4
|
+
import {bridgeErrorCopy, type AppStatus, type Context, type ShellPage} from '@synopackageland/sdk'
|
|
5
|
+
import {AppShell, BackgroundPanel, FilesPage, LoginPage, PortalSettings, createSession, shellPages} from '@synopackageland/sdk/vue'
|
|
6
6
|
import {appCopy} from './i18n'
|
|
7
7
|
import CapabilityPanels from './CapabilityPanels.vue'
|
|
8
8
|
const props = defineProps<{ctx: Context}>()
|
|
@@ -17,17 +17,16 @@ let disposed = false
|
|
|
17
17
|
let unsubscribe: (() => void) | undefined
|
|
18
18
|
// The CLI substitutes this only for a declared package-account background worker.
|
|
19
19
|
const backgroundEnabled = (('__BACKGROUND_ENABLED__') as string) === 'true'
|
|
20
|
-
// In a DSM window, dialogs may leave the window as DSM's own do; a press on one
|
|
21
|
-
// does not reach the window, so it brings the window to the front itself.
|
|
22
|
-
const inDsmWindow = props.ctx.host?.platform().host === 'dsm'
|
|
23
|
-
function toFront() {props.ctx.host.activateWindow()}
|
|
24
20
|
const page = ref<ShellPage>('status')
|
|
25
21
|
const pages = computed(() => shellPages({background:backgroundEnabled}))
|
|
22
|
+
// Only a confirmed sign-in need replaces the App's pages; the sign-in page draws its own title.
|
|
23
|
+
const signIn = computed(() => login.ready.value && login.enabled.value && !login.subject.value && {header: false as const})
|
|
26
24
|
const subtitle = computed(() => app.value ? `${app.value.package} · ${app.value.user} · ${app.value.version}` : undefined)
|
|
27
25
|
function message(e: unknown) {
|
|
28
26
|
const code = (e as {code?: string})?.code || 'request_failed'
|
|
29
27
|
const errors = copy.value.errors as Record<string, string>
|
|
30
|
-
|
|
28
|
+
// A bridge App's runtime adds codes (app_unavailable, a service's refusal) the SDK words.
|
|
29
|
+
return bridgeErrorCopy(e, props.ctx.locale) ?? ((code !== 'request_failed' && errors[code]) || errors.request_failed!.replace('{code}', code))
|
|
31
30
|
}
|
|
32
31
|
let again = false
|
|
33
32
|
async function refresh() {
|
|
@@ -76,33 +75,29 @@ onUnmounted(()=>{disposed=true;unsubscribe?.();login.dispose();text.value=''})
|
|
|
76
75
|
</script>
|
|
77
76
|
|
|
78
77
|
<template>
|
|
79
|
-
<
|
|
80
|
-
<
|
|
81
|
-
<
|
|
82
|
-
<
|
|
83
|
-
<
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
</Form>
|
|
97
|
-
</SettingsSection>
|
|
98
|
-
</div>
|
|
99
|
-
<CapabilityPanels v-if="app" ref="panels" :key="app.user" :ctx="ctx" :admin="app.admin" />
|
|
100
|
-
<FilesPage v-if="page==='status'" :ctx="ctx" />
|
|
101
|
-
<div data-page="background" v-show="page==='background'">
|
|
102
|
-
<BackgroundPanel v-if="backgroundEnabled && app" ref="background" :key="app.user" :ctx="ctx" />
|
|
103
|
-
</div>
|
|
78
|
+
<AppShell app-root v-model="page" :ctx="ctx" :locale="ctx.locale" :pages="pages" :description="subtitle" :denied="signIn" sidebar-storage-key="__PACKAGE__">
|
|
79
|
+
<template #denied><LoginPage :session="login" :locale="ctx.locale" /></template>
|
|
80
|
+
<template #actions><Button variant="secondary" :disabled="busy" @click="refreshAll">{{ copy.refresh }}</Button><Button v-if="login.enabled.value" variant="secondary" :disabled="busy" @click="signOut">{{ copy.signOut }}</Button></template>
|
|
81
|
+
<div class="integration-app" :aria-busy="busy" :aria-label="copy.settings">
|
|
82
|
+
<Callout v-if="error" tone="danger" :title="copy.actionRequired"><span role="alert">{{ error }}</span></Callout>
|
|
83
|
+
<p v-if="notice" role="status">{{ notice }}</p>
|
|
84
|
+
<div data-page="status" v-show="page==='status'">
|
|
85
|
+
<SettingsSection :title="copy.statusTitle" :collapsible="false" :loading="busy && !app">
|
|
86
|
+
<p v-if="app">{{ app.package }} · {{ app.user }} · {{ app.version }}</p>
|
|
87
|
+
<p v-else-if="error" role="status">{{ copy.statusError }}</p>
|
|
88
|
+
</SettingsSection>
|
|
89
|
+
<PortalSettings v-if="ctx.portal && app" :key="portalKey" :portal="ctx.portal" />
|
|
90
|
+
<SettingsSection v-if="app?.admin" :title="copy.greeting" :collapsible="false">
|
|
91
|
+
<Form direction="vertical" @submit.prevent="save"><FormItem :label="copy.greeting"><Textarea v-model="text" :disabled="busy" /></FormItem>
|
|
92
|
+
<FormActions><Button variant="primary" :disabled="busy || !text.trim()" @click="save">{{ copy.save }}</Button></FormActions>
|
|
93
|
+
</Form>
|
|
94
|
+
</SettingsSection>
|
|
104
95
|
</div>
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
96
|
+
<CapabilityPanels v-if="app" ref="panels" :key="app.user" :ctx="ctx" :admin="app.admin" />
|
|
97
|
+
<FilesPage v-if="page==='status'" :ctx="ctx" />
|
|
98
|
+
<div data-page="background" v-show="page==='background'">
|
|
99
|
+
<BackgroundPanel v-if="backgroundEnabled && app" ref="background" :key="app.user" :ctx="ctx" />
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</AppShell>
|
|
108
103
|
</template>
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import {expect,test,vi} from 'vitest'
|
|
3
3
|
import {nextTick} from 'vue'
|
|
4
4
|
import {mount} from '../src/main'
|
|
5
|
-
import type
|
|
5
|
+
import {createStandaloneContext, type Context} from '@synopackageland/sdk'
|
|
6
|
+
import {dsmWindow} from './context'
|
|
6
7
|
async function settle(){for(let i=0;i<16;i++){await Promise.resolve();await nextTick()}}
|
|
7
8
|
function fixture(id:string){
|
|
8
9
|
const reopen:Array<()=>void>=[]
|
|
@@ -12,7 +13,7 @@ function fixture(id:string){
|
|
|
12
13
|
if(method==='greeting.set')return {}
|
|
13
14
|
throw {code:'unknown_method'}
|
|
14
15
|
})
|
|
15
|
-
const ctx={
|
|
16
|
+
const ctx=dsmWindow({call,reopen(fn:()=>void){reopen.push(fn);return ()=>{}}})
|
|
16
17
|
const root=document.createElement('div');document.body.append(root)
|
|
17
18
|
const app=mount(root,ctx)
|
|
18
19
|
return {root,app,call,reopen,ctx,close(){app.unmount();root.remove()}}
|
|
@@ -78,7 +79,7 @@ test('the app renders and loads status while the session check is still pending'
|
|
|
78
79
|
if(method==='greeting.get')return {text:'Hello Eager'}
|
|
79
80
|
throw {code:'unknown_method'}
|
|
80
81
|
})
|
|
81
|
-
const ctx={
|
|
82
|
+
const ctx=dsmWindow({call})
|
|
82
83
|
const root=document.createElement('div');document.body.append(root)
|
|
83
84
|
const app=mount(root,ctx)
|
|
84
85
|
try{
|
|
@@ -99,7 +100,7 @@ test('a status request that raced the session check is retried once the session
|
|
|
99
100
|
if(method==='greeting.get')return {text:'Hello Raced'}
|
|
100
101
|
throw {code:'unknown_method'}
|
|
101
102
|
})
|
|
102
|
-
const ctx={
|
|
103
|
+
const ctx=dsmWindow({call})
|
|
103
104
|
const root=document.createElement('div');document.body.append(root)
|
|
104
105
|
const app=mount(root,ctx)
|
|
105
106
|
try{
|
|
@@ -108,13 +109,14 @@ test('a status request that raced the session check is retried once the session
|
|
|
108
109
|
expect(root.textContent).toContain('Raced · admin · 1')
|
|
109
110
|
}finally{app.unmount();root.remove()}
|
|
110
111
|
})
|
|
111
|
-
function hosted(host
|
|
112
|
+
function hosted(host:'dsm'|'standalone-web'){
|
|
112
113
|
const call=vi.fn(async(method:string)=>{
|
|
113
114
|
if(method==='app.status')return {package:'Hosted',version:'1',user:'admin',admin:false}
|
|
114
115
|
throw {code:'unknown_method'}
|
|
115
116
|
})
|
|
116
|
-
|
|
117
|
-
const ctx=
|
|
117
|
+
// Outside DSM the App owns the page.
|
|
118
|
+
const ctx=host==='dsm'?dsmWindow({call}):createStandaloneContext({call})
|
|
119
|
+
const activateWindow=ctx.host.activateWindow
|
|
118
120
|
const root=document.createElement('div');document.body.append(root)
|
|
119
121
|
const app=mount(root,ctx)
|
|
120
122
|
return {activateWindow,close(){app.unmount();root.remove()}}
|
|
@@ -140,10 +142,8 @@ test('on a narrow viewport the floating dialogs become sheets that stay on scree
|
|
|
140
142
|
}finally{f.close();Object.defineProperty(window,'innerWidth',{configurable:true,value:width})}
|
|
141
143
|
})
|
|
142
144
|
test('outside a DSM window dialogs stay inside the app',async()=>{
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
try{await settle();expect(document.querySelector('[data-ui-floating]')).toBeNull()}finally{f.close()}
|
|
146
|
-
}
|
|
145
|
+
const f=hosted('standalone-web')
|
|
146
|
+
try{await settle();expect(document.querySelector('[data-ui-floating]')).toBeNull()}finally{f.close()}
|
|
147
147
|
})
|
|
148
148
|
test('the status page follows the UI layout rules',async()=>{
|
|
149
149
|
let answer:(value:unknown)=>void=()=>{}
|
|
@@ -153,7 +153,7 @@ test('the status page follows the UI layout rules',async()=>{
|
|
|
153
153
|
throw {code:'unknown_method'}
|
|
154
154
|
})
|
|
155
155
|
const root=document.createElement('div');document.body.append(root)
|
|
156
|
-
const app=mount(root,{
|
|
156
|
+
const app=mount(root,dsmWindow({call}))
|
|
157
157
|
try{
|
|
158
158
|
await settle()
|
|
159
159
|
// A loading section keeps its title and shows a skeleton, not a sentence.
|
|
@@ -166,3 +166,56 @@ test('the status page follows the UI layout rules',async()=>{
|
|
|
166
166
|
expect(root.querySelector('textarea')!.closest('[data-form-item]')!.getAttribute('data-direction')).toBe('vertical')
|
|
167
167
|
}finally{app.unmount();root.remove()}
|
|
168
168
|
})
|
|
169
|
+
test('a code the bridge runtime adds shows the SDK text in the DSM language',async()=>{
|
|
170
|
+
const f=fixture('Bridge')
|
|
171
|
+
try{
|
|
172
|
+
await settle();f.call.mockRejectedValue({code:'app_unavailable'});f.reopen.forEach(fn=>fn());await settle()
|
|
173
|
+
expect(f.root.textContent).toContain('The App is not available right now. Try again later.')
|
|
174
|
+
}finally{f.close()}
|
|
175
|
+
})
|
|
176
|
+
test('a Portal or standalone launch lays the App out full-page; a DSM window keeps the window layout',async()=>{
|
|
177
|
+
// jsdom lays nothing out: give every element a wide desktop width.
|
|
178
|
+
const box=vi.spyOn(HTMLElement.prototype,'getBoundingClientRect').mockReturnValue({width:1400,height:800} as DOMRect)
|
|
179
|
+
const viewport=()=>document.head.querySelector('meta[name="viewport"]')
|
|
180
|
+
const window=fixture('Window')
|
|
181
|
+
const call=vi.fn(async(method:string)=>method==='app.status'?{package:'Page',version:'1',user:'alice',admin:false}:{})
|
|
182
|
+
const ctx=createStandaloneContext({call,displayName:'Page App',appIcon:'webman/3rdparty/Page/images/icon_64.png',user:{name:'alice',isAdmin:false}})
|
|
183
|
+
const root=document.createElement('div');document.body.append(root)
|
|
184
|
+
try{
|
|
185
|
+
await settle()
|
|
186
|
+
expect(window.root.querySelector('[data-ui-layout-mode]')?.getAttribute('data-ui-layout-mode')).toBe('window')
|
|
187
|
+
expect(viewport()).toBeNull()
|
|
188
|
+
// A DSM window has DSM's own title bar and account: the sidebar shows no user.
|
|
189
|
+
expect(window.root.querySelector('[data-ui-sidebar-footer]')).toBeNull()
|
|
190
|
+
const app=mount(root,ctx);await settle()
|
|
191
|
+
expect(root.querySelector('[data-ui-layout-mode]')?.getAttribute('data-ui-layout-mode')).toBe('web')
|
|
192
|
+
// DSM's page has no device-width viewport, so a phone would never reach the mobile layout.
|
|
193
|
+
expect(viewport()?.getAttribute('content')).toContain('width=device-width')
|
|
194
|
+
const brand=root.querySelector('[data-ui-sidebar-brand]')
|
|
195
|
+
expect(brand?.textContent).toContain('Page App')
|
|
196
|
+
expect(brand?.querySelector('img')?.getAttribute('src')).toBe('webman/3rdparty/Page/images/icon_64.png')
|
|
197
|
+
// The signed-in DSM account and its role, from ctx.user.
|
|
198
|
+
expect(root.querySelector('[data-ui-sidebar-footer]')?.textContent).toContain('alice')
|
|
199
|
+
expect(root.querySelector('[data-ui-sidebar-footer]')?.textContent).toContain('DSM user')
|
|
200
|
+
app.unmount()
|
|
201
|
+
expect(viewport()).toBeNull()
|
|
202
|
+
}finally{window.close();root.remove();box.mockRestore()}
|
|
203
|
+
})
|
|
204
|
+
test('a needed sign-in takes the shell page in place of the App pages, inside the App root',async()=>{
|
|
205
|
+
const call=vi.fn(async(method:string)=>{
|
|
206
|
+
if(method==='identity.status')return {enabled:true}
|
|
207
|
+
if(method==='identity.session')return {user:'alice',uid:'1',expiresAt:new Date(Date.now()-1000).toISOString()}
|
|
208
|
+
throw {code:'session_required'}
|
|
209
|
+
})
|
|
210
|
+
const root=document.createElement('div');document.body.append(root)
|
|
211
|
+
const app=mount(root,dsmWindow({call}))
|
|
212
|
+
try{
|
|
213
|
+
await settle()
|
|
214
|
+
const form=root.querySelector('form')
|
|
215
|
+
expect(form).not.toBeNull()
|
|
216
|
+
// The shell lays the sign-in page out as one of its pages: no sidebar, the page body's padding.
|
|
217
|
+
expect(form!.closest('[data-page-content-body]')).not.toBeNull()
|
|
218
|
+
expect(root.querySelector('nav')).toBeNull()
|
|
219
|
+
expect(form!.closest('[data-ui-app]')).not.toBeNull()
|
|
220
|
+
}finally{app.unmount();root.remove()}
|
|
221
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import {vi} from 'vitest'
|
|
2
|
+
import {createStandaloneContext, createStandaloneHost, type Context} from '@synopackageland/sdk'
|
|
3
|
+
/** The ctx the CLI gives a DSM desktop window; activateWindow is a spy. */
|
|
4
|
+
export function dsmWindow(fields: Pick<Context, 'call'> & Partial<Omit<Context, 'call'>>): Context {
|
|
5
|
+
const host = createStandaloneHost({host: 'dsm', dsmStandalone: false})
|
|
6
|
+
vi.spyOn(host, 'activateWindow')
|
|
7
|
+
return createStandaloneContext({fullPage: false, host, ...fields})
|
|
8
|
+
}
|
|
@@ -18,10 +18,9 @@ test('FormItem labels sit above their controls and the UI binds the control id',
|
|
|
18
18
|
expect(items.filter(file=>/<Form(?![^>]*direction="vertical")[\s>]|field\.controlId|#default="\{field\}"/.test(template(file)))).toEqual([])
|
|
19
19
|
})
|
|
20
20
|
// UI built-in text follows the Provider locale (nine locales), so every app root sets it once and no
|
|
21
|
-
// component repeats a per-control locale or download label.
|
|
21
|
+
// component repeats a per-control locale or download label. AppShell's app-root sets it from its locale.
|
|
22
22
|
test('app roots set the UI locale once and components do not translate UI text themselves',()=>{
|
|
23
|
-
|
|
24
|
-
expect(
|
|
25
|
-
expect(roots.filter(file=>!uses(file,'ConfigProvider'))).toEqual([])
|
|
23
|
+
expect(sources(root).filter(file=>/<AppShell[^>]*\sapp-root[\s>]/.test(template(file))).length).toBeGreaterThan(0)
|
|
24
|
+
expect(sources(root).filter(file=>uses(file,'AppRoot')&&!uses(file,'ConfigProvider'))).toEqual([])
|
|
26
25
|
expect(sources(root).filter(file=>/download-label|external-label|<FilePicker[^>]*:locale=/.test(template(file)))).toEqual([])
|
|
27
26
|
})
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
import {expect,test,vi} from 'vitest'
|
|
4
4
|
import {nextTick} from 'vue'
|
|
5
5
|
import {mount} from '../src/main'
|
|
6
|
-
import
|
|
6
|
+
import {dsmWindow} from './context'
|
|
7
7
|
import {appCopy} from '../src/i18n'
|
|
8
8
|
import {identityCopy} from '@synopackageland/sdk/vue'
|
|
9
9
|
async function settle(){for(let i=0;i<20;i++){await Promise.resolve();await nextTick()}}
|
|
10
10
|
const buttons=(root:HTMLElement)=>[...root.querySelectorAll('button')].map(b=>b.textContent?.trim())
|
|
11
11
|
function start(locale:string,call:(method:string)=>Promise<unknown>){
|
|
12
|
-
const ctx={locale,call:vi.fn(call)
|
|
12
|
+
const ctx=dsmWindow({locale,call:vi.fn(call) as never})
|
|
13
13
|
const root=document.createElement('div');document.body.append(root)
|
|
14
14
|
const app=mount(root,ctx)
|
|
15
15
|
return {root,close(){app.unmount();root.remove()}}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# __DISPLAY_NAME__
|
|
2
|
+
|
|
3
|
+
A native DSM package (`__PACKAGE__`): a Node.js App behind the SDK's runtime,
|
|
4
|
+
and a UI that opens in its own DSM window. It installs as an **unsigned** SPK,
|
|
5
|
+
runs as its own DSM package account, needs no containers, and has no root by default.
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
DSM main menu → DSM window (ui/app.js)
|
|
9
|
+
│ ctx.call("greeting.get") same-origin POST + DSM session
|
|
10
|
+
▼
|
|
11
|
+
/webman/3rdparty/__PACKAGE__/cgi/api.cgi (the SDK runtime, runs as package user)
|
|
12
|
+
1. transport guards: POST, JSON, same origin, ≤ 1 MiB, bounded JSON complexity
|
|
13
|
+
2. one DSM authentication, the entry gate and app/permissions.json
|
|
14
|
+
3. DSM's Node.js v20 package runs `backend/app/main.js` once per call
|
|
15
|
+
4. your method, which may ask the runtime for services (files, jobs, executor)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
Needs Node.js 20 or later with `synopkgland` installed. The SPK depends on DSM's **Node.js v20** package (INFO `install_dep_packages`).
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
(cd backend && npm ci && npm test && npm run build) # method tests, then app/*.js
|
|
24
|
+
synopkgland build # → dist/__PACKAGE__-0.1.0-0001-{x86_64,aarch64}.spk
|
|
25
|
+
synopkgland inspect dist/__PACKAGE__-0.1.0-0001-x86_64.spk
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Run `npm run build` in `backend` before every `synopkgland check` or `build`: the SPK ships the compiled `backend/app`.
|
|
29
|
+
|
|
30
|
+
Install: Package Center → Settings → General → Trust Level "Any publisher",
|
|
31
|
+
then Manual Install with the SPK for your NAS's CPU. Open
|
|
32
|
+
**__DISPLAY_NAME__** from the DSM main menu: it shows who you are signed in
|
|
33
|
+
as, what the backend reports, and (for administrators) a form that stores a
|
|
34
|
+
greeting in the package's data folder.
|
|
35
|
+
|
|
36
|
+
## Project layout
|
|
37
|
+
|
|
38
|
+
| Path | What it is | Edit it? |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| `dsm-package.yaml` | Package identity, version/build, arches, where backend and UI come from | Yes |
|
|
41
|
+
| `ui/app.js`, `ui/app.css` | The app inside the DSM window | Yes — your UI |
|
|
42
|
+
| `i18n/messages/<locale>.json` | UI text in nine languages | Yes |
|
|
43
|
+
| `i18n/glossary.json` | DSM's own UI terms, and which messages must use them | Add terms |
|
|
44
|
+
| `backend/src/routes.ts` | Your methods: `build` registers them | Yes — your backend |
|
|
45
|
+
| `backend/src/jobs.ts`, `root-ops.ts`, `delegate.ts` | Capability examples (stubs unless `init --with` added them) | Yes |
|
|
46
|
+
| `backend/src/main.ts` | Runs the App on the runtime's channel | Rarely |
|
|
47
|
+
| `backend/app/` | `npm run build` output (`*.js`, ignored by git), `package.json` and `permissions.json`: the directory the SPK ships | Only `permissions.json` |
|
|
48
|
+
| `backend/test/` | node:test tests; `call` plays the runtime | Yes |
|
|
49
|
+
| `backend/package.json`, `package-lock.json` | TypeScript (NodeNext) and the Node SDK `@synopackageland/app` from `backend/vendor`, which `synopkgland init` copies from the SDK release | Dependencies |
|
|
50
|
+
| `dist/` | Built SPKs (ignored by git) | No |
|
|
51
|
+
|
|
52
|
+
`build` generates everything else: `INFO`, `conf/privilege`
|
|
53
|
+
(`run-as: package`), lifecycle scripts, icons, the DSM window wrapper and the
|
|
54
|
+
runtime CGI at `ui/cgi/api.cgi`.
|
|
55
|
+
|
|
56
|
+
## Add a method
|
|
57
|
+
|
|
58
|
+
Register it in `build`. Every method defaults to DSM administrator membership;
|
|
59
|
+
all access rules live in `backend/app/permissions.json`:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
export function build(cfg: Config): App {
|
|
63
|
+
const app = new App()
|
|
64
|
+
...
|
|
65
|
+
app.method('notes.list', (ctx, params) => listNotes(cfg, ctx, params)) // new: administrators by default
|
|
66
|
+
return app
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function listNotes(cfg: Config, ctx: Context, params: unknown) {
|
|
70
|
+
const { folder } = strings(params, 'folder') // anything else is bad_request
|
|
71
|
+
return { owner: ctx.user, folder, notes: [] }
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Add a test to `backend/test/` with `call(app, 'notes.list', { folder: 'work' }, { services })`. Call it from the UI with `ctx.call("notes.list", { folder: "work" })`.
|
|
76
|
+
|
|
77
|
+
Throw `new AppError(code, message)` (or `AppError.badRequest(message)`) for an error the browser may see; anything else is `internal`, with the stack in the package's log. Call runtime services through `service()` in `src/service.ts`: it passes the runtime's refusals on to the browser.
|
|
78
|
+
|
|
79
|
+
The runtime refuses before your App runs: `session_required`, `admin_required`,
|
|
80
|
+
`unknown_method`, `request_timeout` and the transport guards' codes. The UI
|
|
81
|
+
shows `error.<code>` from its catalogs.
|
|
82
|
+
|
|
83
|
+
### Who can call what
|
|
84
|
+
|
|
85
|
+
The shipped policy is:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{"schema": 1, "default": "administrators", "methods": {}}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The default must remain `administrators`. An intentional exception belongs in
|
|
92
|
+
`methods`, for example `"notes.list": "authenticated"`; `"public"` explicitly
|
|
93
|
+
allows anonymous calls. Such exceptions also enable ordinary-user launcher
|
|
94
|
+
access.
|
|
95
|
+
|
|
96
|
+
| JSON rule | No session | Ordinary user | Administrators group member |
|
|
97
|
+
| --- | --- | --- | --- |
|
|
98
|
+
| absent or `administrators` | `session_required` | `admin_required` | runs |
|
|
99
|
+
| explicit `authenticated` | `session_required` | runs | runs |
|
|
100
|
+
| explicit `public` | runs | runs | runs |
|
|
101
|
+
| method the App does not register | `session_required` | `unknown_method` | `unknown_method` |
|
|
102
|
+
|
|
103
|
+
Missing or invalid JSON, duplicate keys, unknown permissions and rules for
|
|
104
|
+
methods the App does not register fail closed. Changing policy requires
|
|
105
|
+
rebuilding the SPK.
|
|
106
|
+
|
|
107
|
+
## Capabilities
|
|
108
|
+
|
|
109
|
+
`synopkgland init --with` adds examples that use the runtime's services through
|
|
110
|
+
`ctx.runtime`; each comes with its tests:
|
|
111
|
+
|
|
112
|
+
| `--with` | Methods | Runtime services |
|
|
113
|
+
| --- | --- | --- |
|
|
114
|
+
| `jobs` | `hash.submit`, `hash.get`, `hash.cancel` | `files.read` with the caller's session, `bg.submit`, `bg.get`, `bg.cancel` |
|
|
115
|
+
| `root-ops` | `whoami.run` | `root.run` of `root/whoami.sh`, once an administrator approves the executor |
|
|
116
|
+
| `delegate` | `report.save` | `delegate.move_from_staging` into a folder the caller chose |
|
|
117
|
+
|
|
118
|
+
A service refuses with its own code (`limit_exceeded`, `read_failed`, the
|
|
119
|
+
executor's refusals); the examples pass it on to the browser. `synopkgland add`
|
|
120
|
+
works on Go backends only: start a Node.js App with the capabilities it needs.
|
|
121
|
+
|
|
122
|
+
## The SDK
|
|
123
|
+
|
|
124
|
+
`backend.sdk` in dsm-package.yaml names the SDK release: the build ships its runtime and its Node SDK as `backend/app/node_modules/@synopackageland/app`, and reads the method list from `main.js --describe`. `backend/vendor/@synopackageland/app` is the same SDK for tests and type checks; replace it with the new release's when you change `backend.sdk`. Upgrade the SDK by changing that version; `synopkgland check`
|
|
125
|
+
refuses versions this CLI does not support. The bridge protocol, the services
|
|
126
|
+
and their codes are documented in the SDK repository:
|
|
127
|
+
<https://github.com/synopackageland/syno-pkg-land-sdk> (`spec/bridge/v1`).
|
|
128
|
+
|
|
129
|
+
## UI and languages
|
|
130
|
+
|
|
131
|
+
The UI is the same as the Go template's. `ui/app.js` registers
|
|
132
|
+
`window.DsmPackageApps[<package>].mount(root, ctx)`; `ctx.call(method, params)`
|
|
133
|
+
is the only transport and `ctx.t(key, values)` reads
|
|
134
|
+
`i18n/messages/<locale>.json` (`en`, `zh-TW`, `zh-CN`, `ja`, `ko`, `de`,
|
|
135
|
+
`fr`, `es`, `it`). Add `error.<code>` for every code your methods return.
|
|
136
|
+
`synopkgland check` and `build` refuse a language file with missing or extra
|
|
137
|
+
keys, a dropped `{placeholder}` or a listed message that drifts from the
|
|
138
|
+
glossary. `--ui vue` gives the Vue shell in `frontend/` instead.
|
|
139
|
+
|
|
140
|
+
## Data, upgrades, uninstall
|
|
141
|
+
|
|
142
|
+
The App stores data in `/var/packages/__PACKAGE__/var` (`SYNOPKGLAND_DATA_DIR`,
|
|
143
|
+
kept across upgrades). Bump `build` for every SPK you hand out and `version`
|
|
144
|
+
for feature releases. Uninstalling keeps the data unless the administrator
|
|
145
|
+
ticks **Also delete the app data listed above** in the uninstall wizard.
|
|
146
|
+
|
|
147
|
+
## Support matrix
|
|
148
|
+
|
|
149
|
+
| | Status |
|
|
150
|
+
| --- | --- |
|
|
151
|
+
| Build hosts | macOS, Linux |
|
|
152
|
+
| Target CPUs | x86_64 and aarch64 DSM models (one SPK each); armv7 is opt-in in `arches`, built but not NAS-verified |
|
|
153
|
+
| DSM | 7.1 or later declared (`os_min_ver`) |
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "backend",
|
|
3
|
+
"lockfileVersion": 3,
|
|
4
|
+
"requires": true,
|
|
5
|
+
"packages": {
|
|
6
|
+
"": {
|
|
7
|
+
"name": "backend",
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@synopackageland/app": "file:vendor/@synopackageland/app"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/node": "18.19.130",
|
|
13
|
+
"typescript": "5.9.3"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"node_modules/@synopackageland/app": {
|
|
17
|
+
"resolved": "vendor/@synopackageland/app",
|
|
18
|
+
"link": true
|
|
19
|
+
},
|
|
20
|
+
"node_modules/@types/node": {
|
|
21
|
+
"version": "18.19.130",
|
|
22
|
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
|
|
23
|
+
"integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
|
|
24
|
+
"dev": true,
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"undici-types": "~5.26.4"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"node_modules/typescript": {
|
|
31
|
+
"version": "5.9.3",
|
|
32
|
+
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
|
33
|
+
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
|
34
|
+
"dev": true,
|
|
35
|
+
"license": "Apache-2.0",
|
|
36
|
+
"bin": {
|
|
37
|
+
"tsc": "bin/tsc",
|
|
38
|
+
"tsserver": "bin/tsserver"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=14.17"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"node_modules/undici-types": {
|
|
45
|
+
"version": "5.26.5",
|
|
46
|
+
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
|
47
|
+
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
|
48
|
+
"dev": true,
|
|
49
|
+
"license": "MIT"
|
|
50
|
+
},
|
|
51
|
+
"vendor/@synopackageland/app": {
|
|
52
|
+
"version": "0.0.0",
|
|
53
|
+
"license": "MIT",
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=18"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "backend",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc -p tsconfig.build.json",
|
|
7
|
+
"typecheck": "tsc --noEmit",
|
|
8
|
+
"test": "rm -rf build && tsc && node --test build/test/*.test.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@synopackageland/app": "file:vendor/@synopackageland/app"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/node": "18.19.130",
|
|
15
|
+
"typescript": "5.9.3"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Request params, checked strictly: the browser sends what it likes.
|
|
2
|
+
import { AppError } from '@synopackageland/app'
|
|
3
|
+
|
|
4
|
+
/** params as an object with exactly these string fields, else bad_request. */
|
|
5
|
+
export function strings<K extends string>(params: unknown, ...names: K[]): Record<K, string> {
|
|
6
|
+
const ok = typeof params === 'object' && params !== null && !Array.isArray(params)
|
|
7
|
+
&& Object.keys(params).sort().join() === [...names].sort().join()
|
|
8
|
+
&& names.every((name) => typeof (params as Record<string, unknown>)[name] === 'string')
|
|
9
|
+
if (!ok) throw AppError.badRequest(`params must be {${[...names].sort().join(', ')}}`)
|
|
10
|
+
return params as Record<K, string>
|
|
11
|
+
}
|