Skip to content
Closed
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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,22 @@ pnpm test:e2e
pnpm test:production-smoke
```

Publish the current working tree to the permanent Cloudflare Pages development
surface with:

```sh
pnpm dlx wrangler@4.114.0 login # first use only
pnpm deploy:dev
```

This builds a web-only manifest for
<https://staging.tasknotes-app.pages.dev>, connects it to the hosted staging
Connect service and the isolated connector on `127.0.0.1:28486`, deploys only
the Cloudflare `staging` branch, and verifies the live application. It does not
change the production `app.tasknotes.dev` deployment. Start the matching local
connector from the sibling `mdbase-connect` checkout with
`pnpm dev:desktop:staging`.

To update the native projects, run `pnpm cap:sync`. Android builds require Java 21. On macOS, open the iOS project with `pnpm exec cap open ios`.

Contributor documentation:
Expand Down
18 changes: 10 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"manifest:validate:local": "mdbase-connect-dev validate-manifest public/.well-known/mdbase-app.json --allow-local",
"dev": "pnpm manifest:dev && vite",
"dev:e2e": "pnpm manifest:dev && vite --mode e2e",
"deploy:dev": "node scripts/deploy-pages-dev.mjs",
"build": "pnpm manifest && pnpm manifest:validate && tsc -b && vite build && node scripts/write-static-routes.mjs",
"build:e2e": "pnpm manifest && pnpm manifest:validate:local && tsc -b && vite build --mode e2e && node scripts/write-static-routes.mjs",
"preview": "vite preview",
Expand Down Expand Up @@ -50,9 +51,9 @@
"@fullcalendar/list": "6.1.17",
"@fullcalendar/react": "6.1.17",
"@fullcalendar/timegrid": "6.1.17",
"@mdbase-dev/connect": "file:vendor/mdbase-dev-connect-0.1.0-beta.45-66d87bc4bfaf.tgz",
"@mdbase-dev/connect-protocol": "file:vendor/mdbase-dev-connect-protocol-0.1.0-beta.45-66d87bc4bfaf.tgz",
"@tasknotes/model": "file:vendor/tasknotes-model-0.3.0-rc.10.tgz",
"@mdbase-dev/connect": "file:vendor/mdbase-dev-connect-0.1.0-beta.56-da0294c2ad31.tgz",
"@mdbase-dev/connect-protocol": "file:vendor/mdbase-dev-connect-protocol-0.1.0-beta.56-da0294c2ad31.tgz",
"@tasknotes/model": "file:vendor/tasknotes-model-0.3.0-rc.11.tgz",
"dexie": "4.4.4",
"firebase": "12.16.0",
"lucide-react": "1.25.0",
Expand All @@ -68,13 +69,13 @@
"devDependencies": {
"@axe-core/playwright": "^4.12.1",
"@callumalpass/mdbase": "file:vendor/callumalpass-mdbase-0.3.0-rc.5-3c9c89b8000b.tgz",
"@mdbase-dev/connect-dev": "file:vendor/mdbase-dev-connect-dev-0.1.0-beta.45-66d87bc4bfaf.tgz",
"@mdbase-dev/connect-testing": "file:vendor/mdbase-dev-connect-testing-0.1.0-beta.45-66d87bc4bfaf.tgz",
"@capacitor/android": "8.4.2",
"@capacitor/assets": "3.0.5",
"@capacitor/cli": "8.4.2",
"@capacitor/ios": "8.4.2",
"@eslint/js": "10.0.1",
"@mdbase-dev/connect-dev": "file:vendor/mdbase-dev-connect-dev-0.1.0-beta.56-da0294c2ad31.tgz",
"@mdbase-dev/connect-testing": "file:vendor/mdbase-dev-connect-testing-0.1.0-beta.56-da0294c2ad31.tgz",
"@playwright/test": "1.61.1",
"@testing-library/jest-dom": "7.0.0",
"@testing-library/react": "16.3.2",
Expand All @@ -98,8 +99,9 @@
},
"pnpm": {
"overrides": {
"@mdbase-dev/connect-protocol": "file:vendor/mdbase-dev-connect-protocol-0.1.0-beta.45-66d87bc4bfaf.tgz",
"@mdbase-dev/connect": "file:vendor/mdbase-dev-connect-0.1.0-beta.45-66d87bc4bfaf.tgz"
}
"@mdbase-dev/connect-protocol": "file:vendor/mdbase-dev-connect-protocol-0.1.0-beta.56-da0294c2ad31.tgz",
"@mdbase-dev/connect": "file:vendor/mdbase-dev-connect-0.1.0-beta.56-da0294c2ad31.tgz"
},
"patchedDependencies": {}
}
}
74 changes: 37 additions & 37 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

151 changes: 151 additions & 0 deletions scripts/deploy-pages-dev.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { spawn } from "node:child_process";
import { readFile, readdir, unlink, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";

export const developmentDeployment = Object.freeze({
appOrigin: "https://staging.tasknotes-app.pages.dev",
connectOrigin: "https://connect-staging.mdbase.dev",
loopbackOrigin: "http://127.0.0.1:28486",
project: "tasknotes-app",
branch: "staging",
wranglerVersion: "4.114.0",
});

const projectRoot = resolve(import.meta.dirname, "..");
const manifestTargets = [
resolve(projectRoot, "public", ".well-known", "mdbase-app.json"),
resolve(projectRoot, "src", "generated", "mdbase-app.json"),
];
const pnpm = process.platform === "win32" ? "pnpm.cmd" : "pnpm";

if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
await deployDevelopmentTaskNotes(process.env);
}

export function developmentDeploymentEnvironment(environment) {
return {
...environment,
VITE_BASE_PATH: "/",
TASKNOTES_APP_URL: developmentDeployment.appOrigin,
TASKNOTES_WEB_ONLY: "1",
TASKNOTES_FIREBASE_PROJECT_ID: "",
VITE_MDBASE_CONNECT_URL: developmentDeployment.connectOrigin,
VITE_MDBASE_CONNECT_LOOPBACK_URL: developmentDeployment.loopbackOrigin,
};
}

export async function deployDevelopmentTaskNotes(
environment,
dependencies = { run: runCommand, verifyBuild: verifyDevelopmentBuild },
) {
const deploymentEnvironment = developmentDeploymentEnvironment(environment);
const previousManifests = await Promise.all(
manifestTargets.map(async (target) => {
try {
return await readFile(target);
} catch (error) {
if (error.code === "ENOENT") return null;
throw error;
}
}),
);

try {
await dependencies.run(pnpm, ["build"], deploymentEnvironment);
await dependencies.verifyBuild();
} finally {
await Promise.all(
manifestTargets.map((target, index) =>
previousManifests[index] === null
? unlink(target).catch((error) => {
if (error.code !== "ENOENT") throw error;
})
: writeFile(target, previousManifests[index]),
),
);
}

await dependencies.run(
pnpm,
[
"dlx",
`wrangler@${developmentDeployment.wranglerVersion}`,
"pages",
"deploy",
"dist",
`--project-name=${developmentDeployment.project}`,
`--branch=${developmentDeployment.branch}`,
"--commit-dirty=true",
],
deploymentEnvironment,
);
await dependencies.run("node", ["scripts/production-smoke.mjs"], {
...deploymentEnvironment,
TASKNOTES_PRODUCTION_URL: developmentDeployment.appOrigin,
MDBASE_CONNECT_ORIGIN: developmentDeployment.connectOrigin,
});

console.log(
`Development TaskNotes deployed: ${developmentDeployment.appOrigin}/`,
);
}

async function verifyDevelopmentBuild() {
const manifestPath = resolve(
projectRoot,
"dist",
".well-known",
"mdbase-app.json",
);
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
const callback = `${developmentDeployment.appOrigin}/auth/mdbase/callback`;
if (
manifest.homepage !== `${developmentDeployment.appOrigin}/` ||
manifest.icon !== `${developmentDeployment.appOrigin}/icon.png` ||
manifest.redirect_uris?.length !== 1 ||
manifest.redirect_uris[0] !== callback
) {
throw new Error(
`TaskNotes deployment manifest does not declare ${developmentDeployment.appOrigin}.`,
);
}

const assetsDirectory = resolve(projectRoot, "dist", "assets");
const scripts = (await readdir(assetsDirectory))
.filter((file) => file.endsWith(".js"))
.map((file) => resolve(assetsDirectory, file));
const sources = await Promise.all(
scripts.map((script) => readFile(script, "utf8")),
);
for (const expected of [
developmentDeployment.connectOrigin,
developmentDeployment.loopbackOrigin,
]) {
if (!sources.some((source) => source.includes(expected))) {
throw new Error(
`TaskNotes deployment bundle does not contain ${expected}.`,
);
}
}
}

async function runCommand(command, arguments_, environment) {
const child = spawn(command, arguments_, {
cwd: projectRoot,
env: environment,
stdio: "inherit",
});
const exitCode = await new Promise((resolveExit, rejectExit) => {
child.once("error", rejectExit);
child.once("exit", (code, signal) => {
if (signal) rejectExit(new Error(`${command} was stopped by ${signal}.`));
else resolveExit(code);
});
});
if (exitCode !== 0) {
throw new Error(
`${command} ${arguments_.join(" ")} exited with code ${exitCode}.`,
);
}
}
Loading