create-nevela 0.1.2 → 0.1.4
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 +12 -3
- package/index.mjs +194 -115
- package/package.json +41 -37
- package/packagist.mjs +17 -0
- package/project.mjs +126 -0
- package/selfupdate.mjs +94 -0
- package/shell.mjs +21 -0
- package/template/laravel/src/Console/DevCommand.php +104 -0
- package/template/laravel/src/Console/MakeResourceCommand.php +11 -3
- package/template/laravel/src/Console/SetupCommand.php +55 -0
- package/template/laravel/src/Console/StatusCommand.php +195 -0
- package/template/laravel/src/Console/UpdateCommand.php +180 -113
- package/template/laravel/src/Console/VersionCommand.php +20 -0
- package/template/laravel/src/Generator/ResourceGenerator.php +8 -8
- package/template/laravel/src/Nevela.php +161 -151
- package/template/laravel/src/NevelaServiceProvider.php +15 -1
- package/template/laravel/src/Support/DashboardState.php +118 -0
- package/template/laravel/src/Support/DashboardUpdate.php +57 -20
- package/template/laravel/src/Support/Releases.php +45 -0
- package/template/laravel/src/Support/TemplateSource.php +144 -0
- package/template/web/app/auth-actions.ts +12 -4
- package/template/web/package.json +1 -1
- package/template.mjs +127 -94
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Nevela\Laravel\Support;
|
|
4
|
+
|
|
5
|
+
use Illuminate\Support\Facades\Http;
|
|
6
|
+
use Throwable;
|
|
7
|
+
|
|
8
|
+
/** What Packagist knows about nevela/laravel. */
|
|
9
|
+
final class Releases
|
|
10
|
+
{
|
|
11
|
+
private static string|null|false $latest = false;
|
|
12
|
+
|
|
13
|
+
/** The newest stable release, or null when Packagist can't be reached. Asked once per run. */
|
|
14
|
+
public static function latest(): ?string
|
|
15
|
+
{
|
|
16
|
+
if (self::$latest !== false) {
|
|
17
|
+
return self::$latest;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
$releases = Http::timeout(8)->get('https://repo.packagist.org/p2/nevela/laravel.json')->json('packages.nevela/laravel') ?? [];
|
|
21
|
+
} catch (Throwable) {
|
|
22
|
+
return self::$latest = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return self::$latest = self::newest(array_map(fn ($release) => (string) ($release['version'] ?? ''), $releases));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The highest x.y.z in a list of version names. Branches and pre-releases don't count.
|
|
30
|
+
*
|
|
31
|
+
* @param list<string> $names
|
|
32
|
+
*/
|
|
33
|
+
public static function newest(array $names): ?string
|
|
34
|
+
{
|
|
35
|
+
$versions = [];
|
|
36
|
+
foreach ($names as $name) {
|
|
37
|
+
if (preg_match('/^v?(\d+\.\d+\.\d+)$/', $name, $m)) {
|
|
38
|
+
$versions[] = $m[1];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
usort($versions, 'version_compare');
|
|
42
|
+
|
|
43
|
+
return $versions === [] ? null : end($versions);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Nevela\Laravel\Support;
|
|
4
|
+
|
|
5
|
+
use Illuminate\Support\Facades\Http;
|
|
6
|
+
use PharData;
|
|
7
|
+
use RecursiveDirectoryIterator;
|
|
8
|
+
use RecursiveIteratorIterator;
|
|
9
|
+
use RuntimeException;
|
|
10
|
+
use Throwable;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The dashboard as it shipped in a given version of Nevela.
|
|
14
|
+
*
|
|
15
|
+
* Tried in order, so an update never depends on one place being up to date:
|
|
16
|
+
*
|
|
17
|
+
* 1. NEVELA_TEMPLATE_DIR, a folder of create-nevela-<version>.tgz files. For working on
|
|
18
|
+
* Nevela itself, and for updating without a connection.
|
|
19
|
+
* 2. The create-nevela package on npm, which carries the dashboard.
|
|
20
|
+
* 3. The repository's own tag on GitHub. Tags are pushed before anything is published,
|
|
21
|
+
* so this works in the gap between a release and its npm package.
|
|
22
|
+
*/
|
|
23
|
+
final class TemplateSource
|
|
24
|
+
{
|
|
25
|
+
private const NPM = 'https://registry.npmjs.org/create-nevela/-/create-nevela-%s.tgz';
|
|
26
|
+
|
|
27
|
+
private const GITHUB = 'https://codeload.github.com/MarkColeMukisa/nevela/tar.gz/refs/tags/v%s';
|
|
28
|
+
|
|
29
|
+
/** Never part of an app's dashboard: build output, installs and local settings. */
|
|
30
|
+
private const NEVER = ['node_modules', '.next', '.env.local', 'tsconfig.tsbuildinfo', 'next-env.d.ts'];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @return array{files: array<string, string>, from: string} path => contents, and where it came from
|
|
34
|
+
*/
|
|
35
|
+
public static function fetch(string $version): array
|
|
36
|
+
{
|
|
37
|
+
$dir = rtrim(sys_get_temp_dir(), '/\\').DIRECTORY_SEPARATOR.'nevela-template-'.$version.'-'.bin2hex(random_bytes(4));
|
|
38
|
+
mkdir($dir, 0775, true);
|
|
39
|
+
$archive = $dir.DIRECTORY_SEPARATOR.'template.tgz';
|
|
40
|
+
$tried = [];
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
$local = rtrim((string) env('NEVELA_TEMPLATE_DIR', ''), '/\\');
|
|
44
|
+
if ($local !== '' && is_file("{$local}/create-nevela-{$version}.tgz")) {
|
|
45
|
+
copy("{$local}/create-nevela-{$version}.tgz", $archive);
|
|
46
|
+
|
|
47
|
+
return ['files' => self::read($archive, $dir, 'package/template/web', false), 'from' => 'NEVELA_TEMPLATE_DIR'];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
foreach (['npm' => [self::NPM, 'package/template/web', false], 'GitHub' => [self::GITHUB, "nevela-{$version}/apps/web", true]] as $name => [$url, $root, $filter]) {
|
|
51
|
+
try {
|
|
52
|
+
$response = Http::timeout(60)->get(sprintf($url, $version));
|
|
53
|
+
if (! $response->successful()) {
|
|
54
|
+
$tried[] = "{$name}: HTTP {$response->status()}";
|
|
55
|
+
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
file_put_contents($archive, $response->body());
|
|
59
|
+
|
|
60
|
+
return ['files' => self::read($archive, $dir.DIRECTORY_SEPARATOR.strtolower($name), $root, $filter), 'from' => $name];
|
|
61
|
+
} catch (Throwable $e) {
|
|
62
|
+
$tried[] = "{$name}: ".$e->getMessage();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
throw new RuntimeException("version {$version} was not found (".implode('; ', $tried).')');
|
|
67
|
+
} finally {
|
|
68
|
+
self::remove($dir);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Unpack an archive and read the dashboard out of it.
|
|
74
|
+
*
|
|
75
|
+
* @param bool $filter Whether this is the repository's own apps/web, whose example resources aren't part of the template
|
|
76
|
+
* @return array<string, string>
|
|
77
|
+
*/
|
|
78
|
+
private static function read(string $archive, string $into, string $root, bool $filter): array
|
|
79
|
+
{
|
|
80
|
+
if (! is_dir($into)) {
|
|
81
|
+
mkdir($into, 0775, true);
|
|
82
|
+
}
|
|
83
|
+
(new PharData($archive))->extractTo($into, null, true);
|
|
84
|
+
$base = $into.DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, $root);
|
|
85
|
+
if (! is_dir($base)) {
|
|
86
|
+
throw new RuntimeException('the archive has no dashboard in it');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
$files = [];
|
|
90
|
+
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($base, RecursiveDirectoryIterator::SKIP_DOTS)) as $file) {
|
|
91
|
+
$path = str_replace('\\', '/', substr($file->getPathname(), strlen($base) + 1));
|
|
92
|
+
if (array_intersect(explode('/', $path), self::NEVER) !== []) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
// npm can't carry .gitignore files, so its template holds them as _gitignore.
|
|
96
|
+
$path = preg_replace('#(^|/)_gitignore$#', '$1.gitignore', $path);
|
|
97
|
+
$files[$path] = (string) file_get_contents($file->getPathname());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return $filter ? self::withoutExamples($files) : $files;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The repository's dashboard has example resources in it (a Product, say). They are
|
|
105
|
+
* generated from Laravel, not part of what a new app is given.
|
|
106
|
+
*
|
|
107
|
+
* @param array<string, string> $files
|
|
108
|
+
* @return array<string, string>
|
|
109
|
+
*/
|
|
110
|
+
public static function withoutExamples(array $files): array
|
|
111
|
+
{
|
|
112
|
+
$slugs = [];
|
|
113
|
+
foreach ($files as $path => $contents) {
|
|
114
|
+
if (preg_match('#^resources/[^/]+\.resource\.ts$#', $path) && preg_match('/slug:\s*"([^"]+)"/', $contents, $m)) {
|
|
115
|
+
$slugs[] = $m[1];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return array_filter($files, function (string $path) use ($slugs) {
|
|
120
|
+
if (preg_match('#^resources/[^/]+\.resource\.ts$#', $path) || $path === 'resources/index.ts') {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
foreach ($slugs as $slug) {
|
|
124
|
+
if (str_starts_with($path, "app/dashboard/{$slug}/")) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return true;
|
|
130
|
+
}, ARRAY_FILTER_USE_KEY);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private static function remove(string $dir): void
|
|
134
|
+
{
|
|
135
|
+
if (! is_dir($dir)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
$items = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST);
|
|
139
|
+
foreach ($items as $item) {
|
|
140
|
+
$item->isDir() ? @rmdir($item->getPathname()) : @unlink($item->getPathname());
|
|
141
|
+
}
|
|
142
|
+
@rmdir($dir);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use server";
|
|
2
2
|
|
|
3
3
|
import { cookies } from "next/headers";
|
|
4
|
-
import { laravel, TOKEN_COOKIE } from "@/lib/laravel";
|
|
4
|
+
import { API_URL, laravel, TOKEN_COOKIE } from "@/lib/laravel";
|
|
5
5
|
|
|
6
6
|
export type SignInResult = { ok: true } | { ok: false; error: string };
|
|
7
7
|
|
|
@@ -17,12 +17,20 @@ export async function signInAction(email: string, password: string): Promise<Sig
|
|
|
17
17
|
try {
|
|
18
18
|
response = await laravel("auth/token", { method: "POST", body: { email, password }, token: null });
|
|
19
19
|
} catch {
|
|
20
|
-
return { ok: false, error:
|
|
20
|
+
return { ok: false, error: `Can't reach the server at ${API_URL}. Start it with: php nevela dev` };
|
|
21
21
|
}
|
|
22
22
|
if (!response.ok) {
|
|
23
|
-
const body = (response.body ?? {}) as { error?: string; issues?: { message: string }[] };
|
|
23
|
+
const body = (response.body ?? {}) as { error?: string; message?: string; issues?: { message: string }[] };
|
|
24
24
|
if (response.status === 429) return { ok: false, error: "Too many attempts. Wait a minute and try again." };
|
|
25
|
-
|
|
25
|
+
if (response.status === 404 || response.status === 405) {
|
|
26
|
+
// Something answered, but it has no sign-in route: it isn't this app's API. Usually
|
|
27
|
+
// another program already had the port when the API was started.
|
|
28
|
+
return {
|
|
29
|
+
ok: false,
|
|
30
|
+
error: `${API_URL} is answered by a different program, not this app's API. Stop what is using that port, or start both with "php nevela dev", which picks a free one.`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return { ok: false, error: body.issues?.[0]?.message ?? body.error ?? body.message ?? "Sign-in failed. Try again." };
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
const { token } = response.body as { token: string };
|
package/template.mjs
CHANGED
|
@@ -1,94 +1,127 @@
|
|
|
1
|
-
// Where the new app's files come from.
|
|
2
|
-
//
|
|
3
|
-
// Published to npm, this package carries a `template/` folder built by build-template.mjs.
|
|
4
|
-
// Run from the repository, it reads apps/web and packages/laravel directly, so there is
|
|
5
|
-
// one source for the dashboard and nothing to keep in sync by hand.
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
fs.writeFileSync(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
fs.
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
1
|
+
// Where the new app's files come from.
|
|
2
|
+
//
|
|
3
|
+
// Published to npm, this package carries a `template/` folder built by build-template.mjs.
|
|
4
|
+
// Run from the repository, it reads apps/web and packages/laravel directly, so there is
|
|
5
|
+
// one source for the dashboard and nothing to keep in sync by hand.
|
|
6
|
+
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const bundled = path.join(here, 'template');
|
|
14
|
+
// In a checkout the dashboard two folders up is the source, even when a template/ folder
|
|
15
|
+
// is lying around from an earlier `npm pack`: that copy may be stale.
|
|
16
|
+
const fromRepo = fs.existsSync(path.resolve(here, '..', '..', 'apps', 'web', 'package.json')) || !fs.existsSync(bundled);
|
|
17
|
+
|
|
18
|
+
/** True when this is a checkout of the repository rather than the package from npm. */
|
|
19
|
+
export const fromRepository = fromRepo;
|
|
20
|
+
|
|
21
|
+
export const sources = {
|
|
22
|
+
web: fromRepo ? path.resolve(here, '..', '..', 'apps', 'web') : path.join(bundled, 'web'),
|
|
23
|
+
laravel: fromRepo ? path.resolve(here, '..', 'laravel') : path.join(bundled, 'laravel'),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Never part of a new app: build output, installs and local settings. */
|
|
27
|
+
const NEVER = new Set(['node_modules', '.next', '.env.local', 'tsconfig.tsbuildinfo', 'next-env.d.ts', 'vendor', '.phpunit.result.cache', '.phpunit.cache', 'composer.lock']);
|
|
28
|
+
|
|
29
|
+
function copyTree(from, to, skip = () => false, relative = '') {
|
|
30
|
+
fs.mkdirSync(to, { recursive: true });
|
|
31
|
+
for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
|
|
32
|
+
const inside = relative ? `${relative}/${entry.name}` : entry.name;
|
|
33
|
+
if (NEVER.has(entry.name) || skip(inside, entry)) continue;
|
|
34
|
+
const source = path.join(from, entry.name);
|
|
35
|
+
// npm leaves .gitignore files out of a published package, so the bundled template
|
|
36
|
+
// carries them as _gitignore (see buildTemplate) and they get their name back here.
|
|
37
|
+
const target = path.join(to, entry.name === '_gitignore' ? '.gitignore' : entry.name);
|
|
38
|
+
if (entry.isDirectory()) copyTree(source, target, skip, inside);
|
|
39
|
+
else if (entry.isFile()) fs.copyFileSync(source, target);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* What belongs to the framework repository's own example resources, not to a new app:
|
|
45
|
+
* their descriptors, the registry that lists them, and their dashboard pages.
|
|
46
|
+
*/
|
|
47
|
+
export function exampleResourceFilter(webRoot) {
|
|
48
|
+
const resources = path.join(webRoot, 'resources');
|
|
49
|
+
const slugs = new Set();
|
|
50
|
+
if (fs.existsSync(resources)) {
|
|
51
|
+
for (const file of fs.readdirSync(resources)) {
|
|
52
|
+
if (!file.endsWith('.resource.ts')) continue;
|
|
53
|
+
const slug = /slug:\s*"([^"]+)"/.exec(fs.readFileSync(path.join(resources, file), 'utf8'));
|
|
54
|
+
if (slug) slugs.add(slug[1]);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return (inside) =>
|
|
58
|
+
/^resources\/[^/]+\.resource\.ts$/.test(inside)
|
|
59
|
+
|| inside === 'resources/index.ts'
|
|
60
|
+
|| [...slugs].some((slug) => inside === `app/dashboard/${slug}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The dashboard, without the repository's example resources, named for the new app. */
|
|
64
|
+
export function copyWebTemplate(to, { name, title }) {
|
|
65
|
+
copyTree(sources.web, to, exampleResourceFilter(sources.web));
|
|
66
|
+
|
|
67
|
+
const packageFile = path.join(to, 'package.json');
|
|
68
|
+
const manifest = JSON.parse(fs.readFileSync(packageFile, 'utf8'));
|
|
69
|
+
manifest.name = `${name}-web`;
|
|
70
|
+
fs.writeFileSync(packageFile, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
71
|
+
|
|
72
|
+
const siteFile = path.join(to, 'lib', 'site.ts');
|
|
73
|
+
const site = fs.readFileSync(siteFile, 'utf8')
|
|
74
|
+
.replace('name: "Nevela"', `name: ${JSON.stringify(title)}`)
|
|
75
|
+
.replace('description: "Nevela keeps', `description: "${title.replace(/"/g, '')} keeps`);
|
|
76
|
+
fs.writeFileSync(siteFile, site);
|
|
77
|
+
|
|
78
|
+
// The generator fills this in; it has to exist for the first build.
|
|
79
|
+
fs.mkdirSync(path.join(to, 'resources'), { recursive: true });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A fingerprint of every template file, and the template's dependencies: what the app
|
|
84
|
+
* records in .nevela.json so that `nevela update` can tell, later and exactly, which
|
|
85
|
+
* files the developer changed. The same fingerprint the Laravel package computes: SHA-1
|
|
86
|
+
* with line endings ignored.
|
|
87
|
+
*/
|
|
88
|
+
export function templateRecord(version) {
|
|
89
|
+
const skip = exampleResourceFilter(sources.web);
|
|
90
|
+
const files = {};
|
|
91
|
+
const walk = (dir, relative = '') => {
|
|
92
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
93
|
+
const inside = relative ? `${relative}/${entry.name}` : entry.name;
|
|
94
|
+
if (NEVER.has(entry.name) || skip(inside, entry)) continue;
|
|
95
|
+
if (entry.isDirectory()) walk(path.join(dir, entry.name), inside);
|
|
96
|
+
else if (entry.isFile()) {
|
|
97
|
+
const bytes = fs.readFileSync(path.join(dir, entry.name)).toString('latin1').replace(/\r\n/g, '\n');
|
|
98
|
+
files[inside.replace(/(^|\/)_gitignore$/, '$1.gitignore')] = crypto.createHash('sha1').update(bytes, 'latin1').digest('hex');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
walk(sources.web);
|
|
103
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(sources.web, 'package.json'), 'utf8'));
|
|
104
|
+
const dependencies = {};
|
|
105
|
+
for (const section of ['dependencies', 'devDependencies']) if (manifest[section]) dependencies[section] = manifest[section];
|
|
106
|
+
return { template: version, files: Object.fromEntries(Object.entries(files).sort(([a], [b]) => a.localeCompare(b))), dependencies };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The Laravel package, for installing by path until it is on Packagist. */
|
|
110
|
+
export function copyLaravelPackage(to) {
|
|
111
|
+
copyTree(sources.laravel, to, (inside) => inside === 'tests' || inside === 'phpunit.xml');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Used by build-template.mjs: the same filtered copies, into this package's template/. */
|
|
115
|
+
export function buildTemplate(to) {
|
|
116
|
+
copyTree(sources.web, path.join(to, 'web'), exampleResourceFilter(sources.web));
|
|
117
|
+
copyLaravelPackage(path.join(to, 'laravel'));
|
|
118
|
+
hideGitignores(to);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function hideGitignores(dir) {
|
|
122
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
123
|
+
const full = path.join(dir, entry.name);
|
|
124
|
+
if (entry.isDirectory()) hideGitignores(full);
|
|
125
|
+
else if (entry.name === '.gitignore') fs.renameSync(full, path.join(dir, '_gitignore'));
|
|
126
|
+
}
|
|
127
|
+
}
|